/**
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 *
 * @flow
 */

import type {NormalizedWheelDelta} from './utils/normalizeWheel';
import type {Point} from './geometry';

import {useEffect, useRef} from 'react';
import {normalizeWheel} from './utils/normalizeWheel';

export type ClickInteraction = {
  type: 'click',
  payload: {
    event: MouseEvent,
    location: Point,
  },
};
export type DoubleClickInteraction = {
  type: 'double-click',
  payload: {
    event: MouseEvent,
    location: Point,
  },
};
export type MouseDownInteraction = {
  type: 'mousedown',
  payload: {
    event: MouseEvent,
    location: Point,
  },
};
export type MouseMoveInteraction = {
  type: 'mousemove',
  payload: {
    event: MouseEvent,
    location: Point,
  },
};
export type MouseUpInteraction = {
  type: 'mouseup',
  payload: {
    event: MouseEvent,
    location: Point,
  },
};
export type WheelPlainInteraction = {
  type: 'wheel-plain',
  payload: {
    event: WheelEvent,
    location: Point,
    delta: NormalizedWheelDelta,
  },
};
export type WheelWithShiftInteraction = {
  type: 'wheel-shift',
  payload: {
    event: WheelEvent,
    location: Point,
    delta: NormalizedWheelDelta,
  },
};
export type WheelWithControlInteraction = {
  type: 'wheel-control',
  payload: {
    event: WheelEvent,
    location: Point,
    delta: NormalizedWheelDelta,
  },
};
export type WheelWithMetaInteraction = {
  type: 'wheel-meta',
  payload: {
    event: WheelEvent,
    location: Point,
    delta: NormalizedWheelDelta,
  },
};

export type Interaction =
  | ClickInteraction
  | DoubleClickInteraction
  | MouseDownInteraction
  | MouseMoveInteraction
  | MouseUpInteraction
  | WheelPlainInteraction
  | WheelWithShiftInteraction
  | WheelWithControlInteraction
  | WheelWithMetaInteraction;

let canvasBoundingRectCache = null;
function cacheFirstGetCanvasBoundingRect(
  canvas: HTMLCanvasElement,
): ClientRect {
  if (
    canvasBoundingRectCache &&
    canvas.width === canvasBoundingRectCache.width &&
    canvas.height === canvasBoundingRectCache.height
  ) {
    return canvasBoundingRectCache.rect;
  }
  canvasBoundingRectCache = {
    width: canvas.width,
    height: canvas.height,
    rect: canvas.getBoundingClientRect(),
  };
  return canvasBoundingRectCache.rect;
}

export function useCanvasInteraction(
  canvasRef: {current: HTMLCanvasElement | null},
  interactor: (interaction: Interaction) => void,
) {
  const isMouseDownRef = useRef<boolean>(false);
  const didMouseMoveWhileDownRef = useRef<boolean>(false);

  useEffect(() => {
    const canvas = canvasRef.current;
    if (!canvas) {
      return;
    }

    function localToCanvasCoordinates(localCoordinates: Point): Point {
      // $FlowFixMe[incompatible-call] found when upgrading Flow
      const canvasRect = cacheFirstGetCanvasBoundingRect(canvas);
      return {
        x: localCoordinates.x - canvasRect.left,
        y: localCoordinates.y - canvasRect.top,
      };
    }

    const onCanvasClick: MouseEventHandler = event => {
      if (didMouseMoveWhileDownRef.current) {
        return;
      }

      interactor({
        type: 'click',
        payload: {
          event,
          location: localToCanvasCoordinates({x: event.x, y: event.y}),
        },
      });
    };

    const onCanvasDoubleClick: MouseEventHandler = event => {
      if (didMouseMoveWhileDownRef.current) {
        return;
      }

      interactor({
        type: 'double-click',
        payload: {
          event,
          location: localToCanvasCoordinates({x: event.x, y: event.y}),
        },
      });
    };

    const onCanvasMouseDown: MouseEventHandler = event => {
      didMouseMoveWhileDownRef.current = false;
      isMouseDownRef.current = true;

      interactor({
        type: 'mousedown',
        payload: {
          event,
          location: localToCanvasCoordinates({x: event.x, y: event.y}),
        },
      });
    };

    const onDocumentMouseMove: MouseEventHandler = event => {
      if (isMouseDownRef.current) {
        didMouseMoveWhileDownRef.current = true;
      }

      interactor({
        type: 'mousemove',
        payload: {
          event,
          location: localToCanvasCoordinates({x: event.x, y: event.y}),
        },
      });
    };

    const onDocumentMouseUp: MouseEventHandler = event => {
      isMouseDownRef.current = false;

      interactor({
        type: 'mouseup',
        payload: {
          event,
          location: localToCanvasCoordinates({x: event.x, y: event.y}),
        },
      });
    };

    const onCanvasWheel: WheelEventHandler = event => {
      event.preventDefault();
      event.stopPropagation();

      const location = localToCanvasCoordinates({x: event.x, y: event.y});
      const delta = normalizeWheel(event);

      if (event.shiftKey) {
        interactor({
          type: 'wheel-shift',
          payload: {event, location, delta},
        });
      } else if (event.ctrlKey) {
        interactor({
          type: 'wheel-control',
          payload: {event, location, delta},
        });
      } else if (event.metaKey) {
        interactor({
          type: 'wheel-meta',
          payload: {event, location, delta},
        });
      } else {
        interactor({
          type: 'wheel-plain',
          payload: {event, location, delta},
        });
      }

      return false;
    };

    const ownerDocument = canvas.ownerDocument;
    ownerDocument.addEventListener('mousemove', onDocumentMouseMove);
    ownerDocument.addEventListener('mouseup', onDocumentMouseUp);

    canvas.addEventListener('click', onCanvasClick);
    canvas.addEventListener('dblclick', onCanvasDoubleClick);
    canvas.addEventListener('mousedown', onCanvasMouseDown);
    canvas.addEventListener('wheel', onCanvasWheel);

    return () => {
      ownerDocument.removeEventListener('mousemove', onDocumentMouseMove);
      ownerDocument.removeEventListener('mouseup', onDocumentMouseUp);

      canvas.removeEventListener('click', onCanvasClick);
      canvas.removeEventListener('dblclick', onCanvasDoubleClick);
      canvas.removeEventListener('mousedown', onCanvasMouseDown);
      canvas.removeEventListener('wheel', onCanvasWheel);
    };
  }, [canvasRef, interactor]);
}
