#include "Runtime/World/CWorldTransManager.hpp"

#include "Runtime/CGameState.hpp"
#include "Runtime/CSimplePool.hpp"
#include "Runtime/GameGlobalObjects.hpp"
#include "Runtime/Audio/CSfxManager.hpp"
#include "Runtime/Camera/CCameraManager.hpp"
#include "Runtime/Character/CActorLights.hpp"
#include "Runtime/Character/CAnimPlaybackParms.hpp"
#include "Runtime/Character/CAssetFactory.hpp"
#include "Runtime/Character/CCharacterFactory.hpp"
#include "Runtime/Character/CSkinRules.hpp"
#include "Runtime/Character/IAnimReader.hpp"
#include "Runtime/Graphics/CBooRenderer.hpp"
#include "Runtime/Graphics/CModel.hpp"
#include "Runtime/GuiSys/CGuiTextSupport.hpp"
#include "Runtime/GuiSys/CStringTable.hpp"

#include <hecl/CVarManager.hpp>

namespace metaforce {

int CWorldTransManager::GetSuitCharIdx() {
  CPlayerState& state = *g_GameState->GetPlayerState();
  if (state.IsFusionEnabled()) {
    switch (state.x20_currentSuit) {
    case CPlayerState::EPlayerSuit::Power:
      return 4;
    case CPlayerState::EPlayerSuit::Gravity:
      return 6;
    case CPlayerState::EPlayerSuit::Varia:
      return 7;
    case CPlayerState::EPlayerSuit::Phazon:
      return 8;
    default:
      break;
    }
  }
  return int(state.x20_currentSuit);
}

CWorldTransManager::SModelDatas::SModelDatas(const CAnimRes& samusRes) : x0_samusRes(samusRes) {
  x1a0_lights.reserve(8);
}

void CWorldTransManager::UpdateLights(float dt) {
  if (!x4_modelData) {
    return;
  }

  x4_modelData->x1a0_lights.clear();
  constexpr zeus::CVector3f lightPos(0.f, 10.f, 0.f);
  CLight spot = CLight::BuildSpot(lightPos, zeus::skBack, zeus::skWhite, 90.f);
  spot.SetAttenuation(1.f, 0.f, 0.f);

  CLight s1 = spot;
  s1.SetPosition(lightPos + zeus::CVector3f{0.f, 0.f, 2.f * x18_bgOffset - x1c_bgHeight});

  float z = 1.f;
  float delta = x1c_bgHeight - x18_bgOffset;
  if (!x44_26_goingUp && delta < 2.f)
    z = delta * 0.5f;
  else if (x44_26_goingUp && x18_bgOffset < 2.f)
    z = x18_bgOffset * 0.5f;

  if (z < 1.f) {
    CLight s2 = spot;
    float pos = x44_26_goingUp ? x1c_bgHeight : -x1c_bgHeight;
    s2.SetPosition(lightPos + zeus::CVector3f{0.f, 0.f, pos});
    s2.SetColor(zeus::CColor::lerp(zeus::skBlack, zeus::skWhite, 1.f - z));
    x4_modelData->x1a0_lights.push_back(std::move(s2));
    s1.SetColor(zeus::CColor::lerp(zeus::skBlack, zeus::skWhite, z));
  }

  x4_modelData->x1a0_lights.push_back(std::move(s1));
}

void CWorldTransManager::UpdateDisabled(float) {
  if (x0_curTime <= 2.f)
    return;
  x44_24_transFinished = true;
}

void CWorldTransManager::UpdateEnabled(float dt) {
  if (x4_modelData && !x4_modelData->x1c_samusModelData.IsNull()) {
    if (x44_25_stopSoon && !x4_modelData->x1dc_dissolveStarted && x0_curTime > 2.f) {
      x4_modelData->x1dc_dissolveStarted = true;
      x4_modelData->x1d0_dissolveStartTime = x0_curTime;
      x4_modelData->x1d4_dissolveEndTime = 4.f + x0_curTime - 2.f;
      x4_modelData->x1d8_transCompleteTime = 5.f + x0_curTime - 2.f;
    }

    if (x0_curTime > x4_modelData->x1d8_transCompleteTime && x4_modelData->x1dc_dissolveStarted)
      x44_24_transFinished = true;

    x4_modelData->x1c_samusModelData.AdvanceAnimationIgnoreParticles(dt, x20_random, true);
    x4_modelData->x170_gunXf = x4_modelData->x1c_samusModelData.GetScaledLocatorTransform("GUN_LCTR");

    x4_modelData->x1c4_randTimeout -= dt;

    if (x4_modelData->x1c4_randTimeout <= 0.f) {
      x4_modelData->x1c4_randTimeout = x20_random.Range(0.016666668f, 0.1f);
      zeus::CVector2f randVec(x20_random.Range(-0.025f, 0.025f), x20_random.Range(-0.075f, 0.075f));
      x4_modelData->x1bc_shakeDelta = (randVec - x4_modelData->x1b4_shakeResult) / x4_modelData->x1c4_randTimeout;
      x4_modelData->x1cc_blurDelta =
          (x20_random.Range(-2.f, 4.f) - x4_modelData->x1c8_blurResult) / x4_modelData->x1c4_randTimeout;
    }

    x4_modelData->x1b4_shakeResult += x4_modelData->x1bc_shakeDelta * dt;
    x4_modelData->x1c8_blurResult += dt * x4_modelData->x1cc_blurDelta;
  }

  float delta = dt * 50.f;
  if (x44_26_goingUp)
    delta = -delta;

  x18_bgOffset += delta;
  if (x18_bgOffset > x1c_bgHeight)
    x18_bgOffset -= x1c_bgHeight;
  if (x18_bgOffset < 0.f)
    x18_bgOffset += x1c_bgHeight;

  UpdateLights(dt);
}

void CWorldTransManager::UpdateText(float dt) {
  if (x44_28_textDirty) {
    if (xc_strTable.IsLoaded()) {
      x8_textData->SetText(xc_strTable->GetString(x40_strIdx));
      x3c_sfxInterval = 0.f;
      x44_28_textDirty = false;
    } else if (x0_curTime >= x38_textStartTime) {
      x38_textStartTime += dt;
    }
  }

  if (x0_curTime >= x38_textStartTime) {
    x8_textData->Update(dt);

    float nextSfxInterval = x3c_sfxInterval + g_tweakGui->GetWorldTransManagerCharsPerSfx();
    float printed = x8_textData->GetNumCharsPrinted();
    if (printed >= nextSfxInterval) {
      x3c_sfxInterval = nextSfxInterval;
      CSfxManager::SfxStart(SFXsfx059E, 1.f, 0.f, false, 0x7f, false, kInvalidAreaId);
    }
  }

  if (x44_25_stopSoon) {
    if (x8_textData->GetTotalAnimationTime() + 1.f < x8_textData->GetCurTime()) {
      /* Done typing + 1 sec */
      if (x0_curTime - x34_stopTime > 1.f)
        x44_24_transFinished = true;
    } else {
      x34_stopTime = x0_curTime;
    }
  }
}

void CWorldTransManager::Update(float dt) {
  x0_curTime += dt;
  switch (x30_type) {
  case ETransType::Disabled:
    UpdateDisabled(dt);
    break;
  case ETransType::Enabled:
    UpdateEnabled(dt);
    break;
  case ETransType::Text:
    UpdateText(dt);
    break;
  }
}

void CWorldTransManager::DrawPlatformModels(CActorLights* lights) {
  CModelFlags flags = {};
  flags.m_extendedShader = EExtendedShader::Lighting;

  if (!x4_modelData->x100_bgModelData[0].IsNull()) {
    zeus::CTransform xf0 = zeus::CTransform::Translate(0.f, 0.f, -(2.f * x1c_bgHeight - x18_bgOffset));
    x4_modelData->x100_bgModelData[0].Render(CModelData::EWhichModel::Normal, xf0, lights, flags);
  }
  if (!x4_modelData->x100_bgModelData[1].IsNull()) {
    zeus::CTransform xf1 = zeus::CTransform::Translate(0.f, 0.f, x18_bgOffset - x1c_bgHeight);
    x4_modelData->x100_bgModelData[1].Render(CModelData::EWhichModel::Normal, xf1, lights, flags);
  }
  if (!x4_modelData->x100_bgModelData[2].IsNull()) {
    zeus::CTransform xf2 = zeus::CTransform::Translate(0.f, 0.f, x18_bgOffset);
    x4_modelData->x100_bgModelData[2].Render(CModelData::EWhichModel::Normal, xf2, lights, flags);
  }

  if (!x4_modelData->xb4_platformModelData.IsNull()) {
    x4_modelData->xb4_platformModelData.Render(CModelData::EWhichModel::Normal, zeus::CTransform(), lights,
                                               flags);
  }
}

void CWorldTransManager::DrawAllModels(CActorLights* lights) {
  DrawPlatformModels(lights);

  if (!x4_modelData->x1c_samusModelData.IsNull()) {
    CModelFlags flags = {};
    flags.m_extendedShader = EExtendedShader::LightingCubeReflection;

    x4_modelData->x1c_samusModelData.GetAnimationData()->PreRender();
    x4_modelData->x1c_samusModelData.Render(CModelData::EWhichModel::Normal, zeus::CTransform(), lights,
                                            flags);

    if (!x4_modelData->x68_beamModelData.IsNull()) {
      x4_modelData->x68_beamModelData.Render(CModelData::EWhichModel::Normal, x4_modelData->x170_gunXf, lights, flags);
    }
  }
}

void CWorldTransManager::DrawFirstPass(CActorLights* lights) {
  CBooModel::SetReflectionCube(m_reflectionCube[0]);
  zeus::CTransform translateXf = zeus::CTransform::Translate(
      x4_modelData->x1b4_shakeResult.x(), -3.5f * (1.f - zeus::clamp(0.f, x0_curTime / 10.f, 1.f)) - 3.5f,
      x4_modelData->x1b4_shakeResult.y() + 2.f);
  zeus::CTransform rotateXf =
      zeus::CTransform::RotateZ(zeus::degToRad(zeus::clamp(0.f, x0_curTime / 25.f, 100.f) * 360.f + 180.f - 90.f));
  CGraphics::SetViewPointMatrix(rotateXf * translateXf);
  DrawAllModels(lights);
  m_camblur.draw(x4_modelData->x1c8_blurResult);
}

void CWorldTransManager::DrawSecondPass(CActorLights* lights) {
  CBooModel::SetReflectionCube(m_reflectionCube[1]);
  const zeus::CVector3f& samusScale = x4_modelData->x0_samusRes.GetScale();
  zeus::CTransform translateXf =
      zeus::CTransform::Translate(-0.1f * samusScale.x(), -0.5f * samusScale.y(), 1.5f * samusScale.z());
  zeus::CTransform rotateXf = zeus::CTransform::RotateZ(zeus::degToRad(
      48.f * zeus::clamp(0.f, (x0_curTime - x4_modelData->x1d0_dissolveStartTime + 2.f) / 5.f, 1.f) + 180.f - 24.f));
  CGraphics::SetViewPointMatrix(rotateXf * translateXf);
  DrawAllModels(lights);
}

void CWorldTransManager::DrawEnabled() {
  SCOPED_GRAPHICS_DEBUG_GROUP("CWorldTransManager::DrawEnabled", zeus::skPurple);
  CActorLights lights(0, zeus::skZero3f, 4, 4, 0, 0, 0, 0.1f);
  lights.BuildFakeLightList(x4_modelData->x1a0_lights, zeus::CColor{0.1f, 0.1f, 0.1f, 1.0f});

  if (m_reflectionCube[0]) {
    SViewport backupVp = g_Viewport;
    constexpr float width = CUBEMAP_RES;
    CGraphics::g_BooMainCommandQueue->setRenderTarget(m_reflectionCube[0], 0);
    g_Renderer->SetViewport(0, 0, width, width);
    g_Renderer->SetPerspective(90.f, width, width, 0.2f, 750.f);

    if (x0_curTime < x4_modelData->x1d4_dissolveEndTime) {
      zeus::CTransform mainCamXf =
        zeus::CTransform::RotateZ(zeus::degToRad(zeus::clamp(0.f, x0_curTime / 25.f, 100.f) * 360.f + 180.f - 90.f));
      for (int face = 0; face < 6; ++face) {
        CGraphics::g_BooMainCommandQueue->setRenderTarget(m_reflectionCube[0], face);
        CGraphics::g_BooMainCommandQueue->clearTarget();
        zeus::CTransform camXf = zeus::CTransform(mainCamXf.basis * CGraphics::skCubeBasisMats[face], zeus::CVector3f(0.f, 0.f, 1.5f));
        g_Renderer->SetWorldViewpoint(camXf);
        DrawPlatformModels(&lights);
      }
      CGraphics::g_BooMainCommandQueue->generateMipmaps(m_reflectionCube[0]);
    }
    if (x0_curTime > x4_modelData->x1d0_dissolveStartTime) {
      zeus::CTransform mainCamXf = zeus::CTransform::RotateZ(zeus::degToRad(
        48.f * zeus::clamp(0.f, (x0_curTime - x4_modelData->x1d0_dissolveStartTime + 2.f) / 5.f, 1.f) + 180.f - 24.f));
      for (int face = 0; face < 6; ++face) {
        CGraphics::g_BooMainCommandQueue->setRenderTarget(m_reflectionCube[1], face);
        CGraphics::g_BooMainCommandQueue->clearTarget();
        zeus::CTransform camXf = zeus::CTransform(mainCamXf.basis * CGraphics::skCubeBasisMats[face], zeus::CVector3f(0.f, 0.f, 1.5f));
        g_Renderer->SetWorldViewpoint(camXf);
        DrawPlatformModels(&lights);
      }
      CGraphics::g_BooMainCommandQueue->generateMipmaps(m_reflectionCube[1]);
    }

    CBooRenderer::BindMainDrawTarget();
    g_Renderer->SetViewport(backupVp.x0_left, backupVp.x4_top, backupVp.x8_width, backupVp.xc_height);
  }

  float wsAspect = CWideScreenFilter::SetViewportToMatch(1.f);

  g_Renderer->SetPerspective(CCameraManager::FirstPersonFOV(), wsAspect, CCameraManager::NearPlane(),
                             CCameraManager::FarPlane());
  g_Renderer->x318_26_requestRGBA6 = true;

  if (x0_curTime <= x4_modelData->x1d0_dissolveStartTime)
    DrawFirstPass(&lights);
  else if (x0_curTime >= x4_modelData->x1d4_dissolveEndTime)
    DrawSecondPass(&lights);
  else {
    float t = zeus::clamp(0.f, (x0_curTime - x4_modelData->x1d0_dissolveStartTime) / 2.f, 1.f);
    DrawFirstPass(&lights);
    SClipScreenRect rect(g_Viewport);
    CGraphics::ResolveSpareTexture(rect);
    CGraphics::g_BooMainCommandQueue->clearTarget(true, true);
    DrawSecondPass(&lights);
    m_dissolve.drawCropped(zeus::CColor{1.f, 1.f, 1.f, 1.f - t}, 1.f);
  }

  CWideScreenFilter::SetViewportToFull();
  m_widescreen.draw(zeus::skBlack, 1.f);

  float ftbT = 0.f;
  if (x0_curTime < 0.25f)
    ftbT = 1.f - x0_curTime / 0.25f;
  else if (x0_curTime > x4_modelData->x1d8_transCompleteTime)
    ftbT = 1.f;
  else if (x0_curTime > x4_modelData->x1d8_transCompleteTime - 0.25f)
    ftbT = 1.f - (x4_modelData->x1d8_transCompleteTime - x0_curTime) / 0.25f;
  if (ftbT > 0.f)
    m_fadeToBlack.draw(zeus::CColor{0.f, 0.f, 0.f, ftbT});
}

void CWorldTransManager::DrawDisabled() {
  SCOPED_GRAPHICS_DEBUG_GROUP("CWorldTransManager::DrawDisabled", zeus::skPurple);
  m_fadeToBlack.draw(zeus::CColor{0.f, 0.f, 0.f, 0.01f});
}

void CWorldTransManager::DrawText() {
  SCOPED_GRAPHICS_DEBUG_GROUP("CWorldTransManager::DrawText", zeus::skPurple);
  float width = 448.f * g_Viewport.aspect;
  CGraphics::SetOrtho(0.f, width, 448.f, 0.f, -4096.f, 4096.f);
  CGraphics::SetViewPointMatrix(zeus::CTransform());
  CGraphics::SetModelMatrix(zeus::CTransform::Translate((width - 640.f) / 2.f, 0.f, 448.f));
  x8_textData->Render();

  float filterAlpha = 0.f;
  if (x0_curTime < 1.f)
    filterAlpha = 1.f - x0_curTime;
  else if (x44_25_stopSoon)
    filterAlpha = std::min(1.f, x0_curTime - x34_stopTime);

  if (filterAlpha > 0.f) {
    zeus::CColor filterColor = x44_27_fadeWhite ? zeus::skWhite : zeus::skBlack;
    filterColor.a() = filterAlpha;
    m_fadeToBlack.draw(filterColor);
  }
}

void CWorldTransManager::Draw() {
  if (x30_type == ETransType::Disabled)
    DrawDisabled();
  else if (x30_type == ETransType::Enabled)
    DrawEnabled();
  else if (x30_type == ETransType::Text)
    DrawText();
}

void CWorldTransManager::TouchModels() {
  if (!x4_modelData)
    return;

  if (x4_modelData->x68_beamModelData.IsNull() && x4_modelData->x14c_beamModel.IsLoaded() &&
      x4_modelData->x14c_beamModel.GetObj()) {
    x4_modelData->x68_beamModelData = CModelData{
        CStaticRes(x4_modelData->x14c_beamModel.GetObjectTag()->id, x4_modelData->x0_samusRes.GetScale()), 2};
  }

  if (x4_modelData->x1c_samusModelData.IsNull() && x4_modelData->x158_suitModel.IsLoaded() &&
      x4_modelData->x158_suitModel.GetObj() && x4_modelData->x164_suitSkin.IsLoaded() &&
      x4_modelData->x164_suitSkin.GetObj()) {
    CAnimRes animRes(x4_modelData->x0_samusRes.GetId(), GetSuitCharIdx(), x4_modelData->x0_samusRes.GetScale(),
                     x4_modelData->x0_samusRes.GetDefaultAnim(), true);
    x4_modelData->x1c_samusModelData = CModelData{animRes, 2};

    CAnimPlaybackParms aData(animRes.GetDefaultAnim(), -1, 1.f, true);
    x4_modelData->x1c_samusModelData.GetAnimationData()->SetAnimation(aData, false);
  }

  if (!x4_modelData->x1c_samusModelData.IsNull())
    x4_modelData->x1c_samusModelData.Touch(CModelData::EWhichModel::Normal, 0);

  if (!x4_modelData->xb4_platformModelData.IsNull())
    x4_modelData->xb4_platformModelData.Touch(CModelData::EWhichModel::Normal, 0);

  if (!x4_modelData->x100_bgModelData[0].IsNull())
    x4_modelData->x100_bgModelData[0].Touch(CModelData::EWhichModel::Normal, 0);
  if (!x4_modelData->x100_bgModelData[1].IsNull())
    x4_modelData->x100_bgModelData[1].Touch(CModelData::EWhichModel::Normal, 0);
  if (!x4_modelData->x100_bgModelData[2].IsNull())
    x4_modelData->x100_bgModelData[2].Touch(CModelData::EWhichModel::Normal, 0);

  if (!x4_modelData->x68_beamModelData.IsNull())
    x4_modelData->x68_beamModelData.Touch(CModelData::EWhichModel::Normal, 0);
}

void CWorldTransManager::EnableTransition(const CAnimRes& samusRes, CAssetId platRes, const zeus::CVector3f& platScale,
                                          CAssetId bgRes, const zeus::CVector3f& bgScale, bool goingUp) {
  x44_25_stopSoon = false;
  x44_26_goingUp = goingUp;
  x30_type = ETransType::Enabled;
  x4_modelData = std::make_unique<SModelDatas>(samusRes);

  if (!m_reflectionCube[0] && hecl::com_cubemaps->toBoolean())
    CGraphics::CommitResources([this](boo::IGraphicsDataFactory::Context& ctx) {
      m_reflectionCube[0] = ctx.newCubeRenderTexture(CUBEMAP_RES, CUBEMAP_MIPS);
      m_reflectionCube[1] = ctx.newCubeRenderTexture(CUBEMAP_RES, CUBEMAP_MIPS);
      return true;
    } BooTrace);

  x8_textData.reset();
  x20_random.SetSeed(99);

  CAssetId beamModelId = g_tweakPlayerRes->GetBeamCineModel(g_GameState->GetPlayerState()->GetCurrentBeam());

  x4_modelData->x14c_beamModel = g_SimplePool->GetObj(SObjectTag{FOURCC('CMDL'), beamModelId});

  TToken<CCharacterFactory> fac = g_CharFactoryBuilder->GetFactory(samusRes);
  const CCharacterInfo& info = fac.GetObj()->GetCharInfo(GetSuitCharIdx());
  x4_modelData->x158_suitModel = g_SimplePool->GetObj(SObjectTag{FOURCC('CMDL'), info.GetModelId()});
  x4_modelData->x164_suitSkin = g_SimplePool->GetObj(SObjectTag{FOURCC('CSKR'), info.GetSkinRulesId()});

  if (platRes.IsValid()) {
    x4_modelData->xb4_platformModelData = CModelData{CStaticRes(platRes, platScale), 2};
    x4_modelData->xb4_platformModelData.Touch(CModelData::EWhichModel::Normal, 0);
  }

  if (bgRes.IsValid()) {
    const CStaticRes bg(bgRes, bgScale);
    x4_modelData->x100_bgModelData[0] = CModelData{bg};
    x4_modelData->x100_bgModelData[0].Touch(CModelData::EWhichModel::Normal, 0);
    x4_modelData->x100_bgModelData[1] = CModelData{bg};
    x4_modelData->x100_bgModelData[1].Touch(CModelData::EWhichModel::Normal, 0);
    x4_modelData->x100_bgModelData[2] = CModelData{bg};
    x4_modelData->x100_bgModelData[2].Touch(CModelData::EWhichModel::Normal, 0);
    const zeus::CAABox bounds = x4_modelData->x100_bgModelData[0].GetBounds();
    x1c_bgHeight = (bounds.max.z() - bounds.min.z()) * bgScale.z();
  } else {
    x1c_bgHeight = 0.f;
  }

  StartTransition();
  TouchModels();
}

void CWorldTransManager::EnableTransition(CAssetId fontId, CAssetId stringId, u32 strIdx, bool fadeWhite,
                                          float chFadeTime, float chFadeRate, float textStartTime) {
  x40_strIdx = strIdx;
  x38_textStartTime = textStartTime;
  x44_25_stopSoon = false;
  x30_type = ETransType::Text;

  x4_modelData.reset();
  x44_27_fadeWhite = fadeWhite;

  const CGuiTextProperties props(false, true, EJustification::Center, EVerticalJustification::Center);
  x8_textData = std::make_unique<CGuiTextSupport>(fontId, props, zeus::skWhite, zeus::skBlack, zeus::skWhite, 640, 448,
                                                  g_SimplePool, CGuiWidget::EGuiModelDrawFlags::Additive);
  x8_textData->SetTypeWriteEffectOptions(true, chFadeTime, chFadeRate);
  xc_strTable = g_SimplePool->GetObj(SObjectTag{FOURCC('STRG'), stringId});
  x8_textData->SetText(u"");
  StartTransition();
}

void CWorldTransManager::StartTextFadeOut() {
  if (!x44_25_stopSoon)
    x34_stopTime = x0_curTime;
  x44_25_stopSoon = true;
}

void CWorldTransManager::DisableTransition() {
  x30_type = ETransType::Disabled;
  x4_modelData.reset();
  x8_textData.reset();
  x44_26_goingUp = false;
}

void CWorldTransManager::StartTransition() {
  x0_curTime = 0.f;
  x18_bgOffset = 0.f;
  x44_24_transFinished = false;
  x44_28_textDirty = true;
}

void CWorldTransManager::EndTransition() { DisableTransition(); }

bool CWorldTransManager::WaitForModelsAndTextures() {
  std::vector<SObjectTag> tags = g_SimplePool->GetReferencedTags();
  for (const SObjectTag& tag : tags) {
    if (tag.type == FOURCC('TXTR') || tag.type == FOURCC('CMDL'))
      g_SimplePool->GetObj(tag).GetObj();
  }
  return false;
}

void CWorldTransManager::SfxStop() {
  if (x28_sfxHandle) {
    CSfxManager::SfxStop(x28_sfxHandle);
    x28_sfxHandle.reset();
  }
}

void CWorldTransManager::SfxStart() {
  if (!x28_sfxHandle && x24_sfx != 0xFFFF)
    x28_sfxHandle = CSfxManager::SfxStart(x24_sfx, x2c_volume, x2d_panning, false, 127, true, -1);
}

} // namespace metaforce
