import { Reanimated } from '../../../handlers/gestures/reanimatedWrapper';
import { NativeProxy } from '../../NativeProxy';
import type {
  BaseGestureConfig,
  SharedValue,
  SharedValueOrT,
} from '../../types';
import { HandlerCallbacks } from './propsWhiteList';

// Variant of djb2 hash function.
// Taken from https://gist.github.com/eplawless/52813b1d8ad9af510d85?permalink_comment_id=3367765#gistcomment-3367765
function hash(str: string) {
  'worklet';
  const len = str.length;
  let h = 5381;

  for (let i = 0; i < len; i++) {
    h = (h * 33) ^ str.charCodeAt(i);
  }
  return h >>> 0;
}

export const SHARED_VALUE_OFFSET = 1.618;

// Don't transfer entire NativeProxy to the UI thread
const { updateGestureHandlerConfig } = NativeProxy;

export function bindSharedValues<
  TConfig,
  THandlerData,
  TExtendedHandlerData extends THandlerData,
>(
  config: BaseGestureConfig<TConfig, THandlerData, TExtendedHandlerData>,
  handlerTag: number
) {
  if (Reanimated === undefined) {
    return;
  }

  const baseListenerId = handlerTag + SHARED_VALUE_OFFSET;
  const { shouldUseReanimatedDetector } = config;

  const attachListener = (sharedValue: SharedValue, configKey: string) => {
    'worklet';
    const keyHash = hash(configKey);
    const listenerId = baseListenerId + keyHash;

    sharedValue.addListener(listenerId, (value) => {
      updateGestureHandlerConfig(
        handlerTag,
        configKey === 'runOnJS'
          ? {
              dispatchesReanimatedEvents: shouldUseReanimatedDetector && !value,
            }
          : { [configKey]: value }
      );
    });
  };

  for (const [key, maybeSharedValue] of Object.entries(config)) {
    if (!Reanimated.isSharedValue(maybeSharedValue)) {
      continue;
    }

    Reanimated.runOnUI(attachListener)(maybeSharedValue, key);
  }
}

export function unbindSharedValues<
  TConfig,
  THandlerData,
  TExtendedHandlerData extends THandlerData,
>(
  config: BaseGestureConfig<TConfig, THandlerData, TExtendedHandlerData>,
  handlerTag: number
) {
  if (Reanimated === undefined) {
    return;
  }

  const baseListenerId = handlerTag + SHARED_VALUE_OFFSET;

  for (const [key, maybeSharedValue] of Object.entries(config)) {
    if (!Reanimated.isSharedValue(maybeSharedValue)) {
      continue;
    }

    const keyHash = hash(key);
    const listenerId = baseListenerId + keyHash;

    Reanimated.runOnUI(() => {
      maybeSharedValue.removeListener(listenerId);
    })();
  }
}

export function hasWorkletEventHandlers<
  TConfig,
  THandlerData,
  TExtendedHandlerData extends THandlerData,
>(config: BaseGestureConfig<TConfig, THandlerData, TExtendedHandlerData>) {
  for (const key of HandlerCallbacks) {
    const value = config[key];

    if (typeof value === 'function' && '__workletHash' in value) {
      return true;
    }
  }

  return false;
}

export function maybeUnpackValue<T>(
  v: SharedValueOrT<T, boolean> | undefined
): T {
  'worklet';

  return (Reanimated?.isSharedValue(v) ? v.value : v) as T;
}
