#include "main_application.h"

#include "util/types.hpp"
#include "util/logs.hpp"
#include "util/sysinfo.hpp"

#include "Utilities/Thread.h"
#include "Utilities/File.h"
#include "Input/pad_thread.h"
#include "Emu/System.h"
#include "Emu/system_config.h"
#include "Emu/IdManager.h"
#include "Emu/Io/Null/NullKeyboardHandler.h"
#include "Emu/Io/Null/NullMouseHandler.h"
#include "Emu/Io/KeyboardHandler.h"
#include "Emu/Io/MouseHandler.h"
#include "Input/basic_keyboard_handler.h"
#include "Input/basic_mouse_handler.h"

#include "Emu/Audio/AudioBackend.h"
#include "Emu/Audio/Null/NullAudioBackend.h"
#include "Emu/Audio/Null/null_enumerator.h"
#include "Emu/Audio/Cubeb/CubebBackend.h"
#include "Emu/Audio/Cubeb/cubeb_enumerator.h"
#ifdef _WIN32
#include "Emu/Audio/XAudio2/XAudio2Backend.h"
#include "Emu/Audio/XAudio2/xaudio2_enumerator.h"
#endif
#ifdef HAVE_FAUDIO
#include "Emu/Audio/FAudio/FAudioBackend.h"
#include "Emu/Audio/FAudio/faudio_enumerator.h"
#endif

#include <QFileInfo> // This shouldn't be outside rpcs3qt...
#include <QImageReader> // This shouldn't be outside rpcs3qt...
#include <thread>

LOG_CHANNEL(sys_log, "SYS");

/** Emu.Init() wrapper for user management */
void main_application::InitializeEmulator(const std::string& user, bool show_gui)
{
	Emu.SetHasGui(show_gui);
	Emu.SetUsr(user);
	Emu.Init();

	// Log Firmware Version after Emu was initialized
	const std::string firmware_version = utils::get_firmware_version();
	const std::string firmware_string  = firmware_version.empty() ? "Missing Firmware" : ("Firmware version: " + firmware_version);
	sys_log.always()("%s", firmware_string);
}

/** RPCS3 emulator has functions it desires to call from the GUI at times. Initialize them in here. */
EmuCallbacks main_application::CreateCallbacks()
{
	EmuCallbacks callbacks;

	callbacks.init_kb_handler = [this]()
	{
		switch (g_cfg.io.keyboard.get())
		{
		case keyboard_handler::null:
		{
			g_fxo->init<KeyboardHandlerBase, NullKeyboardHandler>(Emu.DeserialManager());
			break;
		}
		case keyboard_handler::basic:
		{
			basic_keyboard_handler* ret = g_fxo->init<KeyboardHandlerBase, basic_keyboard_handler>(Emu.DeserialManager());
			ret->moveToThread(get_thread());
			ret->SetTargetWindow(m_game_window);
			break;
		}
		}
	};

	callbacks.init_mouse_handler = [this]()
	{
		switch (g_cfg.io.mouse.get())
		{
		case mouse_handler::null:
		{
			if (g_cfg.io.move == move_handler::mouse)
			{
				basic_mouse_handler* ret = g_fxo->init<MouseHandlerBase, basic_mouse_handler>(Emu.DeserialManager());
				ret->moveToThread(get_thread());
				ret->SetTargetWindow(m_game_window);
			}
			else
			{
				g_fxo->init<MouseHandlerBase, NullMouseHandler>(Emu.DeserialManager());
			}

			break;
		}
		case mouse_handler::basic:
		{
			basic_mouse_handler* ret = g_fxo->init<MouseHandlerBase, basic_mouse_handler>(Emu.DeserialManager());
			ret->moveToThread(get_thread());
			ret->SetTargetWindow(m_game_window);
			break;
		}
		}
	};

	callbacks.init_pad_handler = [this](std::string_view title_id)
	{
		ensure(g_fxo->init<named_thread<pad_thread>>(get_thread(), m_game_window, title_id));
		extern void process_qt_events();
		while (!pad::g_started) process_qt_events();
	};

	callbacks.get_audio = []() -> std::shared_ptr<AudioBackend>
	{
		std::shared_ptr<AudioBackend> result;
		switch (g_cfg.audio.renderer.get())
		{
		case audio_renderer::null: result = std::make_shared<NullAudioBackend>(); break;
#ifdef _WIN32
		case audio_renderer::xaudio: result = std::make_shared<XAudio2Backend>(); break;
#endif
		case audio_renderer::cubeb: result = std::make_shared<CubebBackend>(); break;
#ifdef HAVE_FAUDIO
		case audio_renderer::faudio: result = std::make_shared<FAudioBackend>(); break;
#endif
		}

		if (!result->Initialized())
		{
			// Fall back to a null backend if something went wrong
			sys_log.error("Audio renderer %s could not be initialized, using a Null renderer instead. Make sure that no other application is running that might block audio access (e.g. Netflix).", result->GetName());
			result = std::make_shared<NullAudioBackend>();
		}
		return result;
	};

	callbacks.get_audio_enumerator = [](u64 renderer) -> std::shared_ptr<audio_device_enumerator>
	{
		switch (static_cast<audio_renderer>(renderer))
		{
		case audio_renderer::null: return std::make_shared<null_enumerator>();
#ifdef _WIN32
		case audio_renderer::xaudio: return std::make_shared<xaudio2_enumerator>();
#endif
		case audio_renderer::cubeb: return std::make_shared<cubeb_enumerator>();
#ifdef HAVE_FAUDIO
		case audio_renderer::faudio: return std::make_shared<faudio_enumerator>();
#endif
		default: fmt::throw_exception("Invalid renderer index %u", renderer);
		}
	};

	callbacks.get_image_info = [](const std::string& filename, std::string& sub_type, s32& width, s32& height, s32& orientation) -> bool
	{
		sub_type.clear();
		width = 0;
		height = 0;
		orientation = 0; // CELL_SEARCH_ORIENTATION_UNKNOWN

		bool success = false;
		Emu.BlockingCallFromMainThread([&]()
		{
			const QImageReader reader(QString::fromStdString(filename));
			if (reader.canRead())
			{
				const QSize size = reader.size();
				width = size.width();
				height = size.height();
				sub_type = reader.subType().toStdString();

				switch (reader.transformation())
				{
				case QImageIOHandler::Transformation::TransformationNone:
					orientation = 1; // CELL_SEARCH_ORIENTATION_TOP_LEFT = 0°
					break;
				case QImageIOHandler::Transformation::TransformationRotate90:
					orientation = 2; // CELL_SEARCH_ORIENTATION_TOP_RIGHT = 90°
					break;
				case QImageIOHandler::Transformation::TransformationRotate180:
					orientation = 3; // CELL_SEARCH_ORIENTATION_BOTTOM_RIGHT = 180°
					break;
				case QImageIOHandler::Transformation::TransformationRotate270:
					orientation = 4; // CELL_SEARCH_ORIENTATION_BOTTOM_LEFT = 270°
					break;
				default:
					// Ignore other transformations for now
					break;
				}

				success = true;
				sys_log.notice("get_image_info found image: filename='%s', sub_type='%s', width=%d, height=%d, orientation=%d", filename, sub_type, width, height, orientation);
			}
			else
			{
				sys_log.error("get_image_info failed to read '%s'. Error='%s'", filename, reader.errorString().toStdString());
			}
		});
		return success;
	};

	callbacks.get_scaled_image = [](const std::string& path, s32 target_width, s32 target_height, s32& width, s32& height, u8* dst, bool force_fit) -> bool
	{
		width = 0;
		height = 0;

		if (target_width <= 0 || target_height <= 0 || !dst || !fs::is_file(path))
		{
			return false;
		}

		bool success = false;
		Emu.BlockingCallFromMainThread([&]()
		{
			// We use QImageReader instead of QImage. This way we can load and scale image in one step.
			QImageReader reader(QString::fromStdString(path));

			if (reader.canRead())
			{
				QSize size = reader.size();
				width = size.width();
				height = size.height();

				if (width <= 0 || height <= 0)
				{
					return;
				}

				if (force_fit || width > target_width || height > target_height)
				{
					const f32 target_ratio = target_width / static_cast<f32>(target_height);
					const f32 image_ratio = width / static_cast<f32>(height);
					const f32 convert_ratio = image_ratio / target_ratio;

					if (convert_ratio > 1.0f)
					{
						size = QSize(target_width, target_height / convert_ratio);
					}
					else if (convert_ratio < 1.0f)
					{
						size = QSize(target_width * convert_ratio, target_height);
					}
					else
					{
						size = QSize(target_width, target_height);
					}

					reader.setScaledSize(size);
					width = size.width();
					height = size.height();
				}

				QImage image = reader.read();

				if (image.format() != QImage::Format::Format_RGBA8888)
				{
					image = image.convertToFormat(QImage::Format::Format_RGBA8888);
				}

				std::memcpy(dst, image.constBits(), std::min(4 * target_width * target_height, image.height() * image.bytesPerLine()));
				success = true;
				sys_log.notice("get_scaled_image scaled image: path='%s', width=%d, height=%d", path, width, height);
			}
			else
			{
				sys_log.error("get_scaled_image failed to read '%s'. Error='%s'", path, reader.errorString().toStdString());
			}
		});
		return success;
	};

	callbacks.resolve_path = [](std::string_view sv)
	{
		return QFileInfo(QString::fromUtf8(sv.data(), static_cast<int>(sv.size()))).canonicalFilePath().toStdString();
	};

	return callbacks;
}
