import { World, hook, Negl } from "negl";
import { Camera, Vector3, MathUtils } from "three";

/**
 * マウスの位置に合せてカメラを移動します。
 * Moves the camera according to the mouse position.
 *
 * @param negl Neglオブジェクト(Negl object)
 * @param getCameraPos カメラの位置を返すコールバック関数(Callback function that returns the camera position)
 * @param lookAt カメラの向き(The direction of the camera)
 * @param destroy 引数なしの時：ページ遷移時にも処理を破棄しない。<br/>
 *                関数の時：ページ遷移が開始した際にこの引数に渡した関数が連続的に実行され、true を返したとき、マウスへの追従を停止します。使用例は Travel サイトの page/menu.js をご覧ください。
 *                When there is no argument: The process is not destroyed even when the page is transitioned.<br/>
 *                When it is a function: When the page transition starts, the function passed to this argument is executed continuously, and when it returns true, the camera will stop following the mouse. Please refer to Travel site's page/menu.js for usage examples.
 */
function mouseCamera(
  negl: Negl,
  getCameraPos: (mouseClipPos: { x: number; y: number }) => Vector3,
  lookAt = new Vector3(0, 0, 0),
  destroy?: (targetPosition: Vector3) => boolean
) {
  const { mouse, viewport, utils } = negl;
  // タッチデバイスの時マウスがないため追従させない。
  // Do not follow the mouse on touch devices because there is no mouse.
  if (utils?.isTouchDevices()) return;

  // カメラのz軸の位置
  // The z-axis position of the camera
  const cameraZ = viewport!.cameraZ;

  // カメラ位置（この位置に向かって緩やかにカメラ位置が移動します）
  // Camera position (the camera moves gently towards this position)
  const targetPosition = new Vector3(0, 0, cameraZ);

  // カメラ移動のダンピング
  // Damping of camera movement
  const damping = 2;

  // マウスへの追従処理を行うかを保持するフラグ
  // Flag to determine whether to follow the mouse
  let destoryMouseCamera = false;
  /**
   * マウスにカメラを追従させる
   * Make the camera follow the mouse
   */
  function mouseCameraAction({ camera, delta }: World) {
    /**
     * マウスへの追従を停止するか
     * Whether to stop following the mouse
     */
    if (typeof destroy === "function" && destoryMouseCamera) {
      /**
       * destroy が true を返すとき、マウスへの追従を停止する
       * 補足）マウスへのカメラの追従を破棄した時点でカメラの位置は更新されなくなるため、カメラを元の位置に戻したい場合は destroy の中で targetPosition を元のポジションに戻す。（Travelサイトの page/menu.js 内で destroy の処理を行っています。）
       * When destroy returns true, stop following the mouse
       * Note: Once the camera's following of the mouse is destroyed, the camera's position will no longer be updated. If you want to return the camera to its original position, reset the targetPosition to the original position within destroy. (This is done inside page/menu.js of the Travel site.)
       */
      if (destroy(targetPosition)) {
        hook.off(hook.RENDER, mouseCameraAction);
      }
    } else {
      // カメラの追従処理
      // The camera following process

      // マウスのクリップ座標を取得
      // Get the mouse clip coordinates
      const mouseClipPos = mouse!.getClipPos();
      // getCameraPos によってカメラ位置を取得（getCameraPosは引数で渡ってきたコールバック関数ですので、ユーザー独自でカメラ位置を設定します。）
      // Obtain the camera position through getCameraPos (getCameraPos is a callback function passed as an argument, so the user sets the camera position uniquely.)
      const { x, y, z } = getCameraPos(mouseClipPos);

      // カメラ位置を変更
      // Change the camera position
      targetPosition.x = x;
      targetPosition.y = y;
      targetPosition.z = z;
    }

    // 実際にカメラの位置を変更
    // Actually change the camera position
    updateCamera(camera, targetPosition, delta);
  }

  /**
   * targetPosition に向けてカメラの位置を変更します。
   * Change the camera's position towards targetPosition.
   * @param camera Cameraオブジェクト(Camera object)
   * @param targetPosition カメラの位置(Camera position)
   * @param delta [World.delta](https://docs.not-equal.dev/reference/classes/World/#delta)
   */
  function updateCamera(
    camera: Camera,
    targetPosition: Vector3,
    delta: number
  ) {
    // カメラのxyz座標をtargetPositionに向けて緩やかに変更します。
    // Gently change the camera's xyz coordinates towards the targetPosition.
    camera.position.x = MathUtils.damp(
      camera.position.x,
      targetPosition.x,
      damping,
      delta
    );
    camera.position.y = MathUtils.damp(
      camera.position.y,
      targetPosition.y,
      damping,
      delta
    );
    camera.position.z = MathUtils.damp(
      camera.position.z,
      targetPosition.z,
      damping,
      delta
    );

    // カメラの向きをlookAtに向けます。
    // Point the camera towards lookAt.
    camera.lookAt(lookAt);
  }

  // RENDERフックに登録します。
  // Register with the RENDER hook.
  hook.on(hook.RENDER, mouseCameraAction);

  // destroyが関数出ない場合、カメラの追従はページトランジションしても停止しません。
  // If destroy is not a function, the camera will not stop following even during page transitions.
  if (typeof destroy !== "function") return;

  // カメラの追従を停止するフラグを立てます。
  // Set a flag to stop the camera from following.
  function destroyMouseCameraAction() {
    destoryMouseCamera = true;
  }
  // ページトランジションが開始した場合、destroyMouseCameraAction が実行されます。
  // If a page transition starts, destroyMouseCameraAction will be executed.
  hook.on(hook.T_BEGIN, destroyMouseCameraAction, { once: true });
}

export { mouseCamera };
export default mouseCamera;
