#include "App.h"

#include <imgui.h>
#include <imgui_impl_sdl3.h>
#include <imgui_impl_sdlrenderer3.h>

#include "Theme.h"
#include "core/Palette.h"
#include "ui/PaletteTab.h"
#include "ui/SourceTab.h"
#include "ui/TilemapTab.h"
#include "ui/TilesTab.h"

namespace bmp2tile
{

bool App::Init()
{
    if (!SDL_Init(SDL_INIT_VIDEO))
    {
        LastError = std::string("SDL_Init failed: ") + SDL_GetError();
        return false;
    }

    Window = SDL_CreateWindow("bmp2tile", 1280, 720, SDL_WINDOW_RESIZABLE | SDL_WINDOW_HIGH_PIXEL_DENSITY);
    if (!Window)
    {
        LastError = std::string("SDL_CreateWindow failed: ") + SDL_GetError();
        return false;
    }

    Renderer = SDL_CreateRenderer(Window, nullptr);
    if (!Renderer)
    {
        LastError = std::string("SDL_CreateRenderer failed: ") + SDL_GetError();
        return false;
    }

    IMGUI_CHECKVERSION();
    ImGui::CreateContext();
    ImGuiIO& io = ImGui::GetIO();
    io.ConfigFlags |= ImGuiConfigFlags_DockingEnable;
    io.Fonts->AddFontDefaultVector();
    ApplyTheme(/*bDarkMode=*/true);

    ImGui_ImplSDL3_InitForSDLRenderer(Window, Renderer);
    ImGui_ImplSDLRenderer3_Init(Renderer);

    bRunning = true;
    return true;
}

void App::Run()
{
    while (bRunning)
    {
        SDL_Event event;
        while (SDL_PollEvent(&event))
        {
            ProcessEvent(event);
        }
        RenderFrame();
    }
}

void App::ProcessEvent(const SDL_Event& event)
{
    ImGui_ImplSDL3_ProcessEvent(&event);

    switch (event.type)
    {
        case SDL_EVENT_QUIT:
            bRunning = false;
            break;
        case SDL_EVENT_WINDOW_CLOSE_REQUESTED:
            if (event.window.windowID == SDL_GetWindowID(Window))
            {
                bRunning = false;
            }
            break;
        case SDL_EVENT_DROP_FILE:
            // Drag-and-drop a bitmap onto the window -- this is the primary
            // workflow the original tool documents ("drag and drop it onto
            // the program"), so it's implemented before any fancier file
            // dialog.
            if (event.drop.data)
            {
                LoadImageFile(event.drop.data);
            }
            break;
        default:
            break;
    }
}

void App::LoadImageFile(const std::string& path)
{
    try
    {
        // 4bpp (16 colour) is bmp2tile's default; a future "bit depth"
        // selector in the UI would thread a different maxColors value
        // through here.
        CurrentImage = LoadImage(path, /*maxColors=*/16);
        bHasImage = true;
        LoadedPath = path;
        LastError.clear();
        LastSaveMessage.clear();
        RebuildSourceTexture();
        RebuildTileSet();
    }
    catch (const std::exception& e)
    {
        LastError = e.what();
    }
}

void App::RebuildTileSet()
{
    if (!bHasImage) return;
    CurrentTileSet =
        ExtractTiles(CurrentImage, bRemoveDuplicates, bUseMirroring, bSprite8x16, bUseSpritePalette, bHighPriority);
    RebuildTileTextures();
}

void App::SavePalette(const std::string& path)
{
    try
    {
        const std::vector<Color> palette = bAlwaysEmit16Colours ? PadPaletteTo16(CurrentImage.Palette) : CurrentImage.Palette;
        SavePaletteBin(path, palette, CurrentPaletteFormat);
        LastSaveMessage = "Saved " + std::to_string(palette.size()) + " colours to " + path;
        LastError.clear();
    }
    catch (const std::exception& e)
    {
        LastError = e.what();
        LastSaveMessage.clear();
    }
}

void App::SaveTiles(const std::string& path)
{
    try
    {
        SaveTilesBin(path, CurrentTileSet.Tiles, TilesCompressionFormat);
        LastSaveMessage = "Saved " + std::to_string(CurrentTileSet.Tiles.size()) + " tiles to " + path;
        LastError.clear();
    }
    catch (const std::exception& e)
    {
        LastError = e.what();
        LastSaveMessage.clear();
    }
}

void App::SaveTilemap(const std::string& path)
{
    try
    {
        SaveTilemapBin(path, CurrentTileSet.Tilemap, CurrentTileSet.TilesWide, TilemapCompressionFormat, TileIndexOffset);
        LastSaveMessage = "Saved " + std::to_string(CurrentTileSet.Tilemap.size()) + " tilemap entries to " + path;
        LastError.clear();
    }
    catch (const std::exception& e)
    {
        LastError = e.what();
        LastSaveMessage.clear();
    }
}

void App::ReleaseTextures()
{
    if (SourceTexture)
    {
        SDL_DestroyTexture(SourceTexture);
        SourceTexture = nullptr;
    }
    for (SDL_Texture* t : TileTextures)
    {
        SDL_DestroyTexture(t);
    }
    TileTextures.clear();
}

namespace
{
std::vector<uint8_t> ToRgba(const IndexedImage& img)
{
    std::vector<uint8_t> rgba(static_cast<size_t>(img.Width) * img.Height * 4);
    for (int y = 0; y < img.Height; ++y)
    {
        for (int x = 0; x < img.Width; ++x)
        {
            const uint8_t idx = img.At(x, y);
            const Color c = (idx < img.Palette.size()) ? img.Palette[idx] : Color{255, 0, 255};
            uint8_t* p = &rgba[(static_cast<size_t>(y) * img.Width + x) * 4];
            p[0] = c.R;
            p[1] = c.G;
            p[2] = c.B;
            p[3] = 255;
        }
    }
    return rgba;
}
}  // namespace

void App::RebuildSourceTexture()
{
    if (SourceTexture)
    {
        SDL_DestroyTexture(SourceTexture);
        SourceTexture = nullptr;
    }
    const std::vector<uint8_t> rgba = ToRgba(CurrentImage);
    SourceTexture =
        SDL_CreateTexture(Renderer, SDL_PIXELFORMAT_RGBA32, SDL_TEXTUREACCESS_STATIC, CurrentImage.Width, CurrentImage.Height);
    if (!SourceTexture) return;

    SDL_UpdateTexture(SourceTexture, nullptr, rgba.data(), CurrentImage.Width * 4);
}

void App::RebuildTileTextures()
{
    for (SDL_Texture* t : TileTextures)
    {
        SDL_DestroyTexture(t);
    }
    TileTextures.clear();
    TileTextures.reserve(CurrentTileSet.Tiles.size());

    std::vector<uint8_t> rgba(TileSize * TileSize * 4);
    for (const Tile& tile : CurrentTileSet.Tiles)
    {
        for (int i = 0; i < TileSize * TileSize; ++i)
        {
            const uint8_t idx = tile.Pixels[static_cast<size_t>(i)];
            const Color c = (idx < CurrentImage.Palette.size()) ? CurrentImage.Palette[idx] : Color{255, 0, 255};
            rgba[i * 4 + 0] = c.R;
            rgba[i * 4 + 1] = c.G;
            rgba[i * 4 + 2] = c.B;
            rgba[i * 4 + 3] = 255;
        }
        SDL_Texture* tex = SDL_CreateTexture(Renderer, SDL_PIXELFORMAT_RGBA32, SDL_TEXTUREACCESS_STATIC, TileSize, TileSize);
        if (tex)
        {
            SDL_UpdateTexture(tex, nullptr, rgba.data(), TileSize * 4);
        }
        // Push nullptr if creation failed so TileTextures stays in sync with Tiles.
        TileTextures.push_back(tex);
    }
}

void App::RenderFrame()
{
    ImGui_ImplSDLRenderer3_NewFrame();
    ImGui_ImplSDL3_NewFrame();
    ImGui::NewFrame();

    const ImGuiViewport* viewport = ImGui::GetMainViewport();
    ImGui::SetNextWindowPos(viewport->WorkPos);
    ImGui::SetNextWindowSize(viewport->WorkSize);
    ImGui::Begin("bmp2tile", nullptr,
                  ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoMove |
                      ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_NoBringToFrontOnFocus);

    if (!LastError.empty())
    {
        ImGui::TextColored(ImVec4(1.0f, 0.4f, 0.4f, 1.0f), "%s", LastError.c_str());
        ImGui::Separator();
    }

    if (ImGui::BeginTabBar("MainTabs"))
    {
        if (ImGui::BeginTabItem("Source"))
        {
            ui::DrawSourceTab(*this);
            ImGui::EndTabItem();
        }
        if (ImGui::BeginTabItem("Tiles"))
        {
            ui::DrawTilesTab(*this);
            ImGui::EndTabItem();
        }
        if (ImGui::BeginTabItem("Tilemap"))
        {
            ui::DrawTilemapTab(*this);
            ImGui::EndTabItem();
        }
        if (ImGui::BeginTabItem("Palette"))
        {
            ui::DrawPaletteTab(*this);
            ImGui::EndTabItem();
        }
        ImGui::EndTabBar();
    }

    ImGui::End();

    ImGui::Render();

    // On a high-density display (Retina etc.), SDL_WINDOW_HIGH_PIXEL_DENSITY
    // makes the window's actual drawable twice (or more) the size it
    // reports via SDL_GetWindowSize() -- see
    // https://wiki.libsdl.org/SDL3/README-highdpi. ImGui already accounts
    // for this in io.DisplayFramebufferScale (set by ImGui_ImplSDL3_NewFrame),
    // but the renderer itself also needs to be told to scale, or ImGui's
    // draw commands -- sized for the logical window size -- only cover a
    // fraction of the physical drawable, appearing squashed into a corner.
    const ImVec2 framebufferScale = ImGui::GetIO().DisplayFramebufferScale;
    SDL_SetRenderScale(Renderer, framebufferScale.x, framebufferScale.y);

    SDL_SetRenderDrawColor(Renderer, 30, 30, 30, 255);
    SDL_RenderClear(Renderer);
    ImGui_ImplSDLRenderer3_RenderDrawData(ImGui::GetDrawData(), Renderer);
    SDL_RenderPresent(Renderer);
}

void App::Shutdown()
{
    ReleaseTextures();
    ImGui_ImplSDLRenderer3_Shutdown();
    ImGui_ImplSDL3_Shutdown();
    ImGui::DestroyContext();

    if (Renderer) SDL_DestroyRenderer(Renderer);
    if (Window) SDL_DestroyWindow(Window);
    SDL_Quit();
}

}  // namespace bmp2tile
