#include "StarMainApplication.hpp"
#include "StarLogging.hpp"
#include "StarSignalHandler.hpp"
#include "StarTickRateMonitor.hpp"
#include "StarRenderer_opengl20.hpp"
#include "StarTtlCache.hpp"
#include "StarImage.hpp"
#include "StarImageProcessing.hpp"

#include "SDL.h"
#include "StarPlatformServices_pc.hpp"

namespace Star {

Maybe<Key> keyFromSdlKeyCode(SDL_Keycode sym) {
  static HashMap<int, Key> KeyCodeMap{
    {SDLK_BACKSPACE, Key::Backspace},
    {SDLK_TAB, Key::Tab},
    {SDLK_CLEAR, Key::Clear},
    {SDLK_RETURN, Key::Return},
    {SDLK_PAUSE, Key::Pause},
    {SDLK_ESCAPE, Key::Escape},
    {SDLK_SPACE, Key::Space},
    {SDLK_EXCLAIM, Key::Exclaim},
    {SDLK_QUOTEDBL, Key::QuotedBL},
    {SDLK_HASH, Key::Hash},
    {SDLK_DOLLAR, Key::Dollar},
    {SDLK_AMPERSAND, Key::Ampersand},
    {SDLK_QUOTE, Key::Quote},
    {SDLK_LEFTPAREN, Key::LeftParen},
    {SDLK_RIGHTPAREN, Key::RightParen},
    {SDLK_ASTERISK, Key::Asterisk},
    {SDLK_PLUS, Key::Plus},
    {SDLK_COMMA, Key::Comma},
    {SDLK_MINUS, Key::Minus},
    {SDLK_PERIOD, Key::Period},
    {SDLK_SLASH, Key::Slash},
    {SDLK_0, Key::Zero},
    {SDLK_1, Key::One},
    {SDLK_2, Key::Two},
    {SDLK_3, Key::Three},
    {SDLK_4, Key::Four},
    {SDLK_5, Key::Five},
    {SDLK_6, Key::Six},
    {SDLK_7, Key::Seven},
    {SDLK_8, Key::Eight},
    {SDLK_9, Key::Nine},
    {SDLK_COLON, Key::Colon},
    {SDLK_SEMICOLON, Key::Semicolon},
    {SDLK_LESS, Key::Less},
    {SDLK_EQUALS, Key::Equals},
    {SDLK_GREATER, Key::Greater},
    {SDLK_QUESTION, Key::Question},
    {SDLK_AT, Key::At},
    {SDLK_LEFTBRACKET, Key::LeftBracket},
    {SDLK_BACKSLASH, Key::Backslash},
    {SDLK_RIGHTBRACKET, Key::RightBracket},
    {SDLK_CARET, Key::Caret},
    {SDLK_UNDERSCORE, Key::Underscore},
    {SDLK_BACKQUOTE, Key::Backquote},
    {SDLK_a, Key::A},
    {SDLK_b, Key::B},
    {SDLK_c, Key::C},
    {SDLK_d, Key::D},
    {SDLK_e, Key::E},
    {SDLK_f, Key::F},
    {SDLK_g, Key::G},
    {SDLK_h, Key::H},
    {SDLK_i, Key::I},
    {SDLK_j, Key::J},
    {SDLK_k, Key::K},
    {SDLK_l, Key::L},
    {SDLK_m, Key::M},
    {SDLK_n, Key::N},
    {SDLK_o, Key::O},
    {SDLK_p, Key::P},
    {SDLK_q, Key::Q},
    {SDLK_r, Key::R},
    {SDLK_s, Key::S},
    {SDLK_t, Key::T},
    {SDLK_u, Key::U},
    {SDLK_v, Key::V},
    {SDLK_w, Key::W},
    {SDLK_x, Key::X},
    {SDLK_y, Key::Y},
    {SDLK_z, Key::Z},
    {SDLK_DELETE, Key::Delete},
    {SDLK_KP_0, Key::Kp0},
    {SDLK_KP_1, Key::Kp1},
    {SDLK_KP_2, Key::Kp2},
    {SDLK_KP_3, Key::Kp3},
    {SDLK_KP_4, Key::Kp4},
    {SDLK_KP_5, Key::Kp5},
    {SDLK_KP_6, Key::Kp6},
    {SDLK_KP_7, Key::Kp7},
    {SDLK_KP_8, Key::Kp8},
    {SDLK_KP_9, Key::Kp9},
    {SDLK_KP_PERIOD, Key::Kp_period},
    {SDLK_KP_DIVIDE, Key::Kp_divide},
    {SDLK_KP_MULTIPLY, Key::Kp_multiply},
    {SDLK_KP_MINUS, Key::Kp_minus},
    {SDLK_KP_PLUS, Key::Kp_plus},
    {SDLK_KP_ENTER, Key::Kp_enter},
    {SDLK_KP_EQUALS, Key::Kp_equals},
    {SDLK_UP, Key::Up},
    {SDLK_DOWN, Key::Down},
    {SDLK_RIGHT, Key::Right},
    {SDLK_LEFT, Key::Left},
    {SDLK_INSERT, Key::Insert},
    {SDLK_HOME, Key::Home},
    {SDLK_END, Key::End},
    {SDLK_PAGEUP, Key::PageUp},
    {SDLK_PAGEDOWN, Key::PageDown},
    {SDLK_F1, Key::F1},
    {SDLK_F2, Key::F2},
    {SDLK_F3, Key::F3},
    {SDLK_F4, Key::F4},
    {SDLK_F5, Key::F5},
    {SDLK_F6, Key::F6},
    {SDLK_F7, Key::F7},
    {SDLK_F8, Key::F8},
    {SDLK_F9, Key::F9},
    {SDLK_F10, Key::F10},
    {SDLK_F11, Key::F11},
    {SDLK_F12, Key::F12},
    {SDLK_F13, Key::F13},
    {SDLK_F14, Key::F14},
    {SDLK_F15, Key::F15},
    {SDLK_NUMLOCKCLEAR, Key::NumLock},
    {SDLK_CAPSLOCK, Key::CapsLock},
    {SDLK_SCROLLLOCK, Key::ScrollLock},
    {SDLK_RSHIFT, Key::RShift},
    {SDLK_LSHIFT, Key::LShift},
    {SDLK_RCTRL, Key::RCtrl},
    {SDLK_LCTRL, Key::LCtrl},
    {SDLK_RALT, Key::RAlt},
    {SDLK_LALT, Key::LAlt},
    {SDLK_RGUI, Key::RGui},
    {SDLK_LGUI, Key::LGui},
    {SDLK_MODE, Key::AltGr},
    {SDLK_APPLICATION, Key::Compose},
    {SDLK_HELP, Key::Help},
    {SDLK_PRINTSCREEN, Key::PrintScreen},
    {SDLK_SYSREQ, Key::SysReq},
    {SDLK_PAUSE, Key::Pause},
    {SDLK_MENU, Key::Menu},
    {SDLK_POWER, Key::Power}
  };

  return KeyCodeMap.maybe(sym);
}

KeyMod keyModsFromSdlKeyMods(uint16_t mod) {
  return static_cast<KeyMod>(mod);
}

MouseButton mouseButtonFromSdlMouseButton(uint8_t button) {
  switch (button) {
    case SDL_BUTTON_LEFT: return MouseButton::Left;
    case SDL_BUTTON_MIDDLE: return MouseButton::Middle;
    case SDL_BUTTON_RIGHT: return MouseButton::Right;
    case SDL_BUTTON_X1: return MouseButton::FourthButton;
    default: return MouseButton::FifthButton;
  }
}

ControllerAxis controllerAxisFromSdlControllerAxis(uint8_t axis) {
  switch (axis) {
    case SDL_CONTROLLER_AXIS_LEFTX: return ControllerAxis::LeftX;
    case SDL_CONTROLLER_AXIS_LEFTY: return ControllerAxis::LeftY;
    case SDL_CONTROLLER_AXIS_RIGHTX: return ControllerAxis::RightX;
    case SDL_CONTROLLER_AXIS_RIGHTY: return ControllerAxis::RightY;
    case SDL_CONTROLLER_AXIS_TRIGGERLEFT: return ControllerAxis::TriggerLeft;
    case SDL_CONTROLLER_AXIS_TRIGGERRIGHT: return ControllerAxis::TriggerRight;
    default: return ControllerAxis::Invalid;
  }
}

ControllerButton controllerButtonFromSdlControllerButton(uint8_t button) {
  switch (button) {
    case SDL_CONTROLLER_BUTTON_A: return ControllerButton::A;
    case SDL_CONTROLLER_BUTTON_B: return ControllerButton::B;
    case SDL_CONTROLLER_BUTTON_X: return ControllerButton::X;
    case SDL_CONTROLLER_BUTTON_Y: return ControllerButton::Y;
    case SDL_CONTROLLER_BUTTON_BACK: return ControllerButton::Back;
    case SDL_CONTROLLER_BUTTON_GUIDE: return ControllerButton::Guide;
    case SDL_CONTROLLER_BUTTON_START: return ControllerButton::Start;
    case SDL_CONTROLLER_BUTTON_LEFTSTICK: return ControllerButton::LeftStick;
    case SDL_CONTROLLER_BUTTON_RIGHTSTICK: return ControllerButton::RightStick;
    case SDL_CONTROLLER_BUTTON_LEFTSHOULDER: return ControllerButton::LeftShoulder;
    case SDL_CONTROLLER_BUTTON_RIGHTSHOULDER: return ControllerButton::RightShoulder;
    case SDL_CONTROLLER_BUTTON_DPAD_UP: return ControllerButton::DPadUp;
    case SDL_CONTROLLER_BUTTON_DPAD_DOWN: return ControllerButton::DPadDown;
    case SDL_CONTROLLER_BUTTON_DPAD_LEFT: return ControllerButton::DPadLeft;
    case SDL_CONTROLLER_BUTTON_DPAD_RIGHT: return ControllerButton::DPadRight;
    case SDL_CONTROLLER_BUTTON_MISC1: return ControllerButton::Misc1;
    case SDL_CONTROLLER_BUTTON_PADDLE1: return ControllerButton::Paddle1;
    case SDL_CONTROLLER_BUTTON_PADDLE2: return ControllerButton::Paddle2;
    case SDL_CONTROLLER_BUTTON_PADDLE3: return ControllerButton::Paddle3;
    case SDL_CONTROLLER_BUTTON_PADDLE4: return ControllerButton::Paddle4;
    case SDL_CONTROLLER_BUTTON_TOUCHPAD: return ControllerButton::Touchpad;
    default: return ControllerButton::Invalid;
  }
}

class SdlPlatform {
public:
  SdlPlatform(ApplicationUPtr application, StringList cmdLineArgs) {
    m_application = std::move(application);

    // extract application path from command line args
    String applicationPath = cmdLineArgs.first();
    cmdLineArgs = cmdLineArgs.slice(1);

    StringList platformArguments;
    eraseWhere(cmdLineArgs, [&platformArguments](String& argument) {
        if (argument.beginsWith("+platform")) {
          platformArguments.append(std::move(argument));
          return true;
        }
        return false;
      });

    Logger::info("Application: Initializing SDL");
    if (SDL_Init(0))
      throw ApplicationException(strf("Couldn't initialize SDL: {}", SDL_GetError()));

    if (char* basePath = SDL_GetBasePath()) {
      File::changeDirectory(basePath);
      SDL_free(basePath);
    }

    m_signalHandler.setHandleInterrupt(true);
    m_signalHandler.setHandleFatal(true);

    try {
      Logger::info("Application: startup...");
      m_application->startup(cmdLineArgs);
    } catch (std::exception const& e) {
      throw ApplicationException("Application threw exception during startup", e);
    }

    Logger::info("Application: Initializing SDL Video");
    if (SDL_InitSubSystem(SDL_INIT_VIDEO))
      throw ApplicationException(strf("Couldn't initialize SDL Video: {}", SDL_GetError()));

    Logger::info("Application: Initializing SDL Controller");
    if (SDL_InitSubSystem(SDL_INIT_GAMECONTROLLER))
      throw ApplicationException(strf("Couldn't initialize SDL Controller: {}", SDL_GetError()));

#ifdef STAR_SYSTEM_WINDOWS // Newer SDL is defaulting to xaudio2, which does not support audio capture
    SDL_setenv("SDL_AUDIODRIVER", "directsound", 1);
#endif

    Logger::info("Application: Initializing SDL Audio");
    if (SDL_InitSubSystem(SDL_INIT_AUDIO))
      throw ApplicationException(strf("Couldn't initialize SDL Audio: {}", SDL_GetError()));

    Logger::info("Application: using Audio Driver '{}'", SDL_GetCurrentAudioDriver());

    SDL_JoystickEventState(SDL_ENABLE);

    m_platformServices = PcPlatformServices::create(applicationPath, platformArguments);
    if (!m_platformServices)
      Logger::info("Application: No platform services available");

    Logger::info("Application: Creating SDL Window");
    m_sdlWindow = SDL_CreateWindow(m_windowTitle.utf8Ptr(), SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED,
        m_windowSize[0], m_windowSize[1], SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE);
    if (!m_sdlWindow)
      throw ApplicationException::format("Application: Could not create SDL window: {}", SDL_GetError());

    SDL_ShowWindow(m_sdlWindow);
    SDL_RaiseWindow(m_sdlWindow);

    int width;
    int height;
    SDL_GetWindowSize(m_sdlWindow, &width, &height);
    m_windowSize = Vec2U(width, height);

    m_sdlGlContext = SDL_GL_CreateContext(m_sdlWindow);
    if (!m_sdlGlContext)
      throw ApplicationException::format("Application: Could not create OpenGL context: {}", SDL_GetError());

    SDL_GL_SwapWindow(m_sdlWindow);
    setVSyncEnabled(m_windowVSync);

    SDL_StopTextInput();

    SDL_AudioSpec desired = {};
    desired.freq = 44100;
    desired.format = AUDIO_S16SYS;
    desired.samples = 1024;
    desired.channels = 2;
    desired.userdata = this;
    desired.callback = [](void* userdata, Uint8* stream, int len) {
      ((SdlPlatform*)(userdata))->getAudioData(stream, len);
    };

    SDL_AudioSpec obtained = {};
    m_sdlAudioOutputDevice = SDL_OpenAudioDevice(NULL, 0, &desired, &obtained, 0);
    if (!m_sdlAudioOutputDevice) {
      Logger::error("Application: Could not open audio device, no sound available!");
    } else if (obtained.freq != desired.freq || obtained.channels != desired.channels || obtained.format != desired.format) {
      SDL_CloseAudioDevice(m_sdlAudioOutputDevice);
      Logger::error("Application: Could not open 44.1khz / 16 bit stereo audio device, no sound available!");
    } else {
      Logger::info("Application: Opened default audio device with 44.1khz / 16 bit stereo audio, {} sample size buffer", obtained.samples);
      SDL_PauseAudioDevice(m_sdlAudioOutputDevice, 0);
    }

    m_renderer = make_shared<OpenGl20Renderer>();
    m_renderer->setScreenSize(m_windowSize);

    m_cursorCache.setTimeToLive(30000);
  }

  ~SdlPlatform() {

    if (m_sdlAudioOutputDevice)
      SDL_CloseAudioDevice(m_sdlAudioOutputDevice);

    closeAudioInputDevice();

    m_renderer.reset();

    Logger::info("Application: Destroying SDL Window");
    SDL_DestroyWindow(m_sdlWindow);

    SDL_Quit();
  }

  bool openAudioInputDevice(const char* name, int freq, int channels, void* userdata, SDL_AudioCallback callback) {
    SDL_AudioSpec desired = {};
    desired.freq = freq;
    desired.format = AUDIO_S16SYS;
    desired.samples = 1024;
    desired.channels = channels;
    desired.userdata = userdata;
    desired.callback = callback;

    closeAudioInputDevice();

    SDL_AudioSpec obtained = {};
    m_sdlAudioInputDevice = SDL_OpenAudioDevice(name, 1, &desired, &obtained, 0);

    if (m_sdlAudioInputDevice) {
      if (name)
        Logger::info("Opened audio input device '{}'", name);
      else
        Logger::info("Opened default audio input device");
      SDL_PauseAudioDevice(m_sdlAudioInputDevice, 0);
    }
    else
      Logger::info("Failed to open audio input device: {}", SDL_GetError());

    return m_sdlAudioInputDevice != 0;
  }

  bool closeAudioInputDevice() {
    if (m_sdlAudioInputDevice) {
      Logger::info("Closing audio input device");
      SDL_CloseAudioDevice(m_sdlAudioInputDevice);
      m_sdlAudioInputDevice = 0;
      return true;
    }
    return false;
  }

  void cleanup() {
    m_cursorCache.ptr(m_currentCursor);
    m_cursorCache.cleanup();
  }

  void run() {
    try {
      Logger::info("Application: initialization...");
      m_application->applicationInit(make_shared<Controller>(this));

      Logger::info("Application: renderer initialization...");
      m_application->renderInit(m_renderer);

      Logger::info("Application: main update loop...");

      m_updateTicker.reset();
      m_renderTicker.reset();

      bool quit = false;
      while (true) {
        cleanup();

        for (auto const& event : processEvents())
          m_application->processInput(event);

        if (m_platformServices)
          m_platformServices->update();

        if (m_platformServices->overlayActive())
          SDL_ShowCursor(1);
        else
          SDL_ShowCursor(m_cursorVisible ? 1 : 0);

        int updatesBehind = max<int>(round(m_updateTicker.ticksBehind()), 1);
        updatesBehind = min<int>(updatesBehind, m_maxFrameSkip + 1);
        for (int i = 0; i < updatesBehind; ++i) {
          m_application->update();
          m_updateRate = m_updateTicker.tick();
        }

        m_renderer->startFrame();
        m_application->render();
        m_renderer->finishFrame();
        SDL_GL_SwapWindow(m_sdlWindow);
        m_renderRate = m_renderTicker.tick();

        if (m_quitRequested) {
          Logger::info("Application: quit requested");
          quit = true;
        }

        if (m_signalHandler.interruptCaught()) {
          Logger::info("Application: Interrupt caught");
          quit = true;
        }

        if (quit) {
          Logger::info("Application: quitting...");
          break;
        }

        int64_t spareMilliseconds = round(m_updateTicker.spareTime() * 1000);
        if (spareMilliseconds > 0)
          Thread::sleepPrecise(spareMilliseconds);
      }
    } catch (std::exception const& e) {
      Logger::error("Application: exception thrown, shutting down: {}", outputException(e, true));
    }

    try {
      Logger::info("Application: shutdown...");
      m_application->shutdown();
    } catch (std::exception const& e) {
      Logger::error("Application: threw exception during shutdown: {}", outputException(e, true));
    }

    SDL_CloseAudioDevice(m_sdlAudioOutputDevice);
    m_SdlControllers.clear();

    SDL_SetCursor(NULL);
    m_cursorCache.clear();

    m_application.reset();
  }

private:
  struct Controller : public ApplicationController {
    Controller(SdlPlatform* parent)
      : parent(parent) {}

    Maybe<String> getClipboard() override {
      Maybe<String> string;
      if (SDL_HasClipboardText()) {
        if (auto text = SDL_GetClipboardText()) {
          if (*text != '\0')
            string.emplace(text);
          SDL_free(text);
        }
      }
      return string;
    }

    void setClipboard(String text) override {
      SDL_SetClipboardText(text.utf8Ptr());
    }

    void setTargetUpdateRate(float targetUpdateRate) override {
      parent->m_updateTicker.setTargetTickRate(targetUpdateRate);
    }

    void setUpdateTrackWindow(float updateTrackWindow) override {
      parent->m_updateTicker.setWindow(updateTrackWindow);
    }

    void setApplicationTitle(String title) override {
      parent->m_windowTitle = std::move(title);
      if (parent->m_sdlWindow)
        SDL_SetWindowTitle(parent->m_sdlWindow, parent->m_windowTitle.utf8Ptr());
    }

    void setFullscreenWindow(Vec2U fullScreenResolution) override {
      if (parent->m_windowMode != WindowMode::Fullscreen || parent->m_windowSize != fullScreenResolution) {
        SDL_DisplayMode requestedDisplayMode = {SDL_PIXELFORMAT_RGB888, (int)fullScreenResolution[0], (int)fullScreenResolution[1], 0, 0};
        int currentDisplayIndex = SDL_GetWindowDisplayIndex(parent->m_sdlWindow);

        SDL_DisplayMode targetDisplayMode;
        if (SDL_GetClosestDisplayMode(currentDisplayIndex, &requestedDisplayMode, &targetDisplayMode) != NULL) {
          if (SDL_SetWindowDisplayMode(parent->m_sdlWindow, &requestedDisplayMode) == 0) {
            if (parent->m_windowMode == WindowMode::Fullscreen)
              SDL_SetWindowFullscreen(parent->m_sdlWindow, 0);
            else if (parent->m_windowMode == WindowMode::Borderless)
              SDL_SetWindowBordered(parent->m_sdlWindow, SDL_TRUE);
            else if (parent->m_windowMode == WindowMode::Maximized)
              SDL_RestoreWindow(parent->m_sdlWindow);
            
            parent->m_windowMode = WindowMode::Fullscreen;
            SDL_SetWindowFullscreen(parent->m_sdlWindow, SDL_WINDOW_FULLSCREEN);
          } else {
            Logger::warn("Failed to set resolution {}, {}", (unsigned)requestedDisplayMode.w, (unsigned)requestedDisplayMode.h);
          }
        } else {
          Logger::warn("Unable to set requested display resolution {}, {}", (int)fullScreenResolution[0], (int)fullScreenResolution[1]);
        }

        SDL_DisplayMode actualDisplayMode;
        if (SDL_GetWindowDisplayMode(parent->m_sdlWindow, &actualDisplayMode) == 0) {
          parent->m_windowSize = {(unsigned)actualDisplayMode.w, (unsigned)actualDisplayMode.h};

          // call these manually since no SDL_WindowEvent is triggered when changing between fullscreen resolutions for some reason
          parent->m_renderer->setScreenSize(parent->m_windowSize);
          parent->m_application->windowChanged(parent->m_windowMode, parent->m_windowSize);
        } else {
          Logger::error("Couldn't get window display mode!");
        }
      }
    }

    void setNormalWindow(Vec2U windowSize) override {
      auto window = parent->m_sdlWindow;
      if (parent->m_windowMode != WindowMode::Normal || parent->m_windowSize != windowSize) {
        if (parent->m_windowMode == WindowMode::Fullscreen)
          SDL_SetWindowFullscreen(window, 0);
        else if (parent->m_windowMode == WindowMode::Borderless)
          SDL_SetWindowBordered(window, SDL_TRUE);
        else if (parent->m_windowMode == WindowMode::Maximized)
          SDL_RestoreWindow(window);

        SDL_SetWindowBordered(window, SDL_TRUE);
        SDL_SetWindowSize(window, windowSize[0], windowSize[1]);
        SDL_SetWindowPosition(window, SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED);

        parent->m_windowMode = WindowMode::Normal;
        parent->m_windowSize = windowSize;
      }
    }

    void setMaximizedWindow() override {
      if (parent->m_windowMode != WindowMode::Maximized) {
        if (parent->m_windowMode == WindowMode::Fullscreen)
          SDL_SetWindowFullscreen(parent->m_sdlWindow, 0);
        else if (parent->m_windowMode == WindowMode::Borderless)
          SDL_SetWindowBordered(parent->m_sdlWindow, SDL_TRUE);

        SDL_RestoreWindow(parent->m_sdlWindow);
        SDL_MaximizeWindow(parent->m_sdlWindow);
        parent->m_windowMode = WindowMode::Maximized;
      }
    }

    void setBorderlessWindow() override {
      if (parent->m_windowMode != WindowMode::Borderless) {
        if (parent->m_windowMode == WindowMode::Fullscreen)
          SDL_SetWindowFullscreen(parent->m_sdlWindow, 0);
        else if (parent->m_windowMode == WindowMode::Maximized)
          SDL_RestoreWindow(parent->m_sdlWindow);

        SDL_SetWindowBordered(parent->m_sdlWindow, SDL_FALSE);
        parent->m_windowMode = WindowMode::Borderless;

        SDL_DisplayMode actualDisplayMode;
        if (SDL_GetDesktopDisplayMode(SDL_GetWindowDisplayIndex(parent->m_sdlWindow), &actualDisplayMode) == 0) {
          parent->m_windowSize = {(unsigned)actualDisplayMode.w, (unsigned)actualDisplayMode.h};

          SDL_SetWindowPosition(parent->m_sdlWindow, 0, 0);
          SDL_SetWindowSize(parent->m_sdlWindow, parent->m_windowSize[0], parent->m_windowSize[1]);
          parent->m_renderer->setScreenSize(parent->m_windowSize);
          parent->m_application->windowChanged(parent->m_windowMode, parent->m_windowSize);
        } else {
          Logger::error("Couldn't get desktop display mode!");
        }
      }
    }

    void setVSyncEnabled(bool vSync) override {
      if (parent->m_windowVSync != vSync) {
        parent->setVSyncEnabled(vSync);
        parent->m_windowVSync = vSync;
      }
    }

    void setMaxFrameSkip(unsigned maxFrameSkip) override {
      parent->m_maxFrameSkip = maxFrameSkip;
    }

    void setCursorVisible(bool cursorVisible) override {
      parent->m_cursorVisible = cursorVisible;
    }

    void setCursorPosition(Vec2I cursorPosition) override {
      SDL_WarpMouseInWindow(parent->m_sdlWindow, cursorPosition[0], cursorPosition[1]);
    }

    bool setCursorImage(const String& id, const ImageConstPtr& image, unsigned scale, const Vec2I& offset) override {
      return parent->setCursorImage(id, image, scale, offset);
    }

    void setAcceptingTextInput(bool acceptingTextInput) override {
      if (acceptingTextInput != parent->m_acceptingTextInput) {
        if (acceptingTextInput)
          SDL_StartTextInput();
        else
          SDL_StopTextInput();

        parent->m_acceptingTextInput = acceptingTextInput;
      }
    }

    AudioFormat enableAudio() override {
      parent->m_audioEnabled = true;
      SDL_PauseAudio(false);
      return AudioFormat{44100, 2};
    }

    void disableAudio() override {
      parent->m_audioEnabled = false;
      SDL_PauseAudio(true);
    }

    bool openAudioInputDevice(const char* name, int freq, int channels, void* userdata, AudioCallback callback) override {
      return parent->openAudioInputDevice(name, freq, channels, userdata, callback);
    };

    bool closeAudioInputDevice() override {
      return parent->closeAudioInputDevice();
    };

    float updateRate() const override {
      return parent->m_updateRate;
    }

    float renderFps() const override {
      return parent->m_renderRate;
    }

    StatisticsServicePtr statisticsService() const override {
      if (parent->m_platformServices)
        return parent->m_platformServices->statisticsService();
      return {};
    }

    P2PNetworkingServicePtr p2pNetworkingService() const override {
      if (parent->m_platformServices)
        return parent->m_platformServices->p2pNetworkingService();
      return {};
    }

    UserGeneratedContentServicePtr userGeneratedContentService() const override {
      if (parent->m_platformServices)
        return parent->m_platformServices->userGeneratedContentService();
      return {};
    }

    DesktopServicePtr desktopService() const override {
      if (parent->m_platformServices)
        return parent->m_platformServices->desktopService();
      return {};
    }

    void quit() override {
      parent->m_quitRequested = true;
    }

    SdlPlatform* parent;
  };

  List<InputEvent> processEvents() {
    List<InputEvent> inputEvents;

    SDL_Event event;
    while (SDL_PollEvent(&event)) {
      Maybe<InputEvent> starEvent;
      switch (event.type) {
      case SDL_WINDOWEVENT:
        if (event.window.event == SDL_WINDOWEVENT_MAXIMIZED || event.window.event == SDL_WINDOWEVENT_RESTORED) {
          auto windowFlags = SDL_GetWindowFlags(m_sdlWindow);

          if (windowFlags & SDL_WINDOW_MAXIMIZED)
            m_windowMode = WindowMode::Maximized;
          else if (windowFlags & SDL_WINDOW_FULLSCREEN)
            m_windowMode = WindowMode::Fullscreen;
          else if (windowFlags & SDL_WINDOW_BORDERLESS)
            m_windowMode = WindowMode::Borderless;
          else
            m_windowMode = WindowMode::Normal;

          m_application->windowChanged(m_windowMode, m_windowSize);

        } else if (event.window.event == SDL_WINDOWEVENT_RESIZED || event.window.event == SDL_WINDOWEVENT_SIZE_CHANGED) {
          m_windowSize = Vec2U(event.window.data1, event.window.data2);
          m_renderer->setScreenSize(m_windowSize);
          m_application->windowChanged(m_windowMode, m_windowSize);
        }
        break;
      case SDL_KEYDOWN:
        if (!event.key.repeat) {
          if (auto key = keyFromSdlKeyCode(event.key.keysym.sym))
            starEvent.set(KeyDownEvent{*key, keyModsFromSdlKeyMods(event.key.keysym.mod)});
        }
        break;
      case SDL_KEYUP:
        if (auto key = keyFromSdlKeyCode(event.key.keysym.sym))
          starEvent.set(KeyUpEvent{*key});
        break;
      case SDL_TEXTINPUT:
        starEvent.set(TextInputEvent{String(event.text.text)});
        break;
      case SDL_MOUSEMOTION:
        starEvent.set(MouseMoveEvent{
            {event.motion.xrel, -event.motion.yrel}, {event.motion.x, (int)m_windowSize[1] - event.motion.y}});
        break;
      case SDL_MOUSEBUTTONDOWN:
        starEvent.set(MouseButtonDownEvent{mouseButtonFromSdlMouseButton(event.button.button),
            {event.button.x, (int)m_windowSize[1] - event.button.y}});
        break;
      case SDL_MOUSEBUTTONUP:
        starEvent.set(MouseButtonUpEvent{mouseButtonFromSdlMouseButton(event.button.button),
            {event.button.x, (int)m_windowSize[1] - event.button.y}});
        break;
      case SDL_MOUSEWHEEL:
        int x, y;
        SDL_GetMouseState(&x, &y);
        starEvent.set(MouseWheelEvent{event.wheel.y < 0 ? MouseWheel::Down : MouseWheel::Up, {x, (int)m_windowSize[1] - y}});
        break;
      case SDL_CONTROLLERAXISMOTION:
        starEvent.set(ControllerAxisEvent{
          (ControllerId)event.caxis.which,
          controllerAxisFromSdlControllerAxis(event.caxis.axis),
          (float)event.caxis.value / 32768.0f
        });
        break;
      case SDL_CONTROLLERBUTTONDOWN:
        starEvent.set(ControllerButtonDownEvent{ (ControllerId)event.cbutton.which, controllerButtonFromSdlControllerButton(event.cbutton.button) });
        break;
      case SDL_CONTROLLERBUTTONUP:
        starEvent.set(ControllerButtonUpEvent{ (ControllerId)event.cbutton.which, controllerButtonFromSdlControllerButton(event.cbutton.button) });
        break;
      case SDL_CONTROLLERDEVICEADDED:
        {
          auto insertion = m_SdlControllers.insert_or_assign(event.cdevice.which, SDLGameControllerUPtr(SDL_GameControllerOpen(event.cdevice.which), SDL_GameControllerClose));
          if (SDL_GameController* controller = insertion.first->second.get())
            Logger::info("Controller device '{}' added", SDL_GameControllerName(controller));
        }
        break;
      case SDL_CONTROLLERDEVICEREMOVED:
        {
          auto find = m_SdlControllers.find(event.cdevice.which);
          if (find != m_SdlControllers.end()) {
            if (SDL_GameController* controller = find->second.get())
              Logger::info("Controller device '{}' removed", SDL_GameControllerName(controller));
            m_SdlControllers.erase(event.cdevice.which);
          }
        }
        break;
      case SDL_QUIT:
        m_quitRequested = true;
        starEvent.reset();
        break;
      }

      if (starEvent)
        inputEvents.append(starEvent.take());
    }

    return inputEvents;
  }

  void getAudioData(Uint8* stream, int len) {
    if (m_audioEnabled) {
      m_application->getAudioData((int16_t*)stream, len / 4);
    } else {
      for (int i = 0; i < len; ++i)
        stream[i] = 0;
    }
  }

  void setVSyncEnabled(bool vsyncEnabled) {
    if (vsyncEnabled) {
      // If VSync is requested, try for late swap tearing first, then fall back
      // to regular VSync
      Logger::info("Application: Enabling VSync with late swap tearing");
      if (SDL_GL_SetSwapInterval(-1) < 0) {
        Logger::info("Application: Enabling VSync late swap tearing failed, falling back to full VSync");
        SDL_GL_SetSwapInterval(1);
      }
    } else {
      Logger::info("Application: Disabling VSync");
      SDL_GL_SetSwapInterval(0);
    }
  }

  static const size_t MaximumCursorDimensions = 128;
  static const size_t MaximumCursorPixelCount = MaximumCursorDimensions * MaximumCursorDimensions;
  bool setCursorImage(const String& id, const ImageConstPtr& image, unsigned scale, const Vec2I& offset) {
    auto imageSize = image->size().piecewiseMultiply(Vec2U::filled(scale));
    if (!scale || imageSize.max() > MaximumCursorDimensions || (size_t)(imageSize[0] * imageSize[1]) > MaximumCursorPixelCount)
      return m_cursorVisible = false;

    auto& entry = m_cursorCache.get(m_currentCursor = { scale, offset, id }, [&](auto const&) {
      auto entry = std::make_shared<CursorEntry>();
      List<ImageOperation> operations;
      if (scale != 1)
        operations = {
          FlipImageOperation{ FlipImageOperation::Mode::FlipY }, // SDL wants an Australian cursor.
          BorderImageOperation{ 1, Vec4B(), Vec4B(), false, false }, // Nearest scaling fucks up and clips half off the edges, work around this with border+crop for now.
          ScaleImageOperation{ ScaleImageOperation::Mode::Nearest, Vec2F::filled(scale) },
          CropImageOperation{ RectI::withSize(Vec2I::filled(ceilf((float)scale / 2)), Vec2I(imageSize)) }
        };
      else
        operations = { FlipImageOperation{ FlipImageOperation::Mode::FlipY } };

      auto newImage = std::make_shared<Image>(processImageOperations(operations, *image));
      // Fix fully transparent pixels inverting the underlying display pixel on Windows (allowing this could be made configurable per cursor later!)
      newImage->forEachPixel([](unsigned /*x*/, unsigned /*y*/, Vec4B& pixel) { if (!pixel[3]) pixel[0] = pixel[1] = pixel[2] = 0; });
      entry->image = std::move(newImage);
      

      auto size = entry->image->size();
      uint32_t pixelFormat;
      switch (entry->image->pixelFormat()) {
        case PixelFormat::RGB24: // I know this conversion looks wrong, but it's correct. I'm confused too.
          pixelFormat = SDL_PIXELFORMAT_BGR888;
          break;
        case PixelFormat::RGBA32:
          pixelFormat = SDL_PIXELFORMAT_ABGR8888;
          break;
        case PixelFormat::BGR24:
          pixelFormat = SDL_PIXELFORMAT_RGB888;
          break;
        case PixelFormat::BGRA32:
          pixelFormat = SDL_PIXELFORMAT_ARGB8888;
          break;
        default:
          pixelFormat = SDL_PIXELFORMAT_UNKNOWN;
      }

      entry->sdlSurface.reset(SDL_CreateRGBSurfaceWithFormatFrom(
        (void*)entry->image->data(),
        size[0], size[1],
        entry->image->bitsPerPixel(),
        entry->image->bytesPerPixel() * size[0],
        pixelFormat)
      );
      entry->sdlCursor.reset(SDL_CreateColorCursor(entry->sdlSurface.get(), offset[0] * scale, offset[1] * scale));

      return entry;
    });

    SDL_SetCursor(entry->sdlCursor.get());
    return m_cursorVisible = true;
  }

  SignalHandler m_signalHandler;

  TickRateApproacher m_updateTicker = TickRateApproacher(60.0f, 1.0f);
  float m_updateRate = 0.0f;
  TickRateMonitor m_renderTicker = TickRateMonitor(1.0f);
  float m_renderRate = 0.0f;

  SDL_Window* m_sdlWindow = nullptr;
  SDL_GLContext m_sdlGlContext = nullptr;
  SDL_AudioDeviceID m_sdlAudioOutputDevice = 0;
  SDL_AudioDeviceID m_sdlAudioInputDevice = 0;

  typedef std::unique_ptr<SDL_GameController, decltype(&SDL_GameControllerClose)> SDLGameControllerUPtr;
  StableHashMap<int, SDLGameControllerUPtr> m_SdlControllers;

  typedef std::unique_ptr<SDL_Surface, decltype(&SDL_FreeSurface)> SDLSurfaceUPtr;
  typedef std::unique_ptr<SDL_Cursor, decltype(&SDL_FreeCursor)> SDLCursorUPtr;
  struct CursorEntry {
    ImageConstPtr image = nullptr;
    SDLSurfaceUPtr sdlSurface;
    SDLCursorUPtr sdlCursor;

    CursorEntry() : image(nullptr), sdlSurface(nullptr, SDL_FreeSurface), sdlCursor(nullptr, SDL_FreeCursor) {};
  };

  typedef tuple<unsigned, Vec2I, String> CursorDescriptor;

  HashTtlCache<CursorDescriptor, std::shared_ptr<CursorEntry>> m_cursorCache;
  CursorDescriptor m_currentCursor;

  Vec2U m_windowSize = {800, 600};
  WindowMode m_windowMode = WindowMode::Normal;

  String m_windowTitle = "Starbound";
  bool m_windowVSync = true;
  unsigned m_maxFrameSkip = 5;
  bool m_cursorVisible = true;
  bool m_acceptingTextInput = false;
  bool m_audioEnabled = false;
  bool m_quitRequested = false;

  OpenGl20RendererPtr m_renderer;
  ApplicationUPtr m_application;
  PcPlatformServicesUPtr m_platformServices;
};

int runMainApplication(ApplicationUPtr application, StringList cmdLineArgs) {
  try {
    {
      SdlPlatform platform(std::move(application), std::move(cmdLineArgs));
      platform.run();
    }
    Logger::info("Application: stopped gracefully");
    return 0;
  } catch (std::exception const& e) {
    fatalException(e, true);
  } catch (...) {
    fatalError("Unknown Exception", true);
  }
  return 1;
}

}