import type { ExecutionContextHostDefined, GCMarker } from '../host-defined/engine.mts';
import { __ts_cast__ } from '../helpers.mts';
import {
  type YieldEvaluator, NullValue, type FunctionObject, Value, type GeneratorObject, type AsyncGeneratorObject, AbstractModuleRecord, type ScriptRecord, EnvironmentRecord, PrivateEnvironmentRecord, CallSite, PromiseCapabilityRecord, Realm,
  surroundingAgent,
  Assert,
  GetIdentifierReference,
  JSStringValue,
  UndefinedValue,
  type EnvironmentRecordWithThisBinding,
  ObjectValue,
} from '#self';


/** https://tc39.es/ecma262/#sec-execution-contexts */
export class ExecutionContext {
  codeEvaluationState?: YieldEvaluator;

  Function: NullValue | FunctionObject = Value.null;

  Generator?: GeneratorObject | AsyncGeneratorObject;

  ScriptOrModule: AbstractModuleRecord | ScriptRecord | NullValue = Value.null;

  VariableEnvironment!: EnvironmentRecord;

  LexicalEnvironment!: EnvironmentRecord;

  PrivateEnvironment: PrivateEnvironmentRecord | NullValue = Value.null;

  HostDefined?: ExecutionContextHostDefined;

  // NON-SPEC
  callSite = new CallSite(this);

  promiseCapability?: PromiseCapabilityRecord;

  poppedForTailCall = false;

  Realm!: Realm;

  copy() {
    const e = new ExecutionContext();
    e.codeEvaluationState = this.codeEvaluationState;
    e.Function = this.Function;
    e.Realm = this.Realm;
    e.ScriptOrModule = this.ScriptOrModule;
    e.VariableEnvironment = this.VariableEnvironment;
    e.LexicalEnvironment = this.LexicalEnvironment;
    e.PrivateEnvironment = this.PrivateEnvironment;
    e.HostDefined = this.HostDefined;

    e.callSite = this.callSite.clone(e);
    e.promiseCapability = this.promiseCapability;
    return e;
  }

  // NON-SPEC
  mark(m: GCMarker) {
    m(this.Function);
    m(this.Realm);
    m(this.ScriptOrModule);
    m(this.VariableEnvironment);
    m(this.LexicalEnvironment);
    m(this.PrivateEnvironment);
    m(this.promiseCapability);
  }
}

/** https://tc39.es/ecma262/#sec-getactivescriptormodule */
export function GetActiveScriptOrModule() {
  for (let i = surroundingAgent.executionContextStack.length - 1; i >= 0; i -= 1) {
    const e = surroundingAgent.executionContextStack[i];
    if (e.ScriptOrModule !== Value.null) {
      return e.ScriptOrModule;
    }
  }
  return Value.null;
}

/** https://tc39.es/ecma262/#sec-resolvebinding */
export function ResolveBinding(name: JSStringValue, env?: EnvironmentRecord | UndefinedValue | NullValue, strict?: boolean) {
  // 1. If env is not present or if env is undefined, then
  if (env === undefined || env === Value.undefined) {
    // a. Set env to the running execution context's LexicalEnvironment.
    env = surroundingAgent.runningExecutionContext.LexicalEnvironment;
  }
  // 2. Assert: env is an Environment Record.
  Assert(env instanceof EnvironmentRecord);
  // 3. If the code matching the syntactic production that is being evaluated is contained in strict mode code, let strict be true; else let strict be false.
  // 4. Return ? GetIdentifierReference(env, name, strict).
  return GetIdentifierReference(env, name, strict ? Value.true : Value.false);
}

/** https://tc39.es/ecma262/#sec-getthisenvironment */
export function GetThisEnvironment(): EnvironmentRecordWithThisBinding {
  // 1. Let env be the running execution context's LexicalEnvironment.
  let env = surroundingAgent.runningExecutionContext.LexicalEnvironment;
  // 2. Repeat,
  while (true) {
    __ts_cast__<EnvironmentRecord>(env);
    // a. Let exists be env.HasThisBinding().
    const exists = env.HasThisBinding();
    // b. If exists is true, return envRec.
    if (exists === Value.true) {
      return env as EnvironmentRecordWithThisBinding;
    }
    // c. Let outer be env.[[OuterEnv]].
    const outer = env.OuterEnv;
    // d. Assert: outer is not null.
    Assert(!(outer instanceof NullValue));
    // e. Set env to outer.
    env = outer;
  }
}

/** https://tc39.es/ecma262/#sec-resolvethisbinding */
export function ResolveThisBinding() {
  const envRec = GetThisEnvironment();
  return envRec.GetThisBinding();
}

/** https://tc39.es/ecma262/#sec-getnewtarget */
export function GetNewTarget(): ObjectValue | UndefinedValue {
  const envRec = GetThisEnvironment();
  Assert('NewTarget' in envRec);
  return envRec.NewTarget;
}

/** https://tc39.es/ecma262/#sec-getglobalobject */
export function GetGlobalObject() {
  const currentRealm = surroundingAgent.currentRealmRecord;
  return currentRealm.GlobalObject;
}
