#include <osi/run.hpp>
#include <osi/simulator.hpp>
#include <utils/assumptions.hpp>
#include <utils/config.hpp>
#include <SDL2/SDL.h>
#include <glad/glad.h>
#include <imgui.h>
#if PLATFORM() == PLATFORM_WINDOWS()
#   include <imgui_impl_sdl2.h> //changed from <imgui_impl_sdl.h>
#else
#   include <imgui_impl_sdl2.h>
#endif
#include <imgui_impl_opengl3.h>
#include <chrono>
#include <algorithm>
#include <memory>
#include <stdexcept>

namespace osi {

static std::unique_ptr<Simulator> simulator_ptr = nullptr;
static SDL_Window* window_ptr = nullptr;
static SDL_GLContext gl_context_ptr = nullptr;
static std::filesystem::path data_root_dir;

static std::string to_key_name(SDL_Keycode const code)
{
    std::string name { SDL_GetKeyName(code) };
    name.erase(std::remove_if(name.begin(), name.end(), isspace), name.end());
    return name;
}

static std::string to_button_name(std::uint8_t const button_index)
{
    switch (button_index)
    {
        case SDL_BUTTON_LEFT: return "MouseLeft";
        case SDL_BUTTON_RIGHT: return "MouseRight";
        case SDL_BUTTON_MIDDLE: return "MouseMiddle";
        case SDL_BUTTON_X1: return "MouseWheelUp";
        case SDL_BUTTON_X2: return "MouseWheelDown";
        default: return "MouseUNKNOWN";
    }
}

static void finish();

static void start(Config const& cfg)
{
    if (cfg.min_dt > cfg.max_dt)
        throw std::runtime_error("Error: In Simulator::Config: min_dt > max_dt.");
    if ((float)cfg.cpu_yield_ms >= 1000.0f * cfg.min_dt)
        throw std::runtime_error("Error: In Simulator::Config: cpu_yield_ms >= 1000.0f * min_dt.");

    if (SDL_Init(SDL_INIT_VIDEO))
        throw std::runtime_error("Error: Initialisation of the SDL2 library has FAILED.");

    SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 4);
    SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 5);
    SDL_GL_SetAttribute(SDL_GL_ACCELERATED_VISUAL, 1);
    SDL_GL_SetAttribute(SDL_GL_RED_SIZE, 8);
    SDL_GL_SetAttribute(SDL_GL_GREEN_SIZE, 8);
    SDL_GL_SetAttribute(SDL_GL_BLUE_SIZE, 8);
    SDL_GL_SetAttribute(SDL_GL_ALPHA_SIZE, 8);
    SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);
    SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 24);
    SDL_GL_SetAttribute(SDL_GL_MULTISAMPLEBUFFERS, 1);
    SDL_GL_SetAttribute(SDL_GL_MULTISAMPLESAMPLES, 8);

    window_ptr = SDL_CreateWindow(
        cfg.window_title.c_str(),
        SDL_WINDOWPOS_CENTERED,
        SDL_WINDOWPOS_CENTERED,
        (int)cfg.window_size.x,
        (int)cfg.window_size.y,
        SDL_WINDOW_OPENGL
            | SDL_WINDOW_SHOWN
            | SDL_WINDOW_MAXIMIZED
            | (cfg.windowed ? (cfg.resizable ? SDL_WINDOW_RESIZABLE : SDL_WINDOW_BORDERLESS) :
                               SDL_WINDOW_FULLSCREEN_DESKTOP)
        );
    if (window_ptr == nullptr)
    {
        finish();
        throw std::runtime_error("Error: The call 'SDL_CreateWindow' function has FAILED.");
    }

    gl_context_ptr = SDL_GL_CreateContext(window_ptr);
    if (gl_context_ptr == nullptr)
    {
        finish();
        throw std::runtime_error("Error: The call 'SDL_GL_CreateContext' function has FAILED.");
    }

    if (!gladLoadGLLoader((GLADloadproc)SDL_GL_GetProcAddress))
        throw std::runtime_error("Error: The call to 'gladLoadGLLoader' has FAILED.");

    // char const* dbg_version_string=(char const*)glGetString(GL_VERSION);

    SDL_GL_SetSwapInterval(cfg.v_sync ? 1 : 0);

    // TODO: Load window icon from 'cfg.data_root_dir / cfg.font_relative_path', if '!cfg.font_relative_path.empty()'.
    // if (!cfg.data_root_dir.empty())
    // {
    //     // SDL_image needs to be linked
    //     SDL_Surface* icon = IMG_Load(cfg.data_root_dir / "rofibotstudio_icon.png");
    //     SDL_SetWindowIcon(window_ptr, icon);
    // }

    
    ImGui::CreateContext();
    ImGui::StyleColorsDark();
    ImGui_ImplSDL2_InitForOpenGL(window_ptr, gl_context_ptr);
    ImGui_ImplOpenGL3_Init();
    if (!cfg.font_relative_path.empty())
        ImGui::GetIO().Fonts->AddFontFromFileTTF((cfg.data_root_dir / cfg.font_relative_path).string().c_str(), cfg.font_pixel_size);
}

static void finish()
{
    ImGui_ImplOpenGL3_Shutdown();
	ImGui_ImplSDL2_Shutdown();
	ImGui::DestroyContext();

    simulator_ptr = nullptr;
    data_root_dir.clear();

    if (gl_context_ptr != nullptr)
    {
        SDL_GL_DeleteContext(gl_context_ptr);
        gl_context_ptr = nullptr;
    }

    SDL_DestroyWindow(window_ptr);
    window_ptr = nullptr;

    SDL_Quit();    
}

void run(Config const& cfg)
{
    if (simulator_ptr != nullptr)
        throw std::logic_error("Error: Function 'osi::run' was called recursively.");

    data_root_dir = std::filesystem::absolute(cfg.data_root_dir);
    if (!std::filesystem::is_directory(data_root_dir))
        throw std::logic_error("Error: The data root directory is not valid: " + data_root_dir.string());

    start(cfg);

    try
    {
        simulator_ptr = cfg.constructor();
    }
    catch (...)
    {
        finish();
        throw;
    }

    if (simulator_ptr == nullptr)
    {
        finish();
        throw std::logic_error("Error: Construction of the simulator has FAILED.");
    }

    Window* const window = &simulator_ptr->m_window;
    Keyboard* const keyboard = &simulator_ptr->m_keyboard;
    Mouse* const mouse = &simulator_ptr->m_mouse;
    Timer* const timer = &simulator_ptr->m_timer;
    Termination* const termination = &simulator_ptr->m_termination;

    window->m_size = cfg.window_size;

    std::chrono::time_point<std::chrono::steady_clock> last_round_start_time = std::chrono::steady_clock::now();
    std::uint8_t num_warm_up_iterations = cfg.num_warm_up_iterations;

    while (true)
    {
        SDL_Event event;
        while (SDL_PollEvent(&event))
        {
            ImGui_ImplSDL2_ProcessEvent(&event);

            switch (event.type)
            {
            case SDL_QUIT:
                termination->set_quit_flag();
                break;
            case SDL_WINDOWEVENT:
                switch (event.window.event)
                {
                    case SDL_WINDOWEVENT_RESIZED:
                    case SDL_WINDOWEVENT_SIZE_CHANGED:
                        {
                            window->m_size.x = event.window.data1; 
                            window->m_size.y = event.window.data2;
                            window->m_is_resized = true; 
                        }
                        break;
                    case SDL_WINDOWEVENT_MINIMIZED:
                        window->m_is_minimized = true; 
                        break;
                    case SDL_WINDOWEVENT_MAXIMIZED:
                    case SDL_WINDOWEVENT_RESTORED:
                        window->m_is_minimized = false; 
                        break;
                    case SDL_WINDOWEVENT_ENTER:
                        window->m_is_mouse_in_window = true; 
                        break;
                    case SDL_WINDOWEVENT_LEAVE:
                        window->m_is_mouse_in_window = false; 
                        break;
                    case SDL_WINDOWEVENT_FOCUS_GAINED:
                        window->m_has_keyboard_focus = true; 
                        break;
                    case SDL_WINDOWEVENT_FOCUS_LOST:
                        window->m_has_keyboard_focus = false; 
                        break;
                    default: break;
                }
                break;
            case SDL_KEYDOWN:
                if (!ImGui::GetIO().WantCaptureKeyboard)
                {
                    std::string const name = to_key_name(event.key.keysym.sym);
                    keyboard->m_just_pressed.insert(name);
                    keyboard->m_down.insert(name);
                }
                break;
            case SDL_KEYUP:
                if (!ImGui::GetIO().WantCaptureKeyboard)
                {
                    std::string const name = to_key_name(event.key.keysym.sym);
                    keyboard->m_just_released.insert(name);
                    keyboard->m_down.erase(name);
                }
                break;
            case SDL_TEXTINPUT:
                if (!ImGui::GetIO().WantCaptureKeyboard)
                    keyboard->m_text += event.text.text;
                break;
            case SDL_MOUSEMOTION:
                if (!ImGui::GetIO().WantCaptureMouse)
                {
                    mouse->m_pos.x = event.motion.x;
                    mouse->m_pos.y = event.motion.y;
                    mouse->m_pos_delta.x = event.motion.xrel;
                    mouse->m_pos_delta.y = event.motion.yrel;
                }
                break;
            case SDL_MOUSEBUTTONDOWN:
                if (!ImGui::GetIO().WantCaptureMouse)
                {
                    std::string const name = to_button_name(event.button.button);
                    mouse->m_just_pressed.insert(name);
                    mouse->m_down.insert(name);
                }
                break;
            case SDL_MOUSEBUTTONUP:
                if (!ImGui::GetIO().WantCaptureMouse)
                {
                    std::string const name = to_button_name(event.button.button);
                    mouse->m_just_released.insert(name);
                    mouse->m_down.erase(name);
                }
                break;
            case SDL_MOUSEWHEEL:
                if (!ImGui::GetIO().WantCaptureMouse)
                {
                    // TODO!
                    if(event.wheel.y > 0) // scroll up
                    {
                        std::string const name = to_button_name(SDL_BUTTON_X1);
                        mouse->m_just_pressed.insert(name);
                    }
                    else if(event.wheel.y < 0) // scroll down
                    {
                        std::string const name = to_button_name(SDL_BUTTON_X2);
                        mouse->m_just_pressed.insert(name);
                    }
                }
                break;
            default: break;
            }
        }
        if (termination->quit_flag())
            break;
        
        if (num_warm_up_iterations != 0U)
            --num_warm_up_iterations;
        else
        {
            std::chrono::time_point<std::chrono::steady_clock> const current_time = std::chrono::steady_clock::now();
            std::chrono::duration<double> const duration = current_time - last_round_start_time;

            last_round_start_time = current_time;
            timer->m_time_step = std::min(timer->m_time_step + static_cast<float>(duration.count()), cfg.max_dt);

            if (timer->m_time_step >= cfg.min_dt)
            {
                ImGui_ImplOpenGL3_NewFrame();
		        ImGui_ImplSDL2_NewFrame();
		        ImGui::NewFrame();

                try
                {
                    simulator_ptr->__update();
                }
                catch (...)
                {
                    finish();
                    throw;
                }

                keyboard->m_just_pressed.clear();
                keyboard->m_just_released.clear();
                keyboard->m_text.clear();

                mouse->m_just_pressed.clear();
                mouse->m_just_released.clear();
                mouse->m_pos_delta.x = 0;
                mouse->m_pos_delta.y = 0;

                window->m_is_resized = false;

                ++timer->m_passed_rounds;
                timer->m_passed_seconds += (double)timer->m_time_step;
                timer->m_time_step = 0.0f; 

                ImGui::Render();
                ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());

                SDL_GL_SwapWindow(window_ptr);
            }
        }

        SDL_Delay(cfg.cpu_yield_ms);
    }    

    finish();
}

}
