#include "Library/LiveActor/ActorAreaFunction.h"

#include "Library/Area/AreaInitInfo.h"
#include "Library/Area/AreaObj.h"
#include "Library/Area/AreaObjDirector.h"
#include "Library/Area/AreaObjGroup.h"
#include "Library/Area/AreaObjMtxConnecter.h"
#include "Library/Area/AreaObjUtil.h"
#include "Library/Area/IUseAreaObj.h"
#include "Library/Area/SwitchKeepOnAreaGroup.h"
#include "Library/Area/SwitchOnAreaGroup.h"
#include "Library/Area/ValidatorBase.h"
#include "Library/LiveActor/ActorFlagFunction.h"
#include "Library/LiveActor/ActorInitInfo.h"
#include "Library/LiveActor/ActorMovementFunction.h"
#include "Library/LiveActor/ActorPoseUtil.h"
#include "Library/LiveActor/ActorSceneInfo.h"
#include "Library/LiveActor/LiveActor.h"
#include "Library/Math/MathUtil.h"
#include "Library/Placement/PlacementFunction.h"
#include "Library/Player/PlayerUtil.h"

namespace al {

AreaObj* tryFindAreaObjPlayerOne(const PlayerHolder* holder, const char* name) {
    s32 playerNum = getPlayerNumMax(holder);

    AreaObj* selectedAreaObj = nullptr;
    for (s32 i = 0; i < playerNum; i++) {
        if (isPlayerDead(holder, i))
            continue;
        if (!isAreaTarget(getPlayerActor(holder, i)))
            continue;
        const sead::Vector3f& playerPos = getPlayerPos(holder, i);
        AreaObj* areaObj = tryFindAreaObj(getPlayerActor(holder, i), name, playerPos);
        if (areaObj &&
            (!selectedAreaObj || selectedAreaObj->getPriority() < areaObj->getPriority())) {
            selectedAreaObj = areaObj;
        }
    }

    return selectedAreaObj;
}

AreaObj* tryFindAreaObjPlayerAll(const PlayerHolder* holder, const char* name) {
    s32 playerNum = getPlayerNumMax(holder);
    if (playerNum == 0)
        return nullptr;

    AreaObjGroup* areaGroup = tryFindAreaObjGroup(getPlayerActor(holder, 0), name);
    if (!areaGroup)
        return nullptr;

    s32 groupSize = areaGroup->getSize();
    AreaObj* selectedAreaObj = nullptr;
    for (s32 i = 0; i < groupSize; i++) {
        AreaObj* areaObj = areaGroup->getAreaObj(i);
        AreaObj* currentAreaObject = nullptr;
        for (s32 j = 0; j < playerNum; j++) {
            if (isPlayerDead(holder, j))
                continue;

            const sead::Vector3f& playerPos = getPlayerPos(holder, j);
            currentAreaObject = areaObj;
            if (!areaObj->isInVolume(playerPos)) {
                currentAreaObject = nullptr;
                break;
            }
        }
        if (currentAreaObject && (!selectedAreaObj || selectedAreaObj->getPriority() <
                                                          currentAreaObject->getPriority())) {
            selectedAreaObj = currentAreaObject;
        }
    }
    return selectedAreaObj;
}

AreaObj* createAreaObj(const ActorInitInfo& actorInitInfo, const char* name) {
    AreaInitInfo areaInitInfo;
    initAreaInitInfo(&areaInitInfo, actorInitInfo);
    AreaObj* areaObj = new AreaObj(name);
    areaObj->init(areaInitInfo);
    return areaObj;
}

void initAreaInitInfo(AreaInitInfo* areaInitInfo, const ActorInitInfo& actorInitInfo) {
    initAreaInitInfo(areaInitInfo, *actorInitInfo.placementInfo, actorInitInfo);
}

AreaObj* createLinkArea(const ActorInitInfo& initInfo, const char* name, const char* areaName) {
    const PlacementInfo& actorPlacementInfo = *initInfo.placementInfo;
    PlacementInfo placementInfo;
    getLinksInfoByIndex(&placementInfo, actorPlacementInfo, name, 0);

    AreaInitInfo areaInfo;
    initAreaInitInfo(&areaInfo, placementInfo, initInfo);

    AreaObj* areaObj = new AreaObj(areaName);
    areaObj->init(areaInfo);
    return areaObj;
}

void initAreaInitInfo(AreaInitInfo* areaInitInfo, const PlacementInfo& placementInfo,
                      const ActorInitInfo& actorInitInfo) {
    areaInitInfo->set(placementInfo, actorInitInfo.stageSwitchDirector,
                      actorInitInfo.actorSceneInfo.sceneObjHolder);
}

AreaObj* tryCreateLinkArea(const ActorInitInfo& initInfo, const char* name, const char* areaName) {
    s32 linkChildNum = calcLinkChildNum(initInfo, name);
    if (linkChildNum < 1)
        return nullptr;

    return createLinkArea(initInfo, name, areaName);
}

__attribute__((always_inline)) AreaObjGroup* createLinkAreaGroup(const ActorInitInfo& initInfo,
                                                                 const char* name,
                                                                 const char* groupName,
                                                                 const char* areaName) {
    s32 linkChildNum = calcLinkChildNum(initInfo, name);
    if (linkChildNum < 1)
        return nullptr;

    AreaObjGroup* areaGroup = new AreaObjGroup(groupName, 0);
    areaGroup->createBuffer(linkChildNum);
    const PlacementInfo& actorPlacementInfo = *initInfo.placementInfo;

    for (s32 i = 0; i < linkChildNum; i++) {
        PlacementInfo placementInfo;
        getLinksInfoByIndex(&placementInfo, actorPlacementInfo, name, i);

        AreaInitInfo areaInfo;
        initAreaInitInfo(&areaInfo, placementInfo, initInfo);

        AreaObj* areaObj = new AreaObj(areaName);
        areaObj->init(areaInfo);

        areaGroup->registerAreaObj(areaObj);
    }
    return areaGroup;
}

AreaObjGroup* createLinkAreaGroup(LiveActor* actor, const ActorInitInfo& initInfo, const char* name,
                                  const char* groupName, const char* areaName) {
    return createLinkAreaGroup(initInfo, name, groupName, areaName);
}

AreaObj* tryFindAreaObj(const LiveActor* actor, const char* name) {
    if (!isAreaTarget(actor))
        return nullptr;

    return tryFindAreaObj(actor, name, getTrans(actor));
}

bool isInAreaObj(const LiveActor* actor, const char* name) {
    if (!isAreaTarget(actor))
        return false;

    return isInAreaObj(actor, name, getTrans(actor));
}

bool isInDeathArea(const LiveActor* actor) {
    if (!isAreaTarget(actor))
        return false;

    return isInDeathArea(actor, getTrans(actor));
}

bool isInWaterArea(const LiveActor* actor) {
    if (!isAreaTarget(actor))
        return false;

    return isInWaterArea(actor, getTrans(actor));
}

bool isInPlayerControlOffArea(const LiveActor* actor) {
    if (!isAreaTarget(actor))
        return false;

    return isInPlayerControlOffArea(actor, getTrans(actor));
}

f32 calcWaterSinkDepth(const LiveActor* actor) {
    return calcWaterSinkDepth(actor, getTrans(actor));
}

void registerAreaHostMtx(const IUseAreaObj* areaObj, const sead::Matrix34f* mtx,
                         const ActorInitInfo& initInfo) {
    areaObj->getAreaObjDirector()->getMtxConnecterHolder()->registerParentMtx(
        mtx, *initInfo.placementInfo, nullptr);
}

void registerAreaHostMtx(const IUseAreaObj* areaObj, const sead::Matrix34f* mtx,
                         const ActorInitInfo& initInfo, const ValidatorBase& validator) {
    areaObj->getAreaObjDirector()->getMtxConnecterHolder()->registerParentMtx(
        mtx, *initInfo.placementInfo, validator.fnc_8());
}

void registerAreaHostMtx(const LiveActor* actor, const ActorInitInfo& initInfo) {
    registerAreaHostMtx(actor, actor->getBaseMtx(), initInfo);
}

void registerAreaHostMtx(const LiveActor* actor, const ActorInitInfo& initInfo,
                         const ValidatorBase& validator) {
    registerAreaHostMtx(actor, actor->getBaseMtx(), initInfo, validator);
}

void registerAreaSyncHostMtx(const IUseAreaObj* areaObj, const sead::Matrix34f* mtx,
                             const ActorInitInfo& initInfo) {
    areaObj->getAreaObjDirector()->getMtxConnecterHolder()->registerSyncParentMtx(
        mtx, *initInfo.placementInfo, nullptr);
}

void registerAreaSyncHostMtx(const IUseAreaObj* areaObj, const sead::Matrix34f* mtx,
                             const ActorInitInfo& initInfo, const ValidatorBase& validator) {
    areaObj->getAreaObjDirector()->getMtxConnecterHolder()->registerSyncParentMtx(
        mtx, *initInfo.placementInfo, validator.fnc_8());
}

void registerAreaSyncHostMtx(const LiveActor* actor, const ActorInitInfo& initInfo) {
    registerAreaSyncHostMtx(actor, actor->getBaseMtx(), initInfo);
}

void registerAreaSyncHostMtx(const LiveActor* actor, const ActorInitInfo& initInfo,
                             const ValidatorBase& validator) {
    registerAreaSyncHostMtx(actor, actor->getBaseMtx(), initInfo, validator);
}

bool tryReviseVelocityInsideAreaObj(sead::Vector3f* nearestEdgePos, LiveActor* actor,
                                    AreaObjGroup* areaGroup, const AreaObj* areaObj) {
    if (!areaGroup || !areaObj)
        return false;

    if (!isInAreaObj(areaGroup, getTrans(actor))) {
        calcNearestAreaObjEdgePos(nearestEdgePos, areaObj, getTrans(actor));
        setTrans(actor, *nearestEdgePos);
        setVelocityZero(actor);
        return true;
    }

    f32 velMagnitude = getVelocity(actor).length();
    sead::Vector3f nextPosition = getTrans(actor) + getVelocity(actor);
    if (isInAreaObj(areaGroup, nextPosition))
        return false;

    calcNearestAreaObjEdgePos(nearestEdgePos, areaObj, nextPosition);
    sead::Vector3f newVelocity = *nearestEdgePos - getTrans(actor);
    if (newVelocity.length() > velMagnitude)
        setLength(&newVelocity, velMagnitude);
    setVelocity(actor, newVelocity);
    return true;
}

SwitchKeepOnAreaGroup* tryCreateSwitchKeepOnAreaGroup(LiveActor* actor,
                                                      const ActorInitInfo& initInfo) {
    AreaObjGroup* areaGroup =
        createLinkAreaGroup(actor, initInfo, "AreaSwitchKeepOn", "子供スイッチキープエリアグループ",
                            "子供スイッチキープエリア");
    if (!areaGroup)
        return nullptr;

    return new SwitchKeepOnAreaGroup(areaGroup);
}

SwitchOnAreaGroup* tryCreateSwitchOnAreaGroup(LiveActor* actor, const ActorInitInfo& initInfo) {
    AreaObjGroup* areaGroup = createLinkAreaGroup(
        actor, initInfo, "AreaSwitchOn", "子供スイッチOnエリアグループ", "子供スイッチOnエリア");
    if (!areaGroup)
        return nullptr;

    return new SwitchOnAreaGroup(areaGroup);
}

}  // namespace al
