/**
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 *
 * @emails react-core
 */

'use strict';

import {
  buttonType,
  buttonsType,
  defaultPointerId,
  defaultPointerSize,
  defaultBrowserChromeSize,
} from './constants';
import * as domEvents from './domEvents';
import {hasPointerEvent, platform} from './domEnvironment';
import * as touchStore from './touchStore';

/**
 * Converts a PointerEvent payload to a Touch
 */
function createTouch(target, payload) {
  const {
    height = defaultPointerSize,
    pageX,
    pageY,
    pointerId,
    pressure = 1,
    twist = 0,
    width = defaultPointerSize,
    x = 0,
    y = 0,
  } = payload;

  return {
    clientX: x,
    clientY: y,
    force: pressure,
    identifier: pointerId,
    pageX: pageX || x,
    pageY: pageY || y,
    radiusX: width / 2,
    radiusY: height / 2,
    rotationAngle: twist,
    target,
    screenX: x,
    screenY: y + defaultBrowserChromeSize,
  };
}

/**
 * Converts a PointerEvent to a TouchEvent
 */
function createTouchEventPayload(target, touch, payload) {
  const {
    altKey = false,
    ctrlKey = false,
    metaKey = false,
    preventDefault,
    shiftKey = false,
    timeStamp,
  } = payload;

  return {
    altKey,
    changedTouches: [touch],
    ctrlKey,
    metaKey,
    preventDefault,
    shiftKey,
    targetTouches: touchStore.getTargetTouches(target),
    timeStamp,
    touches: touchStore.getTouches(),
  };
}

function getPointerType(payload) {
  let pointerType = 'mouse';
  if (payload != null && payload.pointerType != null) {
    pointerType = payload.pointerType;
  }
  return pointerType;
}

/**
 * Pointer events sequences.
 *
 * Creates representative browser event sequences for high-level gestures based on pointers.
 * This allows unit tests to be written in terms of simple pointer interactions while testing
 * that the responses to those interactions account for the complex sequence of events that
 * browsers produce as a result.
 *
 * Every time a new pointer touches the surface a 'touchstart' event should be dispatched.
 * - 'changedTouches' contains the new touch.
 * - 'targetTouches' contains all the active pointers for the target.
 * - 'touches' contains all the active pointers on the surface.
 *
 * Every time an existing pointer moves a 'touchmove' event should be dispatched.
 * - 'changedTouches' contains the updated touch.
 *
 * Every time an existing pointer leaves the surface a 'touchend' event should be dispatched.
 * - 'changedTouches' contains the released touch.
 * - 'targetTouches' contains any of the remaining active pointers for the target.
 */

export function contextmenu(
  target,
  defaultPayload,
  {pointerType = 'mouse', modified} = {},
) {
  const dispatch = arg => target.dispatchEvent(arg);

  const payload = {
    pointerId: defaultPointerId,
    pointerType,
    ...defaultPayload,
  };

  const preventDefault = payload.preventDefault;

  if (pointerType === 'touch') {
    if (hasPointerEvent()) {
      dispatch(
        domEvents.pointerdown({
          ...payload,
          button: buttonType.primary,
          buttons: buttonsType.primary,
        }),
      );
    }
    const touch = createTouch(target, payload);
    touchStore.addTouch(touch);
    const touchEventPayload = createTouchEventPayload(target, touch, payload);
    dispatch(domEvents.touchstart(touchEventPayload));
    dispatch(
      domEvents.contextmenu({
        button: buttonType.primary,
        buttons: buttonsType.none,
        preventDefault,
      }),
    );
    touchStore.removeTouch(touch);
  } else if (pointerType === 'mouse') {
    if (modified === true) {
      const button = buttonType.primary;
      const buttons = buttonsType.primary;
      const ctrlKey = true;
      if (hasPointerEvent()) {
        dispatch(
          domEvents.pointerdown({button, buttons, ctrlKey, pointerType}),
        );
      }
      dispatch(domEvents.mousedown({button, buttons, ctrlKey}));
      if (platform.get() === 'mac') {
        dispatch(
          domEvents.contextmenu({button, buttons, ctrlKey, preventDefault}),
        );
      }
    } else {
      const button = buttonType.secondary;
      const buttons = buttonsType.secondary;
      if (hasPointerEvent()) {
        dispatch(domEvents.pointerdown({button, buttons, pointerType}));
      }
      dispatch(domEvents.mousedown({button, buttons}));
      dispatch(domEvents.contextmenu({button, buttons, preventDefault}));
    }
  }
}

export function pointercancel(target, defaultPayload) {
  const dispatchEvent = arg => target.dispatchEvent(arg);
  const pointerType = getPointerType(defaultPayload);

  const payload = {
    pointerId: defaultPointerId,
    pointerType,
    ...defaultPayload,
  };

  if (hasPointerEvent()) {
    dispatchEvent(domEvents.pointercancel(payload));
  } else {
    if (pointerType === 'mouse') {
      dispatchEvent(domEvents.dragstart(payload));
    } else {
      const touch = createTouch(target, payload);
      touchStore.removeTouch(touch);
      const touchEventPayload = createTouchEventPayload(target, touch, payload);
      dispatchEvent(domEvents.touchcancel(touchEventPayload));
    }
  }
}

export function pointerdown(target, defaultPayload) {
  const dispatch = arg => target.dispatchEvent(arg);
  const pointerType = getPointerType(defaultPayload);

  const payload = {
    button: buttonType.primary,
    buttons: buttonsType.primary,
    pointerId: defaultPointerId,
    pointerType,
    ...defaultPayload,
  };

  if (pointerType === 'mouse') {
    if (hasPointerEvent()) {
      dispatch(domEvents.pointerover(payload));
      dispatch(domEvents.pointerenter(payload));
    }
    dispatch(domEvents.mouseover(payload));
    dispatch(domEvents.mouseenter(payload));
    if (hasPointerEvent()) {
      dispatch(domEvents.pointerdown(payload));
    }
    dispatch(domEvents.mousedown(payload));
    if (document.activeElement !== target) {
      dispatch(domEvents.focus());
    }
  } else {
    if (hasPointerEvent()) {
      dispatch(domEvents.pointerover(payload));
      dispatch(domEvents.pointerenter(payload));
      dispatch(domEvents.pointerdown(payload));
    }
    const touch = createTouch(target, payload);
    touchStore.addTouch(touch);
    const touchEventPayload = createTouchEventPayload(target, touch, payload);
    dispatch(domEvents.touchstart(touchEventPayload));
    if (hasPointerEvent()) {
      dispatch(domEvents.gotpointercapture(payload));
    }
  }
}

export function pointerenter(target, defaultPayload) {
  const dispatch = arg => target.dispatchEvent(arg);

  const payload = {
    pointerId: defaultPointerId,
    ...defaultPayload,
  };

  if (hasPointerEvent()) {
    dispatch(domEvents.pointerover(payload));
    dispatch(domEvents.pointerenter(payload));
  }
  dispatch(domEvents.mouseover(payload));
  dispatch(domEvents.mouseenter(payload));
}

export function pointerexit(target, defaultPayload) {
  const dispatch = arg => target.dispatchEvent(arg);

  const payload = {
    pointerId: defaultPointerId,
    ...defaultPayload,
  };

  if (hasPointerEvent()) {
    dispatch(domEvents.pointerout(payload));
    dispatch(domEvents.pointerleave(payload));
  }
  dispatch(domEvents.mouseout(payload));
  dispatch(domEvents.mouseleave(payload));
}

export function pointerhover(target, defaultPayload) {
  const dispatch = arg => target.dispatchEvent(arg);

  const payload = {
    pointerId: defaultPointerId,
    ...defaultPayload,
  };

  if (hasPointerEvent()) {
    dispatch(domEvents.pointermove(payload));
  }
  dispatch(domEvents.mousemove(payload));
}

export function pointermove(target, defaultPayload) {
  const dispatch = arg => target.dispatchEvent(arg);
  const pointerType = getPointerType(defaultPayload);

  const payload = {
    pointerId: defaultPointerId,
    pointerType,
    ...defaultPayload,
  };

  if (hasPointerEvent()) {
    dispatch(
      domEvents.pointermove({
        pressure: pointerType === 'touch' ? 1 : 0.5,
        ...payload,
      }),
    );
  } else {
    if (pointerType === 'mouse') {
      dispatch(domEvents.mousemove(payload));
    } else {
      const touch = createTouch(target, payload);
      touchStore.updateTouch(touch);
      const touchEventPayload = createTouchEventPayload(target, touch, payload);
      dispatch(domEvents.touchmove(touchEventPayload));
    }
  }
}

export function pointerup(target, defaultPayload) {
  const dispatch = arg => target.dispatchEvent(arg);
  const pointerType = getPointerType(defaultPayload);

  const payload = {
    pointerId: defaultPointerId,
    pointerType,
    ...defaultPayload,
  };

  if (pointerType === 'mouse') {
    if (hasPointerEvent()) {
      dispatch(domEvents.pointerup(payload));
    }
    dispatch(domEvents.mouseup(payload));
    dispatch(domEvents.click(payload));
  } else {
    if (hasPointerEvent()) {
      dispatch(domEvents.pointerup(payload));
      dispatch(domEvents.lostpointercapture(payload));
      dispatch(domEvents.pointerout(payload));
      dispatch(domEvents.pointerleave(payload));
    }
    const touch = createTouch(target, payload);
    touchStore.removeTouch(touch);
    const touchEventPayload = createTouchEventPayload(target, touch, payload);
    dispatch(domEvents.touchend(touchEventPayload));
    dispatch(domEvents.mouseover(payload));
    dispatch(domEvents.mousemove(payload));
    dispatch(domEvents.mousedown(payload));
    if (document.activeElement !== target) {
      dispatch(domEvents.focus());
    }
    dispatch(domEvents.mouseup(payload));
    dispatch(domEvents.click(payload));
  }
}

/**
 * This function should be called after each test to ensure the touchStore is cleared
 * in cases where the mock pointers weren't released before the test completed
 * (e.g., a test failed or ran a partial gesture).
 */
export function resetActivePointers() {
  touchStore.clear();
}