import { TRANSFERABLE, SERIALIZE_METHOD } from '../constants.mjs';
import { isIterator, isBasicObject } from './shared.mjs';

const FUNCTION = '_@f';
const ASYNC_ITERATOR = '_@i';

/**
 * A thread encoder that can transfer supports a wide variety of JavaScript types by
 * converting them to a format that the [`structuredClone`](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm)
 * algorithm can process. This
 *
 * Additionally, this encoder will convert functions into a JSON-friendly representation.
 * These functions will use a manual memory management technique to ensure they can be
 * garbage collected by default, but can be configured to use a `WeakRef`-based automatic
 * memory management system by setting the `garbageCollection: 'weak-ref'` option.
 */
class ThreadSerializationStructuredClone {
  #customSerializer;
  #customDeserializer;
  constructor(options) {
    this.#customSerializer = options?.serialize;
    this.#customDeserializer = options?.deserialize;
  }

  /**
   * Serializes a value into a structured cloning-compatible format that can be transferred between threads.
   */
  serialize(value, thread, transferable) {
    return this.#serializeInternal(value, thread, transferable);
  }
  #serializeInternal(value, thread, transferable, seen = new Map(), isApplyingDefault = false) {
    if (value == null) return value;
    if (seen.has(value)) return seen.get(value);

    // Prevent circular references
    seen.set(value, undefined);
    if (typeof value === 'object') {
      if (this.#customSerializer && !isApplyingDefault) {
        const customValue = this.#customSerializer(value, value => this.#serializeInternal(value, thread, transferable, seen, true), thread, transferable);
        if (customValue !== undefined) {
          seen.set(value, customValue);
          return customValue;
        }
      }
      if (value[TRANSFERABLE]) {
        transferable?.push(value);
        seen.set(value, value);
        return value;
      }
      const serializeValue = value => {
        return this.#serializeInternal(value, thread, transferable, seen);
      };
      if (typeof value[SERIALIZE_METHOD] === 'function') {
        const result = value[SERIALIZE_METHOD]({
          serialize: serializeValue
        });
        seen.set(value, result);
        return result;
      }
      if (Array.isArray(value)) {
        const result = value.map(item => serializeValue(item));
        seen.set(value, result);
        return result;
      }
      if (value instanceof Map) {
        const entries = [...value.entries()].map(([key, value]) => {
          return [serializeValue(key), serializeValue(value)];
        });
        const result = new Map(entries);
        seen.set(value, result);
        return result;
      }
      if (value instanceof Set) {
        const entries = [...value].map(entry => serializeValue(entry));
        const result = new Set(entries);
        seen.set(value, result);
        return result;
      }
      const valueIsIterator = isIterator(value);
      if (isBasicObject(value) || valueIsIterator) {
        const result = {};
        for (const key of Object.keys(value)) {
          result[key] = serializeValue(value[key]);
        }
        if (valueIsIterator) {
          result.next ??= serializeValue(value.next.bind(value));
          result.return ??= serializeValue(value.return.bind(value));
          result.throw ??= serializeValue(value.throw.bind(value));
          result[ASYNC_ITERATOR] = true;
        }
        seen.set(value, result);
        return result;
      }
    }
    if (typeof value === 'function') {
      const serialized = thread.functions.serialize(value, thread, transferable);
      const result = {
        [FUNCTION]: serialized
      };
      seen.set(value, result);
      return result;
    }
    seen.set(value, value);
    return value;
  }

  /**
   * Deserializes a structured cloning-compatible value from another thread.
   */
  deserialize(value, thread) {
    return this.#deserializeInternal(value, thread);
  }
  #deserializeInternal(value, thread, isApplyingDefault = false) {
    if (value == null) return value;
    if (typeof value === 'object') {
      if (this.#customDeserializer && !isApplyingDefault) {
        const customValue = this.#customDeserializer(value, value => this.#deserializeInternal(value, thread, true), thread);
        if (customValue !== undefined) {
          return customValue;
        }
      }
      if (value == null) {
        return value;
      }
      if (Array.isArray(value)) {
        return value.map(value => this.#deserializeInternal(value, thread));
      }
      if (value instanceof Map) {
        return new Map([...value].map(([key, value]) => [this.#deserializeInternal(key, thread), this.#deserializeInternal(value, thread)]));
      }
      if (value instanceof Set) {
        return new Set([...value].map(entry => this.#deserializeInternal(entry, thread)));
      }
      if (FUNCTION in value) {
        const func = thread.functions.deserialize(value[FUNCTION], thread);
        return func;
      }
      if (!isBasicObject(value)) {
        return value;
      }
      const result = {};
      for (const key of Object.keys(value)) {
        if (key === ASYNC_ITERATOR) {
          result[Symbol.asyncIterator] = () => result;
        } else {
          result[key] = this.#deserializeInternal(value[key], thread);
        }
      }
      return result;
    }
    return value;
  }
}

export { ThreadSerializationStructuredClone };
