import { zoom, zoomIdentity } from 'd3-zoom';
import { select, pointer } from 'd3-selection';

import { clamp } from '../../utils';
import { FlowTransform, TranslateExtent, PanOnScrollMode, KeyCode } from '../../types';
import { defineComponent, onMounted, PropType, ref, watch } from 'vue-demi';
import store from '../../store';
import useKeyPress from '../../hooks/useKeyPress';
import useResizeHandler from '../../hooks/useResizeHandler';

interface ZoomPaneProps {
  selectionKeyPressed: boolean;
  elementsSelectable?: boolean;
  zoomOnScroll?: boolean;
  zoomOnPinch?: boolean;
  panOnScroll?: boolean;
  panOnScrollSpeed?: number;
  panOnScrollMode?: PanOnScrollMode;
  zoomOnDoubleClick?: boolean;
  paneMoveable?: boolean;
  defaultPosition?: [number, number];
  defaultZoom?: number;
  translateExtent?: TranslateExtent;
  onMove?: (flowTransform?: FlowTransform) => void;
  onMoveStart?: (flowTransform?: FlowTransform) => void;
  onMoveEnd?: (flowTransform?: FlowTransform) => void;
  zoomActivationKeyCode?: KeyCode;
}

const viewChanged = (prevTransform: FlowTransform, eventTransform: any): boolean =>
  prevTransform.x !== eventTransform.x || prevTransform.y !== eventTransform.y || prevTransform.zoom !== eventTransform.k;

const eventToFlowTransform = (eventTransform: any): FlowTransform => ({
  x: eventTransform.x,
  y: eventTransform.y,
  zoom: eventTransform.k
});

const ZoomPane = defineComponent({
  props: {
    onMove: {
      type: Function() as PropType<ZoomPaneProps['onMove']>,
      required: false,
      default: undefined as any
    },
    onMoveStart: {
      type: Function() as PropType<ZoomPaneProps['onMoveStart']>,
      required: false,
      default: undefined as any
    },
    onMoveEnd: {
      type: Function() as PropType<ZoomPaneProps['onMoveEnd']>,
      required: false,
      default: undefined as any
    },
    zoomOnScroll: {
      type: Boolean as PropType<ZoomPaneProps['zoomOnScroll']>,
      required: false,
      default: true
    },
    zoomOnPinch: {
      type: Boolean as PropType<ZoomPaneProps['zoomOnPinch']>,
      required: false,
      default: true
    },
    panOnScroll: {
      type: Boolean as PropType<ZoomPaneProps['panOnScroll']>,
      required: false,
      default: false
    },
    panOnScrollSpeed: {
      type: Number as PropType<ZoomPaneProps['panOnScrollSpeed']>,
      required: false,
      default: 0.5
    },
    panOnScrollMode: {
      type: String as PropType<ZoomPaneProps['panOnScrollMode']>,
      required: false,
      default: PanOnScrollMode.Free
    },
    zoomOnDoubleClick: {
      type: Boolean as PropType<ZoomPaneProps['zoomOnDoubleClick']>,
      required: false,
      default: true
    },
    selectionKeyPressed: {
      type: Boolean as PropType<ZoomPaneProps['selectionKeyPressed']>,
      required: false,
      default: undefined
    },
    elementsSelectable: {
      type: Boolean as PropType<ZoomPaneProps['elementsSelectable']>,
      required: false,
      default: undefined
    },
    paneMoveable: {
      type: Boolean as PropType<ZoomPaneProps['paneMoveable']>,
      required: false,
      default: true
    },
    defaultPosition: {
      type: Array as unknown as PropType<ZoomPaneProps['defaultPosition']>,
      required: false,
      default: () => [0, 0]
    },
    defaultZoom: {
      type: Number as PropType<ZoomPaneProps['defaultZoom']>,
      required: false,
      default: 1
    },
    translateExtent: {
      type: Array as unknown as PropType<ZoomPaneProps['translateExtent']>,
      required: false,
      default: undefined
    },
    zoomActivationKeyCode: {
      type: (Number || String) as PropType<ZoomPaneProps['zoomActivationKeyCode']>,
      required: false,
      default: undefined
    }
  },
  setup(props, { slots }) {
    const pinia = store();
    const zoomPane = ref<HTMLDivElement | null>(null);
    const prevTransform = ref<FlowTransform>({ x: 0, y: 0, zoom: 0 });
    const zoomActivationKeyPressed = useKeyPress(props.zoomActivationKeyCode);

    watch(zoomPane, () => {
      if (zoomPane.value) useResizeHandler(zoomPane.value);
    });

    onMounted(() => {
      if (zoomPane.value && props.defaultPosition && props.defaultZoom) {
        const currentTranslateExtent =
          typeof props.translateExtent !== 'undefined' ? props.translateExtent : pinia.translateExtent;
        const d3ZoomInstance = zoom().scaleExtent([pinia.minZoom, pinia.maxZoom]).translateExtent(currentTranslateExtent);
        const selection = select(zoomPane.value as Element).call(d3ZoomInstance);

        const clampedX = clamp(props.defaultPosition[0], currentTranslateExtent[0][0], currentTranslateExtent[1][0]);
        const clampedY = clamp(props.defaultPosition[1], currentTranslateExtent[0][1], currentTranslateExtent[1][1]);
        const clampedZoom = clamp(props.defaultZoom, pinia.minZoom, pinia.maxZoom);
        const updatedTransform = zoomIdentity.translate(clampedX, clampedY).scale(clampedZoom);

        d3ZoomInstance.transform(selection, updatedTransform);

        pinia.initD3Zoom({
          d3Zoom: d3ZoomInstance,
          d3Selection: selection,
          d3ZoomHandler: selection.on('wheel.zoom'),
          // we need to pass transform because zoom handler is not registered when we set the initial transform
          transform: [clampedX, clampedY, clampedZoom]
        });
      }
    });

    onMounted(() => {
      if (pinia.d3Selection && pinia.d3Zoom) {
        if (props.panOnScroll && zoomActivationKeyPressed) {
          pinia.d3Selection
            .on('wheel', (event: any) => {
              event.preventDefault();
              event.stopImmediatePropagation();

              const currentZoom = pinia.d3Selection?.property('__zoom').k || 1;

              if (event.ctrlKey && props.zoomOnPinch) {
                const point = pointer(event);
                // taken from https://github.com/d3/d3-zoom/blob/master/src/zoom.js
                const pinchDelta = -event.deltaY * (event.deltaMode === 1 ? 0.05 : event.deltaMode ? 1 : 0.002) * 10;
                const zoom = currentZoom * Math.pow(2, pinchDelta);
                if (pinia.d3Selection) pinia.d3Zoom?.scaleTo(pinia.d3Selection, zoom, point);

                return;
              }

              // increase scroll speed in firefox
              // firefox: deltaMode === 1; chrome: deltaMode === 0
              const deltaNormalize = event.deltaMode === 1 ? 20 : 1;
              const deltaX = props.panOnScrollMode === PanOnScrollMode.Vertical ? 0 : event.deltaX * deltaNormalize;
              const deltaY = props.panOnScrollMode === PanOnScrollMode.Horizontal ? 0 : event.deltaY * deltaNormalize;

              if (pinia.d3Selection && props.panOnScrollSpeed)
                pinia.d3Zoom?.translateBy(
                  pinia.d3Selection,
                  -(deltaX / currentZoom) * props.panOnScrollSpeed,
                  -(deltaY / currentZoom) * props.panOnScrollSpeed
                );
            })
            .on('wheel.zoom', null);
        } else if (typeof pinia.d3ZoomHandler !== 'undefined') {
          pinia.d3Selection.on('wheel', null).on('wheel.zoom', pinia.d3ZoomHandler);
        }
      }
    });

    onMounted(() => {
      if (pinia.d3Zoom) {
        if (props.selectionKeyPressed) {
          pinia.d3Zoom.on('zoom', null);
        } else {
          pinia.d3Zoom.on('zoom', (event: any) => {
            pinia.updateTransform([event.transform.x, event.transform.y, event.transform.k]);

            if (props.onMove) {
              const flowTransform = eventToFlowTransform(event.transform);
              props.onMove(flowTransform);
            }
          });
        }
      }
    });

    onMounted(() => {
      if (pinia.d3Zoom) {
        if (props.onMoveStart) {
          pinia.d3Zoom.on('start', (event: any) => {
            if (viewChanged(prevTransform.value, event.transform)) {
              const flowTransform = eventToFlowTransform(event.transform);
              prevTransform.value = flowTransform;

              if (props.onMoveStart) props.onMoveStart(flowTransform);
            }
          });
        } else {
          pinia.d3Zoom.on('start', null);
        }
      }
    });

    onMounted(() => {
      if (pinia.d3Zoom) {
        if (props.onMoveEnd) {
          pinia.d3Zoom.on('end', (event: any) => {
            if (viewChanged(prevTransform.value, event.transform)) {
              const flowTransform = eventToFlowTransform(event.transform);
              prevTransform.value = flowTransform;

              if (props.onMoveEnd) props.onMoveEnd(flowTransform);
            }
          });
        } else {
          pinia.d3Zoom.on('end', null);
        }
      }
    });

    onMounted(() => {
      if (pinia.d3Zoom) {
        pinia.d3Zoom.filter((event: any) => {
          const zoomScroll = props.zoomOnScroll;
          const pinchZoom = props.zoomOnPinch && event.ctrlKey;

          // if all interactions are disabled, we prevent all zoom events
          if (!props.paneMoveable && !zoomScroll && !props.panOnScroll && !props.zoomOnDoubleClick && !props.zoomOnPinch) {
            return false;
          }

          // during a selection we prevent all other interactions
          if (props.selectionKeyPressed) {
            return false;
          }

          // if zoom on double click is disabled, we prevent the double click event
          if (!props.zoomOnDoubleClick && event.type === 'dblclick') {
            return false;
          }

          if (event.target.closest('.nowheel') && event.type === 'wheel') {
            return false;
          }

          // when the target element is a node, we still allow zooming
          if (
            (event.target.closest('.react-flow__node') || event.target.closest('.react-flow__edge')) &&
            event.type !== 'wheel'
          ) {
            return false;
          }

          // when the target element is a node selection, we still allow zooming
          if (event.target.closest('.react-flow__nodesselection') && event.type !== 'wheel') {
            return false;
          }

          if (!props.zoomOnPinch && event.ctrlKey && event.type === 'wheel') {
            return false;
          }

          // when there is no scroll handling enabled, we prevent all wheel events
          if (!zoomScroll && !props.panOnScroll && !pinchZoom && event.type === 'wheel') {
            return false;
          }

          // if the pane is not movable, we prevent dragging it with mousestart or touchstart
          if (!props.paneMoveable && (event.type === 'mousedown' || event.type === 'touchstart')) {
            return false;
          }

          // default filter for d3-zoom
          return (!event.ctrlKey || event.type === 'wheel') && !event.button;
        });
      }
    });

    return () => (
      <div class="react-flow__renderer react-flow__zoompane" ref={zoomPane}>
        {slots.default ? slots.default() : ''}
      </div>
    );
  }
});

export default ZoomPane;
