import { QueueApi } from '@stencil/core';

import { gestureController } from './gesture-controller';
import { createPointerEvents } from './pointer-events';
import { createPanRecognizer } from './recognizers';

export function createGesture(config: GestureConfig): Gesture {
  const finalConfig = {
    disableScroll: false,
    direction: 'x',
    gesturePriority: 0,
    passive: true,
    maxAngle: 40,
    threshold: 10,

    ...config
  };

  const canStart = finalConfig.canStart;
  const onWillStart = finalConfig.onWillStart;
  const onStart = finalConfig.onStart;
  const onEnd = finalConfig.onEnd;
  const notCaptured = finalConfig.notCaptured;
  const onMove = finalConfig.onMove;
  const threshold = finalConfig.threshold;
  const queue = finalConfig.queue;

  const detail = {
    type: 'pan',
    startX: 0,
    startY: 0,
    startTimeStamp: 0,
    currentX: 0,
    currentY: 0,
    velocityX: 0,
    velocityY: 0,
    deltaX: 0,
    deltaY: 0,
    timeStamp: 0,
    event: undefined as any,
    data: undefined
  };

  const pointerEvents = createPointerEvents(
    finalConfig.el,
    pointerDown,
    pointerMove,
    pointerUp,
    {
      capture: false,
    }
  );

  const pan = createPanRecognizer(finalConfig.direction, finalConfig.threshold, finalConfig.maxAngle);
  const gesture = gestureController.createGesture({
    name: config.gestureName,
    priority: config.gesturePriority,
    disableScroll: config.disableScroll
  });

  let hasCapturedPan = false;
  let hasStartedPan = false;
  let hasFiredStart = true;
  let isMoveQueued = false;

  function pointerDown(ev: UIEvent): boolean {
    const timeStamp = now(ev);
    if (hasStartedPan || !hasFiredStart) {
      return false;
    }

    updateDetail(ev, detail);
    detail.startX = detail.currentX;
    detail.startY = detail.currentY;
    detail.startTimeStamp = detail.timeStamp = timeStamp;
    detail.velocityX = detail.velocityY = detail.deltaX = detail.deltaY = 0;
    detail.event = ev;

    // Check if gesture can start
    if (canStart && canStart(detail) === false) {
      return false;
    }
    // Release fallback
    gesture.release();

    // Start gesture
    if (!gesture.start()) {
      return false;
    }

    hasStartedPan = true;
    if (threshold === 0) {
      return tryToCapturePan();
    }
    pan.start(detail.startX, detail.startY);
    return true;
  }

  function pointerMove(ev: UIEvent) {
    // fast path, if gesture is currently captured
    // do minimun job to get user-land even dispatched
    if (hasCapturedPan) {
      if (!isMoveQueued && hasFiredStart) {
        isMoveQueued = true;
        calcGestureData(detail, ev);
        queue.write(fireOnMove);
      }
      return;
    }

    // gesture is currently being detected
    calcGestureData(detail, ev);
    if (pan.detect(detail.currentX, detail.currentY)) {
      if (!pan.isGesture() || !tryToCapturePan()) {
        abortGesture();
      }
    }
  }

  function fireOnMove() {
    // Since fireOnMove is called inside a RAF, onEnd() might be called,
    // we must double check hasCapturedPan
    if (!hasCapturedPan) {
      return;
    }
    isMoveQueued = false;
    if (onMove) {
      onMove(detail);
    }
  }

  function tryToCapturePan(): boolean {
    if (gesture && !gesture.capture()) {
      return false;
    }
    hasCapturedPan = true;
    hasFiredStart = false;

    // reset start position since the real user-land event starts here
    // If the pan detector threshold is big, not reseting the start position
    // will cause a jump in the animation equal to the detector threshold.
    // the array of positions used to calculate the gesture velocity does not
    // need to be cleaned, more points in the positions array always results in a
    // more acurate value of the velocity.
    detail.startX = detail.currentX;
    detail.startY = detail.currentY;
    detail.startTimeStamp = detail.timeStamp;

    if (onWillStart) {
      onWillStart(detail).then(fireOnStart);
    } else {
      fireOnStart();
    }
    return true;
  }

  function fireOnStart() {
    if (onStart) {
      onStart(detail);
    }
    hasFiredStart = true;
  }

  function abortGesture() {
    reset();
    pointerEvents.stop();
    if (notCaptured) {
      notCaptured(detail);
    }
  }

  function reset() {
    hasCapturedPan = false;
    hasStartedPan = false;
    isMoveQueued = false;
    hasFiredStart = true;

    gesture.release();
  }

  // END *************************

  function pointerUp(ev: UIEvent) {
    const tmpHasCaptured = hasCapturedPan;
    const tmpHasFiredStart = hasFiredStart;
    reset();

    if (!tmpHasFiredStart) {
      return;
    }
    calcGestureData(detail, ev);

    // Try to capture press
    if (tmpHasCaptured) {
      if (onEnd) {
        onEnd(detail);
      }
      return;
    }

    // Not captured any event
    if (notCaptured) {
      notCaptured(detail);
    }
  }

  return {
    setDisabled(disabled: boolean) {
      pointerEvents.setDisabled(disabled);
    },
    destroy() {
      gesture.destroy();
      pointerEvents.destroy();
    }
  };
}

function calcGestureData(detail: GestureDetail, ev: UIEvent) {
  const prevX = detail.currentX;
  const prevY = detail.currentY;
  const prevT = detail.timeStamp;

  updateDetail(ev, detail);

  const currentX = detail.currentX;
  const currentY = detail.currentY;
  const timestamp = detail.timeStamp = now(ev);
  const timeDelta = timestamp - prevT;
  if (timeDelta > 0 && timeDelta < 100) {
    const velocityX = (currentX - prevX) / timeDelta;
    const velocityY = (currentY - prevY) / timeDelta;
    detail.velocityX = velocityX * 0.7 + detail.velocityX * 0.3;
    detail.velocityY = velocityY * 0.7 + detail.velocityY * 0.3;
  }
  detail.deltaX = currentX - detail.startX;
  detail.deltaY = currentY - detail.startY;
  detail.event = ev;
}

function updateDetail(ev: any, detail: GestureDetail) {
  // get X coordinates for either a mouse click
  // or a touch depending on the given event
  let x = 0;
  let y = 0;
  if (ev) {
    const changedTouches = ev.changedTouches;
    if (changedTouches && changedTouches.length > 0) {
      const touch = changedTouches[0];
      x = touch.clientX;
      y = touch.clientY;
    } else if (ev.pageX !== undefined) {
      x = ev.pageX;
      y = ev.pageY;
    }
  }
  detail.currentX = x;
  detail.currentY = y;
}

function now(ev: UIEvent) {
  return ev.timeStamp || Date.now();
}

export interface GestureDetail {
  type: string;
  startX: number;
  startY: number;
  startTimeStamp: number;
  currentX: number;
  currentY: number;
  velocityX: number;
  velocityY: number;
  deltaX: number;
  deltaY: number;
  timeStamp: number;
  event: UIEvent;
  data?: any;
}

export type GestureCallback = (detail?: GestureDetail) => boolean | void;

export interface Gesture {
  setDisabled(disabled: boolean): void;
  destroy(): void;
}

export interface GestureConfig {
  el: Node;
  disableScroll?: boolean;

  queue: QueueApi;
  direction?: 'x' | 'y';
  gestureName: string;
  gesturePriority?: number;
  passive?: boolean;
  maxAngle?: number;
  threshold?: number;

  canStart?: GestureCallback;
  onWillStart?: (_: GestureDetail) => Promise<void>;
  onStart?: GestureCallback;
  onMove?: GestureCallback;
  onEnd?: GestureCallback;
  notCaptured?: GestureCallback;
}
