import { abortSignalToPromise } from '../../abort-signal';
import {
  createAggregateErrorNode,
  createArrayBufferNode,
  createArrayNode,
  createAsyncIteratorFactoryInstanceNode,
  createBigIntNode,
  createBigIntTypedArrayNode,
  createBoxedNode,
  createDataViewNode,
  createDateNode,
  createErrorNode,
  createIteratorFactoryInstanceNode,
  createNumberNode,
  createPluginNode,
  createRegExpNode,
  createSetNode,
  createStreamConstructorNode,
  createStreamNextNode,
  createStreamReturnNode,
  createStreamThrowNode,
  createStringNode,
  createTypedArrayNode,
} from '../../base-primitives';
import { Feature } from '../../compat';
import { NIL, SerovalNodeType } from '../../constants';
import { SerovalParserError, SerovalUnsupportedTypeError } from '../../errors';
import {
  FALSE_NODE,
  NULL_NODE,
  TRUE_NODE,
  UNDEFINED_NODE,
} from '../../literals';
import { createSerovalNode } from '../../node';
import { OpaqueReference } from '../../opaque-reference';
import { SpecialReference } from '../../special-reference';
import type { Stream } from '../../stream';
import { createStreamFromAsyncIterable, isStream } from '../../stream';
import { serializeString } from '../../string';
import type {
  SerovalAbortSignalSyncNode,
  SerovalAggregateErrorNode,
  SerovalArrayNode,
  SerovalBigIntTypedArrayNode,
  SerovalBoxedNode,
  SerovalDataViewNode,
  SerovalErrorNode,
  SerovalMapNode,
  SerovalNode,
  SerovalNullConstructorNode,
  SerovalObjectNode,
  SerovalObjectRecordKey,
  SerovalObjectRecordNode,
  SerovalPluginNode,
  SerovalPromiseNode,
  SerovalSetNode,
  SerovalStreamConstructorNode,
  SerovalTypedArrayNode,
} from '../../types';
import { getErrorOptions } from '../../utils/error';
import { iteratorToSequence } from '../../utils/iterator-to-sequence';
import promiseToResult from '../../utils/promise-to-result';
import type {
  BigIntTypedArrayValue,
  TypedArrayValue,
} from '../../utils/typed-array';
import { BaseParserContext, ParserNodeType } from '../parser';

type ObjectLikeNode =
  | SerovalObjectNode
  | SerovalNullConstructorNode
  | SerovalPromiseNode;

export default abstract class BaseAsyncParserContext extends BaseParserContext {
  private async parseItems(current: unknown[]): Promise<SerovalNode[]> {
    const nodes = [];
    for (let i = 0, len = current.length; i < len; i++) {
      // For consistency in holes
      if (i in current) {
        nodes[i] = await this.parse(current[i]);
      }
    }
    return nodes;
  }

  private async parseArray(
    id: number,
    current: unknown[],
  ): Promise<SerovalArrayNode> {
    return createArrayNode(id, current, await this.parseItems(current));
  }

  private async parseProperties(
    properties: Record<string | symbol, unknown>,
  ): Promise<SerovalObjectRecordNode> {
    const entries = Object.entries(properties);
    const keyNodes: SerovalObjectRecordKey[] = [];
    const valueNodes: SerovalNode[] = [];
    for (let i = 0, len = entries.length; i < len; i++) {
      keyNodes.push(serializeString(entries[i][0]));
      valueNodes.push(await this.parse(entries[i][1]));
    }
    // Check special properties
    let symbol = Symbol.iterator;
    if (symbol in properties) {
      keyNodes.push(this.parseWellKnownSymbol(symbol));
      valueNodes.push(
        createIteratorFactoryInstanceNode(
          this.parseIteratorFactory(),
          await this.parse(
            iteratorToSequence(properties as unknown as Iterable<unknown>),
          ),
        ),
      );
    }
    symbol = Symbol.asyncIterator;
    if (symbol in properties) {
      keyNodes.push(this.parseWellKnownSymbol(symbol));
      valueNodes.push(
        createAsyncIteratorFactoryInstanceNode(
          this.parseAsyncIteratorFactory(),
          await this.parse(
            createStreamFromAsyncIterable(
              properties as unknown as AsyncIterable<unknown>,
            ),
          ),
        ),
      );
    }
    symbol = Symbol.toStringTag;
    if (symbol in properties) {
      keyNodes.push(this.parseWellKnownSymbol(symbol));
      valueNodes.push(createStringNode(properties[symbol] as string));
    }
    symbol = Symbol.isConcatSpreadable;
    if (symbol in properties) {
      keyNodes.push(this.parseWellKnownSymbol(symbol));
      valueNodes.push(properties[symbol] ? TRUE_NODE : FALSE_NODE);
    }
    return {
      k: keyNodes,
      v: valueNodes,
      s: keyNodes.length,
    };
  }

  private async parsePlainObject(
    id: number,
    current: Record<string, unknown>,
    empty: boolean,
  ): Promise<ObjectLikeNode> {
    return this.createObjectNode(
      id,
      current,
      empty,
      await this.parseProperties(current),
    );
  }

  private async parseBoxed(
    id: number,
    current: object,
  ): Promise<SerovalBoxedNode> {
    return createBoxedNode(id, await this.parse(current.valueOf()));
  }

  private async parseTypedArray(
    id: number,
    current: TypedArrayValue,
  ): Promise<SerovalTypedArrayNode> {
    return createTypedArrayNode(id, current, await this.parse(current.buffer));
  }

  private async parseBigIntTypedArray(
    id: number,
    current: BigIntTypedArrayValue,
  ): Promise<SerovalBigIntTypedArrayNode> {
    return createBigIntTypedArrayNode(
      id,
      current,
      await this.parse(current.buffer),
    );
  }

  private async parseDataView(
    id: number,
    current: DataView,
  ): Promise<SerovalDataViewNode> {
    return createDataViewNode(id, current, await this.parse(current.buffer));
  }

  private async parseError(
    id: number,
    current: Error,
  ): Promise<SerovalErrorNode> {
    const options = getErrorOptions(current, this.features);
    return createErrorNode(
      id,
      current,
      options ? await this.parseProperties(options) : NIL,
    );
  }

  private async parseAggregateError(
    id: number,
    current: AggregateError,
  ): Promise<SerovalAggregateErrorNode> {
    const options = getErrorOptions(current, this.features);
    return createAggregateErrorNode(
      id,
      current,
      options ? await this.parseProperties(options) : NIL,
    );
  }

  private async parseMap(
    id: number,
    current: Map<unknown, unknown>,
  ): Promise<SerovalMapNode> {
    const keyNodes: SerovalNode[] = [];
    const valueNodes: SerovalNode[] = [];
    for (const [key, value] of current.entries()) {
      keyNodes.push(await this.parse(key));
      valueNodes.push(await this.parse(value));
    }
    return this.createMapNode(id, keyNodes, valueNodes, current.size);
  }

  private async parseSet(
    id: number,
    current: Set<unknown>,
  ): Promise<SerovalSetNode> {
    const items: SerovalNode[] = [];
    for (const item of current.keys()) {
      items.push(await this.parse(item));
    }
    return createSetNode(id, current.size, items);
  }

  private async parsePromise(
    id: number,
    current: Promise<unknown>,
  ): Promise<SerovalPromiseNode> {
    const [status, result] = await promiseToResult(current);

    return createSerovalNode(
      SerovalNodeType.Promise,
      id,
      status,
      NIL,
      NIL,
      NIL,
      NIL,
      NIL,
      NIL,
      await this.parse(result),
      NIL,
      NIL,
    );
  }

  private async parsePlugin(
    id: number,
    current: unknown,
  ): Promise<SerovalPluginNode | undefined> {
    const currentPlugins = this.plugins;
    if (currentPlugins) {
      for (let i = 0, len = currentPlugins.length; i < len; i++) {
        const plugin = currentPlugins[i];
        if (plugin.parse.async && plugin.test(current)) {
          return createPluginNode(
            id,
            plugin.tag,
            await plugin.parse.async(current, this, {
              id,
            }),
          );
        }
      }
    }
    return NIL;
  }

  private async parseStream(
    id: number,
    current: Stream<unknown>,
  ): Promise<SerovalStreamConstructorNode> {
    return createStreamConstructorNode(
      id,
      this.parseSpecialReference(SpecialReference.StreamConstructor),
      await new Promise<SerovalNode[]>((resolve, reject) => {
        const sequence: SerovalNode[] = [];
        const cleanup = current.on({
          next: value => {
            this.markRef(id);
            this.parse(value).then(
              data => {
                sequence.push(createStreamNextNode(id, data));
              },
              data => {
                reject(data);
                cleanup();
              },
            );
          },
          throw: value => {
            this.markRef(id);
            this.parse(value).then(
              data => {
                sequence.push(createStreamThrowNode(id, data));
                resolve(sequence);
                cleanup();
              },
              data => {
                reject(data);
                cleanup();
              },
            );
          },
          return: value => {
            this.markRef(id);
            this.parse(value).then(
              data => {
                sequence.push(createStreamReturnNode(id, data));
                resolve(sequence);
                cleanup();
              },
              data => {
                reject(data);
                cleanup();
              },
            );
          },
        });
      }),
    );
  }

  private async parseAbortSignalSync(
    id: number,
    current: AbortSignal,
  ): Promise<SerovalAbortSignalSyncNode> {
    return createSerovalNode(
      SerovalNodeType.AbortSignalSync,
      id,
      NIL,
      NIL,
      NIL,
      NIL,
      NIL,
      NIL,
      NIL,
      await this.parse(current.reason),
      NIL,
      NIL,
    );
  }

  private async parseAbortSignal(
    id: number,
    current: AbortSignal,
  ): Promise<SerovalAbortSignalSyncNode> {
    if (!current.aborted) {
      await abortSignalToPromise(current);
    }
    return this.parseAbortSignalSync(id, current);
  }

  // biome-ignore lint/complexity/noExcessiveCognitiveComplexity: ehh
  private async parseObject(id: number, current: object): Promise<SerovalNode> {
    if (Array.isArray(current)) {
      return this.parseArray(id, current);
    }
    if (isStream(current)) {
      return this.parseStream(id, current);
    }
    const currentClass = current.constructor;
    if (currentClass === OpaqueReference) {
      return this.parse(
        (current as OpaqueReference<unknown, unknown>).replacement,
      );
    }
    const parsed = await this.parsePlugin(id, current);
    if (parsed) {
      return parsed;
    }
    switch (currentClass) {
      case Object:
        return this.parsePlainObject(
          id,
          current as Record<string, unknown>,
          false,
        );
      case NIL:
        return this.parsePlainObject(
          id,
          current as Record<string, unknown>,
          true,
        );
      case Date:
        return createDateNode(id, current as unknown as Date);
      case RegExp:
        return createRegExpNode(id, current as unknown as RegExp);
      case Error:
      case EvalError:
      case RangeError:
      case ReferenceError:
      case SyntaxError:
      case TypeError:
      case URIError:
        return this.parseError(id, current as unknown as Error);
      case Number:
      case Boolean:
      case String:
      case BigInt:
        return this.parseBoxed(id, current);
      case ArrayBuffer:
        return createArrayBufferNode(id, current as unknown as ArrayBuffer);
      case Int8Array:
      case Int16Array:
      case Int32Array:
      case Uint8Array:
      case Uint16Array:
      case Uint32Array:
      case Uint8ClampedArray:
      case Float32Array:
      case Float64Array:
        return this.parseTypedArray(id, current as unknown as TypedArrayValue);
      case DataView:
        return this.parseDataView(id, current as unknown as DataView);
      case Map:
        return this.parseMap(id, current as unknown as Map<unknown, unknown>);
      case Set:
        return this.parseSet(id, current as unknown as Set<unknown>);
      default:
        break;
    }
    // Promises
    if (currentClass === Promise || current instanceof Promise) {
      return this.parsePromise(id, current as unknown as Promise<unknown>);
    }
    const currentFeatures = this.features;
    if (
      currentFeatures & Feature.AbortSignal &&
      typeof AbortSignal !== 'undefined' &&
      currentClass === AbortSignal
    ) {
      return this.parseAbortSignal(id, current as unknown as AbortSignal);
    }
    // BigInt Typed Arrays
    if (currentFeatures & Feature.BigIntTypedArray) {
      switch (currentClass) {
        case BigInt64Array:
        case BigUint64Array:
          return this.parseBigIntTypedArray(
            id,
            current as unknown as BigIntTypedArrayValue,
          );
        default:
          break;
      }
    }
    if (
      currentFeatures & Feature.AggregateError &&
      typeof AggregateError !== 'undefined' &&
      (currentClass === AggregateError || current instanceof AggregateError)
    ) {
      return this.parseAggregateError(id, current as unknown as AggregateError);
    }
    // Slow path. We only need to handle Errors and Iterators
    // since they have very broad implementations.
    if (current instanceof Error) {
      return this.parseError(id, current);
    }
    // Generator functions don't have a global constructor
    // despite existing
    if (Symbol.iterator in current || Symbol.asyncIterator in current) {
      return this.parsePlainObject(id, current, !!currentClass);
    }
    throw new SerovalUnsupportedTypeError(current);
  }

  protected async parseFunction(current: unknown): Promise<SerovalNode> {
    const ref = this.getReference(current);
    if (ref.type !== ParserNodeType.Fresh) {
      return ref.value;
    }
    const plugin = await this.parsePlugin(ref.value, current);
    if (plugin) {
      return plugin;
    }
    throw new SerovalUnsupportedTypeError(current);
  }

  async parse<T>(current: T): Promise<SerovalNode> {
    try {
      switch (typeof current) {
        case 'boolean':
          return current ? TRUE_NODE : FALSE_NODE;
        case 'undefined':
          return UNDEFINED_NODE;
        case 'string':
          return createStringNode(current as string);
        case 'number':
          return createNumberNode(current as number);
        case 'bigint':
          return createBigIntNode(current as bigint);
        case 'object': {
          if (current) {
            const ref = this.getReference(current);
            return ref.type === 0
              ? await this.parseObject(ref.value, current as object)
              : ref.value;
          }
          return NULL_NODE;
        }
        case 'symbol':
          return this.parseWellKnownSymbol(current);
        case 'function':
          return this.parseFunction(current);
        default:
          throw new SerovalUnsupportedTypeError(current);
      }
    } catch (error) {
      throw new SerovalParserError(error);
    }
  }
}
