#include "Runtime/MP1/World/CBouncyGrenade.hpp"

#include "Runtime/CPlayerState.hpp"
#include "Runtime/CSimplePool.hpp"
#include "Runtime/CStateManager.hpp"
#include "Runtime/GameGlobalObjects.hpp"
#include "Runtime/Graphics/CBooRenderer.hpp"
#include "Runtime/World/CPlayer.hpp"
#include "Runtime/Collision/CCollisionActor.hpp"

namespace metaforce::MP1 {
CBouncyGrenade::CBouncyGrenade(TUniqueId uid, std::string_view name, const CEntityInfo& info,
                               const zeus::CTransform& xf, CModelData&& mData, const CActorParameters& actParams,
                               TUniqueId parentId, const SBouncyGrenadeData& data, float velocity,
                               float explodePlayerDistance)
: CPhysicsActor(uid, true, name, info, xf, std::move(mData), {EMaterialTypes::Projectile, EMaterialTypes::Solid},
                mData.GetBounds(), SMoverData{data.GetUnkStruct().GetMass()}, actParams, 0.3f, 0.1f)
, x258_data(data)
, x294_numBounces(data.GetNumBounces())
, x298_parentId(parentId)
, x2a0_elementGenCombat(std::make_unique<CElementGen>(g_SimplePool->GetObj({'PART', data.GetElementGenId1()})))
, x2a4_elementGenXRay(std::make_unique<CElementGen>(g_SimplePool->GetObj({'PART', data.GetElementGenId2()})))
, x2a8_elementGenThermal(std::make_unique<CElementGen>(g_SimplePool->GetObj({'PART', data.GetElementGenId3()})))
, x2ac_elementGen4(std::make_unique<CElementGen>(g_SimplePool->GetObj({'PART', data.GetElementGenId4()})))
, x2b0_explodePlayerDistance(explodePlayerDistance) {
  SetMomentumWR({0.f, 0.f, -GravityConstant() * GetMass()});
  SetVelocityWR(velocity * xf.frontVector());
  x2a0_elementGenCombat->SetParticleEmission(false);
  x2a4_elementGenXRay->SetParticleEmission(false);
  x2a8_elementGenThermal->SetParticleEmission(false);
  x2ac_elementGen4->SetParticleEmission(true);
  CMaterialFilter filter = GetMaterialFilter();
  filter.ExcludeList().Add(EMaterialTypes::Character);
  SetMaterialFilter(CMaterialFilter::MakeIncludeExclude(filter.IncludeList(), filter.ExcludeList()));
}

void CBouncyGrenade::AddToRenderer(const zeus::CFrustum& frustum, CStateManager& mgr) {
  CActor::AddToRenderer(frustum, mgr);
  if (!x2b4_24_exploded) {
    g_Renderer->AddParticleGen(*x2ac_elementGen4);
    return;
  }
  const auto visor = mgr.GetPlayerState()->GetActiveVisor(mgr);
  if (visor == CPlayerState::EPlayerVisor::Combat || visor == CPlayerState::EPlayerVisor::Scan) {
    g_Renderer->AddParticleGen(*x2a0_elementGenCombat);
  } else if (visor == CPlayerState::EPlayerVisor::XRay || visor == CPlayerState::EPlayerVisor::Thermal) {
    g_Renderer->AddParticleGen(*x2a8_elementGenThermal);
  }
}

void CBouncyGrenade::CollidedWith(TUniqueId id, const CCollisionInfoList& list, CStateManager& mgr) {
  constexpr auto matList = CMaterialList{
      EMaterialTypes::Solid,
      EMaterialTypes::Ceiling,
      EMaterialTypes::Floor,
      EMaterialTypes::Character,
  };

  bool shouldExplode = false;
  if (x298_parentId != id) {
    const CEntity* const entity = mgr.GetObjectById(id);
    if (entity != nullptr) {
      if (TCastToConstPtr<CCollisionActor> actor = entity) {
        shouldExplode = actor->GetOwnerId() != x298_parentId;
      } else {
        shouldExplode = true;
      }
    }
  }
  if (shouldExplode) {
    Explode(mgr, id);
  } else {
    for (const auto& info : list) {
      if (info.GetMaterialLeft().SharesMaterials(matList)) {
        if (x294_numBounces == 0) {
          Explode(mgr, kInvalidUniqueId);
        } else {
          const zeus::CVector3f* normal = &info.GetNormalLeft();
          if (GetVelocity().dot(info.GetNormalLeft()) > 0.f) {
            normal = &info.GetNormalRight();
          }
          const zeus::CVector3f impulse =
              (x258_data.GetUnkStruct().GetSpeed() * GetConstantForce().magnitude()) * *normal;
          const zeus::CVector3f angle = -x258_data.GetUnkStruct().GetSpeed() * GetAngularMomentum();
          ApplyImpulseWR(impulse, angle);
          CSfxManager::AddEmitter(x258_data.GetBounceSfx(), GetTranslation(), zeus::skUp, false, false, 0x7f,
                                  GetAreaIdAlways());
          --x294_numBounces;
        }
        break;
      }
    }
  }
  CPhysicsActor::CollidedWith(id, list, mgr);
}

std::optional<zeus::CAABox> CBouncyGrenade::GetTouchBounds() const { return GetModelData()->GetBounds(GetTransform()); }

void CBouncyGrenade::Render(CStateManager& mgr) {
  if (!x2b4_24_exploded) {
    GetModelData()->Render(mgr, GetTransform(), nullptr, {0, 0, 3, zeus::skWhite});
  } else if (mgr.GetPlayerState()->GetActiveVisor(mgr) == CPlayerState::EPlayerVisor::XRay) {
    CElementGen::SetSubtractBlend(true);
    CElementGen::SetMoveRedToAlphaBuffer(true);
    CGraphics::SetFog(ERglFogMode::PerspLin, 0.f, 75.f, zeus::skBlack);
    x2a4_elementGenXRay->Render();
    mgr.SetupFogForArea(GetAreaIdAlways());
    CElementGen::SetSubtractBlend(false);
    CElementGen::SetMoveRedToAlphaBuffer(false);
  }
}

void CBouncyGrenade::Think(float dt, CStateManager& mgr) {
  if (GetActive()) {
    const zeus::CTransform& orientation = GetTransform().getRotation();
    const zeus::CVector3f& translation = GetTranslation();
    const zeus::CVector3f& scale = GetModelData()->GetScale();
    auto UpdateElementGen = [ orientation, translation, scale, dt ](CElementGen & gen) {
      gen.SetOrientation(orientation);
      gen.SetGlobalTranslation(translation);
      gen.SetGlobalScale(scale);
      gen.Update(dt);
    };
    if (x2b4_24_exploded) {
      Stop();
      UpdateElementGen(*x2a0_elementGenCombat);
      UpdateElementGen(*x2a4_elementGenXRay);
      UpdateElementGen(*x2a8_elementGenThermal);
    } else {
      UpdateElementGen(*x2ac_elementGen4);
    }
    x29c_ += dt;
    if (x29c_ > 0.3f) {
      x2b4_25_ = true;
    }
    const zeus::CVector3f& playerDistance = mgr.GetPlayer().GetTranslation() +
                                            zeus::CVector3f{0.f, 0.f, 0.5f * mgr.GetPlayer().GetEyeHeight()} -
                                            translation;
    if (playerDistance.magSquared() < x2b0_explodePlayerDistance * x2b0_explodePlayerDistance) {
      Explode(mgr, kInvalidUniqueId);
    }
  }
  if (x2a0_elementGenCombat->IsSystemDeletable() && x2a4_elementGenXRay->IsSystemDeletable() &&
      x2a8_elementGenThermal->IsSystemDeletable()) {
    mgr.FreeScriptObject(GetUniqueId());
  }
}

void CBouncyGrenade::Touch(CActor& act, CStateManager& mgr) { CActor::Touch(act, mgr); }

void CBouncyGrenade::Explode(CStateManager& mgr, TUniqueId uid) {
  if (x2b4_24_exploded) {
    return;
  }

  x2b4_24_exploded = true;
  CSfxManager::AddEmitter(x258_data.GetExplodeSfx(), GetTranslation(), zeus::skUp, false, false, 0x7f,
                          GetAreaIdAlways());
  x2a0_elementGenCombat->SetParticleEmission(true);
  x2a4_elementGenXRay->SetParticleEmission(true);
  x2a8_elementGenThermal->SetParticleEmission(true);
  x2ac_elementGen4->SetParticleEmission(false);

  const CDamageInfo& dInfo = x258_data.GetDamageInfo();
  {
    bool isParent = x298_parentId == uid;
    if (TCastToConstPtr<CCollisionActor> actor = mgr.GetObjectById(uid)) {
      isParent = x298_parentId == actor->GetOwnerId();
    }
    if (uid != kInvalidUniqueId && !isParent) {
      mgr.ApplyDamage(GetUniqueId(), uid, GetUniqueId(), dInfo, CMaterialFilter::MakeInclude({EMaterialTypes::Solid}),
                      zeus::skZero3f);
    }
  }

  const float radius = dInfo.GetRadius();
  if (radius > 1.f) {
    const zeus::CVector3f& pos = GetTranslation();
    const CMaterialFilter filter = CMaterialFilter::MakeInclude({EMaterialTypes::Player, EMaterialTypes::Character});
    rstl::reserved_vector<TUniqueId, 1024> nearList;
    mgr.BuildNearList(nearList, {pos - radius, pos + radius}, filter, nullptr);

    for (const auto& id : nearList) {
      bool isParent = x298_parentId == id;
      if (TCastToConstPtr<CCollisionActor> cActor = mgr.GetObjectById(id)) {
        isParent = x298_parentId == cActor->GetOwnerId();
      }
      if (isParent) {
        continue;
      }

      const auto* actor = static_cast<const CActor*>(mgr.GetObjectById(id));
      if (actor == nullptr) {
        continue;
      }

      const float magnitude = (actor->GetTranslation() - GetTranslation()).magnitude();
      if (radius <= magnitude) {
        continue;
      }

      float scale = (radius - magnitude) / radius;
      const CDamageInfo info{dInfo.GetWeaponMode(), scale * dInfo.GetDamage(), radius,
                             scale * dInfo.GetKnockBackPower()};
      mgr.ApplyDamage(GetUniqueId(), id, GetUniqueId(), info, CMaterialFilter::MakeInclude({EMaterialTypes::Solid}),
                      zeus::skZero3f);
    }
  }
}
} // namespace metaforce::MP1
