// src/main.cpp
#include <algorithm>
#include <ctime>
#include <filesystem>
#include <iostream>
#include <memory>
#include <string>
#include <type_traits>
#include <vector>

#include <SDL3/SDL.h>

#include "defines.hpp"
#include "jail_audio.h"
#include "rendering/shader_backend.hpp"

struct Logger {
    static void info(const std::string& s) { std::cout << "[INFO] " << s << '\n'; }
    static void error(const std::string& s) { std::cerr << "[ERROR] " << s << '\n'; }
};

struct VideoOptions {
    bool fullscreen = false;
    bool vsync = true;
} Options_video;

struct DisplayMonitor {
    std::string name;
    int width = 0;
    int height = 0;
    int refresh_rate = 0;
};

static DisplayMonitor display_monitor_;
static SDL_Window* window_ = nullptr;
static std::unique_ptr<Rendering::IShaderBackend> backend_;

struct ShaderEntry {
    std::filesystem::path folder;
    std::string base_name;
};

static std::vector<ShaderEntry> shader_list_;
static std::vector<std::string> shader_names_;
static std::vector<std::string> shader_authors_;
static size_t current_shader_index_ = 0;
static std::filesystem::path shaders_directory_;
static Uint32 shader_start_ticks_ = 0;

static Uint32 fps_frame_count_ = 0;
static Uint32 fps_last_update_ticks_ = 0;
static float current_fps_ = 0.0f;

static std::vector<std::filesystem::path> music_list_;
static size_t current_music_index_ = 0;
static JA_Music_t* current_music_ = nullptr;

static std::vector<ShaderEntry> scanShaderDirectory(const std::filesystem::path& directory) {
    std::vector<ShaderEntry> shaders;

    if (!std::filesystem::exists(directory) || !std::filesystem::is_directory(directory)) {
        Logger::error("Shader directory does not exist: " + directory.string());
        return shaders;
    }

    for (const auto& entry : std::filesystem::directory_iterator(directory)) {
        if (!entry.is_directory()) { continue; }

        const std::string folder_name = entry.path().filename().string();
        if (folder_name.empty() || folder_name[0] == '_' || folder_name[0] == '.') { continue; }

        const std::filesystem::path gl_source = entry.path() / (folder_name + ".gl.glsl");
        if (!std::filesystem::exists(gl_source)) {
            Logger::info("Skipping " + folder_name + ": missing " + gl_source.filename().string());
            continue;
        }

        shaders.push_back(ShaderEntry{entry.path(), folder_name});
    }

    std::sort(shaders.begin(), shaders.end(),
        [](const ShaderEntry& a, const ShaderEntry& b) { return a.base_name < b.base_name; });

    Logger::info("Found " + std::to_string(shaders.size()) + " shader(s) in " + directory.string());

    shader_names_.resize(shaders.size(), "");
    shader_authors_.resize(shaders.size(), "");

    return shaders;
}

static std::vector<std::filesystem::path> scanMusicDirectory(const std::filesystem::path& directory) {
    std::vector<std::filesystem::path> music_files;

    if (!std::filesystem::exists(directory) || !std::filesystem::is_directory(directory)) {
        Logger::info("Music directory does not exist: " + directory.string());
        return music_files;
    }

    for (const auto& entry : std::filesystem::directory_iterator(directory)) {
        if (entry.is_regular_file()) {
            const auto ext = entry.path().extension().string();
            if (ext == ".ogg") {
                music_files.push_back(entry.path());
            }
        }
    }

    std::sort(music_files.begin(), music_files.end());

    Logger::info("Found " + std::to_string(music_files.size()) + " music file(s) in " + directory.string());
    return music_files;
}

static void playRandomMusic() {
    if (music_list_.empty()) { return; }

    if (current_music_ != nullptr) {
        JA_DeleteMusic(current_music_);
        current_music_ = nullptr;
    }

    current_music_index_ = static_cast<size_t>(rand()) % music_list_.size();

    const auto& music_path = music_list_[current_music_index_];
    current_music_ = JA_LoadMusic(music_path.string().c_str());

    if (current_music_ != nullptr) {
        JA_PlayMusic(current_music_, 0);
        Logger::info("Now playing: " + music_path.filename().string());
    } else {
        Logger::error("Failed to load music: " + music_path.string());
    }
}

static void updateWindowTitle() {
    if (window_ == nullptr || shader_list_.empty()) { return; }

    std::string shaderName;
    if (!shader_names_.empty() && !shader_names_[current_shader_index_].empty()) {
        shaderName = shader_names_[current_shader_index_];
    } else {
        shaderName = shader_list_[current_shader_index_].base_name;
    }

    if (!shader_authors_.empty() && !shader_authors_[current_shader_index_].empty()) {
        shaderName += " by " + shader_authors_[current_shader_index_];
    }

    std::string title = WINDOW_TITLE_PREFIX;
    title += " (";
    title += shaderName;

    if (backend_) {
        title += " - ";
        title += backend_->driverName();
    }

    if (current_fps_ > 0.0f) {
        title += " - ";
        title += std::to_string(static_cast<int>(current_fps_ + 0.5f)) + " FPS";
    }

    if (Options_video.vsync) {
        title += " - VSync";
    }
    title += ")";

    SDL_SetWindowTitle(window_, title.c_str());
}

static bool loadShaderAtIndex(size_t index) {
    if (index >= shader_list_.size()) {
        Logger::error("Invalid shader index: " + std::to_string(index));
        return false;
    }

    const auto& entry = shader_list_[index];
    Logger::info("Loading shader: " + entry.folder.string());

    Rendering::ShaderProgramSpec spec;
    spec.folder = entry.folder;
    spec.base_name = entry.base_name;

    const std::filesystem::path meta_path = entry.folder / "meta.txt";
    if (std::filesystem::exists(meta_path)) {
        spec.metadata = Rendering::parseMetaFile(meta_path);
    } else {
        std::string source;
        const std::filesystem::path gl_source = entry.folder / (entry.base_name + ".gl.glsl");
        if (Rendering::loadFileToString(gl_source, source)) {
            spec.metadata = Rendering::extractShaderMetadata(source);
        }
    }

    if (!spec.metadata.name.empty()) {
        shader_names_[index] = spec.metadata.name;
        Logger::info("Shader name: " + spec.metadata.name);
    }
    if (!spec.metadata.author.empty()) {
        shader_authors_[index] = spec.metadata.author;
        Logger::info("Shader author: " + spec.metadata.author);
    }

    return backend_->loadShader(spec);
}

void getDisplayInfo() {
    int num_displays = 0;
    SDL_DisplayID* displays = SDL_GetDisplays(&num_displays);
    if (displays != nullptr && num_displays > 0) {
        for (int i = 0; i < num_displays; ++i) {
            const SDL_DisplayID instance_id = displays[i];
            const char* name = SDL_GetDisplayName(instance_id);
            Logger::info(std::string("Display ") + std::to_string(instance_id) + ": " + (name != nullptr ? name : "Unknown"));
        }

        const SDL_DisplayMode* dm = SDL_GetCurrentDisplayMode(displays[0]);
        const char* first_display_name = SDL_GetDisplayName(displays[0]);
        display_monitor_.name = (first_display_name != nullptr) ? first_display_name : "Unknown";
        if (dm != nullptr) {
            display_monitor_.width = static_cast<int>(dm->w);
            display_monitor_.height = static_cast<int>(dm->h);
            display_monitor_.refresh_rate = static_cast<int>(dm->refresh_rate);
        } else {
            Logger::info("SDL_GetCurrentDisplayMode returned null");
        }
    } else {
        Logger::info("No displays found or SDL_GetDisplays failed");
    }
}

void setFullscreenMode() {
    if (window_ == nullptr) { return; }

    if (Options_video.fullscreen) {
        if (!SDL_SetWindowFullscreen(window_, true)) {
            Logger::error(std::string("Failed to set fullscreen: ") + SDL_GetError());
            Logger::info("Fallback to windowed mode 800x800");
            SDL_SetWindowFullscreen(window_, false);
            SDL_SetWindowSize(window_, WINDOW_WIDTH, WINDOW_HEIGHT);
            Options_video.fullscreen = false;
            SDL_ShowCursor();
        } else {
            SDL_HideCursor();
        }
    } else {
        SDL_SetWindowFullscreen(window_, false);
        SDL_SetWindowSize(window_, WINDOW_WIDTH, WINDOW_HEIGHT);
        SDL_ShowCursor();
    }
}

void toggleFullscreen() {
    Options_video.fullscreen = !Options_video.fullscreen;
    setFullscreenMode();
}

void toggleVSync() {
    Options_video.vsync = !Options_video.vsync;
    if (backend_) {
        backend_->setVSync(Options_video.vsync);
    }
}

void switchShader(int direction) {
    if (shader_list_.empty()) { return; }

    size_t new_index = current_shader_index_;
    if (direction > 0) {
        new_index = (current_shader_index_ + 1) % shader_list_.size();
    } else if (direction < 0) {
        new_index = (current_shader_index_ == 0) ? shader_list_.size() - 1 : current_shader_index_ - 1;
    }

    if (!loadShaderAtIndex(new_index)) {
        Logger::error("Failed to switch shader, keeping current one");
        return;
    }

    current_shader_index_ = new_index;
    shader_start_ticks_ = SDL_GetTicks();
    updateWindowTitle();
}

void handleDebugEvents(const SDL_Event& event) {
    if (event.type == SDL_EVENT_KEY_DOWN && static_cast<int>(event.key.repeat) == 0) {
        switch (event.key.key) {
            case SDLK_F3: { toggleFullscreen(); break; }
            case SDLK_F4: { toggleVSync(); break; }
            case SDLK_LEFT: { switchShader(-1); break; }
            case SDLK_RIGHT: { switchShader(+1); break; }
            default: break;
        }
    }
}

enum class BackendChoice { Auto, Gpu, OpenGL };

static auto createWindowForBackend(BackendChoice choice) -> SDL_Window* {
    SDL_WindowFlags flags = SDL_WINDOW_RESIZABLE;
    if (choice == BackendChoice::OpenGL) {
        flags |= SDL_WINDOW_OPENGL;
        SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3);
        SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 3);
        SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE);
    }
    return SDL_CreateWindow(APP_NAME, WINDOW_WIDTH, WINDOW_HEIGHT, flags);
}

int main(int argc, char** argv) {
    std::string shaderPath;
    bool fullscreenFlag = false;
    BackendChoice backend_choice = BackendChoice::Auto;
    for (int i = 1; i < argc; ++i) {
        const std::string a = argv[i];
        if (a == "-F" || a == "--fullscreen") { fullscreenFlag = true; continue; }
        if (a == "--backend=gpu") { backend_choice = BackendChoice::Gpu; continue; }
        if (a == "--backend=opengl") { backend_choice = BackendChoice::OpenGL; continue; }
        if (a == "--backend=auto") { backend_choice = BackendChoice::Auto; continue; }
        if (shaderPath.empty()) { shaderPath = a; }
    }
    if (shaderPath.empty()) { shaderPath = "test"; }
    Options_video.fullscreen = fullscreenFlag;

    auto initResult = SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO);
    if constexpr (std::is_same_v<decltype(initResult), bool>) {
        if (!initResult) { Logger::error(SDL_GetError()); return -1; }
    } else {
        if (initResult != 0) { Logger::error(SDL_GetError()); return -1; }
    }

    getDisplayInfo();

    if (backend_choice != BackendChoice::OpenGL) {
        window_ = createWindowForBackend(BackendChoice::Gpu);
        if (window_ != nullptr) {
            backend_ = Rendering::makeSdl3GpuBackend();
            if (!backend_->init(window_)) {
                Logger::info("SDL3 GPU backend init failed, falling back to OpenGL");
                backend_.reset();
                SDL_DestroyWindow(window_);
                window_ = nullptr;
                if (backend_choice == BackendChoice::Gpu) {
                    SDL_Quit();
                    return -1;
                }
            }
        }
    }

    if (backend_ == nullptr) {
        window_ = createWindowForBackend(BackendChoice::OpenGL);
        if (window_ == nullptr) {
            Logger::error(std::string("SDL_CreateWindow error: ") + SDL_GetError());
            SDL_Quit();
            return -1;
        }
        backend_ = Rendering::makeOpenGLBackend();
        if (!backend_->init(window_)) {
            Logger::error("Failed to initialize shader backend");
            SDL_DestroyWindow(window_);
            SDL_Quit();
            return -1;
        }
    }

    setFullscreenMode();
    backend_->setVSync(Options_video.vsync);

    JA_Init(48000, SDL_AUDIO_S16LE, 2);

    const std::string resources_dir = getResourcesDirectory();

    srand(static_cast<unsigned int>(time(nullptr)));

    const std::filesystem::path music_directory = std::filesystem::path(resources_dir) / "data" / "music";
    music_list_ = scanMusicDirectory(music_directory);

    if (!music_list_.empty()) {
        playRandomMusic();
    } else {
        Logger::info("No music files found in " + music_directory.string());
    }

    const std::filesystem::path arg_path(shaderPath);
    std::filesystem::path target_folder;
    if (arg_path.has_parent_path()) {
        target_folder = arg_path;
        shaders_directory_ = arg_path.parent_path();
    } else {
        shaders_directory_ = std::filesystem::path(resources_dir) / "shaders";
        target_folder = shaders_directory_ / shaderPath;
    }

    shader_list_ = scanShaderDirectory(shaders_directory_);
    if (shader_list_.empty()) {
        Logger::error("No shaders found in directory: " + shaders_directory_.string());
        backend_->cleanup();
        SDL_DestroyWindow(window_);
        SDL_Quit();
        return -1;
    }

    size_t initial_index = 0;
    bool found_shader = false;

    for (size_t i = 0; i < shader_list_.size(); ++i) {
        if (shader_list_[i].folder == target_folder) {
            initial_index = i;
            found_shader = true;
            break;
        }
    }

    if (!found_shader) {
        const std::filesystem::path default_folder = std::filesystem::path(resources_dir) / "shaders" / "test";
        for (size_t i = 0; i < shader_list_.size(); ++i) {
            if (shader_list_[i].folder == default_folder) {
                initial_index = i;
                found_shader = true;
                break;
            }
        }
    }

    if (!found_shader) {
        Logger::info("Specified shader not found, using first shader in directory");
        initial_index = 0;
    }

    current_shader_index_ = initial_index;
    if (!loadShaderAtIndex(current_shader_index_)) {
        Logger::error("Failed to load initial shader");
        backend_->cleanup();
        SDL_DestroyWindow(window_);
        SDL_Quit();
        return -1;
    }

    shader_start_ticks_ = SDL_GetTicks();
    fps_last_update_ticks_ = SDL_GetTicks();
    updateWindowTitle();

    bool running = true;

    while (running) {
        fps_frame_count_++;
        const Uint32 current_ticks = SDL_GetTicks();

        if (current_ticks - fps_last_update_ticks_ >= 500) {
            const float elapsed_seconds = static_cast<float>(current_ticks - fps_last_update_ticks_) / 1000.0f;
            current_fps_ = static_cast<float>(fps_frame_count_) / elapsed_seconds;
            fps_frame_count_ = 0;
            fps_last_update_ticks_ = current_ticks;
            updateWindowTitle();
        }

        JA_Update();

        if (!music_list_.empty() && JA_GetMusicState() == JA_MUSIC_STOPPED) {
            playRandomMusic();
        }

        SDL_Event e;
        while (SDL_PollEvent(&e)) {
            if (e.type == SDL_EVENT_QUIT) {
                running = false;
            } else if (e.type == SDL_EVENT_WINDOW_CLOSE_REQUESTED) {
                running = false;
            } else if (e.type == SDL_EVENT_KEY_DOWN) {
                if (e.key.key == SDLK_ESCAPE) { running = false; }
                handleDebugEvents(e);
            }
        }

        Rendering::ShaderUniforms uniforms;
        uniforms.iTime = static_cast<float>(SDL_GetTicks() - shader_start_ticks_) / 1000.0f;

        int w = 0;
        int h = 0;
        SDL_GetWindowSize(window_, &w, &h);
        uniforms.iResolutionX = static_cast<float>(w);
        uniforms.iResolutionY = static_cast<float>(h);

        backend_->render(uniforms);

        if (!Options_video.vsync) {
            SDL_Delay(1);
        }
    }

    backend_->cleanup();
    backend_.reset();

    if (current_music_ != nullptr) {
        JA_DeleteMusic(current_music_);
        current_music_ = nullptr;
    }
    JA_Quit();

    SDL_DestroyWindow(window_);
    SDL_Quit();
    return 0;
}
