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

const ERROR = '_@e';
const FUNCTION = '_@f';
const MAP = '_@m';
const SET = '_@s';
const URL_ID = '_@u';
const DATE = '_@d';
const REGEXP = '_@r';
const ASYNC_ITERATOR = '_@i';
const UINT8_ARRAY = '_@u8';
const UINT16_ARRAY = '_@u16';
const UINT32_ARRAY = '_@u32';

/**
 * A thread serialization that can transfer an extended set of JavaScript types, by converting
 * them to a simpler, JSON-compatible representation. Aside from the types natively supported
 * by JSON, this serializer can also serialize and deserialize the following types:
 *
 * - `Date`
 * - `Error`
 * - `RegExp`
 * - `Map`
 * - `Set`
 * - `Uint8Array`
 */
class ThreadSerializationJSON {
  #customSerializer;
  #customDeserializer;
  constructor(options) {
    this.#customSerializer = options?.serialize;
    this.#customDeserializer = options?.deserialize;
  }

  /**
   * Serializes a value into a JSON-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 Error) {
        const result = {
          [ERROR]: {
            name: value.name,
            message: value.message,
            stack: value.stack
          }
        };
        seen.set(value, result);
        return result;
      }
      if (value instanceof Uint8Array) {
        const result = {
          [UINT8_ARRAY]: serializeUintArray(value)
        };
        seen.set(value, result);
        return result;
      }
      if (value instanceof Uint16Array) {
        const result = {
          [UINT16_ARRAY]: serializeUintArray(value)
        };
        seen.set(value, result);
        return result;
      }
      if (value instanceof Uint32Array) {
        const result = {
          [UINT32_ARRAY]: serializeUintArray(value)
        };
        seen.set(value, result);
        return result;
      }
      if (value instanceof RegExp) {
        const result = {
          [REGEXP]: [value.source, value.flags]
        };
        seen.set(value, result);
        return result;
      }
      if (value instanceof URL) {
        const result = {
          [URL_ID]: value.href
        };
        seen.set(value, result);
        return result;
      }
      if (value instanceof Date) {
        const result = {
          [DATE]: value.toISOString()
        };
        seen.set(value, result);
        return result;
      }
      if (value instanceof Map) {
        const entries = [...value.entries()].map(([key, value]) => {
          return [serializeValue(key), serializeValue(value)];
        });
        const result = {
          [MAP]: entries
        };
        seen.set(value, result);
        return result;
      }
      if (value instanceof Set) {
        const entries = [...value].map(entry => serializeValue(entry));
        const result = {
          [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 JSON-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 (ERROR in value) {
        const serialized = value[ERROR];
        const error = new Error(serialized.message);
        Object.assign(error, serialized);
        return error;
      }
      if (UINT8_ARRAY in value) {
        return deserializeUintArray(value[UINT8_ARRAY]);
      }
      if (UINT16_ARRAY in value) {
        return new Uint16Array(deserializeUintArray(value[UINT16_ARRAY]).buffer);
      }
      if (UINT32_ARRAY in value) {
        return new Uint32Array(deserializeUintArray(value[UINT32_ARRAY]).buffer);
      }
      if (REGEXP in value) {
        return new RegExp(...value[REGEXP]);
      }
      if (URL_ID in value) {
        return new URL(value[URL_ID]);
      }
      if (DATE in value) {
        return new Date(value[DATE]);
      }
      if (MAP in value) {
        return new Map(value[MAP].map(([key, value]) => [this.#deserializeInternal(key, thread), this.#deserializeInternal(value, thread)]));
      }
      if (SET in value) {
        return new Set(value[SET].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;
  }
}
function serializeUintArray(array) {
  let binary = '';
  const bytes = new Uint8Array(array.buffer);
  const length = bytes.byteLength;
  for (let i = 0; i < length; i++) {
    binary += String.fromCharCode(bytes[i]);
  }
  return btoa(binary);
}
function deserializeUintArray(base64String) {
  const binary = atob(base64String);
  const result = new Uint8Array(binary.length);
  for (let i = 0; i < binary.length; i++) {
    result[i] = binary.charCodeAt(i);
  }
  return result;
}

export { ThreadSerializationJSON };
