import type { Protocol } from 'devtools-protocol';
import {
  DynamicParsedCodeRecord, type GCMarker, surroundingAgent,
} from './host-defined/engine.mts';
import { ExecutionContext } from './execution-context/ExecutionContext.mts';
import {
  Value, JSStringValue, ObjectValue, UndefinedValue, NullValue, type PropertyKeyValue,
  SymbolValue,
} from './value.mts';
import { Q } from './completion.mts';
import type { ParseNode } from './parser/ParseNode.mts';
import type {
  Evaluator, EvaluatorNextType, ValueEvaluator, YieldEvaluator,
} from './evaluator.mts';
import type { ErrorObject } from './intrinsics/Error.mts';
import {
  Call,
  isFunctionObject,
  isBuiltinFunctionObject,
  isECMAScriptFunctionObject,
} from '#self';

export const kInternal = Symbol('kInternal');

export class JSStringMap<V> implements Map<JSStringValue, V> {
  #map = new Map<string, V>();

  clear() {
    this.#map.clear();
  }

  delete(key: JSStringValue | string) {
    if (key instanceof JSStringValue) {
      key = key.stringValue();
    }
    return this.#map.delete(key);
  }

  forEach(callbackfn: (value: V, key: JSStringValue, map: Map<JSStringValue, V>) => void, thisArg?: JSStringMap<V>) {
    this.#map.forEach((value, key) => Reflect.apply(callbackfn, thisArg, [value, typeof key === 'string' ? Value(key) : key, this]));
  }

  get(key: JSStringValue | string) {
    if (key instanceof JSStringValue) {
      key = key.stringValue();
    }
    return this.#map.get(key);
  }

  has(key: JSStringValue | string) {
    if (key instanceof JSStringValue) {
      key = key.stringValue();
    }
    return this.#map.has(key);
  }

  set(key: JSStringValue | string, value: V): this {
    if (key instanceof JSStringValue) {
      key = key.stringValue();
    }
    this.#map.set(key, value);
    return this;
  }

  get size() {
    return this.#map.size;
  }

  * entries() {
    for (const [key, value] of this.#map.entries()) {
      yield [Value(key), value] as [JSStringValue, V];
    }
    return undefined;
  }

  * keys() {
    for (const key of this.#map.keys()) {
      yield Value(key);
    }
    return undefined;
  }

  values() {
    return this.#map.values();
  }

  declare [Symbol.iterator]: () => MapIterator<[JSStringValue, V]>;

  declare [Symbol.toStringTag]: string;

  static {
    JSStringMap.prototype[Symbol.toStringTag] = 'JSStringMap';
    JSStringMap.prototype[Symbol.iterator] = JSStringMap.prototype.entries;
  }

  mark(m: GCMarker) {
    for (const [k, v] of this.#map.entries()) {
      m(k);
      m(v);
    }
  }
}

export class PropertyKeyMap<V> implements Map<PropertyKeyValue, V> {
  #map = new Map<string | SymbolValue, V>();

  clear() {
    this.#map.clear();
  }

  delete(key: PropertyKeyValue | string) {
    if (key instanceof JSStringValue) {
      key = key.stringValue();
    }
    return this.#map.delete(key);
  }

  forEach(callbackfn: (value: V, key: PropertyKeyValue, map: Map<PropertyKeyValue, V>) => void, thisArg?: PropertyKeyMap<V>) {
    this.#map.forEach((value, key) => Reflect.apply(callbackfn, thisArg, [value, typeof key === 'string' ? Value(key) : key, this]));
  }

  get(key: PropertyKeyValue | string) {
    if (key instanceof JSStringValue) {
      key = key.stringValue();
    }
    return this.#map.get(key);
  }

  has(key: PropertyKeyValue | string) {
    if (key instanceof JSStringValue) {
      key = key.stringValue();
    }
    return this.#map.has(key);
  }

  set(key: PropertyKeyValue | string, value: V): this {
    if (key instanceof JSStringValue) {
      key = key.stringValue();
    }
    this.#map.set(key, value);
    return this;
  }

  get size() {
    return this.#map.size;
  }

  * entries() {
    for (const [key, value] of this.#map.entries()) {
      if (typeof key === 'string') {
        yield [Value(key), value] as [JSStringValue, V];
      } else {
        yield [key, value] as [SymbolValue, V];
      }
    }
    return undefined;
  }

  * keys() {
    for (const key of this.#map.keys()) {
      if (typeof key === 'string') {
        yield Value(key);
      } else {
        yield key;
      }
    }
    return undefined;
  }

  * values() {
    for (const value of this.#map.values()) {
      yield value;
    }
    return undefined;
  }

  declare [Symbol.iterator]: () => MapIterator<[PropertyKeyValue, V]>;

  declare [Symbol.toStringTag]: string;

  static {
    PropertyKeyMap.prototype[Symbol.toStringTag] = 'PropertyKeyMap';
    PropertyKeyMap.prototype[Symbol.iterator] = PropertyKeyMap.prototype.entries;
  }

  mark(m: GCMarker) {
    for (const [k, v] of this.#map.entries()) {
      m(k);
      m(v);
    }
  }
}

export class JSStringSet {
  #set = new Set<string>();

  constructor(value?: Iterable<JSStringValue | string>) {
    if (value) {
      for (const item of value) {
        this.add(item);
      }
    }
  }

  add(value: JSStringValue | string): this {
    this.#set.add(typeof value === 'string' ? value : value.stringValue());
    return this;
  }

  clear(): void {
    this.#set.clear();
  }

  delete(value: JSStringValue | string): boolean {
    return this.#set.delete(typeof value === 'string' ? value : value.stringValue());
  }

  forEach(callbackfn: (value: JSStringValue, value2: JSStringValue, set: Set<JSStringValue>) => void, thisArg?: JSStringSet): void {
    for (const value of this.#set) {
      Reflect.apply(callbackfn, thisArg, [Value(value), Value(value), this]);
    }
  }

  has(value: JSStringValue | NullValue | string): boolean {
    if (value instanceof NullValue) {
      return false;
    }
    return this.#set.has(typeof value === 'string' ? value : value.stringValue());
  }

  get size() {
    return this.#set.size;
  }

  * entries(): SetIterator<[JSStringValue, JSStringValue]> {
    for (const value of this.#set) {
      yield [Value(value), Value(value)];
    }
    return undefined;
  }

  declare keys: () => SetIterator<JSStringValue>;

  * values() {
    for (const value of this.#set) {
      yield Value(value);
    }
    return undefined;
  }

  declare [Symbol.iterator]: () => SetIterator<JSStringValue>;

  declare [Symbol.toStringTag]: string;

  static {
    JSStringSet.prototype[Symbol.toStringTag] = 'JSStringSet';
    JSStringSet.prototype[Symbol.iterator] = JSStringSet.prototype.values;
    JSStringSet.prototype.keys = JSStringSet.prototype.values;
  }

  mark(_m: GCMarker) { }
}

export class OutOfRange extends RangeError {
  /* node:coverage disable */
  declare cause: unknown;

  detail: unknown;

  constructor(fn: string, detail: unknown) {
    super(`${fn}() argument out of range`, { cause: detail });
    this.detail = detail;
  }
}
/* node:coverage enable */

export function skipDebugger<T>(iterator: Evaluator<T>, maxSteps = Infinity): T {
  let steps = 0;
  while (true) {
    const { done, value } = iterator.next({ type: 'debugger-resume', value: undefined });
    if (done) {
      return value;
    }
    /* node:coverage ignore next 4 */
    steps += 1;
    if (steps > maxSteps) {
      throw new RangeError('Max steps exceeded');
    }
  }
}

export function* resume(context: ExecutionContext, completion: EvaluatorNextType): YieldEvaluator {
  let result;
  while (true) {
    result = context.codeEvaluationState!.next(completion);
    if (result.done) {
      return result.value;
    }
    const { value } = result;
    if (value.type === 'debugger' || value.type === 'potential-debugger') {
      completion = yield value;
    } else if (value.type === 'await' || value.type === 'async-generator-yield') {
      return Value.undefined;
    } else if (value.type === 'yield') {
      return value.value;
    } else {
      unreachable(value);
    }
  }
}

export class CallSite {
  context: ExecutionContext;

  lastNode: ParseNode | null = null;

  nextNode: ParseNode | null = null;

  lastCallNode: ParseNode.CallExpression | null = null;

  inheritedLastCallNode: ParseNode.CallExpression | null = null;

  constructCall = false;

  constructor(context: ExecutionContext) {
    this.context = context;
  }

  clone(context = this.context) {
    const c = new CallSite(context);
    c.lastNode = this.lastNode;
    c.lastCallNode = this.lastCallNode;
    c.inheritedLastCallNode = this.inheritedLastCallNode;
    c.constructCall = this.constructCall;
    return c;
  }

  isTopLevel() {
    return this.context.Function === Value.null;
  }

  isConstructCall() {
    return this.constructCall;
  }

  isAsync() {
    if (!(this.context.Function instanceof NullValue) && isECMAScriptFunctionObject(this.context.Function) && this.context.Function.ECMAScriptCode) {
      const code = this.context.Function.ECMAScriptCode;
      return code.type === 'AsyncBody' || code.type === 'AsyncGeneratorBody';
    }
    return false;
  }

  isNative() {
    return isBuiltinFunctionObject(this.context.Function);
  }

  getFunctionName(): string | null {
    if (isFunctionObject(this.context.Function)) {
      const name = this.context.Function.properties.get('name');
      if (name && name.Value && name.Value instanceof JSStringValue) {
        return name.Value.stringValue();
      }
    }
    return null;
  }

  getSpecifier() {
    if (this.context.HostDefined?.scriptId && surroundingAgent.parsedSources.get(this.context.HostDefined.scriptId) instanceof DynamicParsedCodeRecord) {
      return null;
    }
    if (!(this.context.ScriptOrModule instanceof NullValue)) {
      return this.context.ScriptOrModule.HostDefined.specifier;
    }
    return null;
  }

  getScriptId() {
    const context = this.context.HostDefined?.scriptId;
    if (context) {
      return context;
    }
    if (!(this.context.ScriptOrModule instanceof NullValue)) {
      return this.context.ScriptOrModule.HostDefined.scriptId;
    }
    return undefined;
  }

  setLocation(node: ParseNode) {
    this.lastNode = node;
  }

  setNextLocation(node: ParseNode) {
    this.nextNode = node;
  }

  setCallLocation(node: ParseNode.CallExpression | null) {
    this.lastCallNode = node;
  }

  get lineNumber() {
    if (this.lastNode) {
      return this.lastNode.location.start.line;
    }
    return null;
  }

  get columnNumber() {
    if (this.lastNode) {
      return this.lastNode.location.start.column;
    }
    return null;
  }

  loc() {
    if (this.isNative()) {
      return 'native';
    }
    let out = '';
    const specifier = this.getSpecifier();
    if (specifier) {
      out += specifier;
    } else {
      out += '<anonymous>';
    }
    if (this.lineNumber !== null) {
      out += `:${this.lineNumber}`;
      if (this.columnNumber !== null) {
        out += `:${this.columnNumber}`;
      }
    }
    return out.trim();
  }

  toString() {
    const isAsync = this.isAsync();
    const functionName = this.getFunctionName();
    const isConstructCall = this.isConstructCall();
    const isMethodCall = !isConstructCall && !this.isTopLevel();

    let visualFunctionName;
    if (this.inheritedLastCallNode?.CallExpression.type === 'IdentifierReference') {
      visualFunctionName = this.inheritedLastCallNode.CallExpression.name;
    }
    if (visualFunctionName === functionName) {
      visualFunctionName = undefined;
    }

    let string = isAsync ? 'async ' : '';

    if (isConstructCall) {
      string += 'new ';
    }

    if (isMethodCall || isConstructCall) {
      if (functionName) {
        string += functionName;
      } else {
        string += '<anonymous>';
      }
      if (visualFunctionName) {
        string += ` (as ${visualFunctionName})`;
      }
    } else if (functionName) {
      string += functionName;
      if (visualFunctionName) {
        string += ` (as ${visualFunctionName})`;
      }
    } else {
      return `${string}${this.loc()}`;
    }

    return `${string} (${this.loc()})`;
  }

  toCallFrame(): Protocol.Runtime.CallFrame | undefined {
    const source = this.getScriptId();
    if (source === undefined || source === null) {
      return undefined;
    }
    return {
      columnNumber: (this.columnNumber || 1) - 1,
      lineNumber: (this.lineNumber || 1) - 1,
      functionName: this.getFunctionName() || '<anonymous>',
      scriptId: source,
      url: this.getSpecifier() || '<anonymous>',
    };
  }
}

export class CallFrame {
  columnNumber: number | undefined;

  lineNumber: number | undefined;

  functionName: string | undefined;

  scriptId: string | undefined;

  url: string | undefined;

  toCallFrame(): Protocol.Runtime.CallFrame | undefined {
    if (!this.scriptId) {
      return undefined;
    }
    return {
      columnNumber: (this.columnNumber || 1) - 1,
      lineNumber: (this.lineNumber || 1) - 1,
      functionName: this.functionName || '<anonymous>',
      scriptId: this.scriptId,
      url: this.url || '<anonymous>',
    };
  }
}

export const kAsyncContext = Symbol('kAsyncContext');

function captureAsyncStack(stack: CallSite[]) {
  let promise = stack[0].context.promiseCapability!.Promise;
  for (let i = 0; i < 10; i += 1) {
    if (promise.PromiseFulfillReactions!.length !== 1) {
      return;
    }
    const [reaction] = promise.PromiseFulfillReactions!;
    if (reaction.Handler && reaction.Handler.Callback[kAsyncContext]) {
      const asyncContext = reaction.Handler.Callback[kAsyncContext];
      stack.push(asyncContext.callSite.clone());
      if ('PromiseState' in asyncContext.promiseCapability!.Promise) {
        promise = asyncContext.promiseCapability!.Promise;
      } else {
        return;
      }
    } else if (!(reaction.Capability instanceof UndefinedValue)) {
      if ('PromiseState' in reaction.Capability.Promise) {
        promise = reaction.Capability.Promise;
      } else {
        return;
      }
    }
  }
}

export function getHostDefinedErrorStack(O: Value): (CallSite | CallFrame)[] | undefined {
  if (O instanceof ObjectValue && 'HostDefinedErrorStack' in O && isArray((O as ErrorObject).HostDefinedErrorStack)) {
    return (O as ErrorObject).HostDefinedErrorStack as (CallSite | CallFrame)[];
  }
  return undefined;
}

export function getCurrentStack(excludeGlobalStack = true) {
  const stack: CallSite[] = [];
  for (let i = surroundingAgent.executionContextStack.length - (excludeGlobalStack ? 2 : 1); i >= 0; i -= 1) {
    const e = surroundingAgent.executionContextStack[i];
    if (e.VariableEnvironment === undefined && e.Function === Value.null) {
      break;
    }
    const clone = e.callSite.clone();
    const parent = stack[stack.length - 1];
    if (parent && !parent.context.poppedForTailCall) {
      parent.inheritedLastCallNode = clone.lastCallNode;
    }
    stack.push(clone);
    if (e.callSite.isAsync()) {
      i -= 1; // skip original execution context which has no useful information.
    }
  }

  if (stack.length > 0 && stack[0].context.promiseCapability) {
    captureAsyncStack(stack);
  }
  return stack;
}

export function captureStack() {
  const stack = getCurrentStack();

  let nativeStack: string | undefined;
  if (surroundingAgent.hostDefinedOptions.errorStackAttachNativeStack) {
    const origStackTraceLimit = Error.stackTraceLimit;
    Error.stackTraceLimit = 12;
    try {
      nativeStack = new Error().stack;
    } finally {
      Error.stackTraceLimit = origStackTraceLimit;
    }
  }

  return {
    stack,
    nativeStack,
  };
}

export function* callSiteToErrorString(O: ErrorObject, stack: readonly CallSite[], nativeStack?: string): ValueEvaluator<JSStringValue> {
  const errorString = (Q(yield* Call(surroundingAgent.intrinsic('%Error.prototype.toString%'), O)) as JSStringValue).stringValue();
  const errorStack = callSiteToErrorStack(stack, nativeStack);
  return Value(errorString + errorStack);
}

export function callSiteToErrorStack(stack: readonly CallSite[], nativeStack: string | undefined) {
  let errorString = '';
  stack.forEach((s) => {
    errorString = `${errorString}\n    at ${s.toString()}`;
  });
  if (typeof nativeStack === 'string') {
    errorString = `${errorString}\n    <NATIVE>\n${nativeStack.split('\n').slice(6).join('\n')}`;
  }
  return errorString;
}

export function callable<Class extends object>(
  onCalled = (target: Class, _thisArg: unknown, args: unknown[]) => Reflect.construct(target as new (...args: unknown[]) => unknown, args),
) {
  const handler: ProxyHandler<Class> = Object.freeze({
    __proto__: null,
    apply: onCalled,
  });
  return function decorator(classValue: Class, _classContext: ClassDecoratorContext<Class & (new (...args: readonly unknown[]) => unknown)>) {
    return new Proxy(classValue, handler);
  };
}

export type Mutable<T> = {
  -readonly [P in keyof T]: T[P];
}

export const isArray: (arg: unknown) => arg is readonly unknown[] = Array.isArray;
export function unreachable(_: never): never {
  throw new Error('Unreachable');
}
export function __ts_cast__<T>(_value: unknown): asserts _value is T { }
