import ky from 'ky';
import type React from 'react';
import { useEffect, useRef, useState } from 'react';
import { Link } from 'react-router-dom';
import * as THREE from 'three';
import { TrackballControls } from 'three/examples/jsm/controls/TrackballControls';
import { useAppDispatch } from '../../app/hooks';
import DataUtils from '../data/DataUtils';
import Loading from '../loading/Loading';
import NotFound from '../not-found/NotFound';
import { setTitle } from '../page-title/pageTitleSlice';
import styles from './DataViewer.module.css';

class Viewer3D {
  private mount: HTMLDivElement;
  private frameId: number | null = null;
  private scene: THREE.Scene | null = null;
  private camera: THREE.PerspectiveCamera | null = null;
  private controls: TrackballControls | null = null;
  private renderer: THREE.WebGLRenderer | null = null;

  constructor(mount: HTMLDivElement) {
    this.mount = mount;
    this.start = this.start.bind(this);
    this.stop = this.stop.bind(this);
    this.animate = this.animate.bind(this);
  }

  init(data: string) {
    const lines = data.split('\n');
    const head = lines
      .shift()
      ?.trim()
      .split('\t')
      .map((line) => {
        return parseInt(line.replace(/^#/g, ''), 10) || 0;
      });
    if (typeof head === 'undefined' || head.length !== 3 || head.includes(0)) {
      return false;
    }
    const width = this.mount.clientWidth;
    const height = this.mount.clientHeight;
    const fov = 7.5 + (head[0] * 2.5) / 100.0;
    this.camera = new THREE.PerspectiveCamera(fov, width / height, 1, 10000);
    this.camera.position.x = 1300;
    this.camera.position.y = 1300;
    this.camera.position.z = 1100;
    this.camera.up.set(0, 0, 1);
    this.controls = new TrackballControls(this.camera, this.mount);
    const obj3d = new THREE.Object3D();
    this.scene = new THREE.Scene();
    this.scene.add(obj3d);
    const DrawCube = (width: number, height: number, depth: number) => {
      const mesh = new THREE.Mesh(new THREE.BoxGeometry(width, height, depth * 2), new THREE.MeshBasicMaterial());
      const cube = new THREE.BoxHelper(mesh, 0x000000);
      return cube;
    };
    obj3d.add(DrawCube(head[0], head[1], head[2]));
    const material = {
      e: new THREE.MeshBasicMaterial({ color: 0x0000ff }),
      i: new THREE.MeshBasicMaterial({ color: 0xff0000 }),
      g: new THREE.MeshBasicMaterial({ color: 0x00ff00 }),
      p: new THREE.MeshBasicMaterial({ color: 0xffff00 }),
      s: new THREE.MeshBasicMaterial({ color: 0xff00ff }),
      ps: new THREE.MeshBasicMaterial({ color: 0x00ffff }),
    };
    const geometory = new THREE.SphereGeometry(4, 10, 10);
    lines.forEach((line) => {
      const point = line.trim().split('\t');
      if (point.length === 4 && Object.hasOwn(material, point[0])) {
        const particle = new THREE.Mesh(geometory, material[point[0] as 'e' | 'i' | 'g' | 'p' | 's' | 'ps']);
        particle.position.x = parseFloat(point[1]) - head[0] / 2.0;
        particle.position.y = parseFloat(point[2]) - head[1] / 2.0;
        particle.position.z = head[2] - parseFloat(point[3]) * 2.0;
        obj3d.add(particle);
      }
    });
    try {
      this.renderer = new THREE.WebGLRenderer({ antialias: true });
      this.renderer.setSize(width, height);
      this.renderer.setClearColor(0xffffff);
      this.mount.appendChild(this.renderer.domElement);
    } catch {
      return false;
    }
    return true;
  }

  start() {
    this.frameId = requestAnimationFrame(this.animate);
  }

  stop() {
    if (this.frameId) cancelAnimationFrame(this.frameId);
    if (this.renderer && this.mount) this.mount.removeChild(this.renderer.domElement);
  }

  animate() {
    if (this.controls) this.controls.update();
    if (this.renderer) this.renderer.clear();
    if (this.renderer && this.scene && this.camera) this.renderer.render(this.scene, this.camera);
    this.frameId = requestAnimationFrame(this.animate);
  }
}

interface DataViewerProps {
  name: string;
}

const DataViewer: React.FC<DataViewerProps> = ({ name: initialName }) => {
  const dispatch = useAppDispatch();
  const [name, setName] = useState(initialName);
  const [data, setData] = useState('');
  const mountRef = useRef<HTMLDivElement>(null);
  const viewerRef = useRef<Viewer3D | null>(null);
  const isActiveRef = useRef(true);

  useEffect(() => {
    isActiveRef.current = true;
    if (!DataUtils.exists(name)) {
      if (name !== '') {
        // eslint-disable-next-line react-hooks/set-state-in-effect -- intentional validation reset
        setName('');
      }
      return;
    }
    const controller = new AbortController();
    ky.get(`/data/${name}.dat`, { signal: controller.signal })
      .text()
      .then((data) => {
        if (isActiveRef.current) {
          dispatch(setTitle(`View 3D: ${name}`));
          setData(data);
        }
      })
      .catch(() => {
        if (isActiveRef.current && !controller.signal.aborted) {
          setName('');
        }
      });
    return () => {
      controller.abort();
    };
  }, [name, dispatch]);

  useEffect(() => {
    if (mountRef.current && data !== '') {
      // Stop and clean up the old viewer if switching datasets
      if (viewerRef.current) {
        viewerRef.current.stop();
        viewerRef.current = null;
      }
      const viewer = new Viewer3D(mountRef.current);
      if (viewer.init(data)) {
        viewerRef.current = viewer;
        viewerRef.current.start();
      } else {
        setName('@@NOWEBGL@@');
      }
    }
  }, [data]);

  useEffect(() => {
    return () => {
      isActiveRef.current = false;
      if (viewerRef.current) {
        viewerRef.current.stop();
        viewerRef.current = null;
      }
    };
  }, []);

  switch (name) {
    case '':
      return <NotFound />;
    case '@@NOWEBGL@@':
      return <div className={styles.no_webgl}>It does not appear your computer supports WebGL.</div>;
  }
  if (data === '') {
    return <Loading />;
  }
  return (
    <section>
      <div className={styles.back}>
        <Link to={`/data/${name}`}>Back to {name}</Link>
      </div>
      <div className={styles.viewer} ref={mountRef} />
    </section>
  );
};

export default DataViewer;
