
#if defined(USE_SDL)
#include "types.h"
#include "cfg/cfg.h"
#include "sdl/sdl.h"
#ifdef WIN32
#include <SDL_syswm.h>
#endif
#include <SDL_video.h>
#if defined(USE_VULKAN)
#include <SDL_vulkan.h>
#endif
#endif
#include "hw/maple/maple_devs.h"
#include "sdl_gamepad.h"
#include "sdl_keyboard.h"
#include "wsi/context.h"
#include "emulator.h"
#include "stdclass.h"
#include "imgui/imgui.h"
#if !defined(_WIN32) && !defined(__APPLE__) && !defined(__SWITCH__)
#include "linux-dist/icon.h"
#endif
#ifdef _WIN32
#include "windows/rawinput.h"
#endif
#ifdef __SWITCH__
#include "nswitch.h"
#endif

static SDL_Window* window = NULL;
static u32 windowFlags;

#ifdef TARGET_PANDORA
	#define WINDOW_WIDTH  800
#else
	#define WINDOW_WIDTH  640
#endif
#define WINDOW_HEIGHT  480

static std::shared_ptr<SDLMouse> sdl_mouse;
static std::shared_ptr<SDLKeyboardDevice> sdl_keyboard;
static bool window_fullscreen;
static bool window_maximized;
static SDL_Rect windowPos { SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, WINDOW_WIDTH, WINDOW_HEIGHT };
static bool gameRunning;
static bool mouseCaptured;
static std::string clipboardText;

static struct SDLDeInit
{
	~SDLDeInit() {
		if (initialized)
			SDL_Quit();
	}

	bool initialized = false;
} sqlDeinit;

static void sdl_open_joystick(int index)
{
	SDL_Joystick *pJoystick = SDL_JoystickOpen(index);

	if (pJoystick == NULL)
	{
		INFO_LOG(INPUT, "SDL: Cannot open joystick %d", index + 1);
		return;
	}
	try {
		std::shared_ptr<SDLGamepad> gamepad = std::make_shared<SDLGamepad>(index < MAPLE_PORTS ? index : -1, index, pJoystick);
		SDLGamepad::AddSDLGamepad(gamepad);
	} catch (const FlycastException& e) {
	}
}

static void sdl_close_joystick(SDL_JoystickID instance)
{
	std::shared_ptr<SDLGamepad> gamepad = SDLGamepad::GetSDLGamepad(instance);
	if (gamepad != NULL)
		gamepad->close();
}

static void captureMouse(bool capture)
{
	if (window == nullptr || !gameRunning)
		return;
	if (!capture)
	{
		if (!config::UseRawInput)
			SDL_SetRelativeMouseMode(SDL_FALSE);
		else
			SDL_ShowCursor(SDL_ENABLE);
		SDL_SetWindowTitle(window, "Flycast Dojo");
		mouseCaptured = false;
	}
	else
	{
		if (config::UseRawInput
				|| SDL_SetRelativeMouseMode(SDL_TRUE) == 0)
		{
			if (config::UseRawInput)
				SDL_ShowCursor(SDL_DISABLE);
			SDL_SetWindowTitle(window, "Flycast - mouse capture");
			mouseCaptured = true;
		}
	}
}

static void emuEventCallback(Event event, void *)
{
	switch (event)
	{
	case Event::Pause:
		gameRunning = false;
		if (!config::UseRawInput)
			SDL_SetRelativeMouseMode(SDL_FALSE);
		SDL_ShowCursor(SDL_ENABLE);
		SDL_SetWindowTitle(window, "Flycast Dojo");
		break;
	case Event::Resume:
		gameRunning = true;
		captureMouse(mouseCaptured);
		if (window_fullscreen && !mouseCaptured)
			SDL_ShowCursor(SDL_DISABLE);

		break;
	default:
		break;
	}
}

static void checkRawInput()
{
#if defined(_WIN32) && !defined(TARGET_UWP)
	if ((bool)config::UseRawInput != (bool)sdl_mouse)
		return;
	if (config::UseRawInput)
	{
		GamepadDevice::Unregister(sdl_keyboard);
		sdl_keyboard = nullptr;
		GamepadDevice::Unregister(sdl_mouse);
		sdl_mouse = nullptr;
		rawinput::init();
	}
	else
	{
		rawinput::term();
		sdl_keyboard = std::make_shared<SDLKeyboardDevice>(0);
		GamepadDevice::Register(sdl_keyboard);
		sdl_mouse = std::make_shared<SDLMouse>();
		GamepadDevice::Register(sdl_mouse);
	}
#else
	if (!sdl_keyboard)
	{
		sdl_keyboard = std::make_shared<SDLKeyboardDevice>(0);
		GamepadDevice::Register(sdl_keyboard);
	}
	if (!sdl_mouse)
	{
		sdl_mouse = std::make_shared<SDLMouse>();
		GamepadDevice::Register(sdl_mouse);
	}
#endif
}

void input_sdl_init()
{
	if (SDL_WasInit(SDL_INIT_JOYSTICK) == 0)
	{
		// We want joystick events even if we loose focus
		SDL_SetHint(SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS, "1");
#ifdef _WIN32
		if (cfgLoadBool("input", "DisableXInput", false))
		{
			// Disable XInput for some old joysticks
			NOTICE_LOG(INPUT, "Disabling XInput, using DirectInput");
			SDL_SetHint(SDL_HINT_XINPUT_ENABLED, "0");
		}
		// Don't close the app when pressing the B button
		SDL_SetHint(SDL_HINT_WINRT_HANDLE_BACK_BUTTON, "1");
#endif
		std::string db = get_readonly_data_path("gamecontrollerdb.txt");
		int rv = SDL_GameControllerAddMappingsFromFile(db.c_str());
		if (rv < 0)
		{
			db = get_readonly_config_path("gamecontrollerdb.txt");
			rv = SDL_GameControllerAddMappingsFromFile(db.c_str());
		}
		if (rv > 0)
			DEBUG_LOG(INPUT ,"%d mappings loaded from %s", rv, db.c_str());

		if (SDL_InitSubSystem(SDL_INIT_JOYSTICK) < 0)
			die("SDL: error initializing Joystick subsystem");
	}
	sqlDeinit.initialized = true;

	SDL_SetRelativeMouseMode(SDL_FALSE);

	EventManager::listen(Event::Pause, emuEventCallback);
	EventManager::listen(Event::Resume, emuEventCallback);

	checkRawInput();

#if defined(__SWITCH__) || defined(__OpenBSD__)
    // when railed, both joycons are mapped to joystick #0,
    // else joycons are individually mapped to joystick #0, joystick #1, ...
    // https://github.com/devkitPro/SDL/blob/switch-sdl2/src/joystick/switch/SDL_sysjoystick.c#L45
	for (int joy = 0; joy < 4; joy++)
		sdl_open_joystick(joy);
#endif
	if (SDL_HasScreenKeyboardSupport())
	{
		NOTICE_LOG(INPUT, "On-screen keyboard supported");
		gui_setOnScreenKeyboardCallback([](bool show) {
			// We should be able to use SDL_IsScreenKeyboardShown() but it doesn't seem to work on Xbox
			static bool visible;
			if (window != nullptr && visible != show)
			{
				visible = show;
				if (show)
					SDL_StartTextInput();
				else
					SDL_StopTextInput();
			}
		});
	}
}

void input_sdl_quit()
{
	SDLGamepad::closeAllGamepads();
	SDL_QuitSubSystem(SDL_INIT_JOYSTICK);
}

inline void SDLMouse::setAbsPos(int x, int y) {
	int width, height;
	SDL_GetWindowSize(window, &width, &height);
	if (width != 0 && height != 0)
		Mouse::setAbsPos(x, y, width, height);
}

void input_sdl_handle()
{
	SDLGamepad::UpdateRumble();

	SDL_Event event;
	while (SDL_PollEvent(&event))
	{
		switch (event.type)
		{
			case SDL_QUIT:
				dc_exit();
				break;

			case SDL_KEYDOWN:
			case SDL_KEYUP:
				checkRawInput();
				if (event.key.repeat == 0)
				{
					if (event.type == SDL_KEYDOWN && event.key.keysym.sym == SDLK_RETURN && (event.key.keysym.mod & KMOD_ALT))
					{
						if (window_fullscreen)
						{
							SDL_SetWindowFullscreen(window, 0);
							if (!gameRunning || !mouseCaptured)
								SDL_ShowCursor(SDL_ENABLE);
						}
						else
						{
							SDL_SetWindowFullscreen(window, SDL_WINDOW_FULLSCREEN_DESKTOP);
							if (gameRunning)
								SDL_ShowCursor(SDL_DISABLE);
						}
						window_fullscreen = !window_fullscreen;
					}
					else if (event.type == SDL_KEYDOWN && (event.key.keysym.mod & KMOD_LALT) && (event.key.keysym.mod & KMOD_LCTRL))
					{
						if (cfgLoadBool("input", "EnableMouseCaptureToggle", false))
							captureMouse(!mouseCaptured);
					}
					else if (!config::UseRawInput)
					{
						sdl_keyboard->keyboard_input(event.key.keysym.scancode, event.type == SDL_KEYDOWN);
					}
				}
				break;

			case SDL_TEXTINPUT:
				gui_keyboard_inputUTF8(event.text.text);
				break;

			case SDL_WINDOWEVENT:
				if (event.window.event == SDL_WINDOWEVENT_SIZE_CHANGED
						|| event.window.event == SDL_WINDOWEVENT_RESTORED
						|| event.window.event == SDL_WINDOWEVENT_MINIMIZED
						|| event.window.event == SDL_WINDOWEVENT_MAXIMIZED)
				{
#ifdef USE_VULKAN
					if (windowFlags & SDL_WINDOW_VULKAN)
						SDL_Vulkan_GetDrawableSize(window, &settings.display.width, &settings.display.height);
					else
#endif
#ifdef USE_OPENGL
					if (windowFlags & SDL_WINDOW_OPENGL)
						SDL_GL_GetDrawableSize(window, &settings.display.width, &settings.display.height);
					else
#endif
						SDL_GetWindowSize(window, &settings.display.width, &settings.display.height);
					GraphicsContext::Instance()->resize();
				}
				else if (event.window.event == SDL_WINDOWEVENT_FOCUS_GAINED)
				{
					if (window_fullscreen && gameRunning)
						SDL_ShowCursor(SDL_DISABLE);
				}
				else if (event.window.event == SDL_WINDOWEVENT_FOCUS_LOST)
				{
					if (window_fullscreen)
						SDL_ShowCursor(SDL_ENABLE);
				}
				break;

			case SDL_JOYBUTTONDOWN:
			case SDL_JOYBUTTONUP:
				{
					std::shared_ptr<SDLGamepad> device = SDLGamepad::GetSDLGamepad((SDL_JoystickID)event.jbutton.which);
					if (device != NULL)
						device->gamepad_btn_input(event.jbutton.button, event.type == SDL_JOYBUTTONDOWN);
				}
				break;
			case SDL_JOYAXISMOTION:
				{
					std::shared_ptr<SDLGamepad> device = SDLGamepad::GetSDLGamepad((SDL_JoystickID)event.jaxis.which);
					if (device != NULL)
						device->gamepad_axis_input(event.jaxis.axis, event.jaxis.value);
				}
				break;
			case SDL_JOYHATMOTION:
				{
					std::shared_ptr<SDLGamepad> device = SDLGamepad::GetSDLGamepad((SDL_JoystickID)event.jhat.which);
					if (device != NULL)
					{
						u32 hatid = (event.jhat.hat + 1) << 8;
						if (event.jhat.value & SDL_HAT_UP)
						{
							device->gamepad_btn_input(hatid + 0, true);
							device->gamepad_btn_input(hatid + 1, false);
						}
						else if (event.jhat.value & SDL_HAT_DOWN)
						{
							device->gamepad_btn_input(hatid + 0, false);
							device->gamepad_btn_input(hatid + 1, true);
						}
						else
						{
							device->gamepad_btn_input(hatid + 0, false);
							device->gamepad_btn_input(hatid + 1, false);
						}
						if (event.jhat.value & SDL_HAT_LEFT)
						{
							device->gamepad_btn_input(hatid + 2, true);
							device->gamepad_btn_input(hatid + 3, false);
						}
						else if (event.jhat.value & SDL_HAT_RIGHT)
						{
							device->gamepad_btn_input(hatid + 2, false);
							device->gamepad_btn_input(hatid + 3, true);
						}
						else
						{
							device->gamepad_btn_input(hatid + 2, false);
							device->gamepad_btn_input(hatid + 3, false);
						}
					}
				}
				break;

			case SDL_MOUSEMOTION:
				gui_set_mouse_position(event.motion.x, event.motion.y);
				checkRawInput();
				if (!config::UseRawInput)
				{
					if (mouseCaptured && gameRunning)
						sdl_mouse->setRelPos(event.motion.xrel, event.motion.yrel);
					else
						sdl_mouse->setAbsPos(event.motion.x, event.motion.y);
					sdl_mouse->setButton(Mouse::LEFT_BUTTON, event.motion.state & SDL_BUTTON_LMASK);
					sdl_mouse->setButton(Mouse::RIGHT_BUTTON, event.motion.state & SDL_BUTTON_RMASK);
					sdl_mouse->setButton(Mouse::MIDDLE_BUTTON, event.motion.state & SDL_BUTTON_MMASK);
					sdl_mouse->setButton(Mouse::BUTTON_4, event.motion.state & SDL_BUTTON_X1MASK);
					sdl_mouse->setButton(Mouse::BUTTON_5, event.motion.state & SDL_BUTTON_X2MASK);
				}
				else if (mouseCaptured && gameRunning)
				{
					int x, y;
					SDL_GetWindowSize(window, &x, &y);
					x /= 2;
					y /= 2;
					if (std::abs(x - event.motion.x) > 10 || std::abs(y - event.motion.y) > 10 )
						SDL_WarpMouseInWindow(window, x, y);
				}
				break;

			case SDL_MOUSEBUTTONDOWN:
			case SDL_MOUSEBUTTONUP:
				gui_set_mouse_position(event.button.x, event.button.y);
				gui_set_mouse_button(event.button.button - 1, event.button.state == SDL_PRESSED);
				checkRawInput();
				if (!config::UseRawInput)
				{
					if (!mouseCaptured || !gameRunning)
						sdl_mouse->setAbsPos(event.button.x, event.button.y);
					bool pressed = event.button.state == SDL_PRESSED;
					switch (event.button.button) {
					case SDL_BUTTON_LEFT:
						sdl_mouse->setButton(Mouse::LEFT_BUTTON, pressed);
						break;
					case SDL_BUTTON_RIGHT:
						sdl_mouse->setButton(Mouse::RIGHT_BUTTON, pressed);
						break;
					case SDL_BUTTON_MIDDLE:
						sdl_mouse->setButton(Mouse::MIDDLE_BUTTON, pressed);
						break;
					case SDL_BUTTON_X1:
						sdl_mouse->setButton(Mouse::BUTTON_4, pressed);
						break;
					case SDL_BUTTON_X2:
						sdl_mouse->setButton(Mouse::BUTTON_5, pressed);
						break;
					}
				}
				break;

			case SDL_MOUSEWHEEL:
				gui_set_mouse_wheel(-event.wheel.y * 35);
				checkRawInput();
				if (!config::UseRawInput)
					sdl_mouse->setWheel(-event.wheel.y);
				break;

			case SDL_JOYDEVICEADDED:
				sdl_open_joystick(event.jdevice.which);
				break;

			case SDL_JOYDEVICEREMOVED:
				sdl_close_joystick((SDL_JoystickID)event.jdevice.which);
				break;
		}
	}
}

void sdl_window_set_text(const char* text)
{
	if (window != nullptr)
		SDL_SetWindowTitle(window, text);
}

static float hdpiScaling = 1.f;

static inline void get_window_state()
{
	u32 flags = SDL_GetWindowFlags(window);
	window_fullscreen = flags & SDL_WINDOW_FULLSCREEN_DESKTOP;
	window_maximized = flags & SDL_WINDOW_MAXIMIZED;
    if (!window_fullscreen && !window_maximized){
        SDL_GetWindowSize(window, &windowPos.w, &windowPos.h);
        windowPos.w /= hdpiScaling;
        windowPos.h /= hdpiScaling;
        SDL_GetWindowPosition(window, &windowPos.x, &windowPos.y);
    }
		
}

#if defined(_WIN32) && !defined(TARGET_UWP)
#include <windows.h>

HWND getNativeHwnd()
{
	if (window == nullptr)
		return NULL;
	SDL_SysWMinfo wmInfo;
	SDL_VERSION(&wmInfo.version);
	SDL_GetWindowWMInfo(window, &wmInfo);
	return wmInfo.info.win.window;
}
#endif

bool sdl_recreate_window(u32 flags)
{
	windowFlags = flags;
#ifdef _WIN32
    //Enable HiDPI mode in Windows
    typedef enum PROCESS_DPI_AWARENESS {
        PROCESS_DPI_UNAWARE = 0,
        PROCESS_SYSTEM_DPI_AWARE = 1,
        PROCESS_PER_MONITOR_DPI_AWARE = 2
    } PROCESS_DPI_AWARENESS;
    
    HRESULT(WINAPI *SetProcessDpiAwareness)(PROCESS_DPI_AWARENESS dpiAwareness); // Windows 8.1 and later
    void* shcoreDLL = SDL_LoadObject("SHCORE.DLL");
    if (shcoreDLL) {
        SetProcessDpiAwareness = (HRESULT(WINAPI *)(PROCESS_DPI_AWARENESS)) SDL_LoadFunction(shcoreDLL, "SetProcessDpiAwareness");
        if (SetProcessDpiAwareness) {
            SetProcessDpiAwareness(PROCESS_PER_MONITOR_DPI_AWARE);
            
            if (SDL_GetDisplayDPI(0, &settings.display.dpi, NULL, NULL) != -1){ //SDL_WINDOWPOS_UNDEFINED is Display 0
                //When using HiDPI mode, set correct DPI scaling
            	hdpiScaling = settings.display.dpi / 96.f;
            }
        }
        SDL_UnloadObject(shcoreDLL);
    }
#endif
    
#ifdef __SWITCH__
	AppletOperationMode om = appletGetOperationMode();
	if (om == AppletOperationMode_Handheld)
	{
		windowPos.w  = 1280;
		windowPos.h = 720;
		settings.display.uiScale = 1.5f;
	}
	else
	{
		windowPos.w  = 1920;
		windowPos.h = 1080;
		settings.display.uiScale = 1.4f;
	}
#else
	windowPos.x = cfgLoadInt("window", "left", windowPos.x);
	windowPos.y = cfgLoadInt("window", "top", windowPos.y);
	windowPos.w = cfgLoadInt("window", "width", windowPos.w);
	windowPos.h = cfgLoadInt("window", "height", windowPos.h);
	window_fullscreen = cfgLoadBool("window", "fullscreen", window_fullscreen);
	window_maximized = cfgLoadBool("window", "maximized", window_maximized);
	if (window != nullptr)
		get_window_state();
#endif
	if (window != nullptr)
	{
		SDL_DestroyWindow(window);
		window = nullptr;
	}

#if !defined(GLES)
	flags |= SDL_WINDOW_RESIZABLE | SDL_WINDOW_ALLOW_HIGHDPI;
	if (window_fullscreen)
		flags |= SDL_WINDOW_FULLSCREEN_DESKTOP;
	else if (window_maximized)
		flags |= SDL_WINDOW_MAXIMIZED;
#else
	flags |= SDL_WINDOW_FULLSCREEN;
#endif

	window = SDL_CreateWindow("Flycast Dojo", windowPos.x, windowPos.y,
			windowPos.w * hdpiScaling, windowPos.h * hdpiScaling, flags);
	if (window == nullptr)
	{
		ERROR_LOG(COMMON, "Window creation failed: %s", SDL_GetError());
		return false;
	}
	settings.display.width = windowPos.w * hdpiScaling;
	settings.display.height = windowPos.h * hdpiScaling;

#if !defined(GLES) && !defined(_WIN32) && !defined(__SWITCH__) && !defined(__APPLE__)
	// Set the window icon
	u32 pixels[48 * 48];
	for (int i = 0; i < 48 * 48; i++)
		pixels[i] = window_icon[i + 2];
	SDL_Surface *surface = SDL_CreateRGBSurfaceFrom(pixels, 48, 48, 32, 4 * 48, 0x00ff0000, 0x0000ff00, 0x000000ff, 0xff000000);
	if (surface == NULL)
	  INFO_LOG(COMMON, "Creating icon surface failed: %s", SDL_GetError());
	else
	{
		SDL_SetWindowIcon(window, surface);
		SDL_FreeSurface(surface);
	}
#endif

	void *windowCtx = window;
#ifdef _WIN32
	if (isDirectX(config::RendererType))
#ifdef TARGET_UWP
	{
		SDL_SysWMinfo wmInfo;
		SDL_VERSION(&wmInfo.version);
		SDL_GetWindowWMInfo(window, &wmInfo);
		windowCtx = wmInfo.info.winrt.window;
	}
#else
		windowCtx = getNativeHwnd();
#endif
#endif
	GraphicsContext::Instance()->setWindow(windowCtx);

	int displayIndex = SDL_GetWindowDisplayIndex(window);
	if (displayIndex < 0)
		WARN_LOG(RENDERER, "Cannot get the window display index: %s", SDL_GetError());
	else
	{
		SDL_DisplayMode mode{};
		if (SDL_GetDesktopDisplayMode(displayIndex, &mode) == 0) {
			NOTICE_LOG(RENDERER, "Monitor refresh rate: %d Hz (%d x %d)", mode.refresh_rate, mode.w, mode.h);
			settings.display.refreshRate = mode.refresh_rate;
			if (flags & SDL_WINDOW_FULLSCREEN)
			{
				settings.display.width = mode.w;
				settings.display.height = mode.h;
			}
		}
	}

	return true;
}

static const char *getClipboardText(void *)
{
	clipboardText.clear();
	if (SDL_HasClipboardText())
	{
		char *text = SDL_GetClipboardText();
		clipboardText = text;
		SDL_free(text);
	}
	return clipboardText.c_str();
}

static void setClipboardText(void *, const char *text)
{
	SDL_SetClipboardText(text);
}

#ifdef TARGET_UWP
static int suspendEventFilter(void *userdata, SDL_Event *event)
{
	if (event->type == SDL_APP_WILLENTERBACKGROUND)
	{
		gui_save();
	    if (gameRunning)
	    {
	        emu.stop();
	        if (config::AutoSaveState)
	            dc_savestate(config::SavestateSlot);
	    }
	    return 0;
	}
	return 1;
}
#endif

void sdl_window_create()
{
	if (SDL_WasInit(SDL_INIT_VIDEO) == 0)
	{
		if (SDL_InitSubSystem(SDL_INIT_VIDEO) != 0)
		{
			die("error initializing SDL Video subsystem");
		}
#if defined(__APPLE__) && defined(USE_VULKAN)
		SDL_Vulkan_LoadLibrary("libvulkan.dylib");
#endif
	}
	sqlDeinit.initialized = true;
	initRenderApi();
	// ImGui copy & paste
	ImGui::GetIO().GetClipboardTextFn = getClipboardText;
	ImGui::GetIO().SetClipboardTextFn = setClipboardText;
#ifdef TARGET_UWP
	// Must be fast so an event filter is required
	SDL_SetEventFilter(suspendEventFilter, nullptr);
#endif
}

void sdl_window_destroy()
{
#ifndef __SWITCH__
	get_window_state();
	cfgSaveInt("window", "left", windowPos.x);
	cfgSaveInt("window", "top", windowPos.y);
	cfgSaveInt("window", "width", windowPos.w);
	cfgSaveInt("window", "height", windowPos.h);
	cfgSaveBool("window", "maximized", window_maximized);
	cfgSaveBool("window", "fullscreen", window_fullscreen);
#endif
	termRenderApi();
	SDL_DestroyWindow(window);
	window = nullptr;
	SDL_QuitSubSystem(SDL_INIT_VIDEO);
}

void sdl_fix_steamdeck_dpi(SDL_Window *window)
{
#ifdef __linux__
	// Fixing Steam Deck's incorrect 60mm * 60mm EDID
	if (settings.display.dpi > 500)
	{
		int displayIndex = SDL_GetWindowDisplayIndex(window);
		SDL_DisplayMode mode;
		SDL_GetDisplayMode(displayIndex, 0, &mode);
		if (displayIndex == 0
				&& (strcmp(SDL_GetDisplayName(displayIndex), "ANX7530 U 3\"") == 0
						|| strcmp(SDL_GetDisplayName(displayIndex), "XWAYLAND0 3\"") == 0)
				&& mode.w == 1280 && mode.h == 800)
			settings.display.dpi = 206;
	}
#endif
}
