// demo_pilot.cpp - Implementación de la IA del attract mode
// © 2026 JailDesigner

#include "game/systems/demo_pilot.hpp"

#include <cmath>
#include <cstdlib>

#include "core/types.hpp"

namespace Systems::Demo {

    namespace {

        constexpr float PI = Constants::PI;
        constexpr float TWO_PI = 2.0F * PI;

        // Cadencia y reacción (segundos).
        constexpr float RETARGET_INTERVAL = 0.15F;  // re-evaluar objetivo (reacción humana)
        constexpr float FIRE_COOLDOWN = 0.32F;      // entre disparos

        // Apuntado.
        constexpr float ROTATE_DEADZONE = 0.05F;  // rad: zona muerta para no oscilar
        constexpr float FIRE_TOLERANCE = 0.18F;   // rad: error máximo para disparar
        constexpr float AIM_JITTER_MAX = 0.10F;   // rad: error de apuntado humano
        constexpr float LEAD_TIME = 0.30F;        // s: anticipación sobre el enemigo

        // Distancias (px).
        constexpr float DANGER_RADIUS = 95.0F;     // por debajo: maniobra de esquiva
        constexpr float APPROACH_RADIUS = 250.0F;  // por encima: acercarse al objetivo
        constexpr float WALL_MARGIN = 60.0F;       // px desde el borde: sesgo al centro

        constexpr float WALL_BIAS = 0.6F;  // peso del empuje hacia el centro al esquivar

        // Esquiva de balas enemigas.
        constexpr float DODGE_SCAN_RADIUS = 190.0F;  // px: distancia a la que reacciona
        constexpr float DODGE_HEADING_MIN = 0.25F;   // dot mínimo: la bala viene hacia la nave

        // [-1, 1] aleatorio (estética: jitter de apuntado; no afecta a la simulación).
        auto randSigned() -> float {
            return (static_cast<float>(std::rand()) / static_cast<float>(RAND_MAX) * 2.0F) - 1.0F;
        }

        // Envuelve a [-PI, PI].
        auto wrapPi(float angle) -> float {
            return std::remainder(angle, TWO_PI);
        }

        struct Nearest {
            int index{-1};
            float distance{0.0F};
            Vec2 center{};
            Vec2 velocity{};
        };

        auto findNearest(const Vec2& from,
            const std::array<Enemy, Constants::MAX_ORNIS>& enemies) -> Nearest {
            Nearest best;
            float best_d2 = 0.0F;
            for (std::size_t i = 0; i < enemies.size(); ++i) {
                if (!enemies[i].isActive()) {
                    continue;
                }
                const Vec2 C = enemies[i].getCenter();
                const float D2 = (C - from).lengthSquared();
                if (best.index == -1 || D2 < best_d2) {
                    best.index = static_cast<int>(i);
                    best_d2 = D2;
                    best.center = C;
                    best.velocity = enemies[i].getVelocityVector();
                }
            }
            if (best.index != -1) {
                best.distance = std::sqrt(best_d2);
            }
            return best;
        }

        struct BulletThreat {
            bool found{false};
            Vec2 position{};
            Vec2 velocity{};
        };

        // Bala enemiga más cercana que viene hacia la nave (dentro del radio de
        // reacción). Solo balas de enemic (owner_id >= ENEMY_OWNER_BASE).
        auto findBulletThreat(const Vec2& ship_pos,
            const std::array<Bullet, static_cast<std::size_t>(Defaults::Entities::MAX_BULLETS_TOTAL)>& bullets)
            -> BulletThreat {
            BulletThreat threat;
            float best_d2 = 0.0F;
            for (const auto& bullet : bullets) {
                if (!bullet.isActive() ||
                    bullet.getOwnerId() < Defaults::Entities::ENEMY_OWNER_BASE) {
                    continue;
                }
                const Vec2 BPOS = bullet.getCenter();
                const Vec2 TO_SHIP = ship_pos - BPOS;
                const float D2 = TO_SHIP.lengthSquared();
                if (D2 > DODGE_SCAN_RADIUS * DODGE_SCAN_RADIUS) {
                    continue;
                }
                const Vec2 BVEL = bullet.getBody().velocity;
                if (BVEL.lengthSquared() < 1.0F) {
                    continue;
                }
                // ¿La bala se dirige hacia la nave?
                if (BVEL.normalized().dot(TO_SHIP.normalized()) < DODGE_HEADING_MIN) {
                    continue;
                }
                if (!threat.found || D2 < best_d2) {
                    threat.found = true;
                    best_d2 = D2;
                    threat.position = BPOS;
                    threat.velocity = BVEL;
                }
            }
            return threat;
        }

        // Error angular (rad, [-PI,PI]) entre la nariz de la nave y desired_dir,
        // con el jitter de apuntado aplicado. La nariz apunta hacia (angle - PI/2).
        auto steerError(const Ship& ship, const Vec2& desired_dir, float jitter) -> float {
            const float DESIRED = std::atan2(desired_dir.y, desired_dir.x) + jitter;
            const float NOSE = ship.getAngle() - (PI / 2.0F);
            return wrapPi(DESIRED - NOSE);
        }

        // Setea rotación según el error (RIGHT incrementa ship.angle; LEFT lo
        // decrementa), con zona muerta para no oscilar.
        void applyRotation(Control& ctrl, float error) {
            if (error > ROTATE_DEADZONE) {
                ctrl.right = true;
            } else if (error < -ROTATE_DEADZONE) {
                ctrl.left = true;
            }
        }

    }  // namespace

    auto DemoPilot::compute(const Ship& ship,
        const std::array<Enemy, Constants::MAX_ORNIS>& enemies,
        const std::array<Bullet, static_cast<std::size_t>(Defaults::Entities::MAX_BULLETS_TOTAL)>& bullets,
        const SDL_FRect& play_area,
        float delta_time) -> Control {
        Control ctrl;

        if (fire_cooldown_ > 0.0F) {
            fire_cooldown_ -= delta_time;
        }
        retarget_timer_ -= delta_time;
        if (retarget_timer_ <= 0.0F) {
            retarget_timer_ = RETARGET_INTERVAL;
            aim_jitter_ = randSigned() * AIM_JITTER_MAX;
        }

        if (!ship.isActive()) {
            return ctrl;  // nave muerta: sin control
        }

        const Vec2 SHIP_POS = ship.getCenter();
        const Vec2 PLAY_CENTRE{
            .x = play_area.x + (play_area.w / 2.0F),
            .y = play_area.y + (play_area.h / 2.0F)};
        const Vec2 TO_CENTRE = (PLAY_CENTRE - SHIP_POS).normalized();

        // Prioridad 1: esquivar una bala enemiga entrante. Se mueve perpendicular
        // a la trayectoria de la bala (con sesgo al centro) y no dispara.
        const BulletThreat THREAT = findBulletThreat(SHIP_POS, bullets);
        if (THREAT.found) {
            const Vec2 BV = THREAT.velocity.normalized();
            Vec2 perp{.x = -BV.y, .y = BV.x};
            if ((SHIP_POS - THREAT.position).dot(perp) < 0.0F) {
                perp = -perp;  // hacia el lado en que ya está la nave
            }
            const Vec2 ESCAPE = (perp + (TO_CENTRE * WALL_BIAS)).normalized();
            applyRotation(ctrl, steerError(ship, ESCAPE, aim_jitter_));
            ctrl.thrust = true;
            return ctrl;
        }

        const Nearest TARGET = findNearest(SHIP_POS, enemies);
        target_idx_ = TARGET.index;

        // Sin enemigos: deriva tranquila (gira despacio, sin empuje ni disparo).
        if (TARGET.index == -1) {
            ctrl.right = true;
            return ctrl;
        }

        const bool NEAR_WALL =
            SHIP_POS.x < play_area.x + WALL_MARGIN ||
            SHIP_POS.x > play_area.x + play_area.w - WALL_MARGIN ||
            SHIP_POS.y < play_area.y + WALL_MARGIN ||
            SHIP_POS.y > play_area.y + play_area.h - WALL_MARGIN;

        Vec2 desired_dir;
        const bool DANGER = TARGET.distance < DANGER_RADIUS;
        if (DANGER) {
            // Prioridad 2: alejarse del enemigo pegado, con sesgo al centro.
            const Vec2 AWAY = (SHIP_POS - TARGET.center).normalized();
            desired_dir = (AWAY + (TO_CENTRE * WALL_BIAS)).normalized();
            ctrl.thrust = true;
        } else {
            // Combate: apuntar al enemigo con lead + jitter.
            const Vec2 PREDICTED = TARGET.center + (TARGET.velocity * LEAD_TIME);
            desired_dir = (PREDICTED - SHIP_POS).normalized();
        }

        const float ERROR = steerError(ship, desired_dir, aim_jitter_);
        applyRotation(ctrl, ERROR);

        if (!DANGER) {
            // Acercarse si el objetivo está lejos (mantiene la nave cazando),
            // pero no empujar de cara a una pared.
            const bool FACING_WALL = NEAR_WALL && TO_CENTRE.dot(desired_dir) < 0.0F;
            if (TARGET.distance > APPROACH_RADIUS && !FACING_WALL &&
                std::fabs(ERROR) < FIRE_TOLERANCE) {
                ctrl.thrust = true;
            }
            // Disparar cuando está bien encarada y el cooldown lo permite.
            if (std::fabs(ERROR) < FIRE_TOLERANCE && fire_cooldown_ <= 0.0F) {
                ctrl.shoot = true;
                fire_cooldown_ = FIRE_COOLDOWN;
            }
        }

        return ctrl;
    }

}  // namespace Systems::Demo
