#include "Player/PlayerRecoverySafetyPoint.h"

#include "Library/Area/AreaObj.h"
#include "Library/Area/AreaObjUtil.h"
#include "Library/Camera/CameraUtil.h"
#include "Library/Collision/CollisionPartsKeeperUtil.h"
#include "Library/Collision/CollisionPartsTriangle.h"
#include "Library/Collision/PartsInterpolator.h"
#include "Library/LiveActor/ActorActionFunction.h"
#include "Library/LiveActor/ActorClippingFunction.h"
#include "Library/LiveActor/ActorFlagFunction.h"
#include "Library/LiveActor/ActorInitUtil.h"
#include "Library/LiveActor/ActorMovementFunction.h"
#include "Library/LiveActor/ActorPoseUtil.h"
#include "Library/LiveActor/LiveActor.h"
#include "Library/LiveActor/LiveActorFunction.h"
#include "Library/Math/MathUtil.h"

#include "Player/HackCap.h"
#include "System/GameDataUtil.h"
#include "Util/ActorDimensionUtil.h"
#include "Util/PlayerCollisionUtil.h"
#include "Util/PlayerUtil.h"

PlayerRecoverySafetyPoint::PlayerRecoverySafetyPoint(const al::LiveActor* actor,
                                                     const HackCap* hackCap,
                                                     const al::ActorInitInfo& initInfo,
                                                     const IUseDimension* dimension,
                                                     al::CollisionPartsFilterBase* colFilter,
                                                     al::HitSensor* hitSensor)
    : mActor(actor), mHackCap(hackCap), mDimension(dimension), mColFilter(colFilter),
      mHitSensor(hitSensor) {
    mTractorBubble = new al::LiveActor("復帰泡");
    al::initChildActorWithArchiveNameNoPlacementInfo(mTractorBubble, initInfo, "TractorBubble",
                                                     nullptr);
    al::invalidateClipping(mTractorBubble);
    mTractorBubble->makeActorDead();

    mTractorBubble2D = new al::LiveActor("復帰泡2D");
    al::initChildActorWithArchiveNameNoPlacementInfo(mTractorBubble2D, initInfo, "TractorBubble2D",
                                                     nullptr);
    al::invalidateClipping(mTractorBubble2D);
    mTractorBubble2D->makeActorDead();
}

void PlayerRecoverySafetyPoint::reset() {
    mSafety3D.reset();
    mSafety2D.reset();

    mRecoveryArea = nullptr;
    mDefaultSafetyPos = nullptr;
}

void PlayerRecoverySafetyPoint::setSafetyPoint(const sead::Vector3f& safetyPos,
                                               const sead::Vector3f& safetyNormal,
                                               const al::AreaObj* areaObj) {
    if (!rs::isPlayer2D(mActor)) {
        mSafety3D.set(safetyPos, safetyNormal, al::getGravity(mActor), areaObj);
    } else if (rs::isIn2DArea(mDimension)) {
        sead::Vector3f snapPos = safetyPos;
        rs::calcSnap2DPosition(&snapPos, mDimension, safetyPos, 500.0f);

        mSafety2D.set(snapPos, safetyNormal, al::getGravity(mActor), areaObj);
    }

    mDefaultSafetyPos = nullptr;
}

void PlayerRecoverySafetyPoint::noticeRequestSafetyPoint(const sead::Vector3f& safetyPos,
                                                         const sead::Vector3f& safetyNormal,
                                                         const al::AreaObj* areaObj) {
    sead::Vector3f pos = safetyPos;
    sead::Vector3f normal = safetyNormal;
    sead::Vector3f* defaultPos = nullptr;

    if (rs::isPlayer3D(mActor) && areaObj == nullptr) {
        const sead::Vector3f& gravity = al::getGravity(mActor);

        sead::Vector3f rayStart = safetyPos - gravity * 10.0f;
        sead::Vector3f rayDir = gravity * 110.0f;
        const al::ArrowHitInfo* hitInfo = nullptr;
        al::TriangleFilterGroundOnly filter(gravity);
        if (!alCollisionUtil::getFirstPolyOnArrow(mActor, &hitInfo, rayStart, rayDir, nullptr,
                                                  &filter))
            return;

        if (!rs::isEnableRecordSafetyPoint(&defaultPos, *hitInfo->hitInfo.data(), mHitSensor,
                                           -gravity))
            return;

        pos = alCollisionUtil::getCollisionHitPos(hitInfo->hitInfo.data());
        normal = alCollisionUtil::getCollisionHitNormal(hitInfo->hitInfo.data());
    }

    setSafetyPoint(pos, normal, areaObj);
    mDefaultSafetyPos = defaultPos;
}

void PlayerRecoverySafetyPoint::noticeDangerousPoint(const sead::Vector3f& pos, bool skipIfValid) {
    if (!rs::isPlayer2D(mActor)) {
        slideLastSafetyPoint(&mSafety3D.safetyPos, &mSafety3D.safetyNormal, mSafety3D.hasSafety,
                             pos, skipIfValid, nullptr);
    } else if (rs::isIn2DArea(mDimension)) {
        sead::Vector3f snapPos = pos;
        rs::calcSnap2DPosition(&snapPos, mDimension, pos, 500.0f);
        slideLastSafetyPoint(&mSafety2D.safetyPos, &mSafety2D.safetyNormal, mSafety2D.hasSafety,
                             snapPos, skipIfValid, mColFilter);
    }
}

// Function: PlayerRecoverySafetyPoint::slideLastSafetyPoint
// Description:
// If the player moves slightly from the last recorded safety point, this function
// adjusts ("slides") that point along the ground so it stays in a valid position.
//
// The slide distance is inversely proportional to the player's movement. Small
// movements cause a larger adjustment (~100 units), while larger movements
// cause little or no adjustment. This helps pull the safety point away from edges
// or dangerous geometry without letting it drift too far across the surface.
// I assume this is to allow some leeway for players when you're placed in a
// recovery position.
//
// When skipIfValid is true, a ray is first cast behind the current safety
// point to check whether it is still supported by valid ground. If it is, the
// function returns early and no sliding occurs.

void PlayerRecoverySafetyPoint::slideLastSafetyPoint(sead::Vector3f* safetyPos,
                                                     sead::Vector3f* safetyNormal, bool hasSafety,
                                                     const sead::Vector3f& lastSafetyPos,
                                                     bool skipIfValid,
                                                     al::CollisionPartsFilterBase* colFilter) {
    if (!hasSafety)
        return;

    sead::Vector3f slideDir = {0.0f, 0.0f, 0.0f};
    al::verticalizeVec(&slideDir, *safetyNormal, *safetyPos - lastSafetyPos);

    f32 length = slideDir.length();
    if (length > 100.0f || !al::tryNormalizeOrZero(&slideDir))
        return;

    const sead::Vector3f& gravity = al::getGravity(mActor);
    sead::Vector3f rayDir = 2 * (gravity * 50.0f);
    sead::Vector3f rayOffset = gravity * 50.0f;

    const al::ArrowHitInfo* hitInfo = nullptr;

    if (skipIfValid) {
        sead::Vector3f rayStart = *safetyPos - (length * slideDir) - rayOffset;
        al::TriangleFilterGroundOnly filter(gravity);
        if (alCollisionUtil::getFirstPolyOnArrow(mActor, &hitInfo, rayStart, rayDir, colFilter,
                                                 &filter)) {
            sead::Vector3f* outPos = nullptr;
            if (rs::isEnableRecordSafetyPoint(&outPos, *hitInfo->hitInfo.data(), mHitSensor,
                                              -gravity))
                return;
        }
    }

    // Inverse rubber band logic
    // If the player hasn't moved much, assume the safety point might still be near a dangerous edge
    // and correct it inward. This (presumably, from a design perspective) allows for some leeway
    // when the player is placed in a recovery position in certain areas. However, if the player has
    // moved a large distance, only adjust it a little bit. This prevents the safety point from
    // sliding too far across the surface when the player has moved significantly.

    length = sead::Mathf::clampMin(100.0f - length, 0.0f);
    sead::Vector3f rayStart = *safetyPos + (length * slideDir) - rayOffset;

    sead::Vector3f* outPos = nullptr;
    if (alCollisionUtil::getFirstPolyOnArrow(mActor, &hitInfo, rayStart, rayDir, colFilter,
                                             nullptr)) {
        if (rs::isEnableRecordSafetyPoint(&outPos, *hitInfo->hitInfo.data(), mHitSensor,
                                          -gravity) &&
            rs::isCollisionCodeSafetyPoint(*hitInfo->hitInfo.data())) {
            sead::Vector3f hitPos = alCollisionUtil::getCollisionHitPos(hitInfo->hitInfo.data());
            sead::Vector3f hitNormal =
                alCollisionUtil::getCollisionHitNormal(hitInfo->hitInfo.data());

            al::TriangleFilterGroundOnly filter(al::getGravity(mActor));
            if (!alCollisionUtil::getFirstPolyOnArrow(mActor, &hitInfo, *safetyPos - gravity * 5.0f,
                                                      hitPos - *safetyPos, colFilter, &filter)) {
                *safetyPos = hitPos;
                *safetyNormal = hitNormal;
            }
        }
    }
}

bool PlayerRecoverySafetyPoint::isValid() const {
    if (!isEnableRecovery())
        return false;

    if (rs::isPlayer2D(mActor))
        return mSafety2D.hasSafety;
    return mSafety3D.hasSafety;
}

bool PlayerRecoverySafetyPoint::isEnableRecovery() const {
    return rs::isKidsMode(mActor) || isActiveRecoveryArea() || mHackCap->isEnableRescuePlayer();
}

const sead::Vector3f& PlayerRecoverySafetyPoint::getSafetyPoint() const {
    if (mDefaultSafetyPos)
        return *mDefaultSafetyPos;

    if (rs::isPlayer2D(mActor))
        return mSafety2D.safetyPos;
    else
        return mSafety3D.safetyPos;
}

const sead::Vector3f& PlayerRecoverySafetyPoint::getSafetyPointGravity() const {
    if (rs::isPlayer2D(mActor))
        return mSafety2D.gravity;
    return mSafety3D.gravity;
}

const al::AreaObj* PlayerRecoverySafetyPoint::getSafetyPointArea() const {
    if (rs::isPlayer2D(mActor))
        return mSafety2D.area;
    return mSafety3D.area;
}

void PlayerRecoverySafetyPoint::updateRecoveryAreaValidity() {
    if (mRecoveryArea && !mRecoveryArea->isValid())
        mRecoveryArea = nullptr;
}

void PlayerRecoverySafetyPoint::setRecoveryArea(const al::AreaObj* area) {
    mRecoveryArea = area;
}

bool PlayerRecoverySafetyPoint::isActiveRecoveryArea() const {
    return mRecoveryArea && mRecoveryArea->isValid();
}

void PlayerRecoverySafetyPoint::checkInvalidateArea() {
    if (isValid() && al::isInAreaObj(mActor, "InvalidateRecoveryPosArea", getSafetyPoint()))
        reset();
}

void PlayerRecoverySafetyPoint::startRecovery(f32 height) {
    mBubbleHeight = height;  // height will be 80.0 if the player is 3D, else 60.0.
    if (mIsRecovering)
        return;

    mIsRecovering = true;
    if (rs::isPlayer2D(mActor)) {
        mTractorBubble2D->appear();
        return;
    }

    mTractorBubble->appear();
    al::offCollide(mTractorBubble);
    al::startAction(mTractorBubble, "Appear");
    updateRecoveryBubble();

    if (mHackCap->isEnableRescuePlayer()) {
        al::LiveActor* hat = al::getSubActor(mTractorBubble, "装着帽子");
        hat->appear();
        al::startAction(hat, "LockOn");

        al::startHitReactionHitEffect(mHackCap, "プレイヤー救出出現",
                                      al::getTrans(mTractorBubble) -
                                          al::getGravity(mTractorBubble) * 160.0f);

        al::LiveActor* eye = al::getSubActor(mTractorBubble, "目");
        eye->appear();
        al::startAction(eye, "Appear");
    }
}

void PlayerRecoverySafetyPoint::updateRecoveryBubble() {
    if (rs::isPlayer2D(mActor)) {
        if (al::isDead(mTractorBubble2D))
            return;

        sead::Vector3f up = -al::getGravity(mActor);
        sead::Vector3f side = {0.0f, 0.0f, 0.0f};

        al::calcSideDir(&side, mActor);

        sead::Quatf quat = sead::Quatf::unit;

        if (al::isParallelDirection(side, up, 0.01f))
            al::calcQuat(&quat, mTractorBubble2D);
        else
            al::makeQuatSideUp(&quat, side, up);

        sead::Vector3f upDir = {0.0f, 0.0f, 0.0f};
        al::calcUpDir(&upDir, mActor);

        al::resetPosition(mTractorBubble2D, al::getTrans(mActor) + upDir * mBubbleHeight);
        al::updatePoseQuat(mTractorBubble2D, quat);
        return;
    }

    if (al::isDead(mTractorBubble))
        return;

    if (al::isActionPlaying(mTractorBubble, "Disappear")) {
        if (al::isActionEnd(mTractorBubble)) {
            mTractorBubble->kill();
            al::getSubActor(mTractorBubble, "装着帽子")->kill();
            al::getSubActor(mTractorBubble, "目")->kill();
        }
        return;
    }

    sead::Vector3f upDir = {0.0f, 0.0f, 0.0f};
    al::calcUpDir(&upDir, mActor);

    al::resetPosition(mTractorBubble, al::getTrans(mActor) + upDir * mBubbleHeight);

    sead::Vector3f camFront = {0.0f, 0.0f, 0.0f};
    al::calcCameraFront(&camFront, mActor, 0);

    sead::Vector3f up = -al::getGravity(mActor);

    if (al::isParallelDirection(camFront, up, 0.01f))
        camFront = al::getCameraUp(mActor, 0);
    camFront.negate();
    sead::Quatf quat = sead::Quatf::unit;
    al::makeQuatUpFront(&quat, up, camFront);
    al::updatePoseQuat(mTractorBubble, quat);
}

void PlayerRecoverySafetyPoint::startBubbleWait() {
    if (!rs::isPlayer2D(mActor))
        al::startAction(mTractorBubble, "Wait");
}

void PlayerRecoverySafetyPoint::endRecovery() {
    if (mIsRecovering) {
        if (rs::isPlayer2D(mActor))
            mTractorBubble2D->kill();
        else
            al::startAction(mTractorBubble, "Disappear");

        mIsRecovering = false;
    }
}
