#include "core/resources/resource_cache.hpp"

#include <SDL3/SDL.h>

#include <algorithm>
#include <cstdlib>
#include <iostream>
#include <stdexcept>

#include "core/audio/jail_audio.hpp"
#include "core/resources/resource_helper.hpp"
#include "core/resources/resource_list.hpp"

// gif.h ja s'inclou des de jdraw8.cpp i text.cpp; el seu codi no és static
// ni inline, així que no podem tornar-lo a incloure aquí. Ens fiem de les
// declaracions extern dels símbols que ens calen (linkatge C++ normal,
// igual que fa text.cpp).
// NOLINTBEGIN(readability-identifier-naming) — símbols externs de gif.h.
extern auto LoadGif(unsigned char* data, unsigned short* w, unsigned short* h) -> unsigned char*;
extern auto LoadPalette(unsigned char* data) -> unsigned char*;
// NOLINTEND(readability-identifier-naming)

namespace Resource {

    std::unique_ptr<Cache> Cache::instance;

    void Cache::init() { instance = std::unique_ptr<Cache>(new Cache()); }
    void Cache::destroy() { instance.reset(); }
    auto Cache::get() -> Cache* { return instance.get(); }

    namespace {
        auto basename(const std::string& path) -> std::string {
            auto pos = path.find_last_of("/\\");
            return pos == std::string::npos ? path : path.substr(pos + 1);
        }
    }  // namespace

    auto Cache::getMusic(const std::string& name) -> Ja::Music* {
        auto it = std::ranges::find_if(musics_, [&](const auto& m) { return m.name == name; });
        if (it != musics_.end()) {
            return it->music.get();
        }
        std::cerr << "Resource::Cache: música no trobada: " << name << '\n';
        throw std::runtime_error("Music not found: " + name);
    }

    auto Cache::getSound(const std::string& name) -> Ja::Sound* {
        auto it = std::ranges::find_if(sounds_, [&](const auto& s) { return s.name == name; });
        if (it != sounds_.end()) {
            return it->sound.get();
        }
        std::cerr << "Resource::Cache: so no trobat: " << name << '\n';
        throw std::runtime_error("Sound not found: " + name);
    }

    auto Cache::getSurfacePixels(const std::string& name) -> const std::vector<Uint8>& {
        auto it = std::ranges::find_if(surfaces_, [&](const auto& s) { return s.name == name; });
        if (it != surfaces_.end()) {
            return it->pixels;
        }
        std::cerr << "Resource::Cache: surface no trobada: " << name << '\n';
        throw std::runtime_error("Surface not found: " + name);
    }

    auto Cache::getPaletteBytes(const std::string& name) -> const std::vector<Uint8>& {
        auto it = std::ranges::find_if(surfaces_, [&](const auto& s) { return s.name == name; });
        if (it != surfaces_.end()) {
            return it->palette;
        }
        std::cerr << "Resource::Cache: paleta no trobada: " << name << '\n';
        throw std::runtime_error("Palette not found: " + name);
    }

    auto Cache::getTextFile(const std::string& name) -> const std::vector<uint8_t>& {
        auto it = std::ranges::find_if(text_files_, [&](const auto& t) { return t.name == name; });
        if (it != text_files_.end()) {
            return it->bytes;
        }
        std::cerr << "Resource::Cache: text file no trobat: " << name << '\n';
        throw std::runtime_error("TextFile not found: " + name);
    }

    void Cache::calculateTotal() {
        const auto* list = List::get();
        total_count_ = static_cast<int>(
            list->getListByType(List::Type::MUSIC).size() +
            list->getListByType(List::Type::SOUND).size() +
            list->getListByType(List::Type::BITMAP).size() +
            list->getListByType(List::Type::DATA).size() +
            list->getListByType(List::Type::FONT).size());
        loaded_count_ = 0;
    }

    auto Cache::getProgress() const -> float {
        if (total_count_ == 0) {
            return 1.0F;
        }
        return static_cast<float>(loaded_count_) / static_cast<float>(total_count_);
    }

    void Cache::beginLoad() {
        calculateTotal();
        stage_ = LoadStage::MUSICS;
        stage_index_ = 0;
        std::cout << "Resource::Cache: precarregant " << total_count_ << " assets\n";
    }

    void Cache::stepEachInList(List::Type type, const std::function<void()>& clear_fn, LoadStage next, const std::function<void(size_t)>& load_fn) {
        auto items = List::get()->getListByType(type);
        if (stage_index_ == 0) {
            clear_fn();
        }
        if (stage_index_ >= items.size()) {
            stage_ = next;
            stage_index_ = 0;
            return;
        }
        load_fn(stage_index_++);
    }

    void Cache::stepTextFiles() {
        auto data_items = List::get()->getListByType(List::Type::DATA);
        auto font_items = List::get()->getListByType(List::Type::FONT);
        auto items = data_items;
        items.insert(items.end(), font_items.begin(), font_items.end());
        if (stage_index_ == 0) {
            text_files_.clear();
        }
        if (stage_index_ >= items.size()) {
            stage_ = LoadStage::DONE;
            stage_index_ = 0;
            std::cout << "Resource::Cache: precarrega completada (" << loaded_count_ << "/" << total_count_ << ")\n";
            return;
        }
        loadOneTextFile(stage_index_++);
    }

    auto Cache::loadStep(int budget_ms) -> bool {
        if (stage_ == LoadStage::DONE) {
            return true;
        }

        const Uint64 START_NS = SDL_GetTicksNS();
        const Uint64 BUDGET_NS = static_cast<Uint64>(budget_ms) * 1'000'000ULL;

        while (stage_ != LoadStage::DONE) {
            switch (stage_) {
                case LoadStage::MUSICS:
                    stepEachInList(List::Type::MUSIC, [this] { musics_.clear(); }, LoadStage::SOUNDS, [this](size_t i) { loadOneMusic(i); });
                    break;
                case LoadStage::SOUNDS:
                    stepEachInList(List::Type::SOUND, [this] { sounds_.clear(); }, LoadStage::BITMAPS, [this](size_t i) { loadOneSound(i); });
                    break;
                case LoadStage::BITMAPS:
                    stepEachInList(List::Type::BITMAP, [this] { surfaces_.clear(); }, LoadStage::TEXT_FILES, [this](size_t i) { loadOneBitmap(i); });
                    break;
                case LoadStage::TEXT_FILES:
                    stepTextFiles();
                    break;
                case LoadStage::DONE:
                    break;
            }
            if ((SDL_GetTicksNS() - START_NS) >= BUDGET_NS) {
                break;
            }
        }

        return stage_ == LoadStage::DONE;
    }

    void Cache::loadOneMusic(size_t index) {
        auto items = List::get()->getListByType(List::Type::MUSIC);
        const auto& path = items[index];
        auto name = basename(path);
        current_loading_name_ = name;

        auto bytes = ResourceHelper::loadFile(path);
        if (bytes.empty()) {
            std::cerr << "Resource::Cache: no s'ha pogut llegir " << path << '\n';
            return;
        }
        Ja::Music* music = Ja::loadMusic(bytes.data(), static_cast<Uint32>(bytes.size()), path.c_str());
        if (music == nullptr) {
            std::cerr << "Resource::Cache: Ja::loadMusic ha fallat per " << path << '\n';
            return;
        }
        musics_.push_back(MusicResource{.name = name, .music = std::unique_ptr<Ja::Music, MusicDeleter>(music)});
        ++loaded_count_;
        std::cout << "  [music ] " << name << '\n';
    }

    void Cache::loadOneSound(size_t index) {
        auto items = List::get()->getListByType(List::Type::SOUND);
        const auto& path = items[index];
        auto name = basename(path);
        current_loading_name_ = name;

        auto bytes = ResourceHelper::loadFile(path);
        if (bytes.empty()) {
            std::cerr << "Resource::Cache: no s'ha pogut llegir " << path << '\n';
            return;
        }
        Ja::Sound* sound = Ja::loadSound(bytes.data(), static_cast<uint32_t>(bytes.size()));
        if (sound == nullptr) {
            std::cerr << "Resource::Cache: Ja::loadSound ha fallat per " << path << '\n';
            return;
        }
        sounds_.push_back(SoundResource{.name = name, .sound = std::unique_ptr<Ja::Sound, SoundDeleter>(sound)});
        ++loaded_count_;
        std::cout << "  [sound ] " << name << '\n';
    }

    void Cache::loadOneBitmap(size_t index) {
        auto items = List::get()->getListByType(List::Type::BITMAP);
        const auto& path = items[index];
        auto name = basename(path);
        current_loading_name_ = name;

        auto bytes = ResourceHelper::loadFile(path);
        if (bytes.empty()) {
            std::cerr << "Resource::Cache: no s'ha pogut llegir " << path << '\n';
            return;
        }

        // Decodifica píxels.
        unsigned short w = 0;
        unsigned short h = 0;
        unsigned char* pixels = LoadGif(bytes.data(), &w, &h);
        if (pixels == nullptr) {
            std::cerr << "Resource::Cache: LoadGif ha fallat per " << path << '\n';
            return;
        }
        SurfaceResource res;
        res.name = name;
        res.pixels.assign(pixels, pixels + 64000);
        std::free(pixels);

        // Decodifica paleta des del mateix GIF (necessita una segona passada
        // perquè LoadGif no exposa la paleta).
        unsigned char* palette = LoadPalette(bytes.data());
        if (palette != nullptr) {
            res.palette.assign(palette, palette + 768);
            std::free(palette);
        }

        surfaces_.push_back(std::move(res));
        ++loaded_count_;
        std::cout << "  [bitmap] " << name << '\n';
    }

    void Cache::loadOneTextFile(size_t index) {
        auto data_items = List::get()->getListByType(List::Type::DATA);
        auto font_items = List::get()->getListByType(List::Type::FONT);
        auto items = data_items;
        items.insert(items.end(), font_items.begin(), font_items.end());
        const auto& path = items[index];
        auto name = basename(path);
        current_loading_name_ = name;

        auto bytes = ResourceHelper::loadFile(path);
        if (bytes.empty()) {
            std::cerr << "Resource::Cache: no s'ha pogut llegir " << path << '\n';
            return;
        }
        text_files_.push_back(TextFileResource{.name = name, .bytes = std::move(bytes)});
        ++loaded_count_;
        std::cout << "  [text  ] " << name << '\n';
    }

}  // namespace Resource
