// IWYU pragma: no_include <bits/std_abs.h>
#include "core/rendering/sprite/path_sprite.hpp"

#include <cmath>       // Para abs
#include <functional>  // Para function
#include <utility>     // Para move

// Constructor para paths por puntos (convertido a segundos)
Path::Path(const std::vector<SDL_FPoint>& spots_init, float waiting_time_s_init)
    : spots(spots_init),
      is_point_path(true) {
    waiting_time_s = waiting_time_s_init;
}

// Devuelve un vector con los puntos que conforman la ruta
auto createPath(float start, float end, PathType type, float fixed_pos, int steps, const std::function<double(double)>& easing_function) -> std::vector<SDL_FPoint> {
    std::vector<SDL_FPoint> v;
    v.reserve(steps);

    for (int i = 0; i < steps; ++i) {
        double t = static_cast<double>(i) / (steps - 1);
        double value = start + ((end - start) * easing_function(t));

        if ((start > 0 && end < 0) || (start < 0 && end > 0)) {
            value = start + ((end > 0 ? 1 : -1) * std::abs(end - start) * easing_function(t));
        }

        switch (type) {
            case PathType::HORIZONTAL:
                v.emplace_back(SDL_FPoint{.x = static_cast<float>(value), .y = fixed_pos});
                break;
            case PathType::VERTICAL:
                v.emplace_back(SDL_FPoint{.x = fixed_pos, .y = static_cast<float>(value)});
                break;
            default:
                break;
        }
    }

    return v;
}

// Actualiza la posición y comprueba si ha llegado a su destino
void PathSprite::update(float delta_time) {
    if (enabled_ && !has_finished_) {
        moveThroughCurrentPath(delta_time);
        goToNextPathOrDie();
    }
}

// Muestra el sprite por pantalla
void PathSprite::render() {
    if (enabled_) {
        Sprite::render();
    }
}

// Determina el tipo de centrado basado en el tipo de path
auto PathSprite::determineCenteringType(const Path& path, bool centered) -> PathCentered {
    if (!centered) {
        return PathCentered::NONE;
    }

    if (path.is_point_path) {
        // Lógica de centrado para paths por PUNTOS
        if (!path.spots.empty()) {
            // Si X es constante, es un path Vertical, centramos en X
            return (path.spots.back().x == path.spots.front().x) ? PathCentered::ON_X : PathCentered::ON_Y;
        }
        return PathCentered::NONE;
    }

    // Lógica de centrado para paths GENERADOS
    // Si el tipo es Vertical, centramos en X
    return (path.type == PathType::VERTICAL) ? PathCentered::ON_X : PathCentered::ON_Y;
}

// Aplica centrado en el eje X (para paths verticales)
void PathSprite::centerPathOnX(Path& path, float offset) {
    if (path.is_point_path) {
        const float X_BASE = !path.spots.empty() ? path.spots.front().x : 0.0F;
        const float X = X_BASE - offset;
        for (auto& spot : path.spots) {
            spot.x = X;
        }
    } else {
        // Es un path generado, ajustamos la posición fija (que es X)
        path.fixed_pos -= offset;
    }
}

// Aplica centrado en el eje Y (para paths horizontales)
void PathSprite::centerPathOnY(Path& path, float offset) {
    if (path.is_point_path) {
        const float Y_BASE = !path.spots.empty() ? path.spots.front().y : 0.0F;
        const float Y = Y_BASE - offset;
        for (auto& spot : path.spots) {
            spot.y = Y;
        }
    } else {
        // Es un path generado, ajustamos la posición fija (que es Y)
        path.fixed_pos -= offset;
    }
}

// Añade un recorrido
void PathSprite::addPath(Path path, bool centered) {
    PathCentered path_centered = determineCenteringType(path, centered);

    switch (path_centered) {
        case PathCentered::ON_X:
            centerPathOnX(path, pos_.w / 2.0F);
            break;
        case PathCentered::ON_Y:
            centerPathOnY(path, pos_.h / 2.0F);
            break;
        case PathCentered::NONE:
            break;
    }

    paths_.emplace_back(std::move(path));
}

// Añade un recorrido generado (en segundos)
void PathSprite::addPath(float start, float end, PathType type, float fixed_pos, float duration_s, const std::function<double(double)>& easing_function, float waiting_time_s) {
    paths_.emplace_back(start, end, type, fixed_pos, duration_s, waiting_time_s, easing_function);
}

// Añade un recorrido por puntos (en segundos)
void PathSprite::addPath(const std::vector<SDL_FPoint>& spots, float waiting_time_s) {
    paths_.emplace_back(spots, waiting_time_s);
}

// Habilita el objeto
void PathSprite::enable() {
    if (paths_.empty() || enabled_) {
        return;
    }

    enabled_ = true;

    // Establece la posición inicial
    auto& path = paths_.at(current_path_);
    if (path.is_point_path) {
        const auto& p = path.spots.at(path.counter);
        setPosition(p);
    } else {
        // Para paths generados, establecer posición inicial
        SDL_FPoint initial_pos;
        if (path.type == PathType::HORIZONTAL) {
            initial_pos = {.x = path.start_pos, .y = path.fixed_pos};
        } else {
            initial_pos = {.x = path.fixed_pos, .y = path.start_pos};
        }
        setPosition(initial_pos);
    }
}

// Coloca el sprite en los diferentes puntos del recorrido
void PathSprite::moveThroughCurrentPath(float delta_time) {
    auto& path = paths_.at(current_path_);

    if (path.is_point_path) {
        // Lógica para paths por puntos (compatibilidad)
        const auto& p = path.spots.at(path.counter);
        setPosition(p);

        if (!path.on_destination) {
            ++path.counter;
            if (std::cmp_greater_equal(path.counter, path.spots.size())) {
                path.on_destination = true;
                path.counter = static_cast<int>(path.spots.size()) - 1;
            }
        }

        if (path.on_destination) {
            path.waiting_elapsed += delta_time;
            if (path.waiting_elapsed >= path.waiting_time_s) {
                path.finished = true;
            }
        }
    } else {
        // Lógica para paths generados en tiempo real
        if (!path.on_destination) {
            path.elapsed_time += delta_time;

            // Calcular progreso (0.0 a 1.0)
            float progress = path.elapsed_time / path.duration_s;
            if (progress >= 1.0F) {
                progress = 1.0F;
                path.on_destination = true;
            }

            // Aplicar función de easing
            double eased_progress = path.easing_function(progress);

            // Calcular posición actual
            float current_pos = path.start_pos + ((path.end_pos - path.start_pos) * static_cast<float>(eased_progress));

            // Establecer posición según el tipo
            SDL_FPoint position;
            if (path.type == PathType::HORIZONTAL) {
                position = {.x = current_pos, .y = path.fixed_pos};
            } else {
                position = {.x = path.fixed_pos, .y = current_pos};
            }
            setPosition(position);
        } else {
            // Esperar en destino
            path.waiting_elapsed += delta_time;
            if (path.waiting_elapsed >= path.waiting_time_s) {
                path.finished = true;
            }
        }
    }
}

// Cambia de recorrido o finaliza
void PathSprite::goToNextPathOrDie() {
    // Comprueba si ha terminado el recorrdo actual
    if (paths_.at(current_path_).finished) {
        ++current_path_;
    }

    // Comprueba si quedan mas recorridos
    if (std::cmp_greater_equal(current_path_, paths_.size())) {
        has_finished_ = true;
        current_path_ = 0;
    }
}

// Indica si ha terminado todos los recorridos
auto PathSprite::hasFinished() const -> bool { return has_finished_; }