import React, {
  useMemo,
  useEffect,
  useRef,
  useCallback,
  useState,
} from 'react';
import cn from 'classnames';
import { sort } from 'timsort';
import { useSelector, useDispatch } from '../../app/store';
import { Matrix } from '../../util/matrix';
import { Transform3D } from '../../util/transform3d';
import { selectLoadedMatrix } from '../loader/fileSlice';
import {
  selectObjectOrientation,
  selectObjectPosition,
  selectCameraTowards,
  selectCameraPosition,
  selectCameraProjection,
  selectLightSourcePosition,
  selectObjectColor,
  moveCamera,
  zoomIn,
  zoomOut,
} from './rendererSlice';
import styles from './Canvas.module.css';

export function Canvas() {
  const canvasRef = useRef<HTMLCanvasElement | null>(null);
  const [width, setWidth] = useState<number | undefined>();
  const [height, setHeight] = useState<number | undefined>();
  const [grabbingCounter, setGrabbingCounter] = useState(0);

  const object = useSelector(selectLoadedMatrix);
  const vertices = useMemo(
    () => object && new Matrix(object).reshape(undefined, 3).stackHorizontal(1),
    [object]
  );

  const objectOrientation = useSelector(selectObjectOrientation);
  const orientObjectTransform = useMemo(
    () =>
      Transform3D.rotateX(objectOrientation.x)
        .multiply(Transform3D.rotateY(objectOrientation.y))
        .multiply(Transform3D.rotateZ(objectOrientation.z)),
    [objectOrientation]
  );
  const objectPosition = useSelector(selectObjectPosition);
  const positionObjectTransform = useMemo(
    () =>
      Transform3D.translate(
        objectPosition.x,
        objectPosition.y,
        objectPosition.z
      ),
    [objectPosition]
  );
  const worldTransform = useMemo(
    () => Matrix.multiply(orientObjectTransform, positionObjectTransform),
    [orientObjectTransform, positionObjectTransform]
  );
  const worldVertices = useMemo(
    () => vertices && Matrix.multiply(vertices, worldTransform),
    [vertices, worldTransform]
  );
  const worldTriangles = useMemo(
    () => worldVertices && Matrix.reshape(worldVertices, undefined, 12),
    [worldVertices]
  );

  const centroids = useMemo(
    () =>
      worldTriangles &&
      Matrix.add(worldTriangles.sliceCols(0, 3), worldTriangles.sliceCols(4, 7))
        .add(worldTriangles.sliceCols(8, 11))
        .divide(3),
    [worldTriangles]
  );
  const normals = useMemo(() => {
    if (!worldTriangles) {
      return;
    }
    const a = worldTriangles.sliceCols(0, 3);
    const b = worldTriangles.sliceCols(4, 7);
    const c = worldTriangles.sliceCols(8, 11);
    const v1 = Matrix.subtract(b, a);
    const v2 = Matrix.subtract(c, a);
    const v1_1 = v1.col(0);
    const v1_2 = v1.col(1);
    const v1_3 = v1.col(2);
    const v2_1 = v2.col(0);
    const v2_2 = v2.col(1);
    const v2_3 = v2.col(2);
    const n1 = Matrix.scale(v1_2, v2_3).subtract(Matrix.scale(v1_3, v2_2));
    const n2 = Matrix.scale(v1_3, v2_1).subtract(Matrix.scale(v1_1, v2_3));
    const n3 = Matrix.scale(v1_1, v2_2).subtract(Matrix.scale(v1_2, v2_1));
    const mag = Matrix.scale(n1, n1)
      .add(Matrix.scale(n2, n2))
      .add(Matrix.scale(n3, n3))
      .pow(0.5);
    return n1.stackHorizontal(n2).stackHorizontal(n3).divide(mag);
  }, [worldTriangles]);
  const lightPosition = useSelector(selectLightSourcePosition);
  const lightPositionVector = useMemo(
    () => new Matrix([lightPosition.x, lightPosition.y, lightPosition.z]),
    [lightPosition]
  );
  const objectColor = useSelector(selectObjectColor);
  const objectColorVector = useMemo(
    () => new Matrix([objectColor.r, objectColor.g, objectColor.b]),
    [objectColor]
  );
  const colors = useMemo(() => {
    if (!centroids || !normals) {
      return;
    }
    const lightVecs = Matrix.subtract(lightPositionVector, centroids);
    const lightMags = Matrix.scale(lightVecs, lightVecs).sumCols().pow(0.5);
    return lightVecs
      .divide(lightMags)
      .scale(normals)
      .sumCols()
      .max(0)
      .scale(objectColorVector);
  }, [centroids, normals, lightPositionVector, objectColorVector]);

  const cameraPosition = useSelector(selectCameraPosition);
  const cameraPositionVector = useMemo(
    () => new Matrix([cameraPosition.x, cameraPosition.y, cameraPosition.z]),
    [cameraPosition]
  );
  const cameraTowards = useSelector(selectCameraTowards);
  const cameraTowardsVector = useMemo(
    () => new Matrix([cameraTowards.x, cameraTowards.y, cameraTowards.z]),
    [cameraTowards]
  );
  const viewTransform = useMemo(
    () => Transform3D.lookAt(cameraPositionVector, cameraTowardsVector),
    [cameraPositionVector, cameraTowardsVector]
  );
  const cameraProjection = useSelector(selectCameraProjection);
  const projectionTransform = useMemo(
    () =>
      width !== undefined &&
      height !== undefined &&
      Transform3D.perspective(
        cameraProjection.near,
        cameraProjection.far,
        cameraProjection.vfov,
        width / height
      ),
    [cameraProjection, width, height]
  );
  const viewportTransform = useMemo(() => {
    if (width !== undefined && height !== undefined) {
      return Transform3D.viewport(width, height);
    }
  }, [width, height]);
  const displayTransform = useMemo(
    () =>
      projectionTransform &&
      viewportTransform &&
      Matrix.multiply(viewTransform, projectionTransform).multiply(
        viewportTransform
      ),
    [viewTransform, projectionTransform, viewportTransform]
  );
  const displayTriangles = useMemo(() => {
    if (!worldVertices || !colors || !displayTransform) {
      return;
    }
    const verticesWithW = Matrix.multiply(worldVertices, displayTransform);
    const w = verticesWithW.col(3);
    const triangles = Matrix.divide(verticesWithW, w).reshape(undefined, 12);
    const trianglesWithW = Matrix.reshape(verticesWithW, undefined, 12);
    const wPositiveMask = Matrix.gt(trianglesWithW.col(3), 0)
      .scale(Matrix.gt(trianglesWithW.col(7), 0))
      .scale(Matrix.gt(trianglesWithW.col(11), 0));
    const z1Valid = Matrix.gte(triangles.col(2), 0).scale(
      Matrix.lte(triangles.col(2), 1)
    );
    const z2Valid = Matrix.gte(triangles.col(6), 0).scale(
      Matrix.lte(triangles.col(6), 1)
    );
    const z3Valid = Matrix.gte(triangles.col(10), 0).scale(
      Matrix.lte(triangles.col(10), 1)
    );
    const mask = wPositiveMask.scale(z1Valid.add(z2Valid).add(z3Valid));
    const zAvg = Matrix.add(triangles.col(2), triangles.col(6))
      .add(triangles.col(10))
      .divide(3);
    return zAvg
      .stackHorizontal(triangles.sliceCols(0, 2))
      .stackHorizontal(triangles.sliceCols(4, 6))
      .stackHorizontal(triangles.sliceCols(8, 10))
      .stackHorizontal(colors)
      .mask(mask);
  }, [worldVertices, displayTransform, colors]);

  const [context, setContext] = useState<CanvasRenderingContext2D | null>(null);
  useEffect(() => {
    if (canvasRef.current) {
      setContext(canvasRef.current.getContext('2d'));
    }
  }, []);
  useEffect(() => {
    if (
      !displayTriangles ||
      !context ||
      width === undefined ||
      height === undefined
    ) {
      return;
    }
    const triangleArray = displayTriangles.toArray();
    sort(triangleArray, ([z1], [z2]) => z2 - z1);
    for (let i = 0; i < triangleArray.length; i++) {
      const [, x1, y1, x2, y2, x3, y3, r, g, b] = triangleArray[i];
      context.fillStyle = `rgb(${r}, ${g}, ${b})`;
      context.beginPath();
      context.moveTo(x1, y1);
      context.lineTo(x2, y2);
      context.lineTo(x3, y3);
      context.closePath();
      context.fill();
    }
    return () => context.clearRect(0, 0, width, height);
  }, [displayTriangles, width, height, context]);

  useEffect(() => {
    const onResize = () => {
      if (!canvasRef.current) {
        return;
      }
      setWidth(canvasRef.current.offsetWidth);
      setHeight(canvasRef.current.offsetHeight);
    };
    onResize();
    window.addEventListener('resize', onResize);
    return () => window.removeEventListener('resize', onResize);
  }, []);

  const dispatch = useDispatch();
  const onMouseDown = useCallback(
    (event: React.MouseEvent<HTMLCanvasElement, MouseEvent>) => {
      if (event.button !== 0) {
        return;
      }
      event.stopPropagation();
      let lastX = event.screenX;
      let lastY = event.screenY;
      const onMouseMove = (event: MouseEvent) => {
        event.preventDefault();
        event.stopPropagation();
        const currentX = event.screenX;
        const currentY = event.screenY;
        const deltaX = currentX - lastX;
        const deltaY = !event.shiftKey ? currentY - lastY : 0;
        const deltaZ = event.shiftKey ? currentY - lastY : 0;
        lastX = currentX;
        lastY = currentY;
        dispatch(moveCamera(deltaX, deltaY, deltaZ));
      };
      const onMouseUp = (event: MouseEvent) => {
        event.preventDefault();
        event.stopPropagation();
        window.removeEventListener('mousemove', onMouseMove);
        window.removeEventListener('mouseup', onMouseUp);
        setGrabbingCounter((count) => count - 1);
      };
      window.addEventListener('mousemove', onMouseMove);
      window.addEventListener('mouseup', onMouseUp);
      setGrabbingCounter((count) => count + 1);
    },
    [dispatch]
  );
  const onWheel = useCallback(
    (event: React.WheelEvent) => {
      if (event.ctrlKey) {
        return;
      }
      event.stopPropagation();
      if (event.deltaY < 0) {
        dispatch(zoomIn());
      } else if (event.deltaY > 0) {
        dispatch(zoomOut());
      }
    },
    [dispatch]
  );

  return (
    <div className={styles.canvasContainer}>
      <canvas
        width={width}
        height={height}
        ref={canvasRef}
        onMouseDown={onMouseDown}
        onWheel={onWheel}
        className={cn(
          styles.canvas,
          object && styles.grabbable,
          grabbingCounter !== 0 && styles.grabbing
        )}
      />
    </div>
  );
}
