'use strict';
import { IS_JEST, SHOULD_BE_USE_WEB } from './PlatformChecker';
import { RuntimeKind } from './runtimeKind';
import {
  createSerializable,
  makeShareableCloneOnUIRecursive,
} from './serializable';
import { serializableMappingCache } from './serializableMappingCache';
import { isWorkletFunction } from './workletFunction';
import { WorkletsError } from './WorkletsError';
import { WorkletsModule } from './WorkletsModule';
import type { WorkletFunction, WorkletImport } from './workletTypes';

type UIJob<Args extends unknown[] = unknown[], ReturnValue = unknown> = [
  worklet: WorkletFunction<Args, ReturnValue>,
  args: Args,
  resolve?: (value: ReturnValue) => void,
];

let runOnUIQueue: UIJob[] = [];

export function setupMicrotasks() {
  'worklet';

  let microtasksQueue: Array<() => void> = [];
  let isExecutingMicrotasksQueue = false;
  global.queueMicrotask = (callback: () => void) => {
    microtasksQueue.push(callback);
  };
  global._microtaskQueueFinalizers = [];

  global.__callMicrotasks = () => {
    if (isExecutingMicrotasksQueue) {
      return;
    }
    try {
      isExecutingMicrotasksQueue = true;
      for (let index = 0; index < microtasksQueue.length; index += 1) {
        // we use classic 'for' loop because the size of the currentTasks array may change while executing some of the callbacks due to queueMicrotask calls
        microtasksQueue[index]();
      }
      microtasksQueue = [];
      global._microtaskQueueFinalizers.forEach((finalizer) => finalizer());
    } finally {
      isExecutingMicrotasksQueue = false;
    }
  };
}

function callMicrotasksOnUIThread() {
  'worklet';
  global.__callMicrotasks();
}

export const callMicrotasks = SHOULD_BE_USE_WEB
  ? () => {
      // on web flushing is a noop as immediates are handled by the browser
    }
  : callMicrotasksOnUIThread;

/**
 * Lets you schedule a function to be executed on the [UI
 * Runtime](https://docs.swmansion.com/react-native-worklets/docs/fundamentals/glossary#ui-runtime).
 *
 * - The callback executes asynchronously and doesn't return a value.
 * - Passed function and args are automatically
 *   [workletized](https://docs.swmansion.com/react-native-worklets/docs/fundamentals/glossary#to-workletize)
 *   and serialized.
 * - This function cannot be called from the [UI
 *   Runtime](https://docs.swmansion.com/react-native-worklets/docs/fundamentals/glossary#ui-runtime)
 *   or [Worker
 *   Runtime](https://docs.swmansion.com/react-native-worklets/docs/fundamentals/glossary#worker-worklet-runtime---worker-runtime),
 *   unless you have the [Bundle Mode](/docs/experimental/bundleMode) enabled.
 *
 * @param fun - A reference to a function you want to schedule on the [UI
 *   Runtime](https://docs.swmansion.com/react-native-worklets/docs/fundamentals/glossary#ui-runtime).
 * @param args - Arguments to pass to the function.
 * @see https://docs.swmansion.com/react-native-worklets/docs/threading/scheduleOnUI
 */
export function scheduleOnUI<Args extends unknown[], ReturnValue>(
  worklet: (...args: Args) => ReturnValue,
  ...args: Args
): void {
  runOnUI(worklet)(...args);
}

/**
 * Lets you asynchronously run
 * [workletized](https://docs.swmansion.com/react-native-worklets/docs/fundamentals/glossary#to-workletize)
 * functions on the [UI
 * thread](https://docs.swmansion.com/react-native-worklets/docs/threading/runOnUI/).
 *
 * This method does not schedule the work immediately but instead waits for
 * other worklets to be scheduled within the same JS loop. It uses
 * queueMicrotask to schedule all the worklets at once making sure they will run
 * within the same frame boundaries on the UI thread.
 *
 * @param fun - A reference to a function you want to execute on the [UI
 *   thread](https://docs.swmansion.com/react-native-worklets/docs/threading/runOnUI/)
 *   from the [JavaScript
 *   thread](https://docs.swmansion.com/react-native-worklets/docs/threading/runOnUI/).
 * @returns A function that accepts arguments for the function passed as the
 *   first argument.
 * @see https://docs.swmansion.com/react-native-worklets/docs/threading/runOnUI @deprecated Use `scheduleOnUI` instead.
 */
// @ts-expect-error This overload is correct since it's what user sees in his code
// before it's transformed by Reanimated Babel plugin.
export function runOnUI<Args extends unknown[], ReturnValue>(
  worklet: (...args: Args) => ReturnValue
): (...args: Args) => void;

export function runOnUI<Args extends unknown[], ReturnValue>(
  worklet: WorkletFunction<Args, ReturnValue>
): (...args: Args) => void {
  if (
    __DEV__ &&
    !SHOULD_BE_USE_WEB &&
    !isWorkletFunction(worklet) &&
    !(worklet as unknown as WorkletImport).__bundleData
  ) {
    throw new WorkletsError('`runOnUI` can only be used with worklets.');
  }
  return (...args) => {
    if (IS_JEST) {
      // Mocking time in Jest is tricky as both requestAnimationFrame and queueMicrotask
      // callbacks run on the same queue and can be interleaved. There is no way
      // to flush particular queue in Jest and the only control over mocked timers
      // is by using jest.advanceTimersByTime() method which advances all types
      // of timers including immediate and animation callbacks. Ideally we'd like
      // to have some way here to schedule work along with React updates, but
      // that's not possible, and hence in Jest environment instead of using scheduling
      // mechanism we just schedule the work ommiting the queue. This is ok for the
      // uses that we currently have but may not be ok for future tests that we write.
      WorkletsModule.scheduleOnUI(
        createSerializable(() => {
          'worklet';
          worklet(...args);
        })
      );
      return;
    }
    if (__DEV__) {
      // in DEV mode we call serializable conversion here because in case the object
      // can't be converted, we will get a meaningful stack-trace as opposed to the
      // situation when conversion is only done via microtask queue. This does not
      // make the app particularily less efficient as converted objects are cached
      // and for a given worklet the conversion only happens once.
      createSerializable(worklet);
      createSerializable(args);
    }

    enqueueUI(worklet, args);
  };
}

if (__DEV__) {
  function runOnUIWorklet(): void {
    'worklet';
    throw new WorkletsError(
      '`runOnUI` cannot be called on the UI runtime. Please call the function synchronously or use `queueMicrotask` or `requestAnimationFrame` instead.'
    );
  }

  const serializableRunOnUIWorklet = createSerializable(runOnUIWorklet);
  serializableMappingCache.set(runOnUI, serializableRunOnUIWorklet);
}

/**
 * Lets you run a function synchronously on the [UI
 * Runtime](https://docs.swmansion.com/react-native-worklets/docs/fundamentals/glossary#ui-runtime)
 * from the [RN
 * Runtime](https://docs.swmansion.com/react-native-worklets/docs/fundamentals/glossary#react-native-runtime-rn-runtime).
 * Passed function and args are automatically
 * [workletized](https://docs.swmansion.com/react-native-worklets/docs/fundamentals/glossary#to-workletize)
 * and serialized.
 *
 * - This function cannot be called from the [UI
 *   Runtime](https://docs.swmansion.com/react-native-worklets/docs/fundamentals/glossary#ui-runtime).
 * - This function cannot be called from a [Worker
 *   Runtime](https://docs.swmansion.com/react-native-worklets/docs/fundamentals/glossary#worker-worklet-runtime---worker-runtime).
 *
 * @param fun - A reference to a function you want to execute on the [UI
 *   Runtime](https://docs.swmansion.com/react-native-worklets/docs/fundamentals/glossary#ui-runtime).
 * @param args - Arguments to pass to the function.
 * @returns The return value of the function passed as the first argument.
 * @see https://docs.swmansion.com/react-native-worklets/docs/threading/runOnUISync
 */
export function runOnUISync<Args extends unknown[], ReturnValue>(
  worklet: (...args: Args) => ReturnValue,
  ...args: Args
): ReturnValue {
  return executeOnUIRuntimeSync(worklet)(...args);
}

// @ts-expect-error Check `executeOnUIRuntimeSync` overload above.
export function executeOnUIRuntimeSync<Args extends unknown[], ReturnValue>(
  worklet: (...args: Args) => ReturnValue
): (...args: Args) => ReturnValue;

export function executeOnUIRuntimeSync<Args extends unknown[], ReturnValue>(
  worklet: WorkletFunction<Args, ReturnValue>
): (...args: Args) => ReturnValue {
  return (...args) => {
    return WorkletsModule.executeOnUIRuntimeSync(
      createSerializable(() => {
        'worklet';
        const result = worklet(...args);
        return makeShareableCloneOnUIRecursive(result);
      })
    );
  };
}

type ReleaseRemoteFunction<Args extends unknown[], ReturnValue> = {
  (...args: Args): ReturnValue;
};

type DevRemoteFunction<Args extends unknown[], ReturnValue> = {
  __remoteFunction: (...args: Args) => ReturnValue;
};

type RemoteFunction<Args extends unknown[], ReturnValue> =
  | ReleaseRemoteFunction<Args, ReturnValue>
  | DevRemoteFunction<Args, ReturnValue>;

function runWorkletOnJS<Args extends unknown[], ReturnValue>(
  worklet: WorkletFunction<Args, ReturnValue>,
  ...args: Args
): void {
  // remote function that calls a worklet synchronously on the JS runtime
  worklet(...args);
}

/**
 * Lets you asynchronously run
 * non-[workletized](https://docs.swmansion.com/react-native-worklets/docs/fundamentals/glossary#to-workletize)
 * functions that couldn't otherwise run on the [UI
 * thread](https://docs.swmansion.com/react-native-worklets/docs/fundamentals/glossary#ui-thread).
 * This applies to most external libraries as they don't have their functions
 * marked with "worklet"; directive.
 *
 * @param fun - A reference to a function you want to execute on the JavaScript
 *   thread from the UI thread.
 * @returns A function that accepts arguments for the function passed as the
 *   first argument.
 * @see https://docs.swmansion.com/react-native-worklets/docs/threading/runOnJS
 */
/** @deprecated Use `scheduleOnRN` instead. */
export function runOnJS<Args extends unknown[], ReturnValue>(
  fun:
    | ((...args: Args) => ReturnValue)
    | RemoteFunction<Args, ReturnValue>
    | WorkletFunction<Args, ReturnValue>
): (...args: Args) => void {
  'worklet';
  type FunDevRemote = Extract<typeof fun, DevRemoteFunction<Args, ReturnValue>>;
  if (
    SHOULD_BE_USE_WEB ||
    globalThis.__RUNTIME_KIND === RuntimeKind.ReactNative
  ) {
    // if we are already on the JS thread, we just schedule the worklet on the JS queue
    return (...args) =>
      queueMicrotask(
        args.length
          ? () => (fun as (...args: Args) => ReturnValue)(...args)
          : (fun as () => ReturnValue)
      );
  }
  if (isWorkletFunction<Args, ReturnValue>(fun)) {
    // If `fun` is a worklet, we schedule a call of a remote function `runWorkletOnJS`
    // and pass the worklet as a first argument followed by original arguments.

    return (...args) =>
      runOnJS(runWorkletOnJS<Args, ReturnValue>)(
        fun as WorkletFunction<Args, ReturnValue>,
        ...args
      );
  }
  if ((fun as FunDevRemote).__remoteFunction) {
    // In development mode the function provided as `fun` throws an error message
    // such that when someone accidentally calls it directly on the UI runtime, they
    // see that they should use `runOnJS` instead. To facilitate that we put the
    // reference to the original remote function in the `__remoteFunction` property.
    fun = (fun as FunDevRemote).__remoteFunction;
  }

  const scheduleOnJS =
    typeof fun === 'function'
      ? global._scheduleHostFunctionOnJS
      : global._scheduleRemoteFunctionOnJS;

  return (...args) => {
    scheduleOnJS(
      fun as
        | ((...args: Args) => ReturnValue)
        | WorkletFunction<Args, ReturnValue>,
      args.length > 0 ? makeShareableCloneOnUIRecursive(args) : undefined
    );
  };
}

/**
 * Lets you schedule a function to be executed on the RN runtime from any
 * runtime. Check
 * {@link https://docs.swmansion.com/react-native-worklets/docs/fundamentals/runtimeKinds}
 * for more information about the different runtime kinds.
 *
 * Scheduling function from the RN Runtime (we are already on RN Runtime) simply
 * uses `queueMicrotask`.
 *
 * When functions need to be scheduled from the UI Runtime, first function and
 * args are serialized and then the system passes the scheduling responsibility
 * to the JSScheduler. The JSScheduler then uses the RN CallInvoker to schedule
 * the function asynchronously on the JavaScript thread by calling
 * `jsCallInvoker_->invokeAsync()`.
 *
 * When called from a Worker Runtime, it uses the same JSScheduler mechanism.
 *
 * @param fun - A function you want to schedule on the RN runtime. A function
 *   can be a worklet, a remote function or a regular function.
 * @param args - Arguments to pass to the function.
 * @see https://docs.swmansion.com/react-native-worklets/docs/threading/scheduleOnRN
 */
export function scheduleOnRN<Args extends unknown[], ReturnValue>(
  fun:
    | ((...args: Args) => ReturnValue)
    | RemoteFunction<Args, ReturnValue>
    | WorkletFunction<Args, ReturnValue>,
  ...args: Args
): void {
  'worklet';
  runOnJS(fun)(...args);
}

/**
 * Lets you asynchronously run
 * [workletized](https://docs.swmansion.com/react-native-worklets/docs/fundamentals/glossary#to-workletize)
 * functions on the [UI
 * Runtime](https://docs.swmansion.com/react-native-worklets/docs/fundamentals/glossary#ui-runtime).
 *
 * This method does not schedule the work immediately but instead waits for
 * other worklets to be scheduled within the same JS loop. It uses
 * queueMicrotask to schedule all the worklets at once making sure they will run
 * within the same frame boundaries on the UI thread.
 *
 * @param fun - A reference to a function you want to execute on the [UI
 *   Runtime](https://docs.swmansion.com/react-native-worklets/docs/fundamentals/glossary#ui-runtime).
 *   from the [JavaScript
 *   Runtime](https://docs.swmansion.com/react-native-worklets/docs/fundamentals/glossary#javascript-runtime).
 * @returns A promise that resolves to the return value of the function passed
 *   as the first argument.
 * @see https://docs.swmansion.com/react-native-worklets/docs/threading/runOnUIAsync
 */
export function runOnUIAsync<Args extends unknown[], ReturnValue>(
  worklet: (...args: Args) => ReturnValue
): (...args: Args) => Promise<ReturnValue> {
  if (__DEV__ && !SHOULD_BE_USE_WEB && !isWorkletFunction(worklet)) {
    throw new WorkletsError('`runOnUIAsync` can only be used with worklets.');
  }
  return (...args: Args) => {
    return new Promise<ReturnValue>((resolve) => {
      if (IS_JEST) {
        // Mocking time in Jest is tricky as both requestAnimationFrame and queueMicrotask
        // callbacks run on the same queue and can be interleaved. There is no way
        // to flush particular queue in Jest and the only control over mocked timers
        // is by using jest.advanceTimersByTime() method which advances all types
        // of timers including immediate and animation callbacks. Ideally we'd like
        // to have some way here to schedule work along with React updates, but
        // that's not possible, and hence in Jest environment instead of using scheduling
        // mechanism we just schedule the work ommiting the queue. This is ok for the
        // uses that we currently have but may not be ok for future tests that we write.
        WorkletsModule.scheduleOnUI(
          createSerializable(() => {
            'worklet';
            worklet(...args);
          })
        );
        return;
      }
      if (__DEV__) {
        // in DEV mode we call serializable conversion here because in case the object
        // can't be converted, we will get a meaningful stack-trace as opposed to the
        // situation when conversion is only done via microtask queue. This does not
        // make the app particularily less efficient as converted objects are cached
        // and for a given worklet the conversion only happens once.
        createSerializable(worklet);
        createSerializable(args);
      }

      enqueueUI(worklet as WorkletFunction<Args, ReturnValue>, args, resolve);
    });
  };
}

if (__DEV__) {
  function runOnUIAsyncWorklet(): void {
    'worklet';
    throw new WorkletsError(
      '`runOnUIAsync` cannot be called on the UI runtime. Please call the function synchronously or use `queueMicrotask` or `requestAnimationFrame` instead.'
    );
  }

  const serializableRunOnUIAsyncWorklet =
    createSerializable(runOnUIAsyncWorklet);
  serializableMappingCache.set(runOnUIAsync, serializableRunOnUIAsyncWorklet);
}

function enqueueUI<Args extends unknown[], ReturnValue>(
  worklet: WorkletFunction<Args, ReturnValue>,
  args: Args,
  resolve?: (value: ReturnValue) => void
): void {
  const job = [worklet, args, resolve] as UIJob<Args, ReturnValue>;
  runOnUIQueue.push(job as unknown as UIJob);
  if (runOnUIQueue.length === 1) {
    flushUIQueue();
  }
}

function flushUIQueue(): void {
  queueMicrotask(() => {
    const queue = runOnUIQueue;
    runOnUIQueue = [];
    WorkletsModule.scheduleOnUI(
      createSerializable(() => {
        'worklet';
        queue.forEach(([workletFunction, workletArgs, jobResolve]) => {
          const result = workletFunction(...workletArgs);
          if (jobResolve) {
            runOnJS(jobResolve)(result);
          }
        });
        callMicrotasks();
      })
    );
  });
}

/**
 * Added temporarily for integration with `react-native-audio-api`. Don't depend
 * on this API as it may change without notice.
 */
// eslint-disable-next-line camelcase
export function unstable_eventLoopTask<TArgs extends unknown[], TRet>(
  worklet: (...args: TArgs) => TRet
) {
  return (...args: TArgs) => {
    'worklet';
    worklet(...args);
    callMicrotasks();
  };
}
