// SPDX-License-Identifier: Unlicense

#include "camera.h"

#include "global.h"
#include "opengl.h"
#include "shader.h"
#include "helper.h"

static void calcTargetPositionForHighwayAnchor(const ArrangementIndex selectedArrangementIndex, vec3& targetPosition, const f32 anchorTackingDuration, const f32 xFactor, const f32 x, const f32 yFactor, const f32 y, const f32 zFactor, const f32 z)
{
  ASSERT(Global::selectedSongIndex >= 0);

  // calc the camera Target
  const i32 currentAnchorIndex = Song::getCurrentAnchorIndex(selectedArrangementIndex);

  const Song::Anchor& anchor0 = Global::songTrackLevelAdjusted[selectedArrangementIndex].anchors[currentAnchorIndex];

  i8 left = anchor0.fret;
  i8 right = anchor0.fret + anchor0.width;

  for (i32 i = currentAnchorIndex + 1; i < i32(Global::songTrackLevelAdjusted[selectedArrangementIndex].anchors.size()); ++i)
  {
    const Song::Anchor& anchor1 = Global::songTrackLevelAdjusted[selectedArrangementIndex].anchors[i];

    const TimeNS noteTime1 = -anchor1.timeNS + Global::musicTimeElapsedNS;

    if (noteTime1 < timeNS_From_Seconds(-anchorTackingDuration))
      break;

    left = min_(left, anchor1.fret);
    right = max_(right, i8(anchor1.fret + anchor1.width));
  }

  const i32 width = right - left;
  targetPosition.x = -xFactor * f32(left - 1 + right - 1) * 0.5f - x;
  targetPosition.y = -yFactor * f32(width) * 0.5f - y;
  targetPosition.z = -zFactor * f32(width) * 0.5f - z;
}

static void linearMovement(const vec3& desiredPosition, vec3& currentPosition)
{
  const vec3 framePosChange = vec::multiply(desiredPosition, TimeNS_To_Seconds(Global::frameDelta));
  currentPosition.x += framePosChange.x;
  currentPosition.y += framePosChange.y;
  currentPosition.z += framePosChange.z;
}

static mat4 createTransformedMatrix(const vec3& translation, const f32 xRotation, const f32 yRotation)
{
  const mat4 rotMatX
  {
    .m11 = cosf(xRotation),
    .m21 = sinf(xRotation),
    .m12 = -sinf(xRotation),
    .m22 = cosf(xRotation)
  };

  const mat4 rotMatY
  {
    .m00 = cosf(yRotation),
    .m20 = sinf(yRotation),
    .m02 = -sinf(yRotation),
    .m22 = cosf(yRotation)
  };

  const mat4 translationMat
  {
    .m03 = translation.x,
    .m13 = translation.y,
    .m23 = translation.z
  };

  return vec::multiply(vec::multiply(rotMatX, rotMatY), translationMat);
}

static mat4 noclipMovement(const vec3& cameraTarget)
{
  static vec3 noclipPosition = cameraTarget;

  if (Global::inputD.heldDown)
    noclipPosition.x -= Settings::highwayScrollSpeed * TimeNS_To_Seconds(Global::frameDelta);
  else if (Global::inputA.heldDown)
    noclipPosition.x += Settings::highwayScrollSpeed * TimeNS_To_Seconds(Global::frameDelta);
  if (Global::inputW.heldDown)
    noclipPosition.z += Settings::highwayScrollSpeed * TimeNS_To_Seconds(Global::frameDelta);
  else if (Global::inputS.heldDown)
    noclipPosition.z -= Settings::highwayScrollSpeed * TimeNS_To_Seconds(Global::frameDelta);
  if (Global::inputE.heldDown)
    noclipPosition.y -= Settings::highwayScrollSpeed * TimeNS_To_Seconds(Global::frameDelta);
  else if (Global::inputC.heldDown)
    noclipPosition.y += Settings::highwayScrollSpeed * TimeNS_To_Seconds(Global::frameDelta);

  mat4 translation;
  translation.m03 = noclipPosition.x;
  translation.m13 = noclipPosition.y;
  translation.m23 = noclipPosition.z;

  static i32 initialPosX = Global::inputCursorPosX;
  const f32 rotX = (initialPosX - Global::inputCursorPosX) * 0.004f;

  static i32 initialPosY = Global::inputCursorPosY;
  const f32 rotY = (initialPosY - Global::inputCursorPosY) * -0.004f;

  mat4 rotMatX;
  rotMatX.m11 = cosf(rotY);
  rotMatX.m21 = sinf(rotY);
  rotMatX.m12 = -sinf(rotY);
  rotMatX.m22 = cosf(rotY);

  mat4 rotMatY;
  rotMatY.m00 = cosf(rotX);
  rotMatY.m20 = sinf(rotX);
  rotMatY.m02 = -sinf(rotX);
  rotMatY.m22 = cosf(rotX);

  return vec::multiply(vec::multiply(rotMatX, rotMatY), translation);
}

mat4 Camera::calculateViewMat(const CameraMode cameraMode, const ArrangementIndex selectedArrangementIndex, vec3& cameraTargetPosition, vec3& cameraCurrentPosition)
{
  static bool skipTick = true; // when a song is loaded the game freezes for a few sec. Don't tick with such a high Global::frameDelta. Camera positon will be off.
  if (Global::selectedSongIndex == -1 || Global::musicTimeElapsedNS > (Global::songInfos[Global::selectedSongIndex].songLength + Global::songInfos[Global::selectedSongIndex].shredDelayEnd))
  {
    skipTick = true;
    return {};
  }
  else if (skipTick)
  {
    skipTick = false;
    return {};
  }

  switch (cameraMode)
  {
  case CameraMode::fixed:
    break;
  case CameraMode::parallax:
    calcTargetPositionForHighwayAnchor(selectedArrangementIndex, cameraTargetPosition, Settings::cameraParallaxAnchorTackingDuration, Settings::cameraParallaxXFactor, Settings::cameraParallaxX, Settings::cameraParallaxYFactor, Settings::cameraParallaxY, Settings::cameraParallaxZFactor, Settings::cameraParallaxZ);
    break;
#ifdef SHR3D_OPENXR_PCVR
  case CameraMode::pcVrParallax:
    calcTargetPositionForHighwayAnchor(selectedArrangementIndex, cameraTargetPosition, Settings::cameraPcVrParallaxAnchorTackingDuration, Settings::cameraPcVrParallaxXFactor, Settings::cameraPcVrParallaxX, Settings::cameraPcVrParallaxYFactor, Settings::cameraPcVrParallaxY, Settings::cameraPcVrParallaxZFactor, Settings::cameraPcVrParallaxZ);
    break;
#endif // SHR3D_OPENXR_PCVR
  default:
    unreachable();
  }

  switch (cameraMode)
  {
  case CameraMode::fixed:
    cameraCurrentPosition.x = Settings::cameraFixedX;
    cameraCurrentPosition.y = Settings::cameraFixedY;
    cameraCurrentPosition.z = Settings::cameraFixedZ;
    break;
  case CameraMode::parallax:
#ifdef SHR3D_OPENXR_PCVR
  case CameraMode::pcVrParallax:
#endif // SHR3D_OPENXR_PCVR
  {
    const vec3 desiredPositionDiff =
    {
      .x = cameraTargetPosition.x - cameraCurrentPosition.x,
      .y = cameraTargetPosition.y - cameraCurrentPosition.y,
      .z = cameraTargetPosition.z - cameraCurrentPosition.z,
    };
    linearMovement(desiredPositionDiff, cameraCurrentPosition);
  }
  break;
  default:
    unreachable();
  }

  if (Global::inputNoclip.toggled)
    return noclipMovement(cameraTargetPosition);

  {
    switch (cameraMode)
    {
    case CameraMode::fixed:
      return createTransformedMatrix(cameraCurrentPosition, Settings::cameraFixedXRotation, Settings::cameraFixedYRotation);
    case CameraMode::parallax:
      return createTransformedMatrix(cameraCurrentPosition, Settings::cameraParallaxXRotation, Settings::cameraParallaxYRotation);
#ifdef SHR3D_OPENXR_PCVR
    case CameraMode::pcVrParallax:
      return createTransformedMatrix(cameraCurrentPosition, Settings::cameraPcVrParallaxXRotation, Settings::cameraPcVrParallaxYRotation);
#endif // SHR3D_OPENXR_PCVR
    default:
      unreachable();
    }
  }
}

#ifndef PLATFORM_OPENXR_ANDROID
mat4 Camera::calculateProjectionViewMat(const i32 resolutionWidth, const i32 resolutionHeight, const mat4& viewMat)
{
  mat4 projectionMat;
  const f32 aspect = Settings::graphicsScaling == ScalingMode::HorPlus
    ? f32(resolutionWidth) / f32(resolutionHeight)
    : 16.0f / 9.0f;
  {
    const f32 near_ = 0.1f;
    const f32 far_ = Settings::highwayViewDistance * 2.0f; // * 2.0f is there to not clip off drawing when moving back a lot in VR.

    const f32 divisor = tanf((Settings::cameraFieldOfView * PI_) / 360.0f);
    projectionMat.m00 = 1.0f / (aspect * divisor);
    projectionMat.m11 = 1.0f / divisor;
    projectionMat.m22 = -(far_ + near_) / (far_ - near_);
    projectionMat.m23 = -(2.0f * far_ * near_) / (far_ - near_);
    projectionMat.m32 = -1.0f;
    projectionMat.m33 = 0.0f;
  }

  const mat4 viewProjectionMat = vec::multiply(projectionMat, viewMat);

  return viewProjectionMat;
}
#endif // PLATFORM_OPENXR_ANDROID

static mat4 calcHighwayProjectionViewMat(const mat4& viewProjectionMat, const f32 scale, const f32 rotation, const f32 x, const f32 y, const f32 z)
{
  const mat4 scaleMat{
    .m00 = scale,
    .m11 = scale,
    .m22 = scale
  };

  const mat4 rotMatY{
    .m00 = cosf(rotation),
    .m20 = sinf(rotation),
    .m02 = -sinf(rotation),
    .m22 = cosf(rotation)
  };

  const mat4 translationMat
  {
    .m03 = x,
    .m13 = y,
    .m23 = z
  };

  return vec::multiply(vec::multiply(vec::multiply(viewProjectionMat, scaleMat), rotMatY), translationMat);
}

static mat4 calcHighwayParallax(const mat4& viewProjectionMat, vec3& highwayTargetPosition, vec3& highwayCurrentPosition, const f32 parallaxAnchorTackingDuration, const f32 parallaxHighwayXFactor, const f32 parallaxHighwayYFactor, const f32 parallaxHighwayZFactor, const f32 parallaxHighwayScale, const f32 parallaxHighwayRotation, const f32 parallaxHighwayX, const f32 parallaxHighwayY, const f32 parallaxHighwayZ)
{
  mat4 vpMat;
  if (Global::selectedSongIndex >= 0)
  {
    calcTargetPositionForHighwayAnchor(Global::selectedArrangementIndex, highwayTargetPosition, parallaxAnchorTackingDuration, parallaxHighwayXFactor, 0.0f, parallaxHighwayYFactor, 0.0f, parallaxHighwayZFactor, 0.0f);
    const vec3 desiredPositionDiff =
    {
      .x = highwayTargetPosition.x - highwayCurrentPosition.x,
      .y = highwayTargetPosition.y - highwayCurrentPosition.y,
      .z = highwayTargetPosition.z - highwayCurrentPosition.z,
    };
    linearMovement(desiredPositionDiff, highwayCurrentPosition);
    vpMat = createTransformedMatrix(highwayCurrentPosition, 0.0f, 0.0f);
  }
  vpMat = vec::multiply(viewProjectionMat, vpMat);

  return calcHighwayProjectionViewMat(vpMat, parallaxHighwayScale, parallaxHighwayRotation, parallaxHighwayX, parallaxHighwayY, parallaxHighwayZ);
}

mat4 Camera::calculateHighwayProjectionViewMat(const CameraMode cameraMode, const mat4& viewProjectionMat, vec3& highwayTargetPosition, vec3& highwayCurrentPosition)
{
  switch (cameraMode)
  {
  case CameraMode::fixed:
    return viewProjectionMat;
  case CameraMode::parallax:
    return calcHighwayParallax(viewProjectionMat, highwayTargetPosition, highwayCurrentPosition, Settings::cameraParallaxAnchorTackingDuration, Settings::cameraParallaxHighwayXFactor, Settings::cameraParallaxHighwayYFactor, Settings::cameraParallaxHighwayZFactor, Settings::cameraParallaxHighwayScale, Settings::cameraParallaxHighwayRotation, Settings::cameraParallaxHighwayX, Settings::cameraParallaxHighwayY, Settings::cameraParallaxHighwayZ);
#ifdef SHR3D_OPENXR_PCVR
  case CameraMode::pcVrParallax:
    return calcHighwayParallax(viewProjectionMat, highwayTargetPosition, highwayCurrentPosition, Settings::cameraPcVrParallaxAnchorTackingDuration, Settings::cameraPcVrParallaxHighwayXFactor, Settings::cameraPcVrParallaxHighwayYFactor, Settings::cameraPcVrParallaxHighwayZFactor, Settings::cameraPcVrParallaxHighwayScale, Settings::cameraPcVrParallaxHighwayRotation, Settings::cameraPcVrParallaxHighwayX, Settings::cameraPcVrParallaxHighwayY, Settings::cameraPcVrParallaxHighwayZ);
#endif // SHR3D_OPENXR_PCVR
  default:
    unreachable();
  }
}
