import gsap from "gsap";
import {
  PMREMGenerator,
  ACESFilmicToneMapping,
  Scene,
  MathUtils,
  Vector3,
  Color,
  Fog,
} from "three";

let sky = null,
  ocean = null,
  _world = null,
  pmremGenerator = null,
  renderTarget = null,
  fog = null;

const sun = new Vector3(),
  sceneEnv = new Scene();

/**
 * 海と空のメッシュを配置します。
 * 海と空の配置については、three.js のサンプルコードを参考にしています。
 * Sets up the mesh for the ocean and sky.
 * The setup for the ocean and sky is based on the sample code from three.js.
 * https://threejs.org/examples/?q=ocean#webgl_shaders_ocean
 *
 * @param {*} param0
 * @returns
 */
export async function setupSkyOcean({ world, gui }) {
  _world = world;
  // シーンのトーン（色調）を調整.  Adjust the tone (color tone) of the scene
  // WebGLRenderer.toneMapping - https://threejs.org/docs/#api/en/renderers/WebGLRenderer.toneMapping
  // トーンの種類. Types of tone - https://threejs.org/docs/index.html#api/en/constants/Renderer
  _world.renderer.toneMapping = ACESFilmicToneMapping;
  _world.renderer.toneMappingExposure = 0.5;

  // Fogの追加（シーンに奥行きを持たせます。）. Add fog (gives depth to the scene).
  // Fog - https://threejs.org/docs/index.html?q=Fog#api/en/scenes/Fog
  _world.scene.fog = fog = new Fog(0xc7d4d9, 0, 2500);

  // 海を生成. Create the ocean.
  // world.createOb によって JS からエフェクトを作成します. Create an effect from JS using world.createOb.
  // world.createOb - https://docs.not-equal.dev/reference/classes/World/#createob
  ocean = await _world.createOb(
    `
    <div class="ocean" data-webgl="ocean" data-tex-1='/img/normal/waternormals.jpg'>
    </div>
    `,
    { updateStyle: false }
  );

  // 空を生成. Create the sky.
  // world.createOb によって JS からエフェクトを作成します. Create an effect from JS using world.createOb.
  sky = await _world.createOb(`
    <div class="sky" data-webgl="sky" style="width: 1px; height: 1px;"></div>
    `);

  /**
   * シーンから環境マップ（envMap）を作成します。
   * Create an environment map (envMap) from the scene.
   *
   * 環境マップを使った例は以下を見てみてください。
   * Check out the examples using environment maps below.
   *
   * https://threejs.org/examples/?q=envMap#webgl_materials_envmaps
   * https://threejs.org/examples/?q=envMap#webgl_materials_cubemap_mipmaps
   *
   *
   * PMREMGenerator は シーンから環境マップを作成します. PMREMGenerator creates an environment map from the scene.
   * PMREMGenerator - https://threejs.org/docs/#api/en/extras/PMREMGenerator
   */
  pmremGenerator = new PMREMGenerator(_world.renderer);

  // 昼と夜の配色（テーマ）を定義します.
  // Define the color scheme (theme) for day and night.
  const theme = initTheme();
  // テーマを反映します。
  // Reflect the theme.
  updateTheme(theme);

  // lilGUI を登録します。
  // Register lilGUI.
  gui.add((lilGUI) => debug(lilGUI, theme));

  // Obオブジェクトを返却します。
  // Return the Ob object.
  return { sky, ocean };
}

function updateTheme(theme) {
  // 空のパラメータ変更
  // Change the parameters of the sky
  const skyUniforms = sky.material.uniforms;
  skyUniforms.turbidity.value = theme.turbidity;
  skyUniforms.rayleigh.value = theme.rayleigh;
  skyUniforms.mieCoefficient.value = theme.mieCoefficient;
  skyUniforms.mieDirectionalG.value = theme.mieDirectionalG;

  // 海のパラメータ変更
  // Change the parameters of the ocean
  const oceanUniforms = ocean.material.uniforms;
  oceanUniforms.waterColor.value = new Color(theme.waterColor);
  oceanUniforms.sunColor.value = new Color(theme.sunColor);

  // Fogの変更
  // Change the fog
  fog.color = new Color(theme.fogColor);

  // 環境マップを更新
  // Update the environment map
  updateSun(theme);

  // htmlを更新
  // Update the html
  const bodyClassList = document.body.classList;
  isNight ? bodyClassList.add("night") : bodyClassList.remove("night");
}

function updateSun(theme) {
  // sky の中の太陽の高さ
  // Height of the sun in the sky
  const phi = MathUtils.degToRad(90 - theme.elevation);
  // sky の中の太陽の横の位置
  // Horizontal position of the sun in the sky
  const theta = MathUtils.degToRad(theme.azimuth);

  // 極座標から座標を設定
  // Set the coordinates from the polar coordinates
  // setFromSphericalCoords - https://threejs.org/docs/#api/en/math/Vector3.setFromSphericalCoords
  sun.setFromSphericalCoords(1, phi, theta);

  // 太陽の位置をそれぞれのユニフォームに設定
  // Set the position of the sun in each uniform
  sky.mesh.material.uniforms["sunPosition"].value.copy(sun);
  ocean.mesh.material.uniforms["sunDirection"].value.copy(sun).normalize();

  // PMREMGeneratorで前回作成した renderTarget がある場合には破棄する
  // If there is a renderTarget created by PMREMGenerator last time, discard it
  renderTarget?.dispose?.();

  // 環境マップ用のシーンに sky.mesh を追加
  // Add sky.mesh to the scene for the environment map
  sceneEnv.add(sky.mesh);

  // PMREMGenerator で sky.mesh を元に環境マップを作成
  // Create an environment map based on sky.mesh with PMREMGenerator
  renderTarget = pmremGenerator.fromScene(sceneEnv);

  /**
   * シーンの環境マップとして設定
   * Set as the environment map of the scene
   *
   * [scene.environment](https://threejs.org/docs/index.html?q=scene#api/en/scenes/Scene.environment) に追加された環境マップは MeshStandardMaterial の環境マップとして使用することができます。
   *  The environment map added to [scene.environment](https://threejs.org/docs/index.html?q=scene#api/en/scenes/Scene.environment) can be used as the environment map for MeshStandardMaterial.
   */
  _world.scene.environment = renderTarget.texture;

  // sceneEnv に追加したメッシュは _world.scene から一時的に削除されるため、再度メインのシーンに追加
  // The mesh added to sceneEnv is temporarily removed from _world.scene, so add it back to the main scene

  _world.scene.add(sky.mesh);
}

/**
 * lilGUI に UI を登録します。
 * Register UI with lilGUI.
 * @param {*} lilGUI
 * @param {*} theme
 */
function debug(lilGUI, theme) {
  const folder = lilGUI.addFolder("sun");
  folder.close();
  folder
    .add(theme, "elevation", 0, 90, 0.1)
    .onChange(() => updateSun(theme))
    .listen();
  folder
    .add(theme, "azimuth", -180, 180, 0.1)
    .onChange(() => updateSun(theme))
    .listen();
  folder
    .addColor(theme, "fogColor")
    .onChange(() => {
      fog.color = new Color(theme.fogColor);
    })
    .listen();
}

let isNight = false; // 0: daytime, 1: night
/**
 * 画面ロード時の海と空に設定する uniforms の値（テーマ）を決定します。
 * Determine the values (theme) of the uniforms to be set for the ocean and sky at the time of loading the screen.
 * @returns
 */
function initTheme() {
  let currentTheme = getDaytimeTheme();

  /**
   * ページをロードしたタイミングが夜間の場合 isNight は true を返します。
   * window.ghibli.isNight は html ファイル内にインラインで定義しています。
   * If the page is loaded during the night, isNight will return true.
   * window.ghibli.isNight is defined inline within the HTML file.
   */
  if (window.ghibli.isNight()) {
    currentTheme = getNightTheme();
    isNight = true;
  }

  return currentTheme;
}

/**
 * 日中テーマを返します。Returns the daytime theme.
 * @returns 日中テーマ(Daytime theme)
 */
function getDaytimeTheme() {
  return {
    elevation: 25,
    azimuth: -30,
    waterColor: 0x39e7fe,
    sunColor: 0xffffff,
    turbidity: 0,
    rayleigh: 2,
    mieCoefficient: 0.005,
    mieDirectionalG: 0.8,
    fogColor: "#c7d4d9",
  };
}
/**
 * 夜間テーマを返します。Returns the nighttime theme.
 * @returns 夜間テーマ(Nighttime theme)
 */
function getNightTheme() {
  return {
    elevation: 15,
    azimuth: 26,
    waterColor: 0x39e7fe,
    sunColor: 0xffffff,
    turbidity: 0,
    rayleigh: 0.1,
    mieCoefficient: 0.005,
    mieDirectionalG: 0.8,
    fogColor: "#577cb2",
  };
}

/**
 * テーマを切り替えます。
 * Switch themes.
 */
export function toggleTheme() {
  let currentTheme, nextTheme;

  isNight = !isNight;
  if (isNight) {
    currentTheme = getDaytimeTheme();
    nextTheme = getNightTheme();
  } else {
    currentTheme = getNightTheme();
    nextTheme = getDaytimeTheme();
  }

  // テーマを切り替えます。
  // Switch themes.
  const progress = { value: 0 };
  gsap.to(progress, {
    value: 1,
    duration: 0.4,
    overwrite: true,
    ease: "power4.in",
    onUpdate() {
      /**
       * 現在のテーマと次のテーマの線形補間（interpolate）した値を theme に設定します。
       * Set the value of the theme by linearly interpolating the current theme and the next theme.
       * gsap.utils.interpolate - https://greensock.com/docs/v3/GSAP/UtilityMethods/interpolate()
       */
      const theme = gsap.utils.interpolate(
        currentTheme,
        nextTheme,
        progress.value
      );

      // テーマを更新します。
      // Update the theme
      updateTheme(theme);
    },
  });
}
