import { useContext, useRef } from 'react';
import * as d3 from 'd3';
import { observer } from 'mobx-react';
import { action, autorun, IReactionDisposer } from 'mobx';
import ElementContext from '../utils/ElementContext';
import useCallbackRef from '../utils/useCallbackRef';
import Point from '../geom/Point';
import { Graph, GRAPH_AREA_DRAGGING_EVENT, isGraph, ModelKind } from '../types';
import { ATTR_DATA_KIND } from '../const';

export type PanZoomRef = (node: SVGGElement | null) => void;

// Used to send events prevented by d3.zoom to the document allowing modals, dropdowns, etc, to close
const propagatePanZoomMouseEvent = (e: Event): void => {
  document.dispatchEvent(new MouseEvent(e.type, e));
};

export const usePanZoom = (): PanZoomRef => {
  const element = useContext(ElementContext);
  if (!isGraph(element)) {
    throw new Error('usePanZoom must be used within the scope of a Graph');
  }
  const elementRef = useRef<Graph>(element);
  elementRef.current = element;

  return useCallbackRef<PanZoomRef>((node: SVGGElement | null) => {
    let disposeListener: IReactionDisposer | undefined;
    if (node) {
      // TODO fix any type
      const $svg = d3.select(node.ownerSVGElement) as any;
      if (node && node.ownerSVGElement) {
        node.ownerSVGElement.addEventListener('mousedown', propagatePanZoomMouseEvent);
        node.ownerSVGElement.addEventListener('click', propagatePanZoomMouseEvent);
      }
      const zoom = d3
        .zoom()
        .scaleExtent(elementRef.current.getScaleExtent())
        .on(
          'zoom',
          action((event: d3.D3ZoomEvent<any, any>) => {
            if (event.sourceEvent?.type === 'mousemove') {
              elementRef.current
                .getController()
                .fireEvent(GRAPH_AREA_DRAGGING_EVENT, { graph: elementRef.current, isDragging: true });
            }
            elementRef.current.setPosition(new Point(event.transform.x, event.transform.y));
            elementRef.current.setScale(event.transform.k);
          })
        )
        .on(
          'end',
          action(() => {
            elementRef.current
              .getController()
              .fireEvent(GRAPH_AREA_DRAGGING_EVENT, { graph: elementRef.current, isDragging: false });
          })
        )
        .filter((event: React.MouseEvent) => {
          if (event.ctrlKey || event.shiftKey || event.altKey || event.button) {
            return false;
          }
          // only allow zoom from double clicking the graph directly
          if (event.type === 'dblclick') {
            // check if target is not within a node or edge
            const svg = node.ownerSVGElement;
            let p = event.target;
            while (p && p !== svg) {
              if (p instanceof Element) {
                const kind = p.getAttribute(ATTR_DATA_KIND);
                if (kind) {
                  if (kind !== ModelKind.graph) {
                    return false;
                  }
                  break;
                }
              }
              p = p instanceof Node ? p.parentNode : undefined;
            }
          }
          return true;
        });
      zoom($svg);

      // Update the d3 transform whenever the scale or bounds change.
      // This is kinda hacky because when d3 has already made the most recent transform update,
      // we listen for the model change, due to the above, only to update the d3 transform again.
      disposeListener = autorun(() => {
        const scale = elementRef.current.getScale();
        const scaleExtent = elementRef.current.getScaleExtent();

        // update the min scaling value such that the user can zoom out to the new scale in case
        // it is smaller than the default zoom out scale
        zoom.scaleExtent([Math.min(scale, scaleExtent[0]), scaleExtent[1]]);
        const b = elementRef.current.getBounds();

        // update d3 zoom data directly
        // eslint-disable-next-line no-underscore-dangle
        Object.assign($svg.node().__zoom, {
          k: scale,
          x: b.x,
          y: b.y
        });
      });

      // disable double click zoom
      // $svg.on('dblclick.zoom', null);
    }
    return () => {
      disposeListener && disposeListener();
      if (node) {
        // remove all zoom listeners
        d3.select(node.ownerSVGElement).on('.zoom', null);
        if (node.ownerSVGElement) {
          node.ownerSVGElement.removeEventListener('mousedown', propagatePanZoomMouseEvent);
          node.ownerSVGElement.removeEventListener('click', propagatePanZoomMouseEvent);
        }
      }
    };
  });
};
export interface WithPanZoomProps {
  panZoomRef?: PanZoomRef;
}

export const withPanZoom =
  () =>
  <P extends WithPanZoomProps>(WrappedComponent: React.ComponentType<P>) => {
    const Component: React.FunctionComponent<Omit<P, keyof WithPanZoomProps>> = (props) => {
      const panZoomRef = usePanZoom();
      return <WrappedComponent {...(props as any)} panZoomRef={panZoomRef} />;
    };
    Component.displayName = `withPanZoom(${WrappedComponent.displayName || WrappedComponent.name})`;
    return observer(Component);
  };
