import Openseadragon from 'openseadragon';
import PropTypes from 'prop-types';
import { useEffect, useId, useRef, useReducer, useState, useCallback } from 'react';
import { useDebouncedCallback } from 'use-debounce';
import { useTranslation } from 'react-i18next';
import OpenSeadragonViewerContext from '../contexts/OpenSeadragonViewerContext';

/** Handle setting up OSD for use in mirador + react */
function OpenSeadragonComponent({
  children = undefined,
  Container = 'div',
  osdConfig = {},
  viewerConfig = {},
  onUpdateViewport = () => {},
  setViewer = () => {},
  style = {},
  ...passThruProps
}) {
  const id = useId();
  const ref = useRef();
  const [grabbing, setGrabbing] = useState(false);
  const viewerRef = useRef(undefined);
  const initialViewportSet = useRef(false);
  const lastAppliedBounds = useRef(null);
  const isResettingViewport = useRef(false);
  const [, forceUpdate] = useReducer((x) => x + 1, 0);

  const moveHandler = useDebouncedCallback(
    useCallback(
      (event) => {
        /** Shim to provide a mouse-move event coming from the viewer */
        viewerRef.current?.raiseEvent('mouse-move', event);
      },
      [viewerRef],
    ),
    10,
  );

  const onViewportChange = useCallback(
    (event) => {
      const { viewport } = event.eventSource;

      if (!initialViewportSet.current) return;

      // Don't save viewport changes during automatic recentering
      if (isResettingViewport.current) return;

      onUpdateViewport({
        bounds: viewport.getBounds(),
        flip: viewport.getFlip(),
        rotation: viewport.getRotation(),
        x: Math.round(viewport.centerSpringX.target.value),
        y: Math.round(viewport.centerSpringY.target.value),
        zoom: viewport.zoomSpring.target.value,
      });
    },
    [onUpdateViewport, initialViewportSet],
  );

  const setInitialBounds = useCallback(
    ({ viewport }) => {
      if (initialViewportSet.current) return;
      initialViewportSet.current = true;

      if (viewerConfig.x != null && viewerConfig.y != null) {
        viewport.panTo(new Openseadragon.Point(viewerConfig.x, viewerConfig.y), true);
      }

      if (viewerConfig.zoom != null) {
        viewport.zoomTo(viewerConfig.zoom, new Openseadragon.Point(viewerConfig.x, viewerConfig.y), true);
      }

      if (viewerConfig.rotation != null && viewerConfig.rotation !== viewport.getRotation()) {
        viewport.setRotation(viewerConfig.rotation);
      }

      if (viewerConfig.flip != null && (viewerConfig.flip || false) !== viewport.getFlip()) {
        viewport.setFlip(viewerConfig.flip);
      }

      if (!viewerConfig.x && !viewerConfig.y && !viewerConfig.zoom) {
        if (viewerConfig.bounds) {
          viewport.fitBounds(new Openseadragon.Rect(...viewerConfig.bounds), true);
          lastAppliedBounds.current = viewerConfig.bounds;
        } else {
          viewport.goHome(true);
        }
      }
    },
    [initialViewportSet, viewerConfig],
  );

  useEffect(() => {
    const viewer = viewerRef.current;
    if (!viewer) return;

    const { viewport } = viewer;

    if (!initialViewportSet.current) {
      setInitialBounds(viewer);
      return;
    }

    // Check if bounds changed - always recenter when bounds change)
    if (viewerConfig.bounds) {
      const boundsChanged =
        !lastAppliedBounds.current ||
        viewerConfig.bounds.length !== lastAppliedBounds.current.length ||
        viewerConfig.bounds.some((val, idx) => val !== lastAppliedBounds.current[idx]);

      // Bounds changed - recenter regardless of whether x/y/zoom exist
      if (boundsChanged) {
        isResettingViewport.current = true;
        lastAppliedBounds.current = viewerConfig.bounds;

        // Wait for the tiles to be fully loaded before recentering
        const handleTilesLoaded = () => {
          const rect = new Openseadragon.Rect(...viewerConfig.bounds);
          viewport.fitBoundsWithConstraints(rect, true);
          isResettingViewport.current = false;
        };

        viewer.addOnceHandler('tile-loaded', handleTilesLoaded);
        return;
      }
    }

    // Apply preserved viewport only if bounds haven't changed
    // Don't apply x/y/zoom if we don't have them (rely on bounds instead)
    if (!viewerConfig.x || !viewerConfig.y || !viewerConfig.zoom) {
      return;
    }

    // @ts-expect-error
    if (
      viewerConfig.x != null &&
      viewerConfig.y != null &&
      (Math.round(viewerConfig.x) !== Math.round(viewport.centerSpringX.target.value) ||
        // @ts-expect-error
        Math.round(viewerConfig.y) !== Math.round(viewport.centerSpringY.target.value))
    ) {
      viewport.panTo(new Openseadragon.Point(viewerConfig.x, viewerConfig.y), false);
    }

    // @ts-expect-error
    if (viewerConfig.zoom != null && viewerConfig.zoom !== viewport.zoomSpring.target.value) {
      viewport.zoomTo(viewerConfig.zoom, new Openseadragon.Point(viewerConfig.x, viewerConfig.y), false);
    }

    if (viewerConfig.rotation != null && viewerConfig.rotation !== viewport.getRotation()) {
      viewport.setRotation(viewerConfig.rotation);
    }

    if (viewerConfig.flip != null && (viewerConfig.flip || false) !== viewport.getFlip()) {
      viewport.setFlip(viewerConfig.flip);
    }
  }, [initialViewportSet, setInitialBounds, viewerConfig, viewerRef]);

  // initialize OSD stuff when this component is mounted
  useEffect(() => {
    const viewer = Openseadragon({
      element: ref.current,
      ...osdConfig,
    });

    viewer.addHandler('canvas-drag', () => {
      setGrabbing(true);
    });

    viewer.addHandler('canvas-drag-end', () => {
      setGrabbing(false);
    });

    viewer.addHandler('canvas-double-click', ({ position, shift }) => {
      if (!osdConfig.zoomPerDoubleClick) return;

      const currentZoom = viewer.viewport.getZoom();
      const zoomRatio = shift ? 1.0 / osdConfig.zoomPerDoubleClick : osdConfig.zoomPerDoubleClick;
      viewer.viewport.zoomTo(currentZoom * zoomRatio, viewer.viewport.pointFromPixel(position), false);
    });

    viewer.addHandler('animation-finish', onViewportChange);
    // @ts-expect-error
    viewer.innerTracker.moveHandler = moveHandler;

    viewerRef.current = viewer;
    setViewer(viewer);

    viewer.addOnceHandler('tile-loaded', () => {
      initialViewportSet.current = false;
      setInitialBounds(viewer);
    });

    forceUpdate();
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [ref]);

  // cleanup OSD viewer cruft when this component is unmounted
  useEffect(
    () => () => {
      const viewer = viewerRef.current;

      if (!viewer) return;

      // @ts-expect-error
      if (viewer.innerTracker?.moveHandler === moveHandler) {
        // @ts-expect-error
        viewer.innerTracker.moveHandler = () => {};
      }
      // @ts-expect-error
      viewer.removeAllHandlers();

      viewer.destroy();
      viewerRef.current = undefined;
      setViewer(null);
    },
    // eslint-disable-next-line react-hooks/exhaustive-deps
    [],
  );

  const { t } = useTranslation();

  useEffect(() => {
    const canvas = viewerRef?.current?.canvas?.firstElementChild;
    if (canvas) {
      canvas.setAttribute('role', 'img');
      canvas.setAttribute('aria-label', t('digitizedView'));
      canvas.setAttribute('aria-describedby', id);
    }
  }, [viewerRef?.current?.canvas?.firstElementChild, id, t]);

  return (
    <OpenSeadragonViewerContext.Provider value={viewerRef}>
      <Container id={id} ref={ref} style={{ ...style, cursor: grabbing ? 'grabbing' : 'grab' }} {...passThruProps}>
        {children}
      </Container>
    </OpenSeadragonViewerContext.Provider>
  );
}

OpenSeadragonComponent.propTypes = {
  children: PropTypes.node,
  Container: PropTypes.elementType,
  onUpdateViewport: PropTypes.func,
  osdConfig: PropTypes.object,
  setViewer: PropTypes.func,
  style: PropTypes.object,
  viewerConfig: PropTypes.shape({
    bounds: PropTypes.arrayOf(PropTypes.number),
    flip: PropTypes.bool,
    rotation: PropTypes.number,
    x: PropTypes.number,
    y: PropTypes.number,
    zoom: PropTypes.number,
  }),
};

export default OpenSeadragonComponent;
