#include "Runtime/Character/CGroundMovement.hpp"

#include "Runtime/CStateManager.hpp"
#include "Runtime/Collision/CAABoxFilter.hpp"
#include "Runtime/Collision/CCollisionInfoList.hpp"
#include "Runtime/Collision/CGameCollision.hpp"
#include "Runtime/Collision/CollisionUtil.hpp"
#include "Runtime/World/CPhysicsActor.hpp"
#include "Runtime/World/CPlayer.hpp"
#include "Runtime/World/CScriptPlatform.hpp"
#include "Runtime/World/CWorld.hpp"

#include "TCastTo.hpp" // Generated file, do not modify include path
#include "math.h"

namespace metaforce {

void CGroundMovement::CheckFalling(CPhysicsActor& actor, CStateManager& mgr, float /*dt*/) {
  bool oob = true;
  zeus::CAABox plBox = *actor.GetTouchBounds();
  for (const CGameArea& area : *mgr.GetWorld()) {
    if (area.GetAABB().intersects(plBox)) {
      oob = false;
      break;
    }
  }

  if (oob) {
    mgr.SendScriptMsg(&actor, kInvalidUniqueId, EScriptObjectMessage::OnFloor);
    actor.SetAngularVelocityWR(actor.GetAngularVelocityWR() * 0.98f);
    zeus::CVector3f vel = actor.GetTransform().transposeRotate(actor.GetVelocity());
    vel.z() = 0.f;
    actor.SetVelocityOR(vel);
    actor.SetMomentumWR(zeus::skZero3f);
  } else {
    mgr.SendScriptMsg(&actor, kInvalidUniqueId, EScriptObjectMessage::Falling);
  }
}

void CGroundMovement::MoveGroundCollider(CStateManager& mgr, CPhysicsActor& actor, float dt,
                                         const rstl::reserved_vector<TUniqueId, 1024>* nearList) {
  CMotionState oldState = actor.GetMotionState();
  CMotionState newState = actor.PredictMotion_Internal(dt);
  float deltaMag = newState.x0_translation.magnitude();
  TUniqueId idDetect = kInvalidUniqueId;
  CCollisionInfoList collisionList;
  zeus::CAABox motionVol = actor.GetMotionVolume(dt);
  rstl::reserved_vector<TUniqueId, 1024> useColliderList;
  if (nearList != nullptr) {
    useColliderList = *nearList;
  }
  mgr.BuildColliderList(useColliderList, actor, motionVol);
  CAreaCollisionCache cache(motionVol);
  float collideDt = dt;
  if (actor.GetCollisionPrimitive()->GetPrimType() != FOURCC('OBTG')) {
    CGameCollision::BuildAreaCollisionCache(mgr, cache);
    if (deltaMag > 0.5f * CGameCollision::GetMinExtentForCollisionPrimitive(*actor.GetCollisionPrimitive())) {
      zeus::CVector3f point = actor.GetCollisionPrimitive()->CalculateAABox(actor.GetPrimitiveTransform()).center();
      TUniqueId intersectId = kInvalidUniqueId;
      CMaterialFilter filter = CMaterialFilter::MakeInclude({EMaterialTypes::Solid});
      CRayCastResult result = mgr.RayWorldIntersection(intersectId, point, newState.x0_translation.normalized(),
                                                       deltaMag, filter, useColliderList);
      if (result.IsValid()) {
        collideDt = dt * (result.GetT() / deltaMag);
        newState = actor.PredictMotion_Internal(collideDt);
      }
    }
  }
  actor.MoveCollisionPrimitive(newState.x0_translation);
  if (CGameCollision::DetectCollision_Cached(mgr, cache, *actor.GetCollisionPrimitive(), actor.GetPrimitiveTransform(),
                                             actor.GetMaterialFilter(), useColliderList, idDetect, collisionList)) {
    actor.AddMotionState(newState);
    float resolved = 0.f;
    if (ResolveUpDown(cache, mgr, actor, actor.GetMaterialFilter(), useColliderList, actor.GetStepUpHeight(), 0.f,
                      resolved, collisionList)) {
      actor.SetMotionState(oldState);
      MoveGroundColliderXY(cache, mgr, actor, actor.GetMaterialFilter(), useColliderList, collideDt);
    }

  } else {
    actor.AddMotionState(newState);
  }

  float stepDown = actor.GetStepDownHeight();
  float resolved = 0.f;
  collisionList.Clear();
  TUniqueId stepZId = kInvalidUniqueId;
  if (stepDown >= 0.f && MoveGroundColliderZ(cache, mgr, actor, actor.GetMaterialFilter(), useColliderList, -stepDown,
                                             resolved, collisionList, stepZId)) {
    if (collisionList.GetCount() > 0) {
      CCollisionInfoList filteredList;
      CollisionUtil::FilterByClosestNormal(zeus::CVector3f{0.f, 0.f, 1.f}, collisionList, filteredList);
      if (filteredList.GetCount() > 0) {
        if (CGameCollision::IsFloor(filteredList.Front().GetMaterialLeft(), filteredList.Front().GetNormalLeft())) {
          if (TCastToPtr<CScriptPlatform> plat = mgr.ObjectById(stepZId)) {
            mgr.SendScriptMsg(plat.GetPtr(), actor.GetUniqueId(), EScriptObjectMessage::AddPlatformRider);
          }
          CGameCollision::SendMaterialMessage(mgr, filteredList.Front().GetMaterialLeft(), actor);
          mgr.SendScriptMsg(&actor, kInvalidUniqueId, EScriptObjectMessage::OnFloor);
        } else {
          CheckFalling(actor, mgr, dt);
        }
      }
    }
  } else {
    CheckFalling(actor, mgr, dt);
  }

  actor.ClearForcesAndTorques();
  actor.MoveCollisionPrimitive(zeus::skZero3f);
  if (actor.GetMaterialList().HasMaterial(EMaterialTypes::Player)) {
    CGameCollision::CollisionFailsafe(mgr, cache, actor, *actor.GetCollisionPrimitive(), useColliderList, 0.f, 1);
  }
}

bool CGroundMovement::ResolveUpDown(CAreaCollisionCache& cache, CStateManager& mgr, CPhysicsActor& actor,
                                    const CMaterialFilter& filter, rstl::reserved_vector<TUniqueId, 1024>& nearList,
                                    float stepUp, float stepDown, float& fOut, CCollisionInfoList& list) {
  float zextent = stepDown;
  if (list.GetCount() <= 0) {
    return true;
  }

  zeus::CAABox aabb = zeus::CAABox();
  zeus::CVector3f normAccum = zeus::skZero3f;
  for (CCollisionInfo& info : list) {
    if (CGameCollision::IsFloor(info.GetMaterialLeft(), info.GetNormalLeft())) {
      aabb.accumulateBounds(info.GetPoint());
      aabb.accumulateBounds(info.GetExtreme());
      normAccum += info.GetNormalLeft();
    }
  }

  if (normAccum.canBeNormalized()) {
    normAccum.normalize();
  } else {
    return true;
  }

  zeus::CAABox actorAABB = actor.GetBoundingBox();
  if (normAccum.z() >= 0.f) {
    zextent = aabb.max.z() - actorAABB.min.z() + 0.02f;
    if (zextent > stepUp) {
      return true;
    }
  } else {
    zextent = aabb.min.z() - actorAABB.max.z() - 0.02f;
    if (zextent < -stepDown) {
      return true;
    }
  }

  actor.MoveCollisionPrimitive({0.f, 0.f, zextent});

  if (!CGameCollision::DetectCollisionBoolean_Cached(mgr, cache, *actor.GetCollisionPrimitive(),
                                                     actor.GetPrimitiveTransform(), filter, nearList)) {
    fOut = zextent;
    actor.SetTranslation(actor.GetTranslation() + zeus::CVector3f(0.f, 0.f, zextent));
    actor.MoveCollisionPrimitive(zeus::skZero3f);

    bool floor = false;
    for (CCollisionInfo& info : list) {
      if (CGameCollision::IsFloor(info.GetMaterialLeft(), info.GetNormalLeft())) {
        floor = true;
        break;
      }
    }

    if (!floor) {
      mgr.SendScriptMsg(&actor, kInvalidUniqueId, EScriptObjectMessage::LandOnNotFloor);
    }

    return false;
  }

  return true;
}

bool CGroundMovement::MoveGroundColliderZ(CAreaCollisionCache& cache, CStateManager& mgr, CPhysicsActor& actor,
                                          const CMaterialFilter& filter,
                                          rstl::reserved_vector<TUniqueId, 1024>& nearList, float amt, float& resolved,
                                          CCollisionInfoList& list, TUniqueId& idOut) {
  actor.MoveCollisionPrimitive({0.f, 0.f, amt});

  zeus::CAABox aabb = zeus::CAABox();
  if (CGameCollision::DetectCollision_Cached(mgr, cache, *actor.GetCollisionPrimitive(), actor.GetPrimitiveTransform(),
                                             filter, nearList, idOut, list)) {
    for (CCollisionInfo& info : list) {
      aabb.accumulateBounds(info.GetPoint());
      aabb.accumulateBounds(info.GetExtreme());
    }

    zeus::CAABox actorAABB = actor.GetBoundingBox();
    float zextent = 0.f;
    if (amt > 0.f) {
      zextent = aabb.min.z() - actorAABB.max.z() - 0.02f + amt;
    } else {
      zextent = aabb.max.z() - actorAABB.min.z() + 0.02f + amt;
    }

    actor.MoveCollisionPrimitive({0.f, 0.f, zextent});

    if (!CGameCollision::DetectCollisionBoolean_Cached(mgr, cache, *actor.GetCollisionPrimitive(),
                                                       actor.GetPrimitiveTransform(), filter, nearList)) {
      resolved = zextent;
      actor.SetTranslation(actor.GetTranslation() + zeus::CVector3f(0.f, 0.f, zextent));
      actor.MoveCollisionPrimitive(zeus::skZero3f);
    }

    bool floor = false;
    for (CCollisionInfo& info : list) {
      if (CGameCollision::IsFloor(info.GetMaterialLeft(), info.GetNormalLeft())) {
        floor = true;
        break;
      }
    }

    if (!floor) {
      mgr.SendScriptMsg(&actor, kInvalidUniqueId, EScriptObjectMessage::LandOnNotFloor);
    }

    CCollisionInfoList filteredList;
    if (amt > 0.f) {
      CollisionUtil::FilterByClosestNormal({0.f, 0.f, -1.f}, list, filteredList);
    } else {
      CollisionUtil::FilterByClosestNormal({0.f, 0.f, 1.f}, list, filteredList);
    }

    if (filteredList.GetCount() > 0) {
      CGameCollision::MakeCollisionCallbacks(mgr, actor, idOut, filteredList);
    }

    return true;
  }

  return false;
}

void CGroundMovement::MoveGroundColliderXY(CAreaCollisionCache& cache, CStateManager& mgr, CPhysicsActor& actor,
                                           const CMaterialFilter& filter,
                                           rstl::reserved_vector<TUniqueId, 1024>& nearList, float dt) {
  bool didCollide = false;
  bool isPlayer = actor.GetMaterialList().HasMaterial(EMaterialTypes::Player);
  float remDt = dt;
  float originalDt = dt;
  TCastToPtr<CPhysicsActor> otherActor;
  CCollisionInfoList collisionList;
  CMotionState newMState = actor.PredictMotion_Internal(dt);
  float transMag = newMState.x0_translation.magnitude();
  float divMag = NAN;
  if (isPlayer) {
    divMag = std::max(transMag / 5.f, 0.005f);
  } else {
    divMag = std::max(transMag / 3.f, 0.02f);
  }

  float minExtent = 0.5f * CGameCollision::GetMinExtentForCollisionPrimitive(*actor.GetCollisionPrimitive());
  if (transMag > minExtent) {
    dt = minExtent * (dt / transMag);
    originalDt = dt;
    newMState = actor.PredictMotion_Internal(dt);
    divMag = std::min(divMag, minExtent);
  }

  float nonCollideDt = dt;
  do {
    actor.MoveCollisionPrimitive(newMState.x0_translation);
    collisionList.Clear();
    TUniqueId otherId = kInvalidUniqueId;
    bool collided =
        CGameCollision::DetectCollision_Cached(mgr, cache, *actor.GetCollisionPrimitive(),
                                               actor.GetPrimitiveTransform(), filter, nearList, otherId, collisionList);
    if (collided) {
      otherActor = mgr.ObjectById(otherId);
    }
    actor.MoveCollisionPrimitive(zeus::skZero3f);
    if (collided) {
      didCollide = true;
      if (newMState.x0_translation.magnitude() < divMag) {
        CCollisionInfoList backfaceFilteredList;
        CCollisionInfoList floorFilteredList;
        zeus::CVector3f deltaVel = actor.GetVelocity();
        if (otherActor) {
          deltaVel -= otherActor->GetVelocity();
        }
        CollisionUtil::FilterOutBackfaces(deltaVel, collisionList, backfaceFilteredList);
        CAABoxFilter::FilterBoxFloorCollisions(backfaceFilteredList, floorFilteredList);
        CGameCollision::MakeCollisionCallbacks(mgr, actor, otherId, floorFilteredList);
        if (floorFilteredList.GetCount() == 0 && isPlayer) {
          CMotionState lastNonCollideState = actor.GetLastNonCollidingState();
          lastNonCollideState.x1c_velocity *= zeus::CVector3f(0.5f);
          lastNonCollideState.x28_angularMomentum *= zeus::CVector3f(0.5f);
          actor.SetMotionState(lastNonCollideState);
        }
        for (const CCollisionInfo& info : floorFilteredList) {
          CCollisionInfo infoCopy = info;
          float restitution =
              CGameCollision::GetCoefficientOfRestitution(infoCopy) + actor.GetCoefficientOfRestitutionModifier();
          if (otherActor) {
            CGameCollision::CollideWithDynamicBodyNoRot(actor, *otherActor, infoCopy, restitution, true);
          } else {
            CGameCollision::CollideWithStaticBodyNoRot(actor, infoCopy.GetMaterialLeft(), infoCopy.GetMaterialRight(),
                                                       infoCopy.GetNormalLeft(), restitution, true);
          }
        }
        remDt -= dt;
        nonCollideDt = std::min(originalDt, remDt);
        dt = nonCollideDt;
      } else {
        nonCollideDt *= 0.5f;
        dt *= 0.5f;
      }
    } else {
      actor.AddMotionState(newMState);
      remDt -= dt;
      dt = nonCollideDt;
      actor.MoveCollisionPrimitive(zeus::skZero3f);
    }

    newMState = actor.PredictMotion_Internal(dt);
  } while (remDt > 0.f);

  if (!didCollide && !actor.GetMaterialList().HasMaterial(EMaterialTypes::GroundCollider)) {
    mgr.SendScriptMsg(&actor, kInvalidUniqueId, EScriptObjectMessage::Falling);
  }

  actor.MoveCollisionPrimitive(zeus::skZero3f);
}

zeus::CVector3f CGroundMovement::CollisionDamping(const zeus::CVector3f& vel, const zeus::CVector3f& dir,
                                                  const zeus::CVector3f& cNorm, float normCoeff, float deltaCoeff) {
  zeus::CVector3f dampedDir = (cNorm * -2.f * cNorm.dot(dir) + dir).normalized();
  zeus::CVector3f dampedNorm = cNorm * cNorm.dot(dampedDir);
  return (dampedDir - dampedNorm) * vel.magnitude() * deltaCoeff + normCoeff * vel.magnitude() * dampedNorm;
}

void CGroundMovement::MoveGroundCollider_New(CStateManager& mgr, CPhysicsActor& actor, float dt,
                                             const rstl::reserved_vector<TUniqueId, 1024>* nearList) {
  zeus::CAABox motionVol = actor.GetMotionVolume(dt);
  rstl::reserved_vector<TUniqueId, 1024> useNearList;
  if (nearList != nullptr) {
    useNearList = *nearList;
  } else {
    mgr.BuildColliderList(useNearList, actor, motionVol);
  }

  CAreaCollisionCache cache(motionVol);
  CGameCollision::BuildAreaCollisionCache(mgr, cache);
  auto& player = static_cast<CPlayer&>(actor);
  player.x9c5_28_slidingOnWall = false;
  bool applyJump = player.x258_movementState == CPlayer::EPlayerMovementState::ApplyJump;
  bool dampUnderwater = false;
  if (player.x9c4_31_inWaterMovement) {
    if (!mgr.GetPlayerState()->HasPowerUp(CPlayerState::EItemType::GravitySuit)) {
      dampUnderwater = true;
    }
  }

  bool noJump = (player.x258_movementState != CPlayer::EPlayerMovementState::ApplyJump &&
                 player.x258_movementState != CPlayer::EPlayerMovementState::Jump);

  float stepDown = player.GetStepDownHeight();
  float stepUp = player.GetStepUpHeight();

  bool doStepDown = true;
  CMaterialList material(EMaterialTypes::NoStepLogic);
  SMoveObjectResult result;

  if (!applyJump) {
    const SMovementOptions opts{
        .x0_setWaterLandingForce = false,
        .x4_waterLandingForceCoefficient = 0.f,
        .x8_minimumWaterLandingForce = 0.f,
        .xc_anyZThreshold = 0.37f,
        .x10_downwardZThreshold = 0.25f,
        .x14_waterLandingVelocityReduction = 0.f,
        .x18_dampForceAndMomentum = true,
        .x19_alwaysClip = false,
        .x1a_disableClipForFloorOnly = noJump,
        .x1c_maxCollisionCycles = 4,
        .x20_minimumTranslationDelta = 0.002f,
        .x24_dampedNormalCoefficient = 0.f,
        .x28_dampedDeltaCoefficient = 1.f,
        .x2c_floorElasticForce = 0.f,
        .x30_wallElasticConstant = 0.02f,
        .x34_wallElasticLinear = 0.2f,
        .x38_maxPositiveVerticalVelocity = player.GetMaximumPlayerPositiveVerticalVelocity(mgr),
        .x3c_floorPlaneNormal = player.GetLastFloorPlaneNormal(),
    };

    if (noJump) {
      zeus::CVector3f vel = player.GetVelocity();
      vel.z() = 0.f;
      actor.SetVelocityWR(vel);
      actor.x15c_force.z() = 0.f;
      actor.x150_momentum.z() = 0.f;
      actor.x168_impulse.z() = 0.f;
    }

    CPhysicsState physStatePre = actor.GetPhysicsState();
    CMaterialList material2 = MoveObjectAnalytical(mgr, actor, dt, useNearList, cache, opts, result);
    CPhysicsState physStatePost = actor.GetPhysicsState();

    /* NoStepLogic must be the only set material bit to bypass step logic */
    if (material2.XOR({EMaterialTypes::NoStepLogic}) != 0u) {
      SMovementOptions optsCopy = opts;
      optsCopy.x19_alwaysClip = noJump;
      optsCopy.x30_wallElasticConstant = 0.03f;

      const zeus::CVector3f postToPre = physStatePre.GetTranslation() - physStatePost.GetTranslation();
      const float postToPreMag = postToPre.magSquared();
      const float quarterStepUp = 0.25f * stepUp;

      rstl::reserved_vector<CPhysicsState, 2> physStateList;
      rstl::reserved_vector<float, 2> stepDeltaList;
      rstl::reserved_vector<CCollisionInfo, 2> collisionInfoList;
      rstl::reserved_vector<TUniqueId, 2> uniqueIdList;
      rstl::reserved_vector<CMaterialList, 2> materialListList;
      bool done = false;
      for (int i = 0; i < 2 && !done; ++i) {
        double useStepUp = (i == 0) ? quarterStepUp : stepUp;
        actor.SetPhysicsState(physStatePre);
        CCollisionInfo collisionInfo;
        TUniqueId id = kInvalidUniqueId;
        CGameCollision::DetectCollision_Cached_Moving(mgr, cache, *actor.GetCollisionPrimitive(), actor.GetTransform(),
                                                      actor.GetMaterialFilter(), useNearList, {0.f, 0.f, 1.f}, id,
                                                      collisionInfo, useStepUp);
        if (collisionInfo.IsValid()) {
          useStepUp = std::max(0.0, useStepUp - optsCopy.x20_minimumTranslationDelta);
          done = true;
        }

        if (useStepUp > 0.0005) {
          actor.SetTranslation(actor.GetTranslation() + zeus::CVector3f(0.f, 0.f, static_cast<float>(useStepUp)));

          SMoveObjectResult result2;
          CMaterialList material3 = MoveObjectAnalytical(mgr, actor, dt, useNearList, cache, optsCopy, result2);
          CCollisionInfo collisionInfo2;
          double useStepDown2 = useStepUp + stepDown;
          TUniqueId id2 = kInvalidUniqueId;
          if (useStepDown2 > 0.0) {
            CGameCollision::DetectCollision_Cached_Moving(mgr, cache, *actor.GetCollisionPrimitive(),
                                                          actor.GetTransform(), actor.GetMaterialFilter(), useNearList,
                                                          {0.f, 0.f, -1.f}, id2, collisionInfo2, useStepDown2);
          } else {
            useStepDown2 = 0.0;
          }

          float minStep = std::min(useStepUp, useStepDown2);
          zeus::CVector3f offsetStep = actor.GetTranslation() - zeus::CVector3f(0.f, 0.f, minStep);
          bool floor = (collisionInfo2.IsValid() &&
                        CGameCollision::CanBlock(collisionInfo2.GetMaterialLeft(), collisionInfo2.GetNormalLeft()));
          zeus::CVector3f postToPre2 = physStatePre.GetTranslation() - offsetStep;
          float stepDelta = postToPre2.magSquared();
          if (floor && postToPreMag < stepDelta) {
            useStepDown2 = std::max(0.0, useStepDown2 - 0.0005);
            actor.SetTranslation(actor.GetTranslation() - zeus::CVector3f(0.f, 0.f, static_cast<float>(useStepDown2)));
            physStateList.push_back(actor.GetPhysicsState());
            stepDeltaList.push_back(stepDelta);
            collisionInfoList.push_back(collisionInfo2);
            uniqueIdList.push_back(id2);
            materialListList.push_back(material3);
          }
        }
      }

      if (physStateList.empty()) {
        actor.SetPhysicsState(physStatePost);
        material = material2;
      } else {
        float maxFloat = -1.0e10f;
        int maxIdx = -1;
        for (size_t i = 0; i < physStateList.size(); ++i) {
          if (maxFloat < stepDeltaList[i]) {
            maxFloat = stepDeltaList[i];
            maxIdx = i;
          }
        }

        actor.SetPhysicsState(physStateList[maxIdx]);
        mgr.SendScriptMsg(&actor, kInvalidUniqueId, EScriptObjectMessage::OnFloor);
        if (CEntity* ent = mgr.ObjectById(uniqueIdList[maxIdx])) {
          result.x0_id.emplace(uniqueIdList[maxIdx]);
          result.x8_collision.emplace(collisionInfoList[maxIdx]);
          if (TCastToPtr<CScriptPlatform>(ent)) {
            mgr.SendScriptMsg(ent, actor.GetUniqueId(), EScriptObjectMessage::AddPlatformRider);
          }
        }

        CCollisionInfo& cInfo = collisionInfoList[maxIdx];
        CGameCollision::SendMaterialMessage(mgr, cInfo.GetMaterialLeft(), actor);
        doStepDown = false;
        actor.SetLastFloorPlaneNormal({cInfo.GetNormalLeft()});
      }
    }
  } else {
    const SMovementOptions opts{
        .x0_setWaterLandingForce = true,
        .x4_waterLandingForceCoefficient = dampUnderwater ? 35.f : 1.f,
        .x8_minimumWaterLandingForce = dampUnderwater ? 5.f : 0.f,
        .xc_anyZThreshold = dampUnderwater ? 0.05f : 0.37f,
        .x10_downwardZThreshold = dampUnderwater ? 0.01f : 0.25f,
        .x14_waterLandingVelocityReduction = dampUnderwater ? 0.2f : 0.f,
        .x18_dampForceAndMomentum = false,
        .x19_alwaysClip = false,
        .x1a_disableClipForFloorOnly = false,
        .x1c_maxCollisionCycles = 4,
        .x20_minimumTranslationDelta = 0.002f,
        .x24_dampedNormalCoefficient = 0.f,
        .x28_dampedDeltaCoefficient = 1.f,
        .x2c_floorElasticForce = 0.1f,
        .x30_wallElasticConstant = 0.2f,
        .x38_maxPositiveVerticalVelocity = player.GetMaximumPlayerPositiveVerticalVelocity(mgr),
        .x3c_floorPlaneNormal = player.GetLastFloorPlaneNormal(),
    };

    material = MoveObjectAnalytical(mgr, actor, dt, useNearList, cache, opts, result);
  }

  if (doStepDown) {
    CCollisionInfo collisionInfo;
    double stepDown2 = actor.GetStepDownHeight();
    float zOffset = 0.f;
    TUniqueId id = kInvalidUniqueId;
    if (stepDown2 > FLT_EPSILON) {
      zeus::CTransform xf = actor.GetTransform();
      xf.origin += zeus::CVector3f(0.f, 0.f, 0.0005f);
      if (!CGameCollision::DetectCollisionBoolean_Cached(mgr, cache, *actor.GetCollisionPrimitive(), xf,
                                                         actor.GetMaterialFilter(), useNearList)) {
        actor.SetTranslation(xf.origin);
        zOffset = 0.0005f;
        stepDown2 += 0.0005;
      }

      CGameCollision::DetectCollision_Cached_Moving(mgr, cache, *actor.GetCollisionPrimitive(), actor.GetTransform(),
                                                    actor.GetMaterialFilter(), useNearList, {0.f, 0.f, -1.f}, id,
                                                    collisionInfo, stepDown2);
    }

    if (id != kInvalidUniqueId) {
      result.x0_id.emplace(id);
      result.x8_collision.emplace(collisionInfo);
    }

    if (!collisionInfo.IsValid() ||
        !CGameCollision::CanBlock(collisionInfo.GetMaterialLeft(), collisionInfo.GetNormalLeft())) {
      if (zOffset > 0.f) {
        zeus::CTransform xf = actor.GetTransform();
        xf.origin -= zeus::CVector3f(0.f, 0.f, zOffset);
      }

      if (collisionInfo.IsValid()) {
        player.x9c5_28_slidingOnWall = true;
      }
      CheckFalling(actor, mgr, dt);
      player.SetLastFloorPlaneNormal({});
    } else {
      mgr.SendScriptMsg(&actor, kInvalidUniqueId, EScriptObjectMessage::OnFloor);
      stepDown2 = std::max(0.0, stepDown2 - 0.0005);
      actor.SetTranslation(actor.GetTranslation() - zeus::CVector3f(0.f, 0.f, static_cast<float>(stepDown2)));
      if (TCastToPtr<CScriptPlatform> plat = mgr.ObjectById(id)) {
        mgr.SendScriptMsg(plat.GetPtr(), actor.GetUniqueId(), EScriptObjectMessage::AddPlatformRider);
      }
      CGameCollision::SendMaterialMessage(mgr, collisionInfo.GetMaterialLeft(), actor);
      actor.SetLastFloorPlaneNormal({collisionInfo.GetNormalLeft()});
    }
  }

  actor.ClearForcesAndTorques();
  if (material.HasMaterial(EMaterialTypes::Wall)) {
    player.SetPlayerHitWallDuringMove();
  }

  if (result.x0_id) {
    CCollisionInfoList list;
    list.Add(*result.x8_collision, false);
    CGameCollision::MakeCollisionCallbacks(mgr, actor, *result.x0_id, list);
  }

  CMotionState mState = actor.GetMotionState();
  mState.x0_translation = actor.GetLastNonCollidingState().x0_translation;
  mState.x1c_velocity = actor.GetLastNonCollidingState().x1c_velocity;
  actor.SetLastNonCollidingState(mState);

  const CCollisionPrimitive* usePrim = actor.GetCollisionPrimitive();
  std::unique_ptr<CCollisionPrimitive> prim;
  if (usePrim->GetPrimType() == FOURCC('AABX')) {
    const auto& existingAABB = static_cast<const CCollidableAABox&>(*usePrim);
    prim = std::make_unique<CCollidableAABox>(
        zeus::CAABox(existingAABB.GetBox().min + 0.0001f, existingAABB.GetBox().max - 0.0001f), usePrim->GetMaterial());
    usePrim = prim.get();
  } else if (usePrim->GetPrimType() == FOURCC('SPHR')) {
    const auto& existingSphere = static_cast<const CCollidableSphere&>(*usePrim);
    prim = std::make_unique<CCollidableSphere>(
        zeus::CSphere(existingSphere.GetSphere().position, existingSphere.GetSphere().radius - 0.0001f),
        usePrim->GetMaterial());
    usePrim = prim.get();
  }

  CGameCollision::CollisionFailsafe(mgr, cache, actor, *usePrim, useNearList, 0.f, 1);
}

bool CGroundMovement::RemoveNormalComponent(const zeus::CVector3f& a, const zeus::CVector3f& b, zeus::CVector3f& c,
                                            float& d) {
  float dot = a.dot(c);
  if (std::fabs(dot) > 0.99f) {
    return false;
  }
  float dot2 = b.dot(c);
  float dot3 = b.dot((c - a * dot).normalized());
  if (dot2 > 0.f && dot3 < 0.f) {
    return false;
  }
  if (std::fabs(dot2) > 0.01f && std::fabs(dot3 / dot2) > 4.f) {
    return false;
  }
  c -= dot * a;
  d = dot;
  return true;
}

bool CGroundMovement::RemoveNormalComponent(const zeus::CVector3f& a, zeus::CVector3f& b) {
  float dot = a.dot(b);
  if (std::fabs(dot) > 0.99f) {
    return false;
  }
  b -= a * dot;
  return true;
}

static bool RemovePositiveZComponentFromNormal(zeus::CVector3f& vec) {
  if (vec.z() > 0.f && vec.z() < 0.99f) {
    vec.z() = 0.f;
    vec.normalize();
    return true;
  }
  return false;
}

CMaterialList CGroundMovement::MoveObjectAnalytical(CStateManager& mgr, CPhysicsActor& actor, float dt,
                                                    rstl::reserved_vector<TUniqueId, 1024>& nearList,
                                                    CAreaCollisionCache& cache, const SMovementOptions& opts,
                                                    SMoveObjectResult& result) {
  result.x6c_processedCollisions = 0;
  CMaterialList ret;
  zeus::CVector3f floorPlaneNormal = opts.x3c_floorPlaneNormal ? *opts.x3c_floorPlaneNormal : zeus::skZero3f;
  bool floorCollision = opts.x3c_floorPlaneNormal.has_value();
  float remDt = dt;
  for (size_t i = 0; remDt > 0.f; ++i) {
    float collideDt = remDt;

    CMotionState mState = actor.PredictMotion_Internal(remDt);
    double mag = mState.x0_translation.magnitude();
    zeus::CVector3f normTrans = (1.f / ((float(mag) > FLT_EPSILON) ? float(mag) : 1.f)) * mState.x0_translation;
    TUniqueId id = kInvalidUniqueId;
    CCollisionInfo collisionInfo;

    if (mag > opts.x20_minimumTranslationDelta) {
      double oldMag = mag;
      CGameCollision::DetectCollision_Cached_Moving(mgr, cache, *actor.GetCollisionPrimitive(),
                                                    actor.GetPrimitiveTransform(), actor.GetMaterialFilter(), nearList,
                                                    normTrans, id, collisionInfo, mag);
      if (id != kInvalidUniqueId) {
        result.x0_id.emplace(id);
        result.x8_collision.emplace(collisionInfo);
      }
      collideDt = static_cast<float>(mag / oldMag * remDt);
    }

    mag = std::max(0.f, float(mag) - opts.x20_minimumTranslationDelta);

    zeus::CVector3f collisionNorm = collisionInfo.GetNormalLeft();
    bool floor = CGameCollision::CanBlock(collisionInfo.GetMaterialLeft(), collisionNorm);
    bool clipCollision = true;
    if (!opts.x19_alwaysClip) {
      if (!opts.x1a_disableClipForFloorOnly || floor) {
        clipCollision = false;
      }
    }

    float collisionFloorDot = 0.f;

    if (collisionInfo.IsValid()) {
      result.x6c_processedCollisions += 1;
      if (floor) {
        ret.Add(EMaterialTypes::Floor);
        floorPlaneNormal = collisionInfo.GetNormalLeft();
        floorCollision = true;
      } else {
        ret.Add(EMaterialTypes::Wall);
      }

      if (clipCollision) {
        if (floorCollision) {
          if (!CGroundMovement::RemoveNormalComponent(floorPlaneNormal, normTrans, collisionNorm, collisionFloorDot)) {
            RemovePositiveZComponentFromNormal(collisionNorm);
          } else {
            collisionNorm.normalize();
          }
        } else {
          RemovePositiveZComponentFromNormal(collisionNorm);
        }
      }

      mState = actor.PredictMotion_Internal(collideDt);
    }

    mState.x0_translation = normTrans * static_cast<float>(mag);
    actor.AddMotionState(mState);

    if (collisionInfo.IsValid()) {
      zeus::CVector3f vel =
          actor.GetVelocity().canBeNormalized()
              ? CGroundMovement::CollisionDamping(actor.GetVelocity(), actor.GetVelocity().normalized(), collisionNorm,
                                                  opts.x24_dampedNormalCoefficient, opts.x28_dampedDeltaCoefficient)
              : zeus::skZero3f;
      float elasticForce = floor ? opts.x2c_floorElasticForce
                                 : opts.x34_wallElasticLinear * collisionFloorDot + opts.x30_wallElasticConstant;
      float dot = collisionNorm.dot(vel);
      if (dot < elasticForce) {
        vel += (elasticForce - dot) * collisionNorm;
      }
      if (clipCollision && floorCollision) {
        if (!CGroundMovement::RemoveNormalComponent(floorPlaneNormal, vel)) {
          vel.z() = 0.f;
        }
      }
      if (vel.z() > opts.x38_maxPositiveVerticalVelocity) {
        vel *= zeus::CVector3f(opts.x38_maxPositiveVerticalVelocity / vel.z());
      }

      if (opts.x18_dampForceAndMomentum) {
        if (actor.x15c_force.canBeNormalized()) {
          // zeus::CVector3f prevForce = actor.x15c_force;
          actor.x15c_force = CGroundMovement::CollisionDamping(actor.x15c_force, actor.x15c_force.normalized(),
                                                               collisionNorm, 0.f, 1.f);
        }
        if (actor.x150_momentum.canBeNormalized()) {
          actor.x150_momentum = CGroundMovement::CollisionDamping(actor.x150_momentum, actor.x150_momentum.normalized(),
                                                                  collisionNorm, 0.f, 1.f);
        }
      }

      if (opts.x0_setWaterLandingForce && !floor) {
        if (collisionInfo.GetNormalLeft().z() < -0.1f && vel.z() > 0.f) {
          vel.z() *= 0.5f;
        }

        float zNormAbs = std::fabs(collisionInfo.GetNormalLeft().z());
        if ((zNormAbs > opts.x10_downwardZThreshold && vel.z() < 0.f) || zNormAbs > opts.xc_anyZThreshold) {
          actor.x15c_force = zeus::CVector3f(
              0.f, 0.f,
              -(1.f + std::max(opts.x4_waterLandingForceCoefficient * zNormAbs, opts.x8_minimumWaterLandingForce)) *
                  actor.GetWeight());
          vel *= zeus::CVector3f(1.f - opts.x14_waterLandingVelocityReduction);
        }
      }

      actor.SetVelocityWR(vel);
    } else {
      zeus::CVector3f vel = actor.x138_velocity;
      if (actor.x138_velocity.z() > opts.x38_maxPositiveVerticalVelocity) {
        vel *= zeus::CVector3f(opts.x38_maxPositiveVerticalVelocity / vel.z());
      }

      actor.SetVelocityWR(vel);
    }

    actor.ClearImpulses();

    remDt -= collideDt;
    if (i >= opts.x1c_maxCollisionCycles) {
      break;
    }
  }

  result.x70_processedDt = dt - remDt;
  return ret;
}

} // namespace metaforce
