import type { GestureTouchEvent } from '../../../handlers/gestureHandlerCommon';
import { CALLBACK_TYPE } from '../../../handlers/gestures/gesture';
import type { ReanimatedContext } from '../../../handlers/gestures/reanimatedWrapper';
import { State } from '../../../State';
import { TouchEventType } from '../../../TouchEventType';
import { tagMessage } from '../../../utils';
import type {
  ChangeCalculatorType,
  GestureCallbacks,
  GestureEndEvent,
  GestureEvent,
  GestureHandlerEventWithHandlerData,
  GestureStateChangeEventWithHandlerData,
  GestureUpdateEventWithHandlerData,
} from '../../types';
import {
  flattenAndFilterEvent,
  isEventForHandlerWithTag,
  maybeExtractNativeEvent,
  runCallback,
  touchEventTypeToCallbackType,
} from '../utils';
import { isStateChangeEvent, isTouchEvent } from '../utils/eventUtils';

function handleStateChangeEvent<
  THandlerData,
  TExtendedHandlerData extends THandlerData,
>(
  eventWithData: GestureStateChangeEventWithHandlerData<
    THandlerData | TExtendedHandlerData
  >,
  callbacks: GestureCallbacks<THandlerData, TExtendedHandlerData>,
  context: ReanimatedContext<TExtendedHandlerData>,
  fillInDefaultValues?: (event: GestureEvent<TExtendedHandlerData>) => void
) {
  'worklet';
  const { oldState, state } = eventWithData;
  const event = flattenAndFilterEvent(eventWithData);

  if (oldState === State.UNDETERMINED && state === State.BEGAN) {
    runCallback(CALLBACK_TYPE.BEGAN, callbacks, event);
  } else if (
    (oldState === State.BEGAN || oldState === State.UNDETERMINED) &&
    state === State.ACTIVE
  ) {
    fillInDefaultValues?.(event as GestureEvent<TExtendedHandlerData>);
    runCallback(CALLBACK_TYPE.START, callbacks, event);
  } else if (
    oldState !== state &&
    (state === State.END || state === State.FAILED || state === State.CANCELLED)
  ) {
    const canceled = state === State.FAILED || state === State.CANCELLED;
    const endEvent: GestureEndEvent<THandlerData> = {
      ...event,
      canceled,
    };

    if (oldState === State.ACTIVE) {
      fillInDefaultValues?.(endEvent as GestureEndEvent<TExtendedHandlerData>);
      runCallback<THandlerData, TExtendedHandlerData>(
        CALLBACK_TYPE.END,
        callbacks,
        endEvent
      );
    }
    runCallback<THandlerData, TExtendedHandlerData>(
      CALLBACK_TYPE.FINALIZE,
      callbacks,
      endEvent
    );

    if (context) {
      context.lastUpdateEvent = undefined;
    }
  }
}

export function handleUpdateEvent<
  THandlerData,
  TExtendedHandlerData extends THandlerData,
>(
  eventWithData: GestureUpdateEventWithHandlerData<TExtendedHandlerData>,
  handlers: GestureCallbacks<THandlerData, TExtendedHandlerData>,
  changeEventCalculator: ChangeCalculatorType<TExtendedHandlerData> | undefined,
  context: ReanimatedContext<TExtendedHandlerData>
) {
  'worklet';
  const eventWithChanges = changeEventCalculator
    ? changeEventCalculator(
        eventWithData,
        context ? context.lastUpdateEvent : undefined
      )
    : eventWithData;

  const event = flattenAndFilterEvent(eventWithChanges);

  // This should never happen, but since we don't want to call hooks conditionally, we have to mark
  // context as possibly undefined to make TypeScript happy.
  if (!context) {
    throw new Error(tagMessage('Event handler context is not defined'));
  }

  runCallback(CALLBACK_TYPE.UPDATE, handlers, event);

  context.lastUpdateEvent = eventWithData;
}

export function handleTouchEvent<
  THandlerData,
  TExtendedHandlerData extends THandlerData,
>(
  event: GestureTouchEvent,
  handlers: GestureCallbacks<THandlerData, TExtendedHandlerData>
) {
  'worklet';

  if (event.eventType !== TouchEventType.UNDETERMINED) {
    runCallback(touchEventTypeToCallbackType(event.eventType), handlers, event);
  }
}

export function eventHandler<
  THandlerData,
  TExtendedHandlerData extends THandlerData,
>(
  handlerTag: number,
  sourceEvent: GestureHandlerEventWithHandlerData<
    THandlerData,
    TExtendedHandlerData
  >,
  handlers: GestureCallbacks<THandlerData, TExtendedHandlerData>,
  changeEventCalculator: ChangeCalculatorType<TExtendedHandlerData> | undefined,
  jsContext: ReanimatedContext<TExtendedHandlerData>,
  dispatchesAnimatedEvents: boolean,
  fillInDefaultValues?: (event: GestureEvent<TExtendedHandlerData>) => void
) {
  'worklet';
  const eventWithData = maybeExtractNativeEvent(sourceEvent);

  if (!isEventForHandlerWithTag(handlerTag, eventWithData)) {
    return;
  }

  if (isStateChangeEvent(eventWithData)) {
    handleStateChangeEvent(
      eventWithData,
      handlers,
      jsContext,
      fillInDefaultValues
    );
    return;
  }

  if (isTouchEvent(eventWithData)) {
    handleTouchEvent(eventWithData, handlers);
    return;
  }

  if (!dispatchesAnimatedEvents) {
    handleUpdateEvent(
      eventWithData,
      handlers,
      changeEventCalculator,
      jsContext
    );
  }
}
