import {
  Scene,
  PerspectiveCamera,
  WebGLRenderer,
  PointLight,
  Float32BufferAttribute,
  AudioListener,
  AudioAnalyser,
  Clock,
  Mesh,
  MeshBasicMaterial,
  MathUtils,
  BufferGeometry,
  Points,
  SphereGeometry,
} from "three";
import { GLTF } from "three/examples/jsm/loaders/GLTFLoader";
import { calculateSoundAverages, LoadAudio } from "./audio";
import { LoadModel } from "./model";
import { SETTINGS } from "./settings";
import { CORE, DISTORTION, DistortionMesh, SoundAverages, SRT } from "./types";
import { prng_alea } from "esm-seedrandom";
import parseSRT from "parse-srt";

const CORE: CORE = {
  renderer: null,
  scene: null,
  camera: null,
  clock: null,
  rng: prng_alea(
    "Polka-Jovial-Makeover-Wieldable-Spirited-Footprint-Recall-Handpick-Sacrifice-Jester"
  ),
  avgs: null,
  subtitleDiv: document.getElementById("subtitles"),
  parser: parseSRT,
  srt: null,
  elapsedTime: 0,
  playing: false,
};

const DISTORTION: DISTORTION = {
  light: null,
  hand: null,
  positions: null,
  audioListener: null,
  audioAnalyser: null,
};

const ROTATION = {
  cam: {
    angle: 0,
    angularSpeed: MathUtils.degToRad(20),
    radius: 5,
  },
  light: {
    angle: 30,
    angularSpeed: MathUtils.degToRad(40),
    radius: 2,
  },
};

init().then(() => animate());

async function init() {
  const container = document.getElementById("container");

  CORE.scene = new Scene();
  CORE.camera = new PerspectiveCamera(
    75,
    window.innerWidth / window.innerHeight,
    1,
    10000
  );
  CORE.scene.add(CORE.camera);

  DISTORTION.light = new PointLight(0x119911, 1);
  DISTORTION.light.counter = 0;

  const geometry = new SphereGeometry(0.2);
  const material = new MeshBasicMaterial({ color: 0xcccc00 });
  const sphere = new Mesh(geometry, material);
  DISTORTION.light.add(sphere);
  CORE.scene.add(DISTORTION.light);

  CORE.renderer = new WebGLRenderer();
  CORE.renderer.setPixelRatio(window.devicePixelRatio);
  CORE.renderer.setSize(window.innerWidth, window.innerHeight);
  container.appendChild(CORE.renderer.domElement);
  window.addEventListener("resize", onWindowResize);

  DISTORTION.audioListener = new AudioListener();
  CORE.scene.add(DISTORTION.audioListener);

  CORE.clock = new Clock();

  try {
    const model = await LoadModel();
    initializeModel(model);
    updateRotations(0);
    initializeStars();

    const audio = await LoadAudio(
      DISTORTION.audioListener,
      "/static/who_am_i_with_music.mp3",
      1
    );
    CORE.srt = await loadSRT();
    DISTORTION.audioAnalyser = new AudioAnalyser(audio, 512);

    const startButton = document.getElementById("startButton");
    startButton.style.display = "block";
    startButton.addEventListener("click", () => {
      audio.play();
      CORE.playing = true;
      startButton.remove();
    });
  } catch (err) {
    console.warn(err);
  }
}

async function loadSRT(): Promise<SRT[]> {
  const response = await fetch("static/who_am_i_with_music.srt");
  const responseText = await response.text();
  return CORE.parser(responseText);
}

function initializeModel(model: GLTF) {
  // remove second hand with text above it
  const objToRemove = model.scene.getObjectByName("Object_3");
  objToRemove.parent.remove(objToRemove);

  // turn remaining hand into wireframe
  DISTORTION.hand = <DistortionMesh>model.scene.getObjectByName("Object_4");
  DISTORTION.hand.material.wireframe = true;

  // set up distortion for each vertex
  DISTORTION.hand.originalPositions =
    DISTORTION.hand.geometry.getAttribute("position");
  DISTORTION.hand.distortions = (
    DISTORTION.hand.originalPositions.array as Array<number>
  )
    .slice(0)
    .map(() => CORE.rng() * 2 - 1);
  DISTORTION.positions = DISTORTION.hand.geometry.getAttribute("position");
  DISTORTION.hand.parent.position.x = DISTORTION.hand.parent.position.x + 2.8;
  DISTORTION.hand.parent.position.y = DISTORTION.hand.parent.position.y + 0;
  DISTORTION.hand.parent.position.z = DISTORTION.hand.parent.position.z + 2.2;
  DISTORTION.hand.parent.rotateZ(Math.PI / 2);
  DISTORTION.hand.geometry.center();

  CORE.scene.add(model.scene);
}

function initializeStars() {
  const geometry = new BufferGeometry();
  const vertices = [];
  for (let i = 0; i < 10000; i++) {
    const x = CORE.rng() * 2000 - 1000;
    const y = CORE.rng() * 2000 - 1000;
    const z = CORE.rng() * 2000 - 1000;

    vertices.push(x, y, z);
  }

  geometry.setAttribute("position", new Float32BufferAttribute(vertices, 3));
  const points = new Points(geometry);
  CORE.scene.add(points);
}

function animate() {
  requestAnimationFrame(animate);

  const delta = CORE.clock.getDelta();
  CORE.avgs = calculateSoundAverages(DISTORTION.audioAnalyser);

  render(delta, CORE.avgs);
}

function render(delta: number, soundAvg: SoundAverages) {
  // modulate light intensity
  DISTORTION.light.counter += delta + SETTINGS.lightSpeed;
  DISTORTION.light.intensity =
    Math.sin(DISTORTION.light.counter) / 2 + SETTINGS.lightMin;

  const sound = Math.pow(
    soundAvg.soundAvg * SETTINGS.distortionMax,
    SETTINGS.distortionPower
  );

  if (CORE.playing) {
    updateRotations(delta);
    updateSubtitles(delta);
  }
  const newPositions = new Float32BufferAttribute(
    (DISTORTION.positions.array as Array<number>).map((_position, index) => {
      const distortion = DISTORTION.hand.distortions[index] * sound;
      return distortion / 10 + DISTORTION.hand.originalPositions.array[index];
    }),
    3
  );

  DISTORTION.hand.geometry.setAttribute("position", newPositions);
  CORE.camera.lookAt(DISTORTION.hand.position);
  CORE.renderer.render(CORE.scene, CORE.camera);
}

function updateRotations(delta: number) {
  DISTORTION.hand.parent.rotateZ(3 / 1000);

  CORE.camera.position.x = Math.sin(ROTATION.cam.angle) * ROTATION.cam.radius;
  CORE.camera.position.z = Math.cos(ROTATION.cam.angle) * ROTATION.cam.radius;
  CORE.camera.position.y = Math.cos(ROTATION.cam.angle) * 1.5;
  ROTATION.cam.angle += ROTATION.cam.angularSpeed * delta;

  DISTORTION.light.position.x =
    Math.cos(ROTATION.light.angle) * ROTATION.light.radius;
  DISTORTION.light.position.z =
    Math.sin(ROTATION.light.angle) * ROTATION.light.radius;
  DISTORTION.light.position.y = Math.cos(ROTATION.light.angle) * 5;

  ROTATION.light.angle += ROTATION.light.angularSpeed * delta;
}

function updateSubtitles(delta: number) {
  CORE.elapsedTime += delta;
  let found = false;
  CORE.srt.forEach((srt) => {
    if (CORE.elapsedTime > srt.start && CORE.elapsedTime < srt.end) {
      found = true;
      CORE.subtitleDiv.innerHTML = srt.text;
    }
  });

  if (!found) CORE.subtitleDiv.innerHTML = "";

  if (CORE.elapsedTime > 70) {
    document.getElementById("credits").style.display = "block";
  }
}

function onWindowResize() {
  CORE.camera.aspect = window.innerWidth / window.innerHeight;
  CORE.camera.updateProjectionMatrix();

  CORE.renderer.setSize(window.innerWidth, window.innerHeight);
}
