#include "core/rendering/palette_manager.hpp"

#include <algorithm>
#include <cctype>
#include <climits>
#include <string>
#include <vector>

#include "core/rendering/surface.hpp"
#include "core/resources/resource_cache.hpp"
#include "game/defaults.hpp"
#include "game/options.hpp"
#include "utils/utils.hpp"

// ── Conversión string ↔ PaletteSortMode ──────────────────────────────────────

auto sortModeFromString(const std::string& str) -> PaletteSortMode {
    const std::string LOWER = toLower(str);
    if (LOWER == "optimal") { return PaletteSortMode::OPTIMAL; }
    if (LOWER == "reference") { return PaletteSortMode::REFERENCE; }
    return PaletteSortMode::ORIGINAL;
}

auto sortModeToString(PaletteSortMode mode) -> std::string {
    switch (mode) {
        case PaletteSortMode::OPTIMAL:
            return "optimal";
        case PaletteSortMode::REFERENCE:
            return "reference";
        default:
            return "original";
    }
}

// ── Helpers de color y ordenación de paletas ─────────────────────────────────

namespace {
    // Helpers para extraer componentes RGB de un color ARGB (0xAARRGGBB)
    constexpr auto redOf(Uint32 c) -> int { return static_cast<int>((c >> 16) & 0xFF); }
    constexpr auto greenOf(Uint32 c) -> int { return static_cast<int>((c >> 8) & 0xFF); }
    constexpr auto blueOf(Uint32 c) -> int { return static_cast<int>(c & 0xFF); }

    // Distancia euclídea al cuadrado en espacio RGB (no necesita sqrt para comparar)
    auto rgbDistanceSq(Uint32 a, Uint32 b) -> int {
        const int DR = redOf(a) - redOf(b);
        const int DG = greenOf(a) - greenOf(b);
        const int DB = blueOf(a) - blueOf(b);
        return (DR * DR) + (DG * DG) + (DB * DB);
    }

    // Cuenta los colores activos en la paleta (los que tienen alpha != 0)
    auto countActiveColors(const Palette& palette) -> size_t {
        size_t count = 0;
        for (const auto& c : palette) {
            if (c == 0) { break; }
            ++count;
        }
        return count;
    }

    namespace {
        // Construeix la matriu de cost NxM (ampliada a SZxSZ amb zeros) per a l'algoritme hongarès.
        auto buildCostMatrix(int n_rows, int m_cols, int sz, const Palette& palette, const Palette& reference) -> std::vector<int> {
            std::vector<int> cost(static_cast<size_t>(sz) * static_cast<size_t>(sz), 0);
            for (int i = 0; i < n_rows; ++i) {
                for (int j = 0; j < m_cols; ++j) {
                    cost[(i * sz) + j] = rgbDistanceSq(palette[i], reference[j]);
                }
            }
            return cost;
        }

        // Estat compartit entre les fases d'una iteració del Kuhn-Munkres
        struct HungarianStep {
            int j0;
            int delta;
            int j1;
        };

        // Cerca la columna j1 que minimitza el cost reduït, actualitzant minv[] i way[].
        auto relaxColumns(int sz, const std::vector<int>& cost, int j0, int i0, const std::vector<bool>& used, const std::vector<int>& u, const std::vector<int>& v, std::vector<int>& minv, std::vector<int>& way) -> HungarianStep {
            constexpr int INF = INT_MAX / 2;
            HungarianStep step{.j0 = j0, .delta = INF, .j1 = 0};
            for (int j = 1; j <= sz; ++j) {
                if (used[j]) { continue; }
                const int CUR = cost[((i0 - 1) * sz) + (j - 1)] - u[i0] - v[j];
                if (CUR < minv[j]) {
                    minv[j] = CUR;
                    way[j] = j0;
                }
                if (minv[j] < step.delta) {
                    step.delta = minv[j];
                    step.j1 = j;
                }
            }
            return step;
        }

        // Aplica el delta calculat a potencials (u, v) i a minv[].
        void applyDelta(int sz, int delta, const std::vector<bool>& used, const std::vector<int>& p, std::vector<int>& u, std::vector<int>& v, std::vector<int>& minv) {
            for (int j = 0; j <= sz; ++j) {
                if (used[j]) {
                    u[p[j]] += delta;
                    v[j] -= delta;
                } else {
                    minv[j] -= delta;
                }
            }
        }

        // Algoritme hongarès (Kuhn-Munkres) basat en potencials. Retorna p[],
        // on p[j] = fila assignada a la columna j (índexs 1-based; p[0] no s'usa).
        auto hungarianAssign(int sz, const std::vector<int>& cost) -> std::vector<int> {
            constexpr int INF = INT_MAX / 2;
            std::vector<int> u(sz + 1, 0);  // Potencials de files
            std::vector<int> v(sz + 1, 0);  // Potencials de columnes
            std::vector<int> p(sz + 1, 0);  // p[j] = fila assignada a columna j
            std::vector<int> way(sz + 1, 0);

            for (int i = 1; i <= sz; ++i) {
                p[0] = i;
                int j0 = 0;
                std::vector<int> minv(sz + 1, INF);
                std::vector<bool> used(sz + 1, false);

                do {
                    used[j0] = true;
                    const auto STEP = relaxColumns(sz, cost, j0, p[j0], used, u, v, minv, way);
                    applyDelta(sz, STEP.delta, used, p, u, v, minv);
                    j0 = STEP.j1;
                } while (p[j0] != 0);

                do {
                    const int J1 = way[j0];
                    p[j0] = p[J1];
                    j0 = J1;
                } while (j0 != 0);
            }
            return p;
        }

        // Construeix la paleta resultant a partir de l'assignació p[]. Afegeix els colors
        // de palette sense parella en reference al final (cas N > M).
        auto buildPaletteFromAssignment(int n_rows, int m_cols, const std::vector<int>& p, const Palette& palette) -> Palette {
            Palette out{};
            out.fill(0);
            for (int j = 1; j <= m_cols && j <= n_rows; ++j) {
                const int ROW = p[j] - 1;
                if (ROW >= 0 && ROW < n_rows) { out[j - 1] = palette[ROW]; }
            }
            if (n_rows <= m_cols) { return out; }

            std::vector<bool> used_rows(n_rows, false);
            for (int j = 1; j <= m_cols; ++j) {
                const int ROW = p[j] - 1;
                if (ROW >= 0 && ROW < n_rows) { used_rows[ROW] = true; }
            }
            int out_idx = m_cols;
            for (int i = 0; i < n_rows; ++i) {
                if (!used_rows[i]) { out[out_idx++] = palette[i]; }
            }
            return out;
        }
    }  // namespace

    // Asignación óptima de colores mediante el algoritmo húngaro (Kuhn-Munkres).
    // Minimiza la distancia RGB total entre la paleta y la referencia.
    // O(N³) con N = número de colores activos — para N ≤ 256 es instantáneo.
    auto sortByOptimal(const Palette& palette, const Palette& reference) -> Palette {
        const auto N = static_cast<int>(countActiveColors(palette));
        const auto M = static_cast<int>(countActiveColors(reference));
        const int SZ = std::max(N, M);
        if (SZ == 0) { return palette; }

        const auto COST = buildCostMatrix(N, M, SZ, palette, reference);
        const auto P = hungarianAssign(SZ, COST);
        return buildPaletteFromAssignment(N, M, P, palette);
    }

    // Asignación greedy de colores a la paleta de referencia.
    // Más rápida pero puede asignar colores subóptimamente.
    auto sortByReference(const Palette& palette, const Palette& reference) -> Palette {
        const size_t REF_COUNT = countActiveColors(reference);
        const size_t N = countActiveColors(palette);
        std::vector<Uint32> available(palette.begin(), palette.begin() + static_cast<ptrdiff_t>(N));
        std::vector<Uint32> result;
        result.reserve(N);

        // Para cada color de referencia, buscar el más cercano disponible
        const size_t REFS = std::min(N, REF_COUNT);
        for (size_t i = 0; i < REFS && !available.empty(); ++i) {
            const Uint32 REF = reference[i];
            auto best = std::ranges::min_element(available, [REF](Uint32 a, Uint32 b) {
                return rgbDistanceSq(a, REF) < rgbDistanceSq(b, REF);
            });
            result.push_back(*best);
            available.erase(best);
        }

        // Si quedan colores sin asignar, añadirlos al final
        std::ranges::copy(available, std::back_inserter(result));

        Palette out{};
        out.fill(0);
        std::ranges::copy(result, out.begin());
        return out;
    }
}  // namespace

// ── PaletteManager ───────────────────────────────────────────────────────────

PaletteManager::PaletteManager(
    std::vector<std::string> raw_paths,
    const std::string& initial_name,
    PaletteSortMode initial_sort_mode,
    std::shared_ptr<Surface> game_surface,
    std::shared_ptr<Surface> border_surface,
    OnChangeCallback on_change)
    : palettes_(std::move(raw_paths)),
      sort_mode_(initial_sort_mode),
      game_surface_(std::move(game_surface)),
      border_surface_(std::move(border_surface)),
      on_change_(std::move(on_change)) {
    current_ = findIndex(initial_name);

    // Leer la paleta de referencia directamente desde el archivo
    // (Resource::Cache aún no está disponible en este punto del ciclo de vida)
    const std::string REF_NAME = std::string(Defaults::Video::PALETTE_NAME) + ".pal";
    auto ref_it = std::ranges::find_if(palettes_, [&](const auto& p) {
        return getFileName(p) == REF_NAME;
    });
    if (ref_it != palettes_.end()) {
        reference_palette_ = readPalFile(*ref_it);
    }

    // Leer y aplicar paleta inicial
    const auto INITIAL_PALETTE = sortPalette(readPalFile(palettes_.at(current_)), sort_mode_, reference_palette_);
    game_surface_->setPalette(INITIAL_PALETTE);
    border_surface_->setPalette(INITIAL_PALETTE);

    // Procesar la lista: conservar solo los nombres de archivo (sin ruta)
    processPathList();
}

void PaletteManager::next() {
    if (++current_ == palettes_.size()) {
        current_ = 0;
    }
    apply();
}

void PaletteManager::previous() {
    current_ = (current_ > 0) ? current_ - 1 : palettes_.size() - 1;
    apply();
}

auto PaletteManager::setByName(const std::string& name) -> bool {
    const std::string LOWER_NAME = toLower(name + ".pal");
    for (size_t i = 0; i < palettes_.size(); ++i) {
        if (toLower(palettes_[i]) == LOWER_NAME) {
            current_ = i;
            apply();
            return true;
        }
    }
    return false;
}

auto PaletteManager::getNames() const -> std::vector<std::string> {
    std::vector<std::string> names;
    names.reserve(palettes_.size());
    for (const auto& p : palettes_) {
        std::string name = p;
        const size_t POS = name.find(".pal");
        if (POS != std::string::npos) { name.erase(POS, 4); }
        std::ranges::transform(name, name.begin(), ::tolower);
        names.push_back(std::move(name));
    }
    return names;
}

auto PaletteManager::getCurrentName() const -> std::string {
    std::string name = palettes_.at(current_);
    const size_t POS = name.find(".pal");
    if (POS != std::string::npos) { name.erase(POS, 4); }
    std::ranges::transform(name, name.begin(), ::tolower);
    return name;
}

auto PaletteManager::getPrettyName() const -> std::string {
    std::string name = getCurrentName();
    std::ranges::replace(name, '-', ' ');
    return name;
}

void PaletteManager::nextSortMode() {
    sort_mode_ = static_cast<PaletteSortMode>((static_cast<int>(sort_mode_) + 1) % static_cast<int>(PaletteSortMode::COUNT));
    Options::video.palette_sort = sortModeToString(sort_mode_);
    apply();
}

void PaletteManager::setSortMode(PaletteSortMode mode) {
    sort_mode_ = mode;
    Options::video.palette_sort = sortModeToString(sort_mode_);
    apply();
}

auto PaletteManager::getSortMode() const -> PaletteSortMode {
    return sort_mode_;
}

auto PaletteManager::getSortModeName() const -> std::string {
    return sortModeToString(sort_mode_);
}

void PaletteManager::apply() {
    Palette raw = Resource::Cache::get()->getPalette(palettes_.at(current_));
    Palette sorted = sortPalette(raw, sort_mode_, reference_palette_);
    game_surface_->loadPalette(sorted);
    border_surface_->loadPalette(sorted);

    Options::video.palette = getCurrentName();

    if (on_change_) {
        on_change_();
    }
}

auto PaletteManager::findIndex(const std::string& name) const -> size_t {
    const std::string LOWER_NAME = toLower(name + ".pal");
    for (size_t i = 0; i < palettes_.size(); ++i) {
        if (toLower(getFileName(palettes_[i])) == LOWER_NAME) {
            return i;
        }
    }
    // Fallback: buscar la paleta por defecto
    const std::string DEFAULT_NAME = toLower(std::string(Defaults::Video::PALETTE_NAME) + ".pal");
    for (size_t i = 0; i < palettes_.size(); ++i) {
        if (toLower(getFileName(palettes_[i])) == DEFAULT_NAME) {
            return i;
        }
    }
    return 0;
}

void PaletteManager::processPathList() {
    for (auto& palette : palettes_) {
        palette = getFileName(palette);
    }
}

auto PaletteManager::sortPalette(const Palette& palette, PaletteSortMode mode, const Palette& reference) -> Palette {
    switch (mode) {
        case PaletteSortMode::OPTIMAL:
            return sortByOptimal(palette, reference);
        case PaletteSortMode::REFERENCE:
            return sortByReference(palette, reference);
        default:
            return palette;
    }
}
