import { useState, useEffect, useRef, MouseEvent } from 'react';

interface Styles {
  borderLineWidth: number;
  borderStrokeStyle: string;
  positionLineWidth: number;
  positionStrokeStyle: string;
  hoverPositionStrokeStyle: string;
}

interface Props {
  width: number;
  height: number;
  emptySelectionAction: EmptySelectionAction;
  styles: Styles;
  position: number | null;
  selection: Selection;
  onSelectionChange: (selection: Selection, final: boolean) => void;
}

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

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

export enum EmptySelectionAction {
  SelectNothing,
  SelectPrevious,
}

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

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

export const HudCanvas: React.FC<Props> = ({
  width,
  height,
  emptySelectionAction,
  styles: {
    borderLineWidth,
    borderStrokeStyle,
    positionLineWidth,
    positionStrokeStyle,
    hoverPositionStrokeStyle,
  },
  position,
  selection,
  onSelectionChange,
}: Props) => {
  // selection and newSelection are in canvas logical pixels:
  const [newSelection, setNewSelection] = useState({
    ...emptySelection,
  });
  const [hoverPosition, setHoverPosition] = useState<number | null>(null);
  const [mode, setMode] = useState(Mode.Normal);
  const [hoverState, setHoverState] = useState(HoverState.Normal);
  const [cursor, setCursor] = useState('cursor-auto');

  const canvasRef = useRef<HTMLCanvasElement>(null);
  const moveOffsetX = useRef(0);

  // side effects

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

  useEffect(() => {
    onSelectionChange({ ...newSelection }, mode == Mode.Normal);
  }, [mode, newSelection]);

  // draw the overview HUD
  useEffect(() => {
    requestAnimationFrame(() => {
      const canvas = canvasRef.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;
      }

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

      // draw hover position
      if (
        hoverPosition != null &&
        (hoverPosition < currentSelection.start ||
          hoverPosition > currentSelection.end)
      ) {
        ctx.beginPath();
        ctx.strokeStyle = hoverPositionStrokeStyle;
        ctx.lineWidth = 2;
        ctx.moveTo(hoverPosition, 0);
        ctx.lineTo(hoverPosition, canvas.height);
        ctx.stroke();
      }

      // draw position marker

      if (position == null) {
        return;
      }

      ctx.beginPath();
      ctx.strokeStyle = positionStrokeStyle;
      ctx.lineWidth = positionLineWidth;
      ctx.moveTo(position, 0);
      ctx.lineWidth = position == 0 ? 8 : 4;
      ctx.lineTo(position, canvas.height);
      ctx.stroke();
    });
  }, [selection, newSelection, position, hoverPosition]);

  // handlers

  const hoverOffset = 10;

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

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

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

  const getCanvasX = (evt: MouseEvent<HTMLCanvasElement>): number => {
    const rect = evt.currentTarget.getBoundingClientRect();
    const x = Math.round(((evt.clientX - rect.left) / rect.width) * width);
    return constrainXToCanvas(x);
  };

  const constrainXToCanvas = (x: number): number => {
    if (x < 0) {
      return 0;
    }
    if (x > width) {
      return width;
    }
    return x;
  };

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

    const x = getCanvasX(evt);

    if (isHoveringSelectionStart(x)) {
      setMode(Mode.ResizingStart);
      moveOffsetX.current = x;
    } else if (isHoveringSelectionEnd(x)) {
      setMode(Mode.ResizingEnd);
      moveOffsetX.current = x;
    } else if (isHoveringSelection(x)) {
      setMode(Mode.Dragging);
      setCursor('cursor-move');
      moveOffsetX.current = x;
    } else {
      setMode(Mode.Selecting);
      setCursor('cursor-col-resize');
      moveOffsetX.current = x;
      setNewSelection({ start: x, end: x });
    }
  };

  const handleMouseMove = (evt: MouseEvent<HTMLCanvasElement>) => {
    const x = getCanvasX(evt);
    setHoverPosition(x);

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

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

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

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

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

        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('cursor-auto');

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

  const handleEmptySelectionAction = () => {
    switch (emptySelectionAction) {
      case EmptySelectionAction.SelectPrevious:
        setNewSelection({ ...selection });
        break;
      case EmptySelectionAction.SelectNothing:
        setMode(Mode.Normal);
        setNewSelection({ start: 0, end: 0 });
        break;
    }
  };

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

  return (
    <>
      <canvas
        ref={canvasRef}
        className={`block absolute w-full h-full ${cursor} z-20`}
        width={width}
        height={height}
        onMouseDown={handleMouseDown}
        onMouseMove={handleMouseMove}
        onMouseLeave={handleMouseLeave}
      ></canvas>
    </>
  );
};
