#include "EventLog.h"

#include <iostream>
#include <iomanip>
#include <ctime>
#include <sstream>

#include "2s2h/BenGui/UIWidgets.hpp"
#include "2s2h/Enhancements/GameInteractor/GameInteractor.h"

extern "C" {
#include <z64.h>
}

typedef enum {
    EVENT_LOG_ENTRY_TYPE_FLAG_SET,
    EVENT_LOG_ENTRY_TYPE_FLAG_UNSET,
    EVENT_LOG_ENTRY_TYPE_SCENE_FLAG_SET,
    EVENT_LOG_ENTRY_TYPE_SCENE_FLAG_UNSET,
    EVENT_LOG_ENTRY_TYPE_ACTOR_INIT,
    EVENT_LOG_ENTRY_TYPE_ACTOR_KILL,
    EVENT_LOG_ENTRY_TYPE_SCENE_INIT,
    EVENT_LOG_ENTRY_TYPE_ROOM_INIT,
    EVENT_LOG_ENTRY_TYPE_OPEN_TEXT,
    EVENT_LOG_ENTRY_TYPE_ITEM_GIVE,
} EventLogEntryType;

const char* eventLogEntryTypeNames[] = {
    "FlagSet",   "FlagUnset", "SceneFlagSet", "SceneFlagUnset", "ActorInit",
    "ActorKill", "SceneInit", "RoomInit",     "OpenText",       "ItemGive",
};

struct EventLogEntry {
    std::string timestamp;
    EventLogEntryType type;
    std::string meta;
};

std::vector<EventLogEntry> eventLogEntries;

const char* flagTypeNames[] = {
    "",      "weekEventReg", "eventInf",    "scenesVisible", "owlActivation",
    "chest", "switch",       "clearedRoom", "collectible",   "unk_14",
    "rooms", "chest *",      "switch *",    "clearedRoom *", "collectible *",
};

#define DEFINE_ACTOR(name, _enumValue, _allocType, _debugName, _humanName) { _enumValue, _debugName },
#define DEFINE_ACTOR_INTERNAL(_name, _enumValue, _allocType, _debugName, _humanName) { _enumValue, _debugName },
#define DEFINE_ACTOR_UNSET(_enumValue) { _enumValue, "Unset" },

std::unordered_map<s16, const char*> actorNames = {
#include "tables/actor_table.h"
};

#undef DEFINE_ACTOR
#undef DEFINE_ACTOR_INTERNAL
#undef DEFINE_ACTOR_UNSET

// 2S2H Added columns to scene table: entranceSceneId, betterMapSelectIndex, humanName
#define DEFINE_SCENE(_name, enumValue, _textId, _drawConfig, _restrictionFlags, _persistentCycleFlags, \
                     _entranceSceneId, _betterMapSelectIndex, humanName)                               \
    { enumValue, humanName },
#define DEFINE_SCENE_UNSET(_enumValue)

std::unordered_map<s16, const char*> sceneNames = {
#include "tables/scene_table.h"
};

#undef DEFINE_SCENE
#undef DEFINE_SCENE_UNSET

static HOOK_ID onFlagSetHookId = 0;
static HOOK_ID onFlagUnsetHookId = 0;
static HOOK_ID onSceneFlagSetHookId = 0;
static HOOK_ID onSceneFlagUnsetHookId = 0;
static HOOK_ID onActorInitHookId = 0;
static HOOK_ID onActorKillHookId = 0;
static HOOK_ID onSceneInitHookId = 0;
static HOOK_ID onRoomInitHookId = 0;
static HOOK_ID onOpenTextHookId = 0;
static HOOK_ID onItemGiveHookId = 0;

std::string CurrentTime() {
    auto t = std::time(nullptr);
    auto tm = *std::localtime(&t);

    std::ostringstream oss;
    oss << std::put_time(&tm, "%I:%M:%S");
    return oss.str();
}

void TrimEventLog() {
    while (eventLogEntries.size() > CVarGetInteger("gEventLog.MaxEntries", 1000)) {
        eventLogEntries.pop_back();
    }
}

void RegisterEventLogHooks() {
    GameInteractor::Instance->UnregisterGameHook<GameInteractor::OnFlagSet>(onFlagSetHookId);
    GameInteractor::Instance->UnregisterGameHook<GameInteractor::OnFlagUnset>(onFlagUnsetHookId);
    GameInteractor::Instance->UnregisterGameHook<GameInteractor::OnSceneFlagSet>(onSceneFlagSetHookId);
    GameInteractor::Instance->UnregisterGameHook<GameInteractor::OnSceneFlagUnset>(onSceneFlagUnsetHookId);
    GameInteractor::Instance->UnregisterGameHook<GameInteractor::OnActorInit>(onActorInitHookId);
    GameInteractor::Instance->UnregisterGameHook<GameInteractor::OnActorKill>(onActorKillHookId);
    GameInteractor::Instance->UnregisterGameHook<GameInteractor::OnSceneInit>(onSceneInitHookId);
    GameInteractor::Instance->UnregisterGameHook<GameInteractor::OnRoomInit>(onRoomInitHookId);
    GameInteractor::Instance->UnregisterGameHook<GameInteractor::OnOpenText>(onOpenTextHookId);
    GameInteractor::Instance->UnregisterGameHook<GameInteractor::OnItemGive>(onItemGiveHookId);
    onFlagSetHookId = 0;
    onFlagUnsetHookId = 0;
    onSceneFlagSetHookId = 0;
    onSceneFlagUnsetHookId = 0;
    onActorInitHookId = 0;
    onActorKillHookId = 0;
    onSceneInitHookId = 0;
    onRoomInitHookId = 0;
    onOpenTextHookId = 0;
    onItemGiveHookId = 0;

    if (!CVarGetInteger("gEventLog.Enabled", 0)) {
        return;
    }

    onFlagSetHookId =
        GameInteractor::Instance->RegisterGameHook<GameInteractor::OnFlagSet>([](FlagType type, u32 flag) {
            if (type == FlagType::FLAG_WEEK_EVENT_REG) {
                eventLogEntries.insert(eventLogEntries.begin(),
                                       {
                                           .timestamp = CurrentTime(),
                                           .type = EVENT_LOG_ENTRY_TYPE_FLAG_SET,
                                           .meta = fmt::format("WEEKEVENTREG_{:02d}_{:02x}", flag >> 8, flag & 0xFF),
                                       });
            } else if (type == FlagType::FLAG_EVENT_INF) {
                eventLogEntries.insert(eventLogEntries.begin(), {
                                                                    .timestamp = CurrentTime(),
                                                                    .type = EVENT_LOG_ENTRY_TYPE_FLAG_SET,
                                                                    .meta = fmt::format("EVENTINF_{:02x}", flag),
                                                                });
            } else {
                eventLogEntries.insert(
                    eventLogEntries.begin(),
                    {
                        .timestamp = CurrentTime(),
                        .type = EVENT_LOG_ENTRY_TYPE_FLAG_SET,
                        .meta = fmt::format("{}({:02d}) {:02x}", flagTypeNames[type], flag >> 8, flag & 0xFF),
                    });
            }
            TrimEventLog();
        });

    onFlagUnsetHookId =
        GameInteractor::Instance->RegisterGameHook<GameInteractor::OnFlagUnset>([](FlagType type, u32 flag) {
            if (type == FlagType::FLAG_WEEK_EVENT_REG) {
                eventLogEntries.insert(eventLogEntries.begin(),
                                       {
                                           .timestamp = CurrentTime(),
                                           .type = EVENT_LOG_ENTRY_TYPE_FLAG_UNSET,
                                           .meta = fmt::format("WEEKEVENTREG_{:02d}_{:02x}", flag >> 8, flag & 0xFF),
                                       });
            } else if (type == FlagType::FLAG_EVENT_INF) {
                eventLogEntries.insert(eventLogEntries.begin(), {
                                                                    .timestamp = CurrentTime(),
                                                                    .type = EVENT_LOG_ENTRY_TYPE_FLAG_UNSET,
                                                                    .meta = fmt::format("EVENTINF_{:02x}", flag),
                                                                });
            } else {
                eventLogEntries.insert(
                    eventLogEntries.begin(),
                    {
                        .timestamp = CurrentTime(),
                        .type = EVENT_LOG_ENTRY_TYPE_FLAG_UNSET,
                        .meta = fmt::format("{}({:02d}) {:02x}", flagTypeNames[type], flag >> 8, flag & 0xFF),
                    });
            }
            TrimEventLog();
        });

    onSceneFlagSetHookId = GameInteractor::Instance->RegisterGameHook<GameInteractor::OnSceneFlagSet>(
        [](s16 sceneId, FlagType type, u32 flag) {
            eventLogEntries.insert(
                eventLogEntries.begin(),
                {
                    .timestamp = CurrentTime(),
                    .type = EVENT_LOG_ENTRY_TYPE_SCENE_FLAG_SET,
                    .meta = fmt::format("{}({:02d}) {:02x}", flagTypeNames[type], flag >> 8, flag & 0xFF),
                });
            TrimEventLog();
        });

    onSceneFlagUnsetHookId = GameInteractor::Instance->RegisterGameHook<GameInteractor::OnSceneFlagUnset>(
        [](s16 sceneId, FlagType type, u32 flag) {
            eventLogEntries.insert(
                eventLogEntries.begin(),
                {
                    .timestamp = CurrentTime(),
                    .type = EVENT_LOG_ENTRY_TYPE_SCENE_FLAG_UNSET,
                    .meta = fmt::format("{}[{:02d}] {:02x}", flagTypeNames[type], flag >> 8, flag & 0xFF),
                });
            TrimEventLog();
        });

    onActorInitHookId = GameInteractor::Instance->RegisterGameHook<GameInteractor::OnActorInit>([](Actor* actor) {
        eventLogEntries.insert(eventLogEntries.begin(),
                               {
                                   .timestamp = CurrentTime(),
                                   .type = EVENT_LOG_ENTRY_TYPE_ACTOR_INIT,
                                   .meta = fmt::format("{} {}", actorNames[actor->id], actor->params),
                               });
        TrimEventLog();
    });

    onActorKillHookId = GameInteractor::Instance->RegisterGameHook<GameInteractor::OnActorKill>([](Actor* actor) {
        eventLogEntries.insert(eventLogEntries.begin(),
                               {
                                   .timestamp = CurrentTime(),
                                   .type = EVENT_LOG_ENTRY_TYPE_ACTOR_KILL,
                                   .meta = fmt::format("{} {}", actorNames[actor->id], actor->params),
                               });
        TrimEventLog();
    });

    onSceneInitHookId = GameInteractor::Instance->RegisterGameHook<GameInteractor::OnSceneInit>([](s16 sceneId,
                                                                                                   s8 spawnNum) {
        eventLogEntries.insert(eventLogEntries.begin(), {
                                                            .timestamp = CurrentTime(),
                                                            .type = EVENT_LOG_ENTRY_TYPE_SCENE_INIT,
                                                            .meta = fmt::format("{} {}", sceneNames[sceneId], spawnNum),
                                                        });
        TrimEventLog();
    });

    onRoomInitHookId = GameInteractor::Instance->RegisterGameHook<GameInteractor::OnRoomInit>([](s16 sceneId,
                                                                                                 s8 roomId) {
        eventLogEntries.insert(eventLogEntries.begin(), {
                                                            .timestamp = CurrentTime(),
                                                            .type = EVENT_LOG_ENTRY_TYPE_ROOM_INIT,
                                                            .meta = fmt::format("{} {}", sceneNames[sceneId], roomId),
                                                        });
        TrimEventLog();
    });

    onOpenTextHookId = GameInteractor::Instance->RegisterGameHook<GameInteractor::OnOpenText>([](s16 textId) {
        eventLogEntries.insert(eventLogEntries.begin(), {
                                                            .timestamp = CurrentTime(),
                                                            .type = EVENT_LOG_ENTRY_TYPE_OPEN_TEXT,
                                                            .meta = fmt::format("0x{:02x}", textId),
                                                        });
        TrimEventLog();
    });

    onItemGiveHookId = GameInteractor::Instance->RegisterGameHook<GameInteractor::OnItemGive>([](u8 item) {
        eventLogEntries.insert(eventLogEntries.begin(), {
                                                            .timestamp = CurrentTime(),
                                                            .type = EVENT_LOG_ENTRY_TYPE_ITEM_GIVE,
                                                            .meta = fmt::format("0x{:02x}", item),
                                                        });
        TrimEventLog();
    });
}

void EventLogWindow::DrawElement() {
    ImGui::SetNextWindowSize(ImVec2(520, 600), ImGuiCond_FirstUseEver);
    if (!ImGui::Begin("Event Log", &mIsVisible, ImGuiWindowFlags_NoFocusOnAppearing)) {
        ImGui::End();
        return;
    }

    if (UIWidgets::CVarCheckbox("Enable", "gEventLog.Enabled")) {
        RegisterEventLogHooks();
    }

    ImGui::SameLine();

    if (UIWidgets::Button("Filters", { .size = UIWidgets::Sizes::Inline })) {
        ImGui::OpenPopup("eventLogFiltersPopup");
    }

    if (ImGui::BeginPopup("eventLogFiltersPopup")) {
        bool allChecked =
            CVarGetInteger("gEventLog.Filter.FlagSet", 1) && CVarGetInteger("gEventLog.Filter.FlagUnset", 1) &&
            CVarGetInteger("gEventLog.Filter.SceneFlagSet", 1) &&
            CVarGetInteger("gEventLog.Filter.SceneFlagUnset", 1) && CVarGetInteger("gEventLog.Filter.ActorInit", 1) &&
            CVarGetInteger("gEventLog.Filter.ActorKill", 1) && CVarGetInteger("gEventLog.Filter.SceneInit", 1) &&
            CVarGetInteger("gEventLog.Filter.RoomInit", 1) && CVarGetInteger("gEventLog.Filter.OpenText", 1) &&
            CVarGetInteger("gEventLog.Filter.ItemGive", 1);
        bool someChecked =
            CVarGetInteger("gEventLog.Filter.FlagSet", 1) || CVarGetInteger("gEventLog.Filter.FlagUnset", 1) ||
            CVarGetInteger("gEventLog.Filter.SceneFlagSet", 1) ||
            CVarGetInteger("gEventLog.Filter.SceneFlagUnset", 1) || CVarGetInteger("gEventLog.Filter.ActorInit", 1) ||
            CVarGetInteger("gEventLog.Filter.ActorKill", 1) || CVarGetInteger("gEventLog.Filter.SceneInit", 1) ||
            CVarGetInteger("gEventLog.Filter.RoomInit", 1) || CVarGetInteger("gEventLog.Filter.OpenText", 1) ||
            CVarGetInteger("gEventLog.Filter.ItemGive", 1);

        ImGuiContext* g = ImGui::GetCurrentContext();
        ImGuiItemFlags backup_item_flags = g->CurrentItemFlags;
        if (!allChecked && someChecked)
            g->CurrentItemFlags |= ImGuiItemFlags_MixedValue;
        if (UIWidgets::Checkbox("All", &allChecked)) {
            if (allChecked) {
                CVarSetInteger("gEventLog.Filter.FlagSet", 1);
                CVarSetInteger("gEventLog.Filter.FlagUnset", 1);
                CVarSetInteger("gEventLog.Filter.SceneFlagSet", 1);
                CVarSetInteger("gEventLog.Filter.SceneFlagUnset", 1);
                CVarSetInteger("gEventLog.Filter.ActorInit", 1);
                CVarSetInteger("gEventLog.Filter.ActorKill", 1);
                CVarSetInteger("gEventLog.Filter.SceneInit", 1);
                CVarSetInteger("gEventLog.Filter.RoomInit", 1);
                CVarSetInteger("gEventLog.Filter.OpenText", 1);
                CVarSetInteger("gEventLog.Filter.ItemGive", 1);
            } else {
                CVarSetInteger("gEventLog.Filter.FlagSet", 0);
                CVarSetInteger("gEventLog.Filter.FlagUnset", 0);
                CVarSetInteger("gEventLog.Filter.SceneFlagSet", 0);
                CVarSetInteger("gEventLog.Filter.SceneFlagUnset", 0);
                CVarSetInteger("gEventLog.Filter.ActorInit", 0);
                CVarSetInteger("gEventLog.Filter.ActorKill", 0);
                CVarSetInteger("gEventLog.Filter.SceneInit", 0);
                CVarSetInteger("gEventLog.Filter.RoomInit", 0);
                CVarSetInteger("gEventLog.Filter.OpenText", 0);
                CVarSetInteger("gEventLog.Filter.ItemGive", 0);
            }
            Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesOnNextTick();
        }
        g->CurrentItemFlags = backup_item_flags;

        UIWidgets::CVarCheckbox("FlagSet", "gEventLog.Filter.FlagSet", { .defaultValue = 1 });
        UIWidgets::CVarCheckbox("FlagUnset", "gEventLog.Filter.FlagUnset", { .defaultValue = 1 });
        UIWidgets::CVarCheckbox("SceneFlagSet", "gEventLog.Filter.SceneFlagSet", { .defaultValue = 1 });
        UIWidgets::CVarCheckbox("SceneFlagUnset", "gEventLog.Filter.SceneFlagUnset", { .defaultValue = 1 });
        UIWidgets::CVarCheckbox("ActorInit", "gEventLog.Filter.ActorInit", { .defaultValue = 1 });
        UIWidgets::CVarCheckbox("ActorKill", "gEventLog.Filter.ActorKill", { .defaultValue = 1 });
        UIWidgets::CVarCheckbox("SceneInit", "gEventLog.Filter.SceneInit", { .defaultValue = 1 });
        UIWidgets::CVarCheckbox("RoomInit", "gEventLog.Filter.RoomInit", { .defaultValue = 1 });
        UIWidgets::CVarCheckbox("OpenText", "gEventLog.Filter.OpenText", { .defaultValue = 1 });
        UIWidgets::CVarCheckbox("ItemGive", "gEventLog.Filter.ItemGive", { .defaultValue = 1 });
        ImGui::EndPopup();
    }

    ImGui::SameLine(0);

    std::string maxEntriesStr = std::string("Max Entries: ") + std::to_string(eventLogEntries.size()) + "/";
    ImGui::Text("%s", maxEntriesStr.c_str());

    ImGui::SameLine(0);

    UIWidgets::PushStyleCombobox();
    ImGui::PushItemWidth(50);
    s32 maxEntries = CVarGetInteger("gEventLog.MaxEntries", 1000);
    if (ImGui::InputScalar("##maxEntriesInput", ImGuiDataType_S32, &maxEntries)) {
        CVarSetInteger("gEventLog.MaxEntries", MAX(0, maxEntries));
        Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesOnNextTick();
        TrimEventLog();
    }
    ImGui::PopItemWidth();
    UIWidgets::PopStyleCombobox();

    ImGui::SameLine(0);

    if (UIWidgets::Button("Clear", { .color = UIWidgets::Colors::Red, .size = UIWidgets::Sizes::Inline })) {
        eventLogEntries.clear();
    }

    if (ImGui::BeginTable("events", 3, ImGuiTableFlags_ScrollY, ImVec2(0, 0))) {
        ImGui::TableSetupColumn("Timestamp", ImGuiTableColumnFlags_WidthFixed, 70);
        ImGui::TableSetupColumn("Type", ImGuiTableColumnFlags_WidthFixed, 100);
        ImGui::TableSetupColumn("Meta", ImGuiTableColumnFlags_WidthStretch, 0);
        ImGui::TableSetupScrollFreeze(0, 1);
        ImGui::TableHeadersRow();

        for (int i = 0; i < eventLogEntries.size(); i++) {
            if (!CVarGetInteger(
                    (std::string("gEventLog.Filter.") + eventLogEntryTypeNames[eventLogEntries[i].type]).c_str(), 1)) {
                continue;
            }

            ImGui::TableNextRow();
            ImGui::TableSetColumnIndex(0);
            ImGui::Text("%s", eventLogEntries[i].timestamp.c_str());
            ImGui::TableSetColumnIndex(1);
            ImGui::Text("%s", eventLogEntryTypeNames[eventLogEntries[i].type]);
            ImGui::TableSetColumnIndex(2);
            ImGui::Text("%s", eventLogEntries[i].meta.c_str());
        }

        ImGui::EndTable();
    }

    ImGui::End();
}

void EventLogWindow::InitElement() {
    RegisterEventLogHooks();
}
