import { useState, useEffect, useRef, MouseEvent } from 'react';
import {
  MediaSetServiceClientImpl,
  MediaSet,
  GetAudioProgress,
} from './generated/media_set';
import { Frames, newRPC, VideoPosition } from './App';
import { WaveformCanvas } from './WaveformCanvas';
import { from, Observable } from 'rxjs';
import { map } from 'rxjs/operators';

export interface Selection {
  start: number;
  end: number;
}

interface Props {
  mediaSet: MediaSet;
  height: number;
  offsetPixels: number;
  position: VideoPosition;
  onSelectionChange: (selection: Frames) => void;
}

enum Mode {
  Normal,
  Selecting,
  Dragging,
  ResizingStart,
  ResizingEnd,
}

enum HoverState {
  Normal,
  OverSelectionStart,
  OverSelectionEnd,
  OverSelection,
}

const CanvasLogicalWidth = 2_000;
const CanvasLogicalHeight = 500;

const emptySelection = { start: 0, end: 0 };

export const Overview: React.FC<Props> = ({
  mediaSet,
  height,
  offsetPixels,
  position,
  onSelectionChange,
}: Props) => {
  const hudCanvasRef = useRef<HTMLCanvasElement>(null);
  const [peaks, setPeaks] = useState<Observable<number[]>>(from([]));
  const [mode, setMode] = useState(Mode.Normal);
  const [hoverState, setHoverState] = useState(HoverState.Normal);
  const [newSelection, setNewSelection] = useState({
    ...emptySelection,
  });
  const [selection, setSelection] = useState({ start: 0, end: 100 });
  const [cursor, setCursor] = useState('auto');

  const moveOffsetX = useRef(0);

  // effects

  // handle global mouse up.
  useEffect(() => {
    window.addEventListener('mouseup', handleMouseUp);
    return () => {
      window.removeEventListener('mouseup', handleMouseUp);
    };
  }, [mode, newSelection]);

  // publish onSelectionChange event
  useEffect(() => {
    if (mediaSet == null) {
      return;
    }
    const canvas = hudCanvasRef.current;
    if (canvas == null) {
      console.error('no hud canvas ref available');
      return;
    }
    const ctx = canvas.getContext('2d');
    if (ctx == null) {
      console.error('no hud 2d context available');
      return;
    }

    const width = canvas.getBoundingClientRect().width;
    const selectionPercent = {
      start: (selection.start / width) * 100,
      end: (selection.end / width) * 100,
    };

    onSelectionChange(selectionPercent);
  }, [selection]);

  // load peaks on mediaset change
  useEffect(() => {
    (async function () {
      if (mediaSet == null) {
        return;
      }
      const canvas = hudCanvasRef.current;
      if (canvas == null) {
        console.error('no hud canvas ref available');
        return;
      }
      const ctx = canvas.getContext('2d');
      if (ctx == null) {
        console.error('no hud 2d context available');
        return;
      }
      console.log('fetching audio...');
      const service = new MediaSetServiceClientImpl(newRPC());
      const audioProgressStream = service.GetAudio({
        id: mediaSet.id,
        numBins: CanvasLogicalWidth,
      });
      const peaks = audioProgressStream.pipe(map((progress) => progress.peaks));
      setPeaks(peaks);

      let url = '';
      // TODO: probably a nicer way to do this.
      await audioProgressStream.forEach((progress: GetAudioProgress) => {
        if (progress.url != '') {
          url = progress.url;
        }
      });
      console.log('got audio URL', url);
    })();
  }, [mediaSet]);

  // draw the overview HUD
  useEffect(() => {
    requestAnimationFrame(() => {
      const canvas = hudCanvasRef.current;
      if (canvas == null) {
        console.error('no hud canvas ref available');
        return;
      }
      const ctx = canvas.getContext('2d');
      if (ctx == null) {
        console.error('no hud 2d context available');
        return;
      }
      ctx.clearRect(0, 0, canvas.width, canvas.height);

      // draw selection

      let currentSelection: Selection;
      if (
        mode == Mode.Selecting ||
        mode == Mode.Dragging ||
        mode == Mode.ResizingStart ||
        mode == Mode.ResizingEnd
      ) {
        currentSelection = newSelection;
      } else {
        currentSelection = selection;
      }

      const elementWidth = canvas.getBoundingClientRect().width;
      const start =
        (currentSelection.start / elementWidth) * CanvasLogicalWidth;
      const end = (currentSelection.end / elementWidth) * CanvasLogicalWidth;

      ctx.beginPath();
      ctx.strokeStyle = 'red';
      ctx.lineWidth = 4;
      const alpha = hoverState == HoverState.OverSelection ? '0.15' : '0.13';
      ctx.fillStyle = `rgba(255, 255, 255, ${alpha})`;
      ctx.rect(start, 2, end - start, canvas.height - 10);
      ctx.fill();
      ctx.stroke();

      // draw position marker

      const markerX = canvas.width * (position.percent / 100);

      ctx.beginPath();
      ctx.moveTo(markerX, 0);
      ctx.lineWidth = 4;
      ctx.lineTo(markerX, canvas.height - 4);
      ctx.stroke();
    });
  });

  // handlers

  const isHoveringSelectionStart = (elementX: number): boolean => {
    return elementX > selection.start - 10 && elementX < selection.start + 10;
  };

  const isHoveringSelectionEnd = (elementX: number): boolean => {
    return elementX > selection.end - 10 && elementX < selection.end + 10;
  };

  const isHoveringSelection = (elementX: number): boolean => {
    return elementX >= selection.start && elementX <= selection.end;
  };

  const handleMouseDown = (evt: MouseEvent<HTMLCanvasElement>) => {
    if (mode != Mode.Normal) {
      return;
    }

    const elementX = Math.round(
      evt.clientX - evt.currentTarget.getBoundingClientRect().x
    );

    if (isHoveringSelectionStart(elementX)) {
      setMode(Mode.ResizingStart);
      moveOffsetX.current = elementX;
      return;
    } else if (isHoveringSelectionEnd(elementX)) {
      setMode(Mode.ResizingEnd);
      moveOffsetX.current = elementX;
      return;
    } else if (isHoveringSelection(elementX)) {
      setMode(Mode.Dragging);
      setCursor('pointer');
      moveOffsetX.current = elementX;
      return;
    }

    setMode(Mode.Selecting);
    setCursor('col-resize');
    moveOffsetX.current = elementX;
    setNewSelection({ start: elementX, end: elementX });
  };

  const handleMouseMove = (evt: MouseEvent<HTMLCanvasElement>) => {
    const x = Math.round(
      evt.clientX - evt.currentTarget.getBoundingClientRect().x
    );

    switch (mode) {
      case Mode.Normal: {
        if (isHoveringSelectionStart(x)) {
          setHoverState(HoverState.OverSelectionStart);
          setCursor('col-resize');
        } else if (isHoveringSelectionEnd(x)) {
          setHoverState(HoverState.OverSelectionEnd);
          setCursor('col-resize');
        } else if (isHoveringSelection(x)) {
          setHoverState(HoverState.OverSelection);
          setCursor('pointer');
        } else {
          setCursor('auto');
        }
        break;
      }
      case Mode.ResizingStart: {
        const diff = x - moveOffsetX.current;
        const start = selection.start + diff;

        if (start > selection.end) {
          setNewSelection({ start: selection.end, end: start });
          break;
        }

        setNewSelection({ ...newSelection, start: start });
        break;
      }
      case Mode.ResizingEnd: {
        const diff = x - moveOffsetX.current;
        const start = selection.end + diff;

        if (start < selection.start) {
          setNewSelection({ start: Math.max(0, start), end: selection.start });
          break;
        }

        setNewSelection({ ...newSelection, end: start });
        break;
      }
      case Mode.Dragging: {
        const diff = x - moveOffsetX.current;
        const width = selection.end - selection.start;
        let start = Math.max(0, selection.start + diff);
        let end = start + width;
        if (end > evt.currentTarget.getBoundingClientRect().width) {
          end = evt.currentTarget.getBoundingClientRect().width;
          start = end - width;
        }

        setNewSelection({ start: start, end: end });
        break;
      }
      case Mode.Selecting: {
        if (x < moveOffsetX.current) {
          setNewSelection({
            start: x,
            end: moveOffsetX.current,
          });
        } else {
          setNewSelection({ start: moveOffsetX.current, end: x });
        }
        break;
      }
    }
  };

  const handleMouseUp = () => {
    if (mode == Mode.Normal) {
      return;
    }

    setMode(Mode.Normal);
    setCursor('auto');

    if (newSelection.start == newSelection.end) {
      setSelection({ start: newSelection.start, end: newSelection.end + 5 });
      return;
    }

    if (newSelection.start == newSelection.end) {
      setSelection({ ...emptySelection });
      return;
    }
    setSelection({ ...newSelection });
  };

  const handleMouseLeave = (_evt: MouseEvent<HTMLCanvasElement>) => {
    setHoverState(HoverState.Normal);
  };

  // render component

  const containerStyles = {
    flexGrow: 0,
    position: 'relative',
    margin: `0 ${offsetPixels}px`,
    height: `${height}px`,
  } as React.CSSProperties;

  const hudCanvasStyles = {
    position: 'absolute',
    width: '100%',
    height: '100%',
    display: 'block',
    zIndex: 2,
    cursor: cursor,
  } as React.CSSProperties;

  return (
    <>
      <div style={containerStyles}>
        <WaveformCanvas
          peaks={peaks}
          channels={mediaSet.audioChannels}
          width={CanvasLogicalWidth}
          height={CanvasLogicalHeight}
          strokeStyle="black"
          fillStyle="#003300"
          zIndex={1}
          alpha={1}
        ></WaveformCanvas>
        <canvas
          ref={hudCanvasRef}
          width={CanvasLogicalWidth}
          height={CanvasLogicalHeight}
          style={hudCanvasStyles}
          onMouseDown={handleMouseDown}
          onMouseMove={handleMouseMove}
          onMouseLeave={handleMouseLeave}
        ></canvas>
      </div>
    </>
  );
};
