#include "gui.h"
#include <fmt/core.h>
#include <imgui.h>
#include <platform/windows/utils/platform_tools.h>
#include "config.h"
#include <backends/imgui_impl_opengl3.h>
#include <backends/imgui_impl_sdl.h>
#include "platform/windows/input/key.h"
#include "platform/windows/gui/icons.h"
#include "system.h"
#include "state/state.h"
#include "utils/file.h"
#include "utils/string.h"
#include "images.h"
#include "disc/load.h"
#include "memory_card/card_formats.h"

float GUI::scale = 1.f;

GUI::GUI(SDL_Window* window, void* glContext) : window(window) {
    ImGui::CreateContext();
    ImGui::StyleColorsDark();
    ImGuiIO& io = ImGui::GetIO();
    iniPath = avocado::PATH_USER + "imgui.ini";
    io.IniFilename = iniPath.c_str();
    //    io.ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad;
#ifdef ANDROID
    io.ConfigFlags |= ImGuiConfigFlags_IsTouchScreen;
#endif

    scale = 1.f;
#ifdef ANDROID
    float dpi = 1.f;
    if (SDL_GetDisplayDPI(0, &dpi, nullptr, nullptr) == 0) {
        scale = dpi / 160.f;
    }
#endif
    float fontSize = 16.f * scale;

    ImGuiStyle& style = ImGui::GetStyle();
    style.GrabRounding = 6.f;
    style.FrameRounding = 6.f;
    style.ScaleAllSizes(scale);
#ifdef ANDROID
    style.TouchExtraPadding = ImVec2(10.f, 10.f);
#endif

    ImFontConfig config;
    config.OversampleH = 2;
    config.OversampleV = 2;
    config.FontDataOwnedByAtlas = false;
    config.SizePixels = fontSize;

    {
        auto font = getFileContents(avocado::assetsPath("roboto-mono.ttf"));
        io.Fonts->AddFontFromMemoryTTF(font.data(), font.size(), fontSize, &config);
    }

    {
        ImFontConfig config;
        config.SizePixels = 13.f * scale;
        io.Fonts->AddFontDefault(&config);
    }

    {
        auto font = getFileContents(avocado::assetsPath("fa-solid-900.ttf"));
        const char* glyphs[] = {
            ICON_FA_PLAY, ICON_FA_PAUSE, ICON_FA_SAVE, ICON_FA_COMPACT_DISC, ICON_FA_GAMEPAD, ICON_FA_COMPRESS, ICON_FA_COG, ICON_FA_SYNC,
        };

        ImVector<ImWchar> ranges;
        ImFontGlyphRangesBuilder builder;
        for (auto glyph : glyphs) {
            builder.AddText(glyph);
        }
        builder.BuildRanges(&ranges);
        io.Fonts->AddFontFromMemoryTTF(font.data(), font.size(), fontSize, &config, ranges.Data);
        io.Fonts->Build();
    }

    ImGui_ImplSDL2_InitForOpenGL(window, glContext);
    ImGui_ImplOpenGL3_Init();

    busToken = bus.listen<Event::Gui::Debug::OpenDrawListWindows>([&](auto) {
        gpuDebug.logWindowOpen = true;
        gpuDebug.vramWindowOpen = true;
    });
}

GUI::~GUI() {
    bus.unlistenAll(busToken);
    ImGui::DestroyContext();
}

void GUI::processEvent(SDL_Event* e) { ImGui_ImplSDL2_ProcessEvent(e); }

void GUI::mainMenu(std::unique_ptr<System>& sys) {
    if (!ImGui::BeginMainMenuBar()) {
        return;
    }
    if (ImGui::BeginMenu("File")) {
        ImGui::MenuItem("Open", nullptr, &openFile.openWindowOpen);
#if defined(__APPLE__) || defined(_WIN32) || defined(__linux__)
        ImGui::Separator();
        if (ImGui::MenuItem("Open Avocado directory")) {
            openFileBrowser(avocado::PATH_USER);
        }
#endif
        ImGui::Separator();
        if (ImGui::MenuItem("Exit", "Esc")) bus.notify(Event::File::Exit{});
        ImGui::EndMenu();
    }
    if (ImGui::BeginMenu("Emulation")) {
        if (ImGui::MenuItem("Pause/Resume", Key(config.hotkeys["toggle_pause"]).getButton().c_str())) {
            if (sys->state == System::State::pause) {
                sys->state = System::State::run;
            } else if (sys->state == System::State::run) {
                sys->state = System::State::pause;
            }
        }
        if (ImGui::MenuItem("Soft reset", Key(config.hotkeys["reset"]).getButton().c_str())) {
            bus.notify(Event::System::SoftReset{});
        }

        std::string key = "Shift+";
        key += Key(config.hotkeys["reset"]).getButton();
        if (ImGui::MenuItem("Hard reset", key.c_str())) {
            bus.notify(Event::System::HardReset{});
        }

        const char* shellStatus = sys->cdrom->getShell() ? "Close disk tray" : "Open disk tray";
        if (ImGui::MenuItem(shellStatus, Key(config.hotkeys["close_tray"]).getButton().c_str())) {
            sys->cdrom->setShell(!sys->cdrom->getShell());
        }

        if (ImGui::MenuItem("Single frame", Key(config.hotkeys["single_frame"]).getButton().c_str())) {
            singleFrame = true;
            sys->state = System::State::run;
        }

        ImGui::Separator();

        if (ImGui::MenuItem("Quick save", Key(config.hotkeys["quick_save"]).getButton().c_str())) {
            bus.notify(Event::System::SaveState{});
        }

        bool quickLoadStateExists = fs::exists(state::getStatePath(sys.get()));
        if (ImGui::MenuItem("Quick load", Key(config.hotkeys["quick_load"]).getButton().c_str(), nullptr, quickLoadStateExists))
            bus.notify(Event::System::LoadState{});

        if (ImGui::BeginMenu("Save")) {
            for (int i = 1; i <= 5; i++) {
                if (ImGui::MenuItem(fmt::format("Slot {}##save", i).c_str())) bus.notify(Event::System::SaveState{i});
            }
            ImGui::EndMenu();
        }

        if (ImGui::BeginMenu("Load")) {
            bool anySaveExists = false;
            for (int i = 1; i <= 5; i++) {
                auto path = state::getStatePath(sys.get(), i);
                if (fs::exists(path)) {
                    anySaveExists = true;
                    if (ImGui::MenuItem(fmt::format("Slot {}##load", i).c_str())) bus.notify(Event::System::LoadState{i});
                }
            }

            if (!anySaveExists) {
                ImGui::TextUnformatted("No save states");
            }
            ImGui::EndMenu();
        }

        ImGui::EndMenu();
    }
    if (ImGui::BeginMenu("Debug")) {
        if (ImGui::MenuItem("System log", nullptr, &sys->debugOutput)) {
            config.debug.log.system = (int)sys->debugOutput;
        }
        if (ImGui::MenuItem("Syscall log", nullptr, (bool*)&sys->biosLog)) {
            config.debug.log.bios = sys->biosLog;
        }
#ifdef ENABLE_IO_LOG
        ImGui::MenuItem("IO log", nullptr, &ioDebug.logWindowOpen);
#endif
        ImGui::MenuItem("GTE log", nullptr, &gteDebug.logWindowOpen);
        ImGui::MenuItem("GPU log", nullptr, &gpuDebug.logWindowOpen);

        ImGui::Separator();

        ImGui::MenuItem("Debugger", nullptr, &cpuDebug.debuggerWindowOpen);
        ImGui::MenuItem("Breakpoints", nullptr, &cpuDebug.breakpointsWindowOpen);
        ImGui::MenuItem("Watch", nullptr, &cpuDebug.watchWindowOpen);
        ImGui::MenuItem("RAM", nullptr, &cpuDebug.ramWindowOpen);

        ImGui::Separator();

        ImGui::MenuItem("CDROM", nullptr, &cdromDebug.cdromWindowOpen);
        ImGui::MenuItem("Timers", nullptr, &timersDebug.timersWindowOpen);
        ImGui::MenuItem("GPU", nullptr, &gpuDebug.registersWindowOpen);
        ImGui::MenuItem("GTE", nullptr, &gteDebug.registersWindowOpen);
        ImGui::MenuItem("SPU", nullptr, &spuDebug.spuWindowOpen);
        ImGui::MenuItem("VRAM", nullptr, &gpuDebug.vramWindowOpen);
        ImGui::MenuItem("Kernel", nullptr, &showKernelWindow);

        ImGui::Separator();
        if (ImGui::MenuItem("Dump state")) {
            sys->dumpRam();
            sys->spu->dumpRam();
            sys->gpu->dumpVram();
            toast("State dumped");
        }
        ImGui::EndMenu();
    }
    if (ImGui::BeginMenu("Options")) {
        if (ImGui::MenuItem("Graphics", nullptr)) showGraphicsOptionsWindow = true;
        ImGui::MenuItem("BIOS", nullptr, &biosOptions.biosWindowOpen);
        if (ImGui::MenuItem("Controller", nullptr)) showControllerSetupWindow = true;
        if (ImGui::MenuItem("Hotkeys", nullptr)) showHotkeysSetupWindow = true;
        ImGui::MenuItem("Memory Card", nullptr, &memoryCardOptions.memoryCardWindowOpen);

        ImGui::MenuItem("System", nullptr, &systemOptions.systemWindowOpen);

        bool soundEnabled = config.options.sound.enabled;
        if (ImGui::MenuItem("Sound", nullptr, &soundEnabled)) {
            config.options.sound.enabled = soundEnabled;
            bus.notify(Event::Config::Spu{});
        }

        ImGui::Separator();

        bool preserveState = config.options.emulator.preserveState;
        if (ImGui::MenuItem("Save state on close", nullptr, &preserveState)) {
            config.options.emulator.preserveState = preserveState;
        }

        bool timeTravel = config.options.emulator.timeTravel;
        if (ImGui::MenuItem("Time travel", Key(config.hotkeys["rewind_state"]).getButton().c_str(), &timeTravel)) {
            config.options.emulator.timeTravel = timeTravel;
        }

        ImGui::EndMenu();
    }
    if (ImGui::BeginMenu("Help")) {
        ImGui::MenuItem("About", nullptr, &aboutHelp.aboutWindowOpen);
        ImGui::EndMenu();
    }

    std::string info;
    if (statusMouseLocked) {
        info += " | Press Alt to unlock mouse";
    }
    if (sys->state == System::State::pause) {
        info += " | Paused";
    } else {
        info += fmt::format(" | {:.0f} FPS", statusFps);
        if (!statusFramelimitter) {
            info += " (Unlimited)";
        }
    }
    auto size = ImGui::CalcTextSize(info.c_str());
    ImGui::SameLine(ImGui::GetWindowWidth() - size.x - ImGui::GetStyle().FramePadding.x * 4);
    ImGui::TextUnformatted(info.c_str());

    if (ImGui::IsItemHovered()) {
        ImGui::BeginTooltip();
        ImGui::TextUnformatted(fmt::format("Frame time: {:.2f} ms\nTab to disable frame limiting", (1000.0 / statusFps)).c_str());
        ImGui::EndTooltip();
    }
    ImGui::EndMainMenuBar();
}

void GUI::render(std::unique_ptr<System>& sys) {
    ImGui_ImplOpenGL3_NewFrame();
    ImGui_ImplSDL2_NewFrame(window);
    ImGui::NewFrame();

    if (showGui) {
        if (showMenu) mainMenu(sys);

        // File
        openFile.displayWindows();

        // Debug
        if (showKernelWindow) kernelWindow(sys.get());

        cdromDebug.displayWindows(sys.get());
        cpuDebug.displayWindows(sys.get());
        gpuDebug.displayWindows(sys.get());
        timersDebug.displayWindows(sys.get());
        gteDebug.displayWindows(sys.get());
        spuDebug.displayWindows(sys.get());
        ioDebug.displayWindows(sys.get());

        // Options
        if (showGraphicsOptionsWindow) graphicsOptionsWindow();
        if (showControllerSetupWindow) controllerSetupWindow();
        if (showHotkeysSetupWindow) hotkeysSetupWindow();

        biosOptions.displayWindows();
        memoryCardOptions.displayWindows(sys.get());
        systemOptions.displayWindows();

        // Help
        aboutHelp.displayWindows();
    }

    toasts.display();

    if (!config.isEmulatorConfigured() && !notInitializedWindowShown) {
        notInitializedWindowShown = true;
        ImGui::OpenPopup("Avocado");
    } else if (droppedItem) {
        if (droppedItemDialog == DroppedItemDialog::None) {
            if (memory_card::isMemoryCardImage(*droppedItem)) {
                droppedItemDialog = DroppedItemDialog::MemoryCard;
                ImGui::OpenPopup("Memory card##select_file");
            } else if (disc::isDiscImage(*droppedItem)) {
                droppedItemDialog = DroppedItemDialog::Disc;
                ImGui::OpenPopup("Disc##select_file");
            }
        }
    } else {
        drawControls(sys);
    }

    if (ImGui::BeginPopupModal("Avocado", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) {
        ImGui::Text("Avocado needs to be set up before running.");
        ImGui::Text("You need one of BIOS files placed in data/bios directory.");

        if (ImGui::Button("Select BIOS file")) {
            notInitializedWindowShown = true;
            biosOptions.biosWindowOpen = true;
            ImGui::CloseCurrentPopup();
        }

        ImGui::EndPopup();
    }

    memoryCardDialog();
    discDialog();

    // Work in progress
    //    renderController();

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

void GUI::memoryCardDialog() {
    if (!ImGui::BeginPopupModal("Memory card##select_file", nullptr, ImGuiWindowFlags_NoMove | ImGuiWindowFlags_AlwaysAutoResize)) {
        return;
    }

    ImGui::Text("Select card slot for %s", getFilenameExt(*droppedItem).c_str());
    for (int slot = 0; slot < 2; slot++) {
        if (ImGui::Button(fmt::format("Slot {}", slot + 1).c_str())) {
            config.memoryCard[slot].path = *droppedItem;
            bus.notify(Event::Controller::MemoryCardSwapped{slot});
            ImGui::CloseCurrentPopup();
        }
        ImGui::SameLine();
    }

    if (ImGui::Button("Cancel")) {
        ImGui::CloseCurrentPopup();
    }
    ImGui::EndPopup();

    if (!ImGui::IsPopupOpen("Memory card##select_file")) {
        droppedItem = {};
        droppedItemDialog = DroppedItemDialog::None;
    }
}
void GUI::discDialog() {
    if (!ImGui::BeginPopupModal("Disc##select_file", nullptr, ImGuiWindowFlags_NoMove | ImGuiWindowFlags_AlwaysAutoResize)) {
        return;
    }

    ImGui::Text("%s", getFilenameExt(*droppedItem).c_str());
    if (ImGui::Button("Boot")) {
        bus.notify(Event::File::Load{*droppedItem, Event::File::Load::Action::slowboot});
        ImGui::CloseCurrentPopup();
    }
    if (ImGui::IsItemHovered()) ImGui::SetTooltip("Restart console and boot the CD");

    ImGui::SameLine();
    if (ImGui::Button("Fast boot")) {
        bus.notify(Event::File::Load{*droppedItem, Event::File::Load::Action::fastboot});
        ImGui::CloseCurrentPopup();
    }
    if (ImGui::IsItemHovered()) ImGui::SetTooltip("Restart console and boot the CD (skipping BIOS intro)");

    ImGui::SameLine();
    if (ImGui::Button("Swap disc")) {
        bus.notify(Event::File::Load{*droppedItem, Event::File::Load::Action::swap});
        ImGui::CloseCurrentPopup();
    }
    if (ImGui::IsItemHovered()) ImGui::SetTooltip("Swap currently inserted disc");
    ImGui::SameLine();

    if (ImGui::Button("Cancel")) {
        ImGui::CloseCurrentPopup();
    }
    ImGui::EndPopup();

    if (!ImGui::IsPopupOpen("Disc##select_file")) {
        droppedItem = {};
        droppedItemDialog = DroppedItemDialog::None;
    }
}

void GUI::drawControls(std::unique_ptr<System>& sys) {
    auto symbolButton = [](const char* hint, const char* symbol, ImVec4 bg = ImVec4(1, 1, 1, 0.08f)) -> bool {
        auto padding = ImGui::GetStyle().FramePadding;
        padding.x *= 3.f;
        padding.y *= 2.5f;
        ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, padding);
        ImGui::PushFont(ImGui::GetIO().Fonts->Fonts[2]);
        ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(bg.x, bg.y, bg.z, bg.w));
        ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImVec4(bg.x, bg.y, bg.z, 0.5));
        ImGui::PushStyleColor(ImGuiCol_ButtonActive, ImVec4(bg.x, bg.y, bg.z, 0.75));

        bool clicked = ImGui::Button(symbol);

        ImGui::PopStyleColor(3);
        ImGui::PopFont();
        ImGui::PopStyleVar();

        if (ImGui::IsItemHovered()) {
            ImGui::SetTooltip("%s", hint);
        }

        return clicked;
    };
    auto io = ImGui::GetIO();

    static auto lastTime = std::chrono::steady_clock::now();
    // Reset timer if mouse was moved
    if (io.MouseDelta.x != 0 || io.MouseDelta.y != 0) {
        lastTime = std::chrono::steady_clock::now();
    }

    auto now = std::chrono::steady_clock::now();
    float timeDiff = std::chrono::duration_cast<std::chrono::milliseconds>(now - lastTime).count() / 1000.f;

    const float visibleFor = 1.5f;       // Seconds
    const float fadeoutDuration = 0.5f;  // Seconds

    // Display for 1.5 seconds, then fade out 0.5 second, then hide
    float alpha;
    if (timeDiff < visibleFor) {
        alpha = 1.f;
    } else if (timeDiff < visibleFor + fadeoutDuration) {
        alpha = lerp(1.f, 0.f, (timeDiff - visibleFor) / fadeoutDuration);
    } else {
        return;
    }

    // TODO: Handle gamepad "home" button

    auto displaySize = io.DisplaySize;
    auto pos = ImVec2(displaySize.x / 2.f, displaySize.y * 0.9f);
    ImGui::SetNextWindowPos(pos, 0, ImVec2(.5f, .5f));

    ImGui::PushStyleVar(ImGuiStyleVar_Alpha, alpha);
    ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.f);
    ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 8.f);
    ImGui::Begin("##controls", nullptr, ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_AlwaysAutoResize);

    ImVec4 pauseColor = ImVec4(1, 0, 0, 0.25f);
    if (sys->state == System::State::run) {
        if (symbolButton("Pause emulation", ICON_FA_PAUSE, pauseColor)) {
            sys->state = System::State::pause;
        }
    } else {
        if (symbolButton("Resume emulation", ICON_FA_PLAY, pauseColor)) {
            sys->state = System::State::run;
        }
    }

    ImGui::SameLine();

    symbolButton("Save/Load", ICON_FA_SAVE);
    if (ImGui::BeginPopupContextItem(nullptr, 0)) {
        if (fs::exists(state::getStatePath(sys.get()))) {
            if (ImGui::Selectable("Quick load")) bus.notify(Event::System::LoadState{});
            ImGui::Separator();
        }
        if (ImGui::Selectable("Quick save")) bus.notify(Event::System::SaveState{});
        ImGui::EndPopup();
    }

    ImGui::SameLine();

    std::string game{};

    auto cdPath = sys->cdrom->disc->getFile();
    if (!cdPath.empty()) {
        game = getFilename(cdPath);
    }
    if (!game.empty()) {
        ImGui::TextUnformatted(game.c_str());
    } else {
        if (symbolButton("Open", ICON_FA_COMPACT_DISC, ImVec4(0.25f, 0.5f, 1, 0.4f))) {
            openFile.openWindowOpen = true;
        }
    }

    ImGui::SameLine();

    symbolButton("Settings", ICON_FA_COG);
    if (ImGui::BeginPopupContextItem(nullptr, 0)) {
        if (ImGui::Selectable("Controller")) showControllerSetupWindow = !showControllerSetupWindow;
        ImGui::Separator();
        ImGui::MenuItem("Show menu", Key(config.hotkeys["toggle_menu"]).getButton().c_str(), &showMenu);
        ImGui::EndPopup();
    }

    ImGui::SameLine();

    std::string hint = "Toggle fullscreen (" + (std::string)Key(config.hotkeys["toggle_fullscreen"]).getButton().c_str() + ")";
    if (symbolButton(hint.c_str(), ICON_FA_COMPRESS)) {
        bus.notify(Event::Gui::ToggleFullscreen{});
    }

    ImGui::End();
    ImGui::PopStyleVar(3);
}

void GUI::renderController() {
    ImDrawList* drawList = ImGui::GetBackgroundDrawList();
    float size = 64.f;
    auto button = [drawList, size](const char* button, float _x, float _y) {
        auto btn = getImage(button, avocado::assetsPath("buttons/"));
        if (!btn) return;
        // AddImage(ImTextureID user_texture_id, const ImVec2& p_min, const ImVec2& p_max, const ImVec2& uv_min = ImVec2(0, 0), const
        // ImVec2& uv_max = ImVec2(1, 1), ImU32 col = IM_COL32_WHITE);
        float x = ImGui::GetIO().DisplaySize.x * _x;
        float y = ImGui::GetIO().DisplaySize.y * lerp(0.3f, 0.6f, _y);
        float r = size / 2 * 1.2;

        drawList->AddCircleFilled(ImVec2(x, y), r, ImColor(0, 0, 0, 128));
        drawList->AddImage((ImTextureID)btn->id, ImVec2(x - size / 2, y - size / 2), ImVec2(x + size / 2, y + size / 2), ImVec2(0, 0),
                           ImVec2(1, 1), ImColor(0xff, 0xff, 0xff, 192));
    };

    float COL = 1.f / 12.f;
    float ROW = 1.f / 3.f;

    button("dpad_up", 2 * COL, 1 * ROW);
    button("dpad_left", 1 * COL, 2 * ROW);
    button("dpad_right", 3 * COL, 2 * ROW);
    button("dpad_down", 2 * COL, 3 * ROW);

    button("select", 5 * COL, 3 * ROW);
    button("start", 7 * COL, 3 * ROW);

    button("triangle", 1 - 2 * COL, 1 * ROW);
    button("square", 1 - 1 * COL, 2 * ROW);
    button("circle", 1 - 3 * COL, 2 * ROW);
    button("cross", 1 - 2 * COL, 3 * ROW);
}
