import React, { useEffect, useRef, memo, CSSProperties } from 'react';
import classnames from 'classnames';
import { ResizeObserver } from 'resize-observer';

import { useStoreState, useStoreActions } from '../../store/hooks';
import NodeRenderer from '../NodeRenderer';
import EdgeRenderer from '../EdgeRenderer';
import UserSelection from '../../components/UserSelection';
import NodesSelection from '../../components/NodesSelection';
import useKeyPress from '../../hooks/useKeyPress';
import useD3Zoom from '../../hooks/useD3Zoom';
import useGlobalKeyHandler from '../../hooks/useGlobalKeyHandler';
import useElementUpdater from '../../hooks/useElementUpdater';
import { getDimensions } from '../../utils';
import { fitView, zoomIn, zoomOut, project, getElements } from '../../utils/graph';
import {
  Elements,
  NodeTypesType,
  EdgeTypesType,
  OnLoadFunc,
  Node,
  Edge,
  Connection,
  ConnectionLineType,
} from '../../types';

export interface GraphViewProps {
  elements: Elements;
  onElementClick?: (element: Node | Edge) => void;
  onElementsRemove?: (elements: Elements) => void;
  onNodeDragStart?: (node: Node) => void;
  onNodeDragStop?: (node: Node) => void;
  onConnect?: (connection: Connection | Edge) => void;
  onLoad?: OnLoadFunc;
  onMove?: () => void;
  selectionKeyCode: number;
  nodeTypes: NodeTypesType;
  edgeTypes: EdgeTypesType;
  connectionLineType: ConnectionLineType;
  connectionLineStyle?: CSSProperties;
  deleteKeyCode: number;
  snapToGrid: boolean;
  snapGrid: [number, number];
  onlyRenderVisibleNodes: boolean;
  isInteractive: boolean;
  selectNodesOnDrag: boolean;
  minZoom: number;
  maxZoom: number;
  defaultZoom: number;
}

const GraphView = memo(
  ({
    nodeTypes,
    edgeTypes,
    onMove,
    onLoad,
    onElementClick,
    onNodeDragStart,
    onNodeDragStop,
    connectionLineType,
    connectionLineStyle,
    selectionKeyCode,
    onElementsRemove,
    deleteKeyCode,
    elements,
    onConnect,
    snapToGrid,
    snapGrid,
    onlyRenderVisibleNodes,
    isInteractive,
    selectNodesOnDrag,
    minZoom,
    maxZoom,
    defaultZoom,
  }: GraphViewProps) => {
    const zoomPane = useRef<HTMLDivElement>(null);
    const rendererNode = useRef<HTMLDivElement>(null);
    const width = useStoreState((s) => s.width);
    const height = useStoreState((s) => s.height);
    const d3Initialised = useStoreState((s) => s.d3Initialised);
    const nodesSelectionActive = useStoreState((s) => s.nodesSelectionActive);

    const updateSize = useStoreActions((actions) => actions.updateSize);
    const setNodesSelection = useStoreActions((actions) => actions.setNodesSelection);
    const setOnConnect = useStoreActions((a) => a.setOnConnect);
    const setSnapGrid = useStoreActions((actions) => actions.setSnapGrid);
    const setInteractive = useStoreActions((actions) => actions.setInteractive);
    const updateTransform = useStoreActions((actions) => actions.updateTransform);
    const setMinMaxZoom = useStoreActions((actions) => actions.setMinMaxZoom);

    const selectionKeyPressed = useKeyPress(selectionKeyCode);
    const rendererClasses = classnames('react-flow__renderer', { 'is-interactive': isInteractive });

    const onZoomPaneClick = () => setNodesSelection({ isActive: false });

    const updateDimensions = () => {
      if (!rendererNode.current) {
        return;
      }

      const size = getDimensions(rendererNode.current);

      if (size.height === 0 || size.width === 0) {
        throw new Error('The React Flow parent container needs a width and a height to render the graph.');
      }

      updateSize(size);
    };

    useEffect(() => {
      let resizeObserver: ResizeObserver;

      updateDimensions();
      window.onresize = updateDimensions;

      if (onConnect) {
        setOnConnect(onConnect);
      }

      if (defaultZoom !== 1) {
        updateTransform({ x: 0, y: 0, k: defaultZoom });
      }

      if (rendererNode.current) {
        resizeObserver = new ResizeObserver((entries) => {
          for (let _ of entries) {
            updateDimensions();
          }
        });

        resizeObserver.observe(rendererNode.current);
      }

      return () => {
        window.onresize = null;

        if (resizeObserver && rendererNode.current) {
          resizeObserver.unobserve(rendererNode.current!);
        }
      };
    }, []);

    useD3Zoom({ zoomPane, onMove, selectionKeyPressed });

    useEffect(() => {
      if (d3Initialised && onLoad) {
        onLoad({
          fitView,
          zoomIn,
          zoomOut,
          project,
          getElements,
        });
      }
    }, [d3Initialised, onLoad]);

    useEffect(() => {
      setSnapGrid({ snapToGrid, snapGrid });
    }, [snapToGrid]);

    useEffect(() => {
      setInteractive(isInteractive);
    }, [isInteractive]);

    useEffect(() => {
      setMinMaxZoom({ minZoom, maxZoom });
    }, [minZoom, maxZoom]);

    useGlobalKeyHandler({ onElementsRemove, deleteKeyCode });
    useElementUpdater(elements);

    return (
      <div className={rendererClasses} ref={rendererNode}>
        <NodeRenderer
          nodeTypes={nodeTypes}
          onElementClick={onElementClick}
          onNodeDragStop={onNodeDragStop}
          onNodeDragStart={onNodeDragStart}
          onlyRenderVisibleNodes={onlyRenderVisibleNodes}
          selectNodesOnDrag={selectNodesOnDrag}
        />
        <EdgeRenderer
          width={width}
          height={height}
          edgeTypes={edgeTypes}
          onElementClick={onElementClick}
          connectionLineType={connectionLineType}
          connectionLineStyle={connectionLineStyle}
        />
        {selectionKeyPressed && <UserSelection isInteractive={isInteractive} />}
        {nodesSelectionActive && <NodesSelection />}
        <div className="react-flow__zoompane" onClick={onZoomPaneClick} ref={zoomPane} />
      </div>
    );
  }
);

GraphView.displayName = 'GraphView';

export default GraphView;
