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

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

enum Mode {
  Normal,
  Selecting,
  Dragging,
}

const CanvasLogicalWidth = 2_000;
const CanvasLogicalHeight = 500;

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

// TODO: render position marker during playback
export const Overview: React.FC<Props> = ({
  mediaSet,
  height,
  offsetPixels,
  position,
  selection,
  onSelectionStart,
  onSelectionChange,
}: Props) => {
  const hudCanvasRef = useRef<HTMLCanvasElement>(null);
  const [peaks, setPeaks] = useState<Observable<number[]>>(from([]));
  const [mode, setMode] = useState(Mode.Normal);
  const [newSelection, setNewSelection] = useState({ ...emptySelection });
  const [dragStart, setDragStart] = useState(0);

  // effects

  // handle global mouse up.
  // Currently this adds and removes the global event listener every time the
  // component is rerendered (which is often when dragging or redrawing). It
  // works but probably better to optimize this for performance reasons.
  useEffect(() => {
    window.addEventListener('mouseup', handleMouseUp);
    return () => {
      window.removeEventListener('mouseup', handleMouseUp);
    };
  }, [mode, newSelection]);

  // 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);
    })();
  }, [mediaSet]);

  // draw the overview HUD
  useEffect(() => {
    (async function () {
      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: Frames;
      if (mode == Mode.Selecting || mode == Mode.Dragging) {
        currentSelection = newSelection;
      } else {
        currentSelection = selection;
      }

      if (currentSelection.start < currentSelection.end) {
        const x1 =
          (currentSelection.start / mediaSet.audioFrames) * CanvasLogicalWidth;
        const x2 =
          (currentSelection.end / mediaSet.audioFrames) * CanvasLogicalWidth;

        ctx.beginPath();
        ctx.strokeStyle = 'red';
        ctx.lineWidth = 4;
        ctx.fillStyle = 'rgba(255, 255, 255, 0.15)';
        ctx.rect(x1, 2, x2 - x1, canvas.height - 10);
        ctx.fill();
        ctx.stroke();
      }

      // draw position marker:
      const fullSelection = { start: 0, end: mediaSet.audioFrames }; // constantize?
      const x = secsToCanvasX(
        position,
        mediaSet.audioSampleRate,
        fullSelection
      );
      // should never happen:
      if (x == null) {
        return;
      }

      ctx.strokeStyle = 'red';
      ctx.beginPath();
      ctx.moveTo(x, 0);
      ctx.lineWidth = 4;
      ctx.lineTo(x, canvas.height - 4);
      ctx.stroke();
    })();
  });

  // publish event on new selection start
  useEffect(() => {
    onSelectionStart(newSelection.start);
  }, [newSelection]);

  // handlers

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

    const frame = Math.floor(
      mediaSet.audioFrames *
        (mouseEventToCanvasX(evt) / evt.currentTarget.width)
    );

    if (frame >= selection.start && frame < selection.end) {
      setMode(Mode.Dragging);
      setDragStart(frame);
      return;
    }

    setMode(Mode.Selecting);
    setNewSelection({ start: frame, end: frame });
  };

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

    const frame = Math.floor(
      mediaSet.audioFrames *
        (mouseEventToCanvasX(evt) / evt.currentTarget.width)
    );

    if (mode == Mode.Dragging) {
      const diff = frame - dragStart;
      const frameCount = selection.end - selection.start;
      let start = Math.max(0, selection.start + diff);
      let end = start + frameCount;
      if (end > mediaSet.audioFrames) {
        end = mediaSet.audioFrames;
        start = end - frameCount;
      }
      setNewSelection({
        start: start,
        end: end,
      });
      return;
    }

    if (frame == newSelection.end) {
      return;
    }

    setNewSelection({ ...newSelection, end: frame });
  };

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

    setMode(Mode.Normal);
    onSelectionChange({ ...newSelection });
  };

  // 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,
  } 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}
        ></canvas>
      </div>
    </>
  );
};
