import gsap from "gsap";
import { MathUtils } from "three";
import { movies } from "../movies";

/**
 * トランジションタイプ "ghibli" のページ遷移時の処理を[フック](https://docs.not-equal.dev/reference/classes/Hook/)に登録します。
 * Registers the page transition process for the transition type "ghibli" in the [Hook](https://docs.not-equal.dev/reference/classes/Hook/).
 * @param {Negl}
 */
function registTransitionGhibli({ hook, world, INode, config }) {
  // トランジションタイプ "ghibli"
  // Transition type "ghibli"
  const type = "ghibli";

  // トランジション 開始時
  // At the start of the transition
  // T_BEGIN - https://docs.not-equal.dev/tutorial/transition/#t_beginページトランジションの開始時
  hook.on(hook.T_BEGIN, storeSlideNum, { type });
  hook.on(hook.T_BEGIN, keepOb, { type, priority: 100 });
  hook.on(hook.T_BEGIN, fadeInNextTitle, { type });
  hook.on(hook.T_BEGIN, fadeOutCurrent, { type });

  // トランジション 次のページのOBインスタンス取得可能
  // Transition when the OB instance of the next page can be obtained
  // T_BOTH_OB_EXIST - https://docs.not-equal.dev/tutorial/transition/#t_both_ob_exist次のページの-ob-オブジェクトがアクセス可能な状態
  hook.on(hook.T_BOTH_OB_EXIST, fadeOutNextTitle, { type, priority: 100 });
  hook.on(hook.T_BOTH_OB_EXIST, fadeInNext, { type });

  /**
   * 現在のアクティブなスライダの番号を保持します。
   * 保持した値は再度同じページに戻ってきた場合のスライダ表示に使用します。
   * Stores the number of the currently active slider.
   * The stored value is used for the slider display when returning to the same page again.
   * @param [TransitionState](https://docs.not-equal.dev/reference/interfaces/TransitionState/)
   */
  function storeSlideNum({ current }) {
    const sliderOs = world.getObByElAll(".slider");
    const slideIdx = Math.round(sliderOs[0].activeSlideIdx);
    current.store.set("slideIdx", slideIdx);
  }

  /**
   * 海（.ocean）とsky（.sky）のエフェクトをシーン内に維持します。
   * Maintains the effects of the ocean (.ocean) and sky (.sky) within the scene.
   * @param [TransitionState](https://docs.not-equal.dev/reference/interfaces/TransitionState/)
   */
  function keepOb({ current }) {
    // 次のページでも保持し続けるObのセレクター
    // Selector for the Ob to be kept on the next page as well
    const keepObs = [".ocean", ".sky"];
    keepObs.forEach((selector) => {
      const ob = world.getObByEl(selector);
      const idx = current.os.indexOf(ob);
      // current.os に含まれるエフェクトは T_BOTH_OB_EXIST フックの完了後、自動的にシーンから除去されるため、current.os からシーン内に残したいエフェクトを削除します。
      // Effects included in current.os are automatically removed from the scene after the T_BOTH_OB_EXIST hook is completed, so remove the effects you want to keep in the scene from current.os.
      // https://docs.not-equal.dev/tutorial/transition/#t_both_ob_exist次のページの-ob-オブジェクトがアクセス可能な状態
      current.os.splice(idx, 1);
    });
  }

  /**
   * 現在ページの表示をフェードアウトします。
   * Fades out the display of the current page.
   * @param [TransitionState](https://docs.not-equal.dev/reference/interfaces/TransitionState/)
   * @returns
   */
  async function fadeOutCurrent({ current }) {
    /**
     * 画面上の全てのスライダを取得
     * トップページ(index.html) では ３つの slider-3d エフェクトが取得されます。
     * 下層ページでは slider-3d エフェクトが一つ取得されます。
     *
     * Retrieves all sliders on the screen.
     * On the top page (index.html), three slider-3d effects are retrieved.
     * On subpages, one slider-3d effect is retrieved.
     */
    const sliderOs = world.getObByElAll(".slider");

    /**
     * 各スライダのz座標の値を保持します。
     * * 後にz軸方向のアニメーションを行うため。
     *
     * Stores the z-coordinate values of each slider.
     * * For later z-axis direction animation.
     */
    const originalPosZs = sliderOs.map((sliderO) => sliderO.mesh.position.z);

    // アニメーション用の progress を定義します。（1 -> 0）に動かします。
    // Defines the progress for the animation. Moves from (1 -> 0).
    const progress = { value: 1 };

    /**
     * slider-3d エフェクトの render メソッドを一旦変数に保持します。
     * Temporarily stores the render method of the slider-3d effect in a variable.
     */
    const originalRender = sliderOs[0].render;
    return new Promise((resolve) => {
      gsap.to(progress, {
        value: 0,
        duration: 0.5,
        ease: "power3.out",
        onStart() {
          /**
           * slider-3d の render メソッドを空の状態にします。
           * Empties the render method of the slider-3d.
           *
           * （理由）
           * slider-3d エフェクトの各スライダの位置は slider-3d クラスの setSlidePos で決定されます。
           * また、setSlidePos は render メソッド内でループして連続的に呼ばれています。
           *
           * その為、setSlidePos が連続的に呼ばれている状態でエフェクトの位置を下記の onUpdate 内で変更しても、
           * その変更は setSlidePos によって上書きされるため、メッシュの位置が変わりません。
           *
           * その為このフェードアウトのアニメーション中は render メソッド無理やり空の関数にし、setSlidePos が実行されないようにしています。
           *
           * (Reason)
           * The position of each slider in the slider-3d effect is determined by the setSlidePos of the slider-3d class.
           * Also, setSlidePos is called continuously in a loop within the render method.
           *
           * Therefore, even if the position of the effect is changed within the following onUpdate while setSlidePos is being called continuously,
           * the change will be overwritten by setSlidePos, so the position of the mesh will not change.
           *
           * Therefore, during this fade-out animation, the render method is forcibly made an empty function to prevent setSlidePos from executing.
           */
          sliderOs.forEach((sliderO, idx) => {
            // slider-3d の render メソッドを一時的に空の関数にする。
            // Temporarily makes the render method of slider-3d an empty function.
            sliderO.render = () => {};
          });
        },
        onUpdate() {
          // フェードアウトアニメーション
          // Fade-out animation
          sliderOs.forEach((sliderO, idx) => {
            // slider-3d エフェクトのz座標の値を取得
            // Retrieves the z-coordinate value of the slider-3d effect
            const originalPosZ = originalPosZs[idx];
            // 透明度は 1 -> 0 に更新
            // Updates the opacity from 1 -> 0
            sliderO.setOpacity(progress.value);
            // z座標を originalPosZ -> originalPosZ - 100 に更新（少し奥に移動）
            // Updates the z-coordinate from originalPosZ -> originalPosZ - 100 (moves slightly back)
            sliderO.mesh.position.z = MathUtils.lerp(
              originalPosZ,
              originalPosZ - 100,
              1 - progress.value
            );
          });
          // .page-container の透明度を 1 -> 0 に更新
          // Updates the opacity of .page-container from 1 -> 0
          current.el.style.opacity = progress.value;
        },
        onComplete() {
          // アニメーション完了後、mesh.visible を false に変更
          // After the animation is complete, change mesh.visible to false
          world.hideMeshes(current.os);
          // スライダを元の位置に戻す（この時点では画面上は非表示）
          // Returns the slider to its original position (not visible on the screen at this point)
          sliderOs.forEach((sliderO, idx) => {
            sliderO.mesh.position.z = originalPosZs[idx];
            sliderO.render = originalRender;
          });
          // 非同期処理が解決（次の処理に進む）
          // Asynchronous processing is resolved (proceeds to the next process)
          resolve();
        },
      });
    });
  }

  let titleContainerEl, titleInnerEl;
  const move = 20,
    duration = 0.5;
  /**
   * 次のページのタイトルとローダーを表示します。
   * Displays the title and loader of the next page.
   * @param [TransitionState](https://docs.not-equal.dev/reference/interfaces/TransitionState/)
   * @returns
   */
  async function fadeInNextTitle({ next }) {
    let key;
    /**
     * 次のページを取得
     * Retrieves the next page.
     *
     * T_BEGIN のフックで実行するため pageType は取れないため、url から次のページを判定
     * Since it is executed in the T_BEGIN hook and pageType cannot be obtained, the next page is determined from the url.
     *
     * https://docs.not-equal.dev/tutorial/transition/#t_beginページトランジションの開始時
     */

    if ((key = next.url.match(/\/(\w*)[.\w]*$/))) {
      // key[1] : proco, totoro, etc
      key = key[1];
    }

    /**
     * タイトルを取得
     * Retrieves the title.
     */
    const movie = movies[key];
    const title = movie?.title ?? "ジブリギャラリー";
    // The title or "Ghibli Gallery" if not found

    /**
     * 表示用のタイトルとローダーを含む DOM を作成
     * Creates a DOM containing the title and loader for display.
     */
    titleContainerEl = INode.htmlToEl(`
      <div class="before-page">
        <div class="before-page-inner">
          <p class="before-page-title">${title}</p>
          <div class="before-page-loader"></div>
        </div>
      </div>
    `);

    /**
     * タイトルの DOM を取得
     * Retrieves the DOM of the title.
     * firstElementChild - https://developer.mozilla.org/ja/docs/Web/API/Element/firstElementChild
     */
    titleInnerEl = titleContainerEl.firstElementChild;

    /**
     * #global-container を取得
     * Retrieves #global-container.
     */
    const globalContainer = INode.qs(config.$.globalContainer);

    /**
     * タイトルとローダーをフェードインアニメーション
     * Fade-in animation for the title and loader.
     */
    const tl = gsap.timeline();
    return new Promise((resolve) => {
      tl.set(titleInnerEl, {
        opacity: 0,
        y: move,
      }).to(titleInnerEl, {
        opacity: 1,
        y: 0,
        duration,
        ease: "power2.in",
        onStart() {
          // アニメーション開始時に #global-container に追加
          // Adds to #global-container at the start of the animation
          globalContainer.append(titleContainerEl);
        },
        onComplete() {
          resolve();
        },
      });
    });
  }

  /**
   * タイトルとローダのフェードアウトアニメーション
   * Fade-out animation for the title and loader.
   * @returns
   */
  async function fadeOutNextTitle() {
    return new Promise((resolve) => {
      gsap.to(titleInnerEl, {
        opacity: 0,
        y: -move,
        delay: duration,
        duration,
        ease: "power2.out",
        onComplete() {
          // アニメーションが終了したらタイトルとローダを含む DOM を削除
          // Removes the DOM containing the title and loader after the animation ends
          titleContainerEl.remove();
          resolve();
        },
      });
    });
  }

  /**
   * 次のページをフェードインアニメーション
   * Fade-in animation for the next page.
   *
   * @param [TransitionState](https://docs.not-equal.dev/reference/interfaces/TransitionState/)
   * @returns
   */
  async function fadeInNext({ next }) {
    // 次のページタイプによって、アニメーション対象のObを取得
    // Retrieves the Ob for animation based on the next page type
    const sliderOs =
      next.pageType === "home"
        ? world.getObByElAll(".slider")
        : world.getObByElAll(".dc-slider");

    // z軸に少し移動させながらフェードインするため、元のz軸の位置を取得しておく。
    // To fade in while moving slightly along the z-axis, the original z-axis position is obtained.
    const originalPosZs = sliderOs.map((sliderO) => sliderO.mesh.position.z);

    // 次のページのObのvisibleを全てtrueに変更する
    // Changes the visible property of all Ob of the next page to true
    world.displayMeshes(next.os);

    sliderOs.forEach((sliderO, idx) => {
      const originalPosZ = originalPosZs[idx];
      // メッシュを透明の状態にしておく
      // Makes the mesh transparent
      sliderO.setOpacity(0);
      // 元のzの位置に+100した地点からアニメーションを始める
      // Starts the animation from the original z position plus 100
      sliderO.mesh.position.z = originalPosZ + 100;
    });

    // アニメーション用progress
    // Progress for animation
    const progress = { value: 0 }; // 0(開始時点) -> 1(終了時点)
    return new Promise((resolve) => {
      // フェードインアニメーション開始
      // Starts the fade-in animation
      gsap.to(progress, {
        value: 1,
        duration: 0.5,
        delay: 0.3,
        ease: "power3.out",
        onUpdate() {
          // プログレスの値を更新する
          // Updates the value of progress
          sliderOs.forEach((sliderO, idx) => {
            // 元のz軸の位置
            // The original position on the z-axis
            const originalPosZ = originalPosZs[idx];
            // 透明度を変更
            // Changes the opacity
            sliderO.setOpacity(progress.value);
            // z軸の地点を変更
            // Changes the position on the z-axis
            sliderO.mesh.position.z = MathUtils.lerp(
              originalPosZ + 100,
              originalPosZ,
              progress.value
            );
          });
          // 次のページの .page-container 要素も合せて透明度を 0 -> 1 に変更
          // Also changes the opacity of the .page-container element of the next page from 0 -> 1
          next.el.style.opacity = progress.value;
        },
        onComplete() {
          // 次のページの Pageモジュール（pageフォルダ配下のJSファイル）の loadAnimation メソッドを実行
          // Executes the loadAnimation method of the Page module (JS file under the page folder) of the next page
          next.page.loadAnimation();
          // 非同期処理全て完了（次の処理が進む）
          // All asynchronous processing is completed (proceeds to the next process)
          resolve();
        },
      });
    });
  }
}

export { registTransitionGhibli };
