// src/lisp/core/Environment.ts
// Environment management for LISP evaluation

import type { SExpr } from '../SExpr';
import type { ILambdaClosure } from '../ILambdaClosure';
import type { IMacroDef } from '../IMacroDef';
import type { Environment, Definitions } from '../EnvironmentTypes';

/**
 * EvalContext manages the evaluation environment including:
 * - Local variable bindings (lexical scope)
 * - Global definitions (def/define)
 * - Macro definitions
 */
export class EvalContext {
  /** Environment for variable bindings (lexical scope) */
  private _env: Environment = {};

  /** Global definitions (def/define) */
  private _definitions: Definitions = {};

  /**
   * Create a new EvalContext, optionally copying from another context
   */
  constructor(parent?: EvalContext) {
    if (parent) {
      this._env = { ...parent._env };
      this._definitions = { ...parent._definitions };
    }
  }

  // ===========================================================================
  // Environment access
  // ===========================================================================

  /**
   * Get current environment
   */
  get env(): Environment {
    return this._env;
  }

  /**
   * Set entire environment (for scope changes)
   */
  set env(value: Environment) {
    this._env = value;
  }

  /**
   * Get definitions
   */
  get definitions(): Definitions {
    return this._definitions;
  }

  // ===========================================================================
  // Variable lookup
  // ===========================================================================

  /**
   * Look up a variable in the environment or definitions
   */
  lookup(name: string): unknown {
    // Check environment (local bindings from let/lambda) first
    if (this._env[name] !== undefined) {
      return this._env[name];
    }
    // Check global definitions (from def/define)
    if (this._definitions[name] !== undefined) {
      return this._definitions[name];
    }
    return undefined;
  }

  /**
   * Check if a variable is defined
   */
  isDefined(name: string): boolean {
    return this._env[name] !== undefined || this._definitions[name] !== undefined;
  }

  /**
   * Check if a variable is a callable (lambda or macro)
   */
  isCallable(name: string): boolean {
    const val = this.lookup(name);
    return val !== undefined && typeof val === 'object' && val !== null && '__type' in val;
  }

  // ===========================================================================
  // Variable binding
  // ===========================================================================

  /**
   * Define a global definition
   */
  define(name: string, value: unknown): void {
    // If it's a lambda, patch its env so it can call itself (recursion!)
    if (this.isLambda(value)) {
      (value as ILambdaClosure).env[name] = value;
    }
    this._definitions[name] = value;
  }

  /**
   * Bind a variable in the current environment
   */
  bind(name: string, value: unknown): void {
    this._env[name] = value;
  }

  /**
   * Create a new scope with additional bindings
   */
  withBindings(bindings: Record<string, unknown>): EvalContext {
    const child = new EvalContext(this);
    child._env = { ...this._env, ...bindings };
    return child;
  }

  /**
   * Create a child scope (for let, lambda application)
   */
  pushScope(): { context: EvalContext; restore: () => void } {
    const savedEnv = this._env;
    this._env = { ...this._env };
    return {
      context: this,
      restore: () => { this._env = savedEnv; }
    };
  }

  // ===========================================================================
  // Type checking helpers
  // ===========================================================================

  /**
   * Check if a value is a lambda closure
   */
  isLambda(value: unknown): value is ILambdaClosure {
    return value !== null &&
           typeof value === 'object' &&
           '__type' in value &&
           (value as ILambdaClosure).__type === 'lambda';
  }

  /**
   * Check if a value is a macro definition
   */
  isMacro(value: unknown): value is IMacroDef {
    return value !== null &&
           typeof value === 'object' &&
           '__type' in value &&
           (value as IMacroDef).__type === 'macro';
  }

  /**
   * Get a macro definition by name
   */
  getMacro(name: string): IMacroDef | undefined {
    const val = this._definitions[name];
    if (this.isMacro(val)) {
      return val;
    }
    return undefined;
  }

  /**
   * Define a macro
   */
  defineMacro(name: string, params: string[], body: SExpr): void {
    this._definitions[name] = {
      __type: 'macro',
      params,
      body,
    } as IMacroDef;
  }
}
