import { observer } from 'mobx-react-lite';
import { useEffect, useRef, useState } from 'react';
import * as THREE from 'three';
import { ClearPass } from 'three/addons/postprocessing/ClearPass.js';
import {
  EffectComposer,
  Pass,
} from 'three/addons/postprocessing/EffectComposer.js';
import {
  ClearMaskPass,
  MaskPass,
} from 'three/addons/postprocessing/MaskPass.js';
import { OutputPass } from 'three/addons/postprocessing/OutputPass.js';
import { RenderPass } from 'three/addons/postprocessing/RenderPass.js';
import { ShaderPass } from 'three/addons/postprocessing/ShaderPass.js';
import { FXAAShader } from 'three/addons/shaders/FXAAShader.js';

import { useStores } from '@/app/(root)/AppProviders';
import { Clip } from '@/state/clipStore';

interface WaveformProps {
  clip: Clip;
  onSelectionChange?: (change: { start: number; end?: number }) => void;
  height?: number;
  initialStartValue?: number;
  selection?: {
    start: number;
    end?: number;
  };
  maxSelectionRangeSecs?: number;
  selectionType?: string;
}

const Waveform = observer(
  ({
    clip,
    onSelectionChange,
    height,
    initialStartValue,
    selection,
    selectionType = 'timestamp',
    maxSelectionRangeSecs,
  }: WaveformProps) => {
    const { playbar } = useStores();
    const canvasRef = useRef<HTMLCanvasElement>(null);
    const [composer, setComposer] = useState<EffectComposer | undefined>(
      undefined
    );

    const [mask, setMask] = useState<Pass | undefined>(undefined);
    const [outputPass, setOutputPass] = useState<Pass | undefined>(undefined);
    const [clearMaskPass, setClearMaskPass] = useState<Pass | undefined>(
      undefined
    );

    const [renderPass, setRenderPass] = useState<Pass | undefined>(undefined);
    const [fxaaPass, setFxaaPass] = useState<Pass | undefined>(undefined);
    const [scene, setScene] = useState<THREE.Scene | undefined>(undefined);
    const [camera, setCamera] = useState<THREE.OrthographicCamera | undefined>(
      undefined
    );

    const [drawn, setDrawn] = useState(false);
    const [clipSamples, setClipSamples] = useState<number[]>([]);

    // Selection interaction
    const [isDragging, setIsDragging] = useState(false);
    const [startPosition, setStartPosition] = useState<number | null>(null);
    const [endPosition, setEndPosition] = useState<number | null>(null);
    const [inputStartPosition, setInputStartPosition] = useState<number | null>(
      null
    );
    const [selectionBoxLeftOffset, setSelectionBoxLeftOffset] = useState<
      number | undefined
    >(undefined);
    const [selectionBoxTopOffset, setSelectionBoxTopOffset] = useState<
      number | undefined
    >(undefined);
    /*const [isDraggingSelectionBox, setIsDraggingSelectionBox] = useState(false);
    const [draggingSelectionBoxStart, setDraggingSelectionBoxStart] =
      useState<any>(null);
    const [selectionBoxDragOffset, setSelectionBoxDragOffset] = useState(0);*/

    // Constants
    // Waveform component height
    const HEIGHT_PIXELS = height || 200;
    // How many samples to average for each point
    const CHUNK_SAMPLES = 4000;
    // How many points to pull from spline curve
    const SPLINE_POINTS = 2000;
    // Percentage of height to scale the mesh
    const SCALE_HEIGHT = 0.75;
    // Camera configuration
    const CAMERA_Z = 2000;
    const CAMERA_ZOOM = 0.0001;
    const ENABLE_FXAA = true;

    const resetSelection = () => {
      setIsDragging(false);
      setStartPosition(null);
      setEndPosition(null);
    };

    useEffect(() => {
      const rectWidth = canvasRef.current?.getBoundingClientRect().width;
      if (
        clip.metadata.duration !== null &&
        clip.metadata.duration !== undefined &&
        initialStartValue !== undefined &&
        rectWidth !== undefined
      ) {
        const startPos =
          (initialStartValue / clip.metadata.duration) * rectWidth;
        setStartPosition(startPos);
        setSelectionBoxLeftOffset(
          startPos + (canvasRef.current?.offsetLeft || 0)
        );
      }
    }, [initialStartValue, clip.metadata.duration]);

    useEffect(() => {
      const process = async () => {
        setDrawn(false);
        resetSelection();

        let audioDataForClip: AudioBuffer | null | undefined =
          playbar.audioData[clip.id];
        if (!audioDataForClip || drawn) {
          audioDataForClip = await playbar.decodeAudio(clip);
        }
        if (!audioDataForClip) {
          return;
        }
        const left = audioDataForClip.getChannelData(0);
        const right = audioDataForClip.getChannelData(1);

        // Break down decoded audio into chunks and take root-mean-squared of each chunk
        const numSamples = Math.floor(left.length / CHUNK_SAMPLES);
        const sampleRMS = Array.from(Array(numSamples).keys()).map(
          (index: number) => {
            const leftSamples = left.slice(
              index * CHUNK_SAMPLES,
              (index + 1) * CHUNK_SAMPLES
            );
            const rightSamples = right.slice(
              index * CHUNK_SAMPLES,
              (index + 1) * CHUNK_SAMPLES
            );
            const leftSum = leftSamples
              .map((ls: number) => ls ** 2)
              .reduce((a: number, b: number) => a + b, 0);
            const rightSum = rightSamples
              .map((rs: number) => rs ** 2)
              .reduce((a: number, b: number) => a + b, 0);
            return Math.sqrt((leftSum + rightSum) / (CHUNK_SAMPLES * 2));
          }
        );
        const scaleVal = SCALE_HEIGHT / Math.max(...sampleRMS);
        setClipSamples(sampleRMS.map((s) => s * scaleVal));
      };
      process();
    }, [clip]);

    useEffect(() => {
      const canvas = canvasRef.current;

      const context =
        canvas && canvas.getContext('webgl2', { antialias: true });

      console.log('getting webGL context from waveform.tsx');

      if (canvas && context && (clipSamples || []).length > 0 && !drawn) {
        canvas.height = HEIGHT_PIXELS;
        if (canvas?.parentElement?.getBoundingClientRect().width) {
          canvas.width = canvas?.parentElement?.getBoundingClientRect().width;
        }

        const w = canvas.width;
        const h = canvas.height;

        // Renderer config
        const renderer = new THREE.WebGLRenderer({
          canvas,
          context: context || undefined,
          antialias: true,
        });

        renderer.setPixelRatio(window.devicePixelRatio);
        renderer.setClearColor(0x252526);
        renderer.setClearAlpha(0.13);

        // set up camera
        const _camera = new THREE.OrthographicCamera(
          -1 * (w / 2),
          w / 2,
          HEIGHT_PIXELS / 2,
          -1 * (HEIGHT_PIXELS / 2),
          1,
          CAMERA_Z
        );
        _camera.zoom = CAMERA_ZOOM;
        _camera.position.set(w / 2, 0, CAMERA_Z);
        _camera.lookAt(w / 2, 0, 0);

        // Create spline curves for waveform
        const positiveVectors = (clipSamples || []).map((clipSample, index) => {
          const x = w * ((index + 1) / (clipSamples || []).length);
          const y = clipSample * (HEIGHT_PIXELS / 2);
          return new THREE.Vector2(x, y);
        });
        const negativeVectors = (clipSamples || []).map((clipSample, index) => {
          const x = w * ((index + 1) / (clipSamples || []).length);
          const y = clipSample * -1 * (HEIGHT_PIXELS / 2);
          return new THREE.Vector2(x, y);
        });
        const positiveCurve = new THREE.SplineCurve([
          new THREE.Vector2(0, 0),
          ...positiveVectors,
          new THREE.Vector2(w, 0),
        ]);
        const negativeCurve = new THREE.SplineCurve([
          new THREE.Vector2(0, 0),
          ...negativeVectors,
          new THREE.Vector2(w, 0),
        ]);
        const positivePoints = positiveCurve.getPoints(SPLINE_POINTS);
        const negativePoints = negativeCurve.getPoints(SPLINE_POINTS);
        const shape = new THREE.Shape([
          ...positivePoints,
          ...negativePoints.toReversed(),
        ]);
        const shapeGeometry = new THREE.ShapeGeometry(shape);
        const material = new THREE.LineBasicMaterial({
          color: 0x726e6c,
          //color: 0x000000,
          linewidth: 2,
        });
        const centerLinePoints = [
          new THREE.Vector3(0, 0, 0),
          new THREE.Vector3(w, 0, 0),
        ];
        const centerLineGeo = new THREE.BufferGeometry().setFromPoints(
          centerLinePoints
        );
        const line = new THREE.Line(centerLineGeo, material);

        // Create the final waveform mesh to add to the scene
        const waveMesh = new THREE.Mesh(shapeGeometry, material);
        const scene = new THREE.Scene();
        scene.add(line);
        scene.add(waveMesh);

        const _scene = new THREE.Scene();
        const _renderPass = new RenderPass(scene, _camera);
        _renderPass.clear = false;
        const renderTargetParameters = {
          minFilter: THREE.LinearFilter,
          magFilter: THREE.LinearFilter,
          format: THREE.RGBAFormat,
          stencilBuffer: true,
          samples: renderer.capabilities.maxSamples,
        };
        const renderTarget = new THREE.WebGLRenderTarget(
          w,
          HEIGHT_PIXELS,
          renderTargetParameters
        );

        // Mask pass
        const _mask = new MaskPass(scene, _camera);
        const secondaryRenderPass = new RenderPass(_scene, _camera);
        secondaryRenderPass.clear = false;

        // Effect composer
        const _composer = new EffectComposer(renderer, renderTarget);

        const _outputPass = new OutputPass();
        const _clearMaskPass = new ClearMaskPass();

        // FXAA Shader for anti-aliasing post-processing
        const _fxaaPass = new ShaderPass(FXAAShader);
        const pixelRatio = renderer.getPixelRatio();
        const uniforms = _fxaaPass.material.uniforms;
        uniforms['resolution'].value.x = 0.01 / (w * pixelRatio);
        uniforms['resolution'].value.y = 0.01 / (h * pixelRatio);

        // do 2 render passes so waveform can also act as render mask
        _composer.addPass(new ClearPass());
        _composer.addPass(_renderPass);
        _composer.addPass(_mask);
        _composer.addPass(secondaryRenderPass);
        _composer.addPass(_clearMaskPass);
        if (ENABLE_FXAA) {
          _composer.addPass(_fxaaPass);
        }
        _composer.addPass(_outputPass);

        // set objects in state so they can be used in other effect
        setComposer(_composer);
        setClearMaskPass(_clearMaskPass);
        setOutputPass(_outputPass);
        setMask(_mask);
        setRenderPass(_renderPass);
        setFxaaPass(_fxaaPass);
        setScene(_scene);
        setCamera(_camera);

        _composer.render();
        setDrawn(true);
      }
    }, [clipSamples, canvasRef.current]);

    useEffect(() => {
      const w = canvasRef.current?.getBoundingClientRect().width || 0;
      if (
        composer !== undefined &&
        scene !== undefined &&
        camera &&
        renderPass &&
        mask &&
        clearMaskPass &&
        outputPass
      ) {
        // Create shape for play duration
        // that can be masked by waveform shape
        const x =
          playbar.clip?.id === clip.id
            ? (playbar.currentTime / playbar.duration) * w
            : 0;

        const durationScene = new THREE.Scene();
        const durationShape = new THREE.Shape([
          new THREE.Vector2(0, -1 * (HEIGHT_PIXELS / 2)),
          new THREE.Vector2(0, HEIGHT_PIXELS / 2),
          new THREE.Vector2(x, HEIGHT_PIXELS / 2),
          new THREE.Vector2(x, -1 * (HEIGHT_PIXELS / 2)),
        ]);
        const durationShapeGeometry = new THREE.ShapeGeometry(durationShape);
        const durationMaterial = new THREE.MeshBasicMaterial({
          color: 0xfaf7f5,
        });
        const durationMesh = new THREE.Mesh(
          durationShapeGeometry,
          durationMaterial
        );
        durationScene.add(durationMesh);
        const secondaryRenderPass = new RenderPass(durationScene, camera);
        secondaryRenderPass.clear = false;

        // use passes from state (except secondary render pass)
        composer.addPass(new ClearPass());
        composer.addPass(renderPass);
        composer.addPass(mask);
        composer.addPass(secondaryRenderPass);
        composer.addPass(clearMaskPass);
        if (ENABLE_FXAA) {
          if (fxaaPass) composer.addPass(fxaaPass);
        }
        composer.addPass(outputPass);
        composer.render();
      }
    }, [playbar.clip?.id, playbar.currentTime]);

    useEffect(() => {
      setSelectionBoxLeftOffset(
        startPosition !== null && !!clip.metadata?.duration
          ? startPosition + (canvasRef.current?.offsetLeft || 0)
          : 0
      );
      setSelectionBoxTopOffset(HEIGHT_PIXELS * ((1 - SCALE_HEIGHT) / 2));
    }, [startPosition, canvasRef.current?.offsetLeft]);

    useEffect(() => {
      if (selection?.start) {
        const canvasWidth = canvasRef.current?.getBoundingClientRect().width;
        if (canvasWidth && clip.metadata?.duration) {
          setStartPosition(
            canvasWidth * (selection.start / clip.metadata?.duration)
          );
        }
      }
      if (selection?.end) {
        const canvasWidth = canvasRef.current?.getBoundingClientRect().width;
        if (canvasWidth && clip.metadata?.duration) {
          setEndPosition(
            canvasWidth * (selection.end / clip.metadata?.duration)
          );
        }
      } else {
        setEndPosition(null);
      }
    }, [selection]);

    return (
      <div className='relative'>
        <div
          className='rounded-4 pointer-events-none absolute border border-accent-orange'
          style={{
            display:
              startPosition !== null && !!onSelectionChange ? 'block' : 'none',
            backgroundColor: 'rgba(255,255,255,0.3)',
            height: `${HEIGHT_PIXELS * SCALE_HEIGHT}px`,
            top: `${selectionBoxTopOffset}px`,
            left: `${selectionBoxLeftOffset}px`,
            width:
              endPosition !== null &&
              startPosition !== null &&
              !!clip.metadata?.duration
                ? `${endPosition - startPosition}px`
                : startPosition !== null
                  ? '1px'
                  : 0,
          }}
        />
        <canvas
          ref={canvasRef}
          style={{
            width: '100%',
            height: `${HEIGHT_PIXELS}px`,
            borderRadius: '4px',
          }}
          onMouseDown={(e: any) => {
            setEndPosition(null);
            const boundingRect = e.target.getBoundingClientRect();
            const newStart = e.clientX - boundingRect.x;
            setStartPosition(newStart);
            setInputStartPosition(newStart);
            setIsDragging(true);
            onSelectionChange?.({
              start:
                (newStart / boundingRect.width) *
                (clip.metadata?.duration || 0),
              end: undefined,
            });
          }}
          onMouseMove={(e: any) => {
            if (isDragging) {
              const durations: any = {};
              const boundingRect = e.target.getBoundingClientRect();
              const newEndPosition = e.clientX - boundingRect.x;

              // enforce the selection being at most maxSelectionRangeSecs

              const start = Math.min(
                Math.max(
                  newEndPosition,
                  !!inputStartPosition && clip.metadata?.duration
                    ? inputStartPosition -
                        ((maxSelectionRangeSecs || clip.metadata?.duration) /
                          clip.metadata?.duration) *
                          boundingRect.width
                    : 0
                ),
                inputStartPosition || 0
              );
              const end = Math.max(
                Math.min(
                  newEndPosition,
                  !!inputStartPosition && clip.metadata?.duration
                    ? inputStartPosition +
                        ((maxSelectionRangeSecs || clip.metadata?.duration) /
                          clip.metadata?.duration) *
                          boundingRect.width
                    : 0
                ),
                inputStartPosition || 0
              );

              if (selectionType === 'range') {
                durations.start =
                  (start / boundingRect.width) * (clip.metadata?.duration || 0);
                durations.end =
                  (end / boundingRect.width) * (clip.metadata?.duration || 0);
                setStartPosition(start);
                setEndPosition(end);
              } else {
                durations.start =
                  (newEndPosition / boundingRect.width) *
                  (clip.metadata?.duration || 0);
                durations.end = undefined;
                setStartPosition(newEndPosition);
                setEndPosition(null);
              }

              onSelectionChange?.(durations);
            }
          }}
          onMouseUp={() => {
            setIsDragging(false);
          }}
          onMouseOut={() => {
            setIsDragging(false);
          }}
          onBlur={() => {}}
        />
      </div>
    );
  }
);

export default Waveform;
