#include "core/rendering/sprite/animated_sprite.hpp"

#include <cmath>      // Para std::fmod
#include <cstddef>    // Para size_t
#include <fstream>    // Para basic_ostream, basic_istream, operator<<, basic...
#include <iostream>   // Para cout, cerr
#include <numeric>    // Para std::accumulate
#include <sstream>    // Para basic_stringstream
#include <stdexcept>  // Para runtime_error
#include <utility>

#include "core/rendering/surface.hpp"          // Para Surface
#include "core/resources/resource_cache.hpp"   // Para Resource
#include "core/resources/resource_helper.hpp"  // Para ResourceHelper
#include "external/fkyaml_node.hpp"            // Para fkyaml::node
#include "utils/utils.hpp"                     // Para printWithDots

// Helper: Convierte un nodo YAML de frames (array) a vector de SDL_FRect
auto convertYAMLFramesToRects(const fkyaml::node& frames_node, float frame_width, float frame_height, int frames_per_row, int max_tiles) -> std::vector<SDL_FRect> {
    std::vector<SDL_FRect> frames;
    SDL_FRect rect = {.x = 0.0F, .y = 0.0F, .w = frame_width, .h = frame_height};

    for (const auto& frame_index_node : frames_node) {
        const int NUM_TILE = frame_index_node.get_value<int>();
        if (NUM_TILE <= max_tiles) {
            rect.x = (NUM_TILE % frames_per_row) * frame_width;
            rect.y = (NUM_TILE / frames_per_row) * frame_height;
            frames.emplace_back(rect);
        }
    }

    return frames;
}

// Helper: parsea el array de animaciones de un nodo YAML
static auto parseAnimations(const fkyaml::node& yaml, float frame_width, float frame_height, int frames_per_row, int max_tiles) -> std::vector<AnimatedSprite::AnimationData> {
    std::vector<AnimatedSprite::AnimationData> animations;
    if (!yaml.contains("animations") || !yaml["animations"].is_sequence()) {
        return animations;
    }

    for (const auto& anim_node : yaml["animations"]) {
        AnimatedSprite::AnimationData animation;

        if (anim_node.contains("name")) {
            animation.name = anim_node["name"].get_value<std::string>();
        }

        if (anim_node.contains("frames") && anim_node["frames"].is_sequence()) {
            animation.frames = convertYAMLFramesToRects(
                anim_node["frames"],
                frame_width,
                frame_height,
                frames_per_row,
                max_tiles);
        }

        if (anim_node.contains("speed")) {
            const auto& speed_node = anim_node["speed"];
            if (speed_node.is_sequence()) {
                for (const auto& s : speed_node) {
                    animation.speeds.push_back(s.get_value<float>());
                }
            } else {
                auto spd = speed_node.get_value<float>();
                if (spd > 0.0F) {
                    animation.speeds.assign(animation.frames.size(), spd);
                }
            }
        }

        if (anim_node.contains("loopFrom")) {
            animation.loop_from = anim_node["loopFrom"].get_value<int>();
        }

        animations.push_back(animation);
    }
    return animations;
}

// Helper: parsea la configuración global (tileSetFile, frameWidth, frameHeight) y calcula parámetros del spritesheet
struct SheetParams {
    float frame_width{0.0F};
    float frame_height{0.0F};
    int frames_per_row{1};
    int max_tiles{1};
};

static auto parseGlobalConfig(const fkyaml::node& yaml, std::shared_ptr<Surface>& surface) -> SheetParams {
    SheetParams params;
    if (yaml.contains("tileSetFile")) {
        surface = Resource::Cache::get()->getSurface(yaml["tileSetFile"].get_value<std::string>());
    }
    if (yaml.contains("frameWidth")) {
        params.frame_width = static_cast<float>(yaml["frameWidth"].get_value<int>());
    }
    if (yaml.contains("frameHeight")) {
        params.frame_height = static_cast<float>(yaml["frameHeight"].get_value<int>());
    }
    if (surface && params.frame_width > 0.0F && params.frame_height > 0.0F) {
        params.frames_per_row = surface->getWidth() / static_cast<int>(params.frame_width);
        const int H = surface->getHeight() / static_cast<int>(params.frame_height);
        params.max_tiles = params.frames_per_row * H;
    }
    return params;
}

// Constructor con bytes YAML del cache
AnimatedSprite::AnimatedSprite(const AnimationResource& cached_data) {
    std::string yaml_content(cached_data.yaml_data.begin(), cached_data.yaml_data.end());

    try {
        auto yaml = fkyaml::node::deserialize(yaml_content);
        auto params = parseGlobalConfig(yaml, surface_);
        animations_ = parseAnimations(yaml, params.frame_width, params.frame_height, params.frames_per_row, params.max_tiles);
        buildNameIndex();

        setWidth(params.frame_width);
        setHeight(params.frame_height);

        if (!animations_.empty() && !animations_[0].frames.empty()) {
            setClip(animations_[0].frames[0]);
        }
    } catch (const fkyaml::exception& e) {
        std::cerr << "YAML parsing error in animation " << cached_data.name << ": " << e.what() << '\n';
        throw;
    } catch (const std::exception& e) {
        std::cerr << "Error loading animation " << cached_data.name << ": " << e.what() << '\n';
        throw;
    }
}

// Constructor per a subclasses amb surface directa (sense YAML)
AnimatedSprite::AnimatedSprite(std::shared_ptr<Surface> surface, SDL_FRect pos)
    : MovingSprite(std::move(surface), pos) {
    // animations_ queda buit (protegit per el guard de animate())
    if (surface_) {
        clip_ = {.x = 0, .y = 0, .w = static_cast<float>(surface_->getWidth()), .h = static_cast<float>(surface_->getHeight())};
    }
}

// Construye el mapa nombre→índice para búsqueda O(1)
void AnimatedSprite::buildNameIndex() {
    animation_index_.clear();
    for (int i = 0; i < static_cast<int>(animations_.size()); ++i) {
        animation_index_[animations_[i].name] = i;
    }
}

// Calcula el frame correspondiente a la animación (time-based)
void AnimatedSprite::animate(float delta_time) {
    if (animations_.empty()) { return; }
    auto& anim = animations_[current_animation_];
    if (anim.speeds.empty()) { return; }  // Animación estática

    anim.accumulated_time += delta_time;

    // Calcular duración total de la animación
    const float TOTAL = std::accumulate(anim.speeds.begin(), anim.speeds.end(), 0.0F);

    // Si hemos superado la duración total, manejar loop o congelar
    if (anim.accumulated_time >= TOTAL) {
        if (anim.loop_from < 0) {
            // Sin loop: congelar en el último frame
            anim.current_frame = static_cast<int>(anim.frames.size()) - 1;
            anim.completed = true;
            setClip(anim.frames[anim.current_frame]);
            return;
        }
        // Con loop: envolver el tiempo en el rango del loop
        const float LOOP_START = std::accumulate(anim.speeds.begin(), anim.speeds.begin() + anim.loop_from, 0.0F);
        const float LOOP_LEN = TOTAL - LOOP_START;
        anim.accumulated_time = LOOP_START + std::fmod(anim.accumulated_time - LOOP_START, LOOP_LEN);
    }

    // Buscar el frame correspondiente al tiempo acumulado
    float cursor = 0.0F;
    for (int i = 0; i < static_cast<int>(anim.frames.size()); ++i) {
        cursor += anim.speeds[i];
        if (anim.accumulated_time < cursor) {
            anim.current_frame = i;
            setClip(anim.frames[i]);
            return;
        }
    }

    // Seguridad: último frame
    anim.current_frame = static_cast<int>(anim.frames.size()) - 1;
    setClip(anim.frames[anim.current_frame]);
}

// Comprueba si ha terminado la animación
auto AnimatedSprite::animationIsCompleted() -> bool {
    return animations_[current_animation_].completed;
}

// Establece la animacion actual por nombre
void AnimatedSprite::setCurrentAnimation(const std::string& name) {
    auto it = animation_index_.find(name);
    if (it == animation_index_.end()) {
        std::cout << "** Warning: could not find \"" << name << "\" animation" << '\n';
        return;
    }
    setCurrentAnimation(it->second);
}

// Establece la animacion actual
void AnimatedSprite::setCurrentAnimation(int index) {
    const auto NEW_ANIMATION = index;
    if (current_animation_ != NEW_ANIMATION) {
        current_animation_ = NEW_ANIMATION;
        animations_[current_animation_].current_frame = 0;
        animations_[current_animation_].accumulated_time = 0.0F;
        animations_[current_animation_].completed = false;
        setClip(animations_[current_animation_].frames[animations_[current_animation_].current_frame]);
    }
}

// Actualiza las variables del objeto (time-based)
void AnimatedSprite::update(float delta_time) {
    animate(delta_time);
    MovingSprite::update(delta_time);
}

// Reinicia la animación
void AnimatedSprite::resetAnimation() {
    animations_[current_animation_].current_frame = 0;
    animations_[current_animation_].accumulated_time = 0.0F;
    animations_[current_animation_].completed = false;
}

// Establece el frame actual de la animación
void AnimatedSprite::setCurrentAnimationFrame(int num) {
    // Descarta valores fuera de rango
    if (num < 0 || num >= static_cast<int>(animations_[current_animation_].frames.size())) {
        num = 0;
    }

    // Cambia el valor de la variable
    animations_[current_animation_].current_frame = num;

    // Escoge el frame correspondiente de la animación
    setClip(animations_[current_animation_].frames[animations_[current_animation_].current_frame]);
}