#include "solid_actor_manager.hpp"

#include <algorithm>  // Para std::ranges::find, std::max, std::min

#include "game/entities/solid_actor.hpp"  // Para SolidActor
#include "utils/defines.hpp"              // Para Collision::NONE, PlayArea::WIDTH/HEIGHT

// Registro de actores
void SolidActorManager::registerActor(SolidActor* actor) {
    if (actor == nullptr) { return; }
    actors_.push_back(actor);
}

// Desregistro (no borra el objeto — la entidad concreta vive en su propio manager)
void SolidActorManager::unregisterActor(SolidActor* actor) {
    auto it = std::ranges::find(actors_, actor);
    if (it != actors_.end()) {
        actors_.erase(it);
    }
}

void SolidActorManager::clear() {
    actors_.clear();
}

void SolidActorManager::setAdjacent(const AdjacentActors& adjacent) {
    adjacent_ = adjacent;
}

void SolidActorManager::clearAdjacent() {
    adjacent_ = {};
}

// Itera sobre todos los actores (local + 4 vecinos cardinales), aplicando
// el offset pertinente al AABB vecino para traerlo al sistema de coordenadas
// de la room actual. Las rooms vecinas tienen sus actores en coordenadas
// locales propias; para verlos desde aquí, se traslada:
//   - left: +WIDTH en X (porque en la room actual están a la izquierda del
//     borde 0 de coordenadas; trasladándolos por -WIDTH los colocamos "a la
//     izquierda" del Player, con x negativas).
//   - right: -WIDTH en X (análogo).
//   - upper: +HEIGHT en Y.
//   - lower: -HEIGHT en Y.
//
// NOTA: el Player siempre está en [0, WIDTH) × [0, HEIGHT) de la room actual.
// Los actores vecinos se muestran con coordenadas fuera de ese rango, y los
// sweeps solo los ven cuando el Player está lo bastante cerca del borde como
// para solapar con ellos.
template <typename Fn>
void SolidActorManager::forEachActor(Fn&& fn) const {
    // Locales
    for (auto* a : actors_) {
        fn(a, a->getAABB());
    }

    // Adyacente izquierda: sus actores están en [0, WIDTH) de SU room;
    // desde la room actual, esos actores están en [-WIDTH, 0). Offset: -WIDTH en X.
    if (adjacent_.left != nullptr) {
        for (auto* a : adjacent_.left->actors_) {
            SDL_FRect r = a->getAABB();
            r.x -= PlayArea::WIDTH;
            fn(a, r);
        }
    }
    // Adyacente derecha: offset +WIDTH en X.
    if (adjacent_.right != nullptr) {
        for (auto* a : adjacent_.right->actors_) {
            SDL_FRect r = a->getAABB();
            r.x += PlayArea::WIDTH;
            fn(a, r);
        }
    }
    // Adyacente superior: offset -HEIGHT en Y (arriba tiene y negativa).
    if (adjacent_.upper != nullptr) {
        for (auto* a : adjacent_.upper->actors_) {
            SDL_FRect r = a->getAABB();
            r.y -= PlayArea::HEIGHT;
            fn(a, r);
        }
    }
    // Adyacente inferior: offset +HEIGHT en Y.
    if (adjacent_.lower != nullptr) {
        for (auto* a : adjacent_.lower->actors_) {
            SDL_FRect r = a->getAABB();
            r.y += PlayArea::HEIGHT;
            fn(a, r);
        }
    }
}

// Devuelve el borde derecho del actor bloqueante "a la izquierda" del Player.
// Criterio: el AABB del actor solapa el rect del Player y el borde izquierdo
// del actor está a la izquierda del borde izquierdo del Player (el Player se
// mete en él por la izquierda). El test del borde derecho es contact-inclusive
// (>=) para que el contacto flush cuente como pared, igual que TileCollider.
// Sin esto, Player::stuckAgainstWall no detectaba puertas cerradas en contacto
// y la animación "walk" se reproducía aunque el clamp de moveHorizontal
// dejase x_ inmóvil.
auto SolidActorManager::checkWallLeft(float px, float py, float pw, float ph) const -> float {
    float result = Collision::NONE;
    forEachActor([&](const SolidActor* a, const SDL_FRect& r) {
        if (!a->isBlocking()) { return; }
        if (a->isOneWayTop()) { return; }  // Los jump-through no son pared lateral
        // Y-overlap
        if (py + ph <= r.y) { return; }
        if (py >= r.y + r.h) { return; }
        // X-overlap y "a la izquierda": el Player (px) está dentro del actor
        // o en contacto exacto con su borde derecho.
        const float RIGHT_EDGE = r.x + r.w;
        if (r.x < px && RIGHT_EDGE >= px) {
            if (result == Collision::NONE || RIGHT_EDGE > result) {
                result = RIGHT_EDGE;
            }
        }
    });
    return result;
}

// Devuelve el borde izquierdo del actor bloqueante "a la derecha" del Player.
// Test contact-inclusive (<=) por las mismas razones que checkWallLeft.
auto SolidActorManager::checkWallRight(float px, float py, float pw, float ph) const -> float {
    float result = Collision::NONE;
    forEachActor([&](const SolidActor* a, const SDL_FRect& r) {
        if (!a->isBlocking()) { return; }
        if (a->isOneWayTop()) { return; }
        // Y-overlap
        if (py + ph <= r.y) { return; }
        if (py >= r.y + r.h) { return; }
        // X-overlap y "a la derecha": el borde derecho del Player entra en el
        // actor o está en contacto exacto con su borde izquierdo.
        const float PLAYER_RIGHT = px + pw;
        if (r.x <= PLAYER_RIGHT && r.x + r.w > PLAYER_RIGHT) {
            if (result == Collision::NONE || r.x < result) {
                result = r.x;
            }
        }
    });
    return result;
}

// Techo: devuelve el borde inferior del actor bloqueante que está por encima
// del Player y solapa su parte superior.
auto SolidActorManager::checkCeiling(float px, float py, float pw) const -> float {
    float result = Collision::NONE;
    forEachActor([&](const SolidActor* a, const SDL_FRect& r) {
        if (!a->isBlocking()) { return; }
        if (a->isOneWayTop()) { return; }  // Los jump-through no tienen techo
        // X-overlap
        if (px + pw <= r.x) { return; }
        if (px >= r.x + r.w) { return; }
        // Y: el actor debe estar por encima del Player (su bottom edge entre py y py+ph)
        const float BOTTOM_EDGE = r.y + r.h;
        if (r.y < py && BOTTOM_EDGE > py) {
            if (result == Collision::NONE || BOTTOM_EDGE > result) {
                result = BOTTOM_EDGE;
            }
        }
    });
    return result;
}

// Suelo: el actor más cercano (por arriba) cuyo top edge esté entre
// foot_y_cur y foot_y_new. Para ONEWAY_TOP, solo cuentan si foot_y_cur <= r.y.
auto SolidActorManager::checkFloor(float px, float foot_y_cur, float pw, float foot_y_new) const -> FloorHit {
    FloorHit hit;
    forEachActor([&](SolidActor* a, const SDL_FRect& r) {
        // Un actor participa en el suelo si es bloqueante O si tiene CARRY_ON_TOP
        // (las plataformas jump-through tienen CARRY_ON_TOP sin BLOCKS_PLAYER).
        const bool PARTICIPATES = a->isBlocking() || a->carriesOnTop();
        if (!PARTICIPATES) { return; }
        // X-overlap
        if (px + pw <= r.x) { return; }
        if (px >= r.x + r.w) { return; }
        // Y: el top del actor debe estar en [foot_y_cur, foot_y_new]
        if (r.y < foot_y_cur) { return; }  // actor por encima: no es suelo, es techo
        if (r.y > foot_y_new) { return; }  // actor fuera del rango de caída
        // ONEWAY_TOP: ya garantizado por foot_y_cur <= r.y (chequeo implícito arriba)
        // Actualizar hit si es más alto (menor y) que el actual
        if (hit.y == Collision::NONE || r.y < hit.y) {
            hit.y = r.y;
            hit.carrier = a->carriesOnTop() ? a : nullptr;
        }
    });
    return hit;
}

// Hay suelo "inmediato" debajo (tolerancia 1 px). Usado por checkFalling para
// no desengancharse de plataformas móviles que bajan/suben pixel a pixel.
auto SolidActorManager::hasGroundBelow(float px, float foot_y, float pw) const -> bool {
    bool found = false;
    forEachActor([&](const SolidActor* a, const SDL_FRect& r) {
        if (found) { return; }
        const bool PARTICIPATES = a->isBlocking() || a->carriesOnTop();
        if (!PARTICIPATES) { return; }
        // X-overlap
        if (px + pw <= r.x) { return; }
        if (px >= r.x + r.w) { return; }
        // Top del actor dentro de [foot_y, foot_y + 1]
        if (r.y >= foot_y && r.y <= foot_y + 1.0F) {
            found = true;
        }
    });
    return found;
}
