'use strict';
import { registerWorkletStackDetails } from './errors';
import { isSynchronizable } from './isSynchronizable';
import { logger } from './logger';
import { SHOULD_BE_USE_WEB } from './PlatformChecker';
import {
  serializableMappingCache,
  serializableMappingFlag,
} from './serializableMappingCache';
import type { Synchronizable } from './synchronizable';
import { jsVersion } from './utils/jsVersion';
import { isWorkletFunction } from './workletFunction';
import { WorkletsError } from './WorkletsError';
import { WorkletsModule } from './WorkletsModule';
import type {
  FlatSerializableRef,
  SerializableRef,
  WorkletFunction,
  WorkletImport,
} from './workletTypes';

// for web and jest environments this file provides a stub implementation
// where no serializable references are used. Instead, the objects themselves are used
// instead of serializable references, because of the fact that we don't have to deal with
// running the code on separate VMs.

const MAGIC_KEY = 'REANIMATED_MAGIC_KEY';

function isHostObject(value: NonNullable<object>) {
  'worklet';
  // We could use JSI to determine whether an object is a host object, however
  // the below workaround works well and is way faster than an additional JSI call.
  // We use the fact that host objects have broken implementation of `hasOwnProperty`
  // and hence return true for all `in` checks regardless of the key we ask for.
  return MAGIC_KEY in value;
}

export function isSerializableRef(value: unknown): value is SerializableRef {
  return (
    typeof value === 'object' && value !== null && '__serializableRef' in value
  );
}

function isPlainJSObject(object: object): object is Record<string, unknown> {
  'worklet';
  return Object.getPrototypeOf(object) === Object.prototype;
}

function isTurboModuleLike(object: object): object is Record<string, unknown> {
  return isHostObject(Object.getPrototypeOf(object));
}

function getFromCache(value: object) {
  const cached = serializableMappingCache.get(value);
  if (cached === serializableMappingFlag) {
    // This means that `value` was already a clone and we should return it as is.
    return value;
  }
  return cached;
}

// The below object is used as a replacement for objects that cannot be transferred
// as serializable values. In createSerializable we detect if an object is of
// a plain Object.prototype and only allow such objects to be transferred. This lets
// us avoid all sorts of react internals from leaking into the UI runtime. To make it
// possible to catch errors when someone actually tries to access such object on the UI
// runtime, we use the below Proxy object which is instantiated on the UI runtime and
// throws whenever someone tries to access its fields.
const INACCESSIBLE_OBJECT = {
  __init: () => {
    'worklet';
    return new Proxy(
      {},
      {
        get: (_: unknown, prop: string | symbol) => {
          if (
            prop === '_isReanimatedSharedValue' ||
            prop === '__remoteFunction'
          ) {
            // not very happy about this check here, but we need to allow for
            // "inaccessible" objects to be tested with isSerializableRef check
            // as it is being used in the mappers when extracting inputs recursively
            // as well as with isRemoteFunction when cloning objects recursively.
            // Apparently we can't check if a key exists there as HostObjects always
            // return true for such tests, so the only possibility for us is to
            // actually access that key and see if it is set to true. We therefore
            // need to allow for this key to be accessed here.
            return false;
          }
          throw new WorkletsError(
            `Trying to access property \`${String(
              prop
            )}\` of an object which cannot be sent to the UI runtime.`
          );
        },
        set: () => {
          throw new WorkletsError(
            'Trying to write to an object which cannot be sent to the UI runtime.'
          );
        },
      }
    );
  },
};

const VALID_ARRAY_VIEWS_NAMES = [
  'Int8Array',
  'Uint8Array',
  'Uint8ClampedArray',
  'Int16Array',
  'Uint16Array',
  'Int32Array',
  'Uint32Array',
  'Float32Array',
  'Float64Array',
  'BigInt64Array',
  'BigUint64Array',
  'DataView',
];

const DETECT_CYCLIC_OBJECT_DEPTH_THRESHOLD = 30;
// Below variable stores object that we process in createSerializable at the specified depth.
// We use it to check if later on the function reenters with the same object
let processedObjectAtThresholdDepth: unknown;

function createSerializableWeb<T>(value: T): SerializableRef<T> {
  return value as SerializableRef<T>;
}

function createSerializableNative<T>(
  value: T,
  shouldPersistRemote = false,
  depth = 0
): SerializableRef<T> {
  detectCyclicObject(value, depth);

  const isObject = typeof value === 'object';
  const isFunction = typeof value === 'function';

  if (typeof value === 'string') {
    return cloneString(value) as SerializableRef<T>;
  }

  if (typeof value === 'number') {
    return cloneNumber(value) as SerializableRef<T>;
  }

  if (typeof value === 'boolean') {
    return cloneBoolean(value) as SerializableRef<T>;
  }

  if (typeof value === 'bigint') {
    return cloneBigInt(value) as SerializableRef<T>;
  }

  if (value === undefined) {
    return cloneUndefined() as SerializableRef<T>;
  }

  if (value === null) {
    return cloneNull() as SerializableRef<T>;
  }

  if ((!isObject && !isFunction) || value === null) {
    return clonePrimitive(value, shouldPersistRemote);
  }

  const cached = getFromCache(value);
  if (cached !== undefined) {
    return cached as SerializableRef<T>;
  }

  if (Array.isArray(value)) {
    return cloneArray(value, shouldPersistRemote, depth);
  }
  if (
    globalThis._WORKLETS_BUNDLE_MODE &&
    isFunction &&
    (value as WorkletImport).__bundleData
  ) {
    return cloneImport(value as WorkletImport) as SerializableRef<T>;
  }
  if (isFunction && !isWorkletFunction(value)) {
    return cloneRemoteFunction(value);
  }
  // RN has introduced a new representation of TurboModules as a JS object whose prototype is the host object
  // More details: https://github.com/facebook/react-native/blob/main/packages/react-native/ReactCommon/react/nativemodule/core/ReactCommon/TurboModuleBinding.cpp#L182
  if (isTurboModuleLike(value)) {
    return cloneTurboModuleLike(value, shouldPersistRemote, depth);
  }
  if (isHostObject(value)) {
    return cloneHostObject(value);
  }
  if (isPlainJSObject(value) && value.__init) {
    return cloneInitializer(
      value,
      shouldPersistRemote,
      depth
    ) as SerializableRef<T>;
  }
  if (isPlainJSObject(value) && value.__workletContextObjectFactory) {
    return cloneContextObject(value);
  }
  if ((isPlainJSObject(value) || isFunction) && isWorkletFunction(value)) {
    return cloneWorklet(value, shouldPersistRemote, depth);
  }
  if (isSynchronizable(value)) {
    return cloneSynchronizable(value) as SerializableRef<T>;
  }
  if (isPlainJSObject(value) || isFunction) {
    return clonePlainJSObject(value, shouldPersistRemote, depth);
  }
  if (value instanceof Set) {
    return cloneSet(value);
  }
  if (value instanceof Map) {
    return cloneMap(value);
  }
  if (value instanceof RegExp) {
    return cloneRegExp(value);
  }
  if (value instanceof Error) {
    return cloneError(value);
  }
  if (value instanceof ArrayBuffer) {
    return cloneArrayBuffer(value, shouldPersistRemote);
  }
  if (ArrayBuffer.isView(value)) {
    // typed array (e.g. Int32Array, Uint8ClampedArray) or DataView
    return cloneArrayBufferView(value);
  }
  return inaccessibleObject(value);
}

if (globalThis._WORKLETS_BUNDLE_MODE) {
  // TODO: Do it programatically.
  createSerializableNative.__bundleData = {
    imported: 'createSerializable',
    // @ts-expect-error resolveWeak is defined by Metro
    source: require.resolveWeak('./index'),
  };
}

interface CreateSerializable {
  <T>(value: T): SerializableRef<T>;
  <T>(
    value: T,
    shouldPersistRemote: boolean,
    depth: number
  ): SerializableRef<T>;
}

export const createSerializable: CreateSerializable = SHOULD_BE_USE_WEB
  ? createSerializableWeb
  : createSerializableNative;

function detectCyclicObject(value: unknown, depth: number) {
  if (depth >= DETECT_CYCLIC_OBJECT_DEPTH_THRESHOLD) {
    // if we reach certain recursion depth we suspect that we are dealing with a cyclic object.
    // this type of objects are not supported and cannot be transferred as serializable, so we
    // implement a simple detection mechanism that remembers the value at a given depth and
    // tests whether we try reenter this method later on with the same value. If that happens
    // we throw an appropriate error.
    if (depth === DETECT_CYCLIC_OBJECT_DEPTH_THRESHOLD) {
      processedObjectAtThresholdDepth = value;
    } else if (value === processedObjectAtThresholdDepth) {
      throw new WorkletsError(
        'Trying to convert a cyclic object to a serializable. This is not supported.'
      );
    }
  } else {
    processedObjectAtThresholdDepth = undefined;
  }
}

function clonePrimitive<T>(
  value: T,
  shouldPersistRemote: boolean
): SerializableRef<T> {
  return WorkletsModule.createSerializable(value, shouldPersistRemote);
}

function cloneString(value: string): SerializableRef<string> {
  return WorkletsModule.createSerializableString(value);
}

function cloneNumber(value: number): SerializableRef<number> {
  return WorkletsModule.createSerializableNumber(value);
}

function cloneBoolean(value: boolean): SerializableRef<boolean> {
  return WorkletsModule.createSerializableBoolean(value);
}

function cloneBigInt(value: bigint): SerializableRef<bigint> {
  return WorkletsModule.createSerializableBigInt(value);
}

function cloneUndefined(): SerializableRef<undefined> {
  return WorkletsModule.createSerializableUndefined();
}

function cloneNull(): SerializableRef<null> {
  return WorkletsModule.createSerializableNull();
}

function cloneObjectProperties<T extends object>(
  value: T,
  shouldPersistRemote: boolean,
  depth: number
): Record<string, unknown> {
  const clonedProps: Record<string, unknown> = {};
  for (const [key, element] of Object.entries(value)) {
    // We don't need to clone __initData field as it contains long strings
    // representing the worklet code, source map, and location, and we will
    // serialize/deserialize it once.
    if (key === '__initData' && clonedProps.__initData !== undefined) {
      continue;
    }
    clonedProps[key] = createSerializable(
      element,
      shouldPersistRemote,
      depth + 1
    );
  }
  return clonedProps;
}

function cloneInitializer(
  value: object,
  shouldPersistRemote = false,
  depth = 0
): SerializableRef<object> {
  const clonedProps: Record<string, unknown> = cloneObjectProperties(
    value,
    shouldPersistRemote,
    depth
  );
  return WorkletsModule.createSerializableInitializer(clonedProps);
}

function cloneArray<T extends unknown[]>(
  value: T,
  shouldPersistRemote: boolean,
  depth: number
): SerializableRef<T> {
  const clonedElements = value.map((element) =>
    createSerializable(element, shouldPersistRemote, depth + 1)
  );
  const clone = WorkletsModule.createSerializableArray(
    clonedElements,
    shouldPersistRemote
  ) as SerializableRef<T>;
  serializableMappingCache.set(value, clone);
  serializableMappingCache.set(clone);

  freezeObjectInDev(value);
  return clone;
}

function cloneRemoteFunction<TArgs extends unknown[], TReturn>(
  value: (...args: TArgs) => TReturn
): SerializableRef<TReturn> {
  const clone = WorkletsModule.createSerializableFunction(value);
  serializableMappingCache.set(value, clone);
  serializableMappingCache.set(clone);

  freezeObjectInDev(value);
  return clone;
}

function cloneHostObject<T extends object>(value: T): SerializableRef<T> {
  // for host objects we pass the reference to the object as serializable and
  // then recreate new host object wrapping the same instance on the UI thread.
  // there is no point of iterating over keys as we do for regular objects.
  const clone = WorkletsModule.createSerializableHostObject(value);
  serializableMappingCache.set(value, clone);
  serializableMappingCache.set(clone);

  return clone;
}

function cloneWorklet<T extends WorkletFunction>(
  value: T,
  shouldPersistRemote: boolean,
  depth: number
): SerializableRef<T> {
  if (__DEV__) {
    const babelVersion = (value as WorkletFunction).__pluginVersion;
    if (babelVersion !== undefined && babelVersion !== jsVersion) {
      throw new WorkletsError(
        `Mismatch between JavaScript code version and Worklets Babel plugin version (${jsVersion} vs. ${babelVersion}).
    See \`https://docs.swmansion.com/react-native-worklets/docs/guides/troubleshooting#mismatch-between-javascript-code-version-and-worklets-babel-plugin-version\` for more details.
    Offending code was: \`${getWorkletCode(value)}\``
      );
    }
    registerWorkletStackDetails(
      value.__workletHash,
      (value as WorkletFunction).__stackDetails!
    );
  }
  if ((value as WorkletFunction).__stackDetails) {
    // `Error` type of value cannot be copied to the UI thread, so we
    // remove it after we handled it in dev mode or delete it to ignore it in production mode.
    // Not removing this would cause an infinite loop in production mode and it just
    // seems more elegant to handle it this way.
    delete (value as WorkletFunction).__stackDetails;
  }
  const clonedProps: Record<string, unknown> = cloneObjectProperties(
    value,
    true,
    depth
  );
  // to save on transferring static __initData field of worklet structure
  // we request serializable value to persist its UI counterpart. This means
  // that the __initData field that contains long strings representing the
  // worklet code, source map, and location, will always be
  // serialized/deserialized once.
  clonedProps.__initData = createSerializable(
    value.__initData,
    true,
    depth + 1
  );

  const clone = WorkletsModule.createSerializableWorklet(
    clonedProps,
    // TODO: Check after refactor if we can remove shouldPersistRemote parameter (imho it's redundant here since worklets are always persistent)
    // retain all worklets
    true
  ) as SerializableRef<T>;
  serializableMappingCache.set(value, clone);
  serializableMappingCache.set(clone);

  freezeObjectInDev(value);
  return clone;
}

/**
 * TurboModuleLike objects are JS objects that have a TurboModule as their
 * prototype.
 */
function cloneTurboModuleLike<T extends object>(
  value: T,
  shouldPersistRemote: boolean,
  depth: number
): SerializableRef<T> {
  const proto = Object.getPrototypeOf(value);
  const clonedProps = cloneObjectProperties(value, shouldPersistRemote, depth);
  const clone = WorkletsModule.createSerializableTurboModuleLike(
    clonedProps,
    proto
  ) as SerializableRef<T>;
  return clone;
}

function cloneContextObject<T extends object>(value: T): SerializableRef<T> {
  const workletContextObjectFactory = (value as Record<string, unknown>)
    .__workletContextObjectFactory as () => T;
  const handle = cloneInitializer({
    __init: () => {
      'worklet';
      return workletContextObjectFactory();
    },
  });
  serializableMappingCache.set(value, handle);
  return handle as SerializableRef<T>;
}

function clonePlainJSObject<T extends object>(
  value: T,
  shouldPersistRemote: boolean,
  depth: number
): SerializableRef<T> {
  const clonedProps: Record<string, unknown> = cloneObjectProperties(
    value,
    shouldPersistRemote,
    depth
  );
  const clone = WorkletsModule.createSerializableObject(
    clonedProps,
    shouldPersistRemote,
    value
  ) as SerializableRef<T>;
  serializableMappingCache.set(value, clone);
  serializableMappingCache.set(clone);

  freezeObjectInDev(value);
  return clone;
}

function cloneMap<T extends Map<unknown, unknown>>(
  value: T
): SerializableRef<T> {
  const clonedKeys: unknown[] = [];
  const clonedValues: unknown[] = [];
  for (const [key, element] of value.entries()) {
    clonedKeys.push(createSerializable(key));
    clonedValues.push(createSerializable(element));
  }
  const clone = WorkletsModule.createSerializableMap(
    clonedKeys,
    clonedValues
  ) as SerializableRef<T>;
  serializableMappingCache.set(value, clone);
  serializableMappingCache.set(clone);

  freezeObjectInDev(value);
  return clone;
}

function cloneSet<T extends Set<unknown>>(value: T): SerializableRef<T> {
  const clonedElements: unknown[] = [];
  for (const element of value) {
    clonedElements.push(createSerializable(element));
  }
  const clone = WorkletsModule.createSerializableSet(
    clonedElements
  ) as SerializableRef<T>;
  serializableMappingCache.set(value, clone);
  serializableMappingCache.set(clone);

  freezeObjectInDev(value);
  return clone;
}

function cloneRegExp<T extends RegExp>(value: T): SerializableRef<T> {
  const pattern = value.source;
  const flags = value.flags;
  const handle = cloneInitializer({
    __init: () => {
      'worklet';
      return new RegExp(pattern, flags);
    },
  }) as unknown as SerializableRef<T>;
  serializableMappingCache.set(value, handle);

  return handle;
}

function cloneError<T extends Error>(value: T): SerializableRef<T> {
  const { name, message, stack } = value;
  const handle = cloneInitializer({
    __init: () => {
      'worklet';
      // eslint-disable-next-line reanimated/use-worklets-error
      const error = new Error();
      error.name = name;
      error.message = message;
      error.stack = stack;
      return error;
    },
  });
  serializableMappingCache.set(value, handle);
  return handle as unknown as SerializableRef<T>;
}

function cloneArrayBuffer<T extends ArrayBuffer>(
  value: T,
  shouldPersistRemote: boolean
): SerializableRef<T> {
  const clone = WorkletsModule.createSerializable(
    value,
    shouldPersistRemote,
    value
  );
  serializableMappingCache.set(value, clone);
  serializableMappingCache.set(clone);

  return clone;
}

function cloneArrayBufferView<T extends ArrayBufferView>(
  value: T
): SerializableRef<T> {
  const buffer = value.buffer;
  const typeName = value.constructor.name;
  const handle = cloneInitializer({
    __init: () => {
      'worklet';
      if (!VALID_ARRAY_VIEWS_NAMES.includes(typeName)) {
        throw new WorkletsError(`Invalid array view name \`${typeName}\`.`);
      }
      const constructor = global[typeName as keyof typeof global];
      if (constructor === undefined) {
        throw new WorkletsError(`Constructor for \`${typeName}\` not found.`);
      }
      return new constructor(buffer);
    },
  }) as unknown as SerializableRef<T>;
  serializableMappingCache.set(value, handle);

  return handle;
}

function cloneSynchronizable<TValue>(
  value: Synchronizable<TValue>
): SerializableRef<TValue> {
  serializableMappingCache.set(value);
  return value;
}

function cloneImport<TValue extends WorkletImport>(
  value: TValue
): SerializableRef<TValue> {
  const { source, imported } = value.__bundleData;
  const clone = WorkletsModule.createSerializableImport(source, imported);

  serializableMappingCache.set(value, clone);
  serializableMappingCache.set(clone);

  return clone as SerializableRef<TValue>;
}

function inaccessibleObject<T extends object>(value: T): SerializableRef<T> {
  // This is reached for object types that are not of plain Object.prototype.
  // We don't support such objects from being transferred as serializables to
  // the UI runtime and hence we replace them with "inaccessible object"
  // which is implemented as a Proxy object that throws on any attempt
  // of accessing its fields. We argue that such objects can sometimes leak
  // as attributes of objects being captured by worklets but should never
  // be used on the UI runtime regardless. If they are being accessed, the user
  // will get an appropriate error message.
  const clone = createSerializable<T>(INACCESSIBLE_OBJECT as T);
  serializableMappingCache.set(value, clone);
  return clone;
}

const WORKLET_CODE_THRESHOLD = 255;

function getWorkletCode(value: WorkletFunction) {
  const code = value?.__initData?.code;
  if (!code) {
    return 'unknown';
  }
  if (code.length > WORKLET_CODE_THRESHOLD) {
    return `${code.substring(0, WORKLET_CODE_THRESHOLD)}...`;
  }
  return code;
}

type RemoteFunction<T> = {
  __remoteFunction: FlatSerializableRef<T>;
};

function isRemoteFunction<T>(value: {
  __remoteFunction?: unknown;
}): value is RemoteFunction<T> {
  'worklet';
  return !!value.__remoteFunction;
}

/**
 * We freeze
 *
 * - Arrays,
 * - Remote functions,
 * - Plain JS objects,
 *
 * That are transformed to a serializable with a meaningful warning. This should
 * help detect issues when someone modifies data after it's been converted.
 * Meaning that they may be doing a faulty assumption in their code expecting
 * that the updates are going to automatically propagate to the object sent to
 * the UI thread. If the user really wants some objects to be mutable they
 * should use shared values instead.
 */
function freezeObjectInDev<T extends object>(value: T) {
  if (!__DEV__) {
    return;
  }
  Object.entries(value).forEach(([key, element]) => {
    const descriptor = Object.getOwnPropertyDescriptor(value, key)!;
    if (!descriptor.configurable) {
      return;
    }
    Object.defineProperty(value, key, {
      get() {
        return element;
      },
      set() {
        logger.warn(
          `Tried to modify key \`${key}\` of an object which has been already passed to a worklet. See
        https://docs.swmansion.com/react-native-reanimated/docs/guides/troubleshooting#tried-to-modify-key-of-an-object-which-has-been-converted-to-a-serializable
        for more details.`
        );
      },
    });
  });
  Object.preventExtensions(value);
}

function makeShareableCloneOnUIRecursiveLEGACY<T>(
  value: T
): FlatSerializableRef<T> {
  'worklet';
  if (SHOULD_BE_USE_WEB) {
    // @ts-ignore web is an interesting place where we don't run a secondary VM on the UI thread
    // see more details in the comment where USE_STUB_IMPLEMENTATION is defined.
    return value;
  }
  // eslint-disable-next-line @typescript-eslint/no-shadow
  function cloneRecursive(value: T): FlatSerializableRef<T> {
    if (
      (typeof value === 'object' && value !== null) ||
      typeof value === 'function'
    ) {
      if (isHostObject(value)) {
        // We call `_createSerializableClone` to wrap the provided HostObject
        // inside SerializableJSRef.
        return global._createSerializableHostObject(
          value
        ) as FlatSerializableRef<T>;
      }
      if (isRemoteFunction<T>(value)) {
        // RemoteFunctions are created by us therefore they are
        // a Serializable out of the box and there is no need to
        // call `_createSerializableClone`.
        return value.__remoteFunction;
      }
      if (Array.isArray(value)) {
        return global._createSerializableArray(
          value.map(cloneRecursive)
        ) as FlatSerializableRef<T>;
      }
      if ((value as Record<string, unknown>).__synchronizableRef) {
        return global._createSerializableSynchronizable(
          value
        ) as FlatSerializableRef<T>;
      }
      const toAdapt: Record<string, FlatSerializableRef<T>> = {};
      for (const [key, element] of Object.entries(value)) {
        toAdapt[key] = cloneRecursive(element);
      }
      return global._createSerializable(
        toAdapt,
        value
      ) as FlatSerializableRef<T>;
    }

    if (typeof value === 'string') {
      return global._createSerializableString(value);
    }

    if (typeof value === 'number') {
      return global._createSerializableNumber(value);
    }

    if (typeof value === 'boolean') {
      return global._createSerializableBoolean(value);
    }

    if (typeof value === 'bigint') {
      return global._createSerializableBigInt(value);
    }

    if (value === undefined) {
      return global._createSerializableUndefined();
    }

    if (value === null) {
      return global._createSerializableNull();
    }

    return global._createSerializable(value, undefined);
  }
  return cloneRecursive(value);
}

/** @deprecated This function is no longer supported. */
export const makeShareableCloneOnUIRecursive = (
  globalThis._WORKLETS_BUNDLE_MODE
    ? createSerializable
    : makeShareableCloneOnUIRecursiveLEGACY
) as typeof makeShareableCloneOnUIRecursiveLEGACY;

function makeShareableJS<T>(value: T): T {
  return value;
}

function makeShareableNative<T extends object>(value: T): T {
  if (serializableMappingCache.get(value)) {
    return value;
  }
  const handle = createSerializable({
    __init: () => {
      'worklet';
      return value;
    },
  });
  serializableMappingCache.set(value, handle);
  return value;
}

/**
 * This function creates a value on UI with persistent state - changes to it on
 * the UI thread will be seen by all worklets. Use it when you want to create a
 * value that is read and written only on the UI thread.
 */
/** @deprecated This function is no longer supported. */
export const makeShareable = SHOULD_BE_USE_WEB
  ? makeShareableJS
  : makeShareableNative;
