import { useRef, useEffect, useCallback, useMemo } from 'react';
import ReactDOM from 'react-dom';
import PropTypes from 'prop-types';
import { useDebouncedCallback } from 'use-debounce';
import flatten from 'lodash/flatten';
import sortBy from 'lodash/sortBy';
import xor from 'lodash/xor';
import OpenSeadragonCanvasOverlay from '../lib/OpenSeadragonCanvasOverlay';
import CanvasWorld from '../lib/CanvasWorld';
import CanvasAnnotationDisplay from '../lib/CanvasAnnotationDisplay';

/** @private */
function isAnnotationAtPoint(canvasWorld, osdCanvasOverlay, resource, canvas, point) {
  const [canvasX, canvasY] = canvasWorld.canvasToWorldCoordinates(canvas.id);
  const relativeX = point.x - canvasX;
  const relativeY = point.y - canvasY;

  if (resource.svgSelector) {
    const context = osdCanvasOverlay.context2d;
    const { svgPaths } = new CanvasAnnotationDisplay({ resource });
    return [...svgPaths].some((path) => context.isPointInPath(new Path2D(path.attributes.d.nodeValue), relativeX, relativeY));
  }

  if (resource.fragmentSelector) {
    const [x, y, w, h] = resource.fragmentSelector;
    return x <= relativeX && relativeX <= x + w && y <= relativeY && relativeY <= y + h;
  }
  return false;
}

/**
 * Represents a OpenSeadragonViewer in the mirador workspace. Responsible for mounting
 * and rendering OSD.
 */
export function AnnotationsOverlay({
  annotations = [],
  canvasWorld,
  deselectAnnotation = () => {},
  drawAnnotations = true,
  drawSearchAnnotations = true,
  highlightAllAnnotations = false,
  hoverAnnotation = () => {},
  hoveredAnnotationIds = [],
  palette = {},
  searchAnnotations = [],
  selectAnnotation = () => {},
  selectedAnnotationId = null,
  viewer = null,
  windowId,
}) {
  const ref = useRef();

  const osdCanvasOverlay = useMemo(() => new OpenSeadragonCanvasOverlay(viewer, ref), [ref, viewer]);

  const toggleAnnotation = useCallback(
    (id) => {
      if (selectedAnnotationId === id) {
        deselectAnnotation(windowId, id);
      } else {
        selectAnnotation(windowId, id);
      }
    },
    [selectedAnnotationId, deselectAnnotation, selectAnnotation, windowId],
  );

  /**
   * annotationsToContext - converts anontations to a canvas context
   */
  const annotationsToContext = useCallback(
    (renderedAnnotations, currentPalette) => {
      const context = osdCanvasOverlay.context2d;
      const zoomRatio = viewer.viewport.getZoom(true) / viewer.viewport.getMaxZoom();
      renderedAnnotations.forEach((annotation) => {
        annotation.resources.forEach((resource) => {
          if (!canvasWorld.canvasIds.includes(resource.targetId)) return;
          const offset = canvasWorld.offsetByCanvas(resource.targetId);
          const canvasAnnotationDisplay = new CanvasAnnotationDisplay({
            hovered: hoveredAnnotationIds.includes(resource.id),
            offset,
            palette: {
              ...currentPalette,
              default: {
                ...currentPalette.default,
                ...(!highlightAllAnnotations && currentPalette.hidden),
              },
            },
            resource,
            selected: selectedAnnotationId === resource.id,
            zoomRatio,
          });
          canvasAnnotationDisplay.toContext(context);
        });
      });
    },
    [osdCanvasOverlay, viewer, canvasWorld, highlightAllAnnotations, hoveredAnnotationIds, selectedAnnotationId],
  );

  const renderAnnotations = useCallback(() => {
    if (drawSearchAnnotations) {
      annotationsToContext(searchAnnotations, palette.search);
    }

    if (drawAnnotations) {
      annotationsToContext(annotations, palette.annotations);
    }
  }, [annotations, annotationsToContext, drawAnnotations, drawSearchAnnotations, palette, searchAnnotations]);

  const updateCanvas = useCallback(() => {
    if (!osdCanvasOverlay) return;

    osdCanvasOverlay.clear();
    osdCanvasOverlay.resize();
    osdCanvasOverlay.canvasUpdate(renderAnnotations);
  }, [osdCanvasOverlay, renderAnnotations]);

  const annotationsAtPoint = useCallback(
    (canvas, point) => {
      const lists = [...annotations, ...searchAnnotations];
      const annos = flatten(lists.map((l) => l.resources)).filter((resource) => {
        if (canvas.id !== resource.targetId) return false;

        return isAnnotationAtPoint(canvasWorld, osdCanvasOverlay, resource, canvas, point);
      });

      return annos;
    },
    [annotations, canvasWorld, osdCanvasOverlay, searchAnnotations],
  );

  const onCanvasClick = useCallback(
    (event) => {
      const {
        position: webPosition,
        eventSource: { viewport },
      } = event;
      const point = viewport.pointFromPixel(webPosition);

      const canvas = canvasWorld.canvasAtPoint(point);
      if (!canvas) return;
      const [_canvasX, _canvasY, canvasWidth, canvasHeight] = canvasWorld.canvasToWorldCoordinates(canvas.id);

      // get all the annotations that contain the click
      const annos = annotationsAtPoint(canvas, point);

      if (annos.length > 0) {
        event.preventDefaultAction = true; // eslint-disable-line no-param-reassign
      }

      if (annos.length === 1) {
        toggleAnnotation(annos[0].id);
      } else if (annos.length > 0) {
        /**
         * Try to find the "right" annotation to select after a click.
         *
         * This is perhaps a naive method, but seems to deal with rectangles and SVG shapes:
         *
         * - figure out how many points around a circle are inside the annotation shape
         * - if there's a shape with the fewest interior points, it's probably the one
         *       with the closest boundary?
         * - if there's a tie, make the circle bigger and try again.
         */
        const annosWithClickScore = (radius) => {
          const degreesToRadians = Math.PI / 180;

          return (anno) => {
            let score = 0;
            for (let degrees = 0; degrees < 360; degrees += 1) {
              const x = Math.cos(degrees * degreesToRadians) * radius + point.x;
              const y = Math.sin(degrees * degreesToRadians) * radius + point.y;

              if (isAnnotationAtPoint(canvasWorld, osdCanvasOverlay, anno, canvas, { x, y })) score += 1;
            }

            return { anno, score };
          };
        };

        let radius = 1;
        let annosWithScore = sortBy(annos.map(annosWithClickScore(radius)), 'score');

        while (radius < Math.max(canvasWidth, canvasHeight) && annosWithScore[0].score === annosWithScore[1].score) {
          radius *= 2;
          annosWithScore = sortBy(annos.map(annosWithClickScore(radius)), 'score');
        }

        toggleAnnotation(annosWithScore[0].anno.id);
      }
    },
    [annotationsAtPoint, canvasWorld, osdCanvasOverlay, toggleAnnotation],
  );

  const onCanvasMouseMove = useDebouncedCallback(
    useCallback(
      (event) => {
        if (annotations.length === 0 && searchAnnotations.length === 0) return;

        const { position: webPosition } = event;
        const point = viewer.viewport.pointFromPixel(webPosition);

        const canvas = canvasWorld.canvasAtPoint(point);
        if (!canvas) {
          hoverAnnotation(windowId, []);
          return;
        }

        const annos = annotationsAtPoint(canvas, point);

        if (
          xor(
            hoveredAnnotationIds,
            annos.map((a) => a.id),
          ).length > 0
        ) {
          hoverAnnotation(
            windowId,
            annos.map((a) => a.id),
          );
        }
      },
      [annotations, annotationsAtPoint, canvasWorld, hoverAnnotation, hoveredAnnotationIds, searchAnnotations, viewer, windowId],
    ),
    10,
  );

  const onCanvasExit = useCallback(() => {
    // a move event may be queued up by the debouncer
    onCanvasMouseMove.cancel();
    hoverAnnotation(windowId, []);
  }, [hoverAnnotation, onCanvasMouseMove, windowId]);

  const onUpdateViewport = useCallback(() => {
    updateCanvas();
  }, [updateCanvas]);

  useEffect(() => {
    if (!viewer) return undefined;

    viewer.addHandler('canvas-click', onCanvasClick);
    viewer.addHandler('canvas-exit', onCanvasExit);
    viewer.addHandler('mouse-move', onCanvasMouseMove);
    viewer.addHandler('update-viewport', onUpdateViewport);

    return () => {
      viewer.removeHandler('canvas-click', onCanvasClick);
      viewer.removeHandler('canvas-exit', onCanvasExit);
      viewer.removeHandler('mouse-move', onCanvasMouseMove);
      viewer.removeHandler('update-viewport', onUpdateViewport);
    };
  }, [onCanvasClick, onCanvasExit, onCanvasMouseMove, onUpdateViewport, viewer]);

  useEffect(() => {
    if (viewer) viewer.forceRedraw();
  }, [
    annotations,
    drawAnnotations,
    drawSearchAnnotations,
    highlightAllAnnotations,
    hoveredAnnotationIds,
    searchAnnotations,
    selectedAnnotationId,
    viewer,
  ]);

  useEffect(() => {
    if (!ref.current) return;

    if (hoveredAnnotationIds.length > 0) {
      ref.current.style.cursor = 'pointer';
    } else {
      ref.current.style.cursor = '';
    }
  }, [hoveredAnnotationIds, ref]);

  if (!viewer) return null;

  return ReactDOM.createPortal(
    <div
      ref={ref}
      style={{
        height: '100%',
        left: 0,
        position: 'absolute',
        top: 0,
        width: '100%',
      }}
    >
      <canvas />
    </div>,
    viewer.canvas,
  );
}

AnnotationsOverlay.propTypes = {
  annotations: PropTypes.arrayOf(PropTypes.object),
  canvasWorld: PropTypes.instanceOf(CanvasWorld).isRequired,
  deselectAnnotation: PropTypes.func,
  drawAnnotations: PropTypes.bool,
  drawSearchAnnotations: PropTypes.bool,
  highlightAllAnnotations: PropTypes.bool,
  hoverAnnotation: PropTypes.func,
  hoveredAnnotationIds: PropTypes.arrayOf(PropTypes.string),
  palette: PropTypes.object,
  searchAnnotations: PropTypes.arrayOf(PropTypes.object),
  selectAnnotation: PropTypes.func,
  selectedAnnotationId: PropTypes.string,
  viewer: PropTypes.object,
  windowId: PropTypes.string.isRequired,
};
