import { UnknownSymbolError, InvalidType, type TracingLogger } from '@/utils';
import { matchSignature, type Denotable } from '.';

export class Environment {
  private scope: Map<string, Denotable>;
  private parent: Environment | null;
  private logger: TracingLogger;

  constructor(logger: TracingLogger, parent: Environment | null = null) {
    this.logger = logger;
    this.parent = parent;
    this.scope = new Map();
  }

  public set(name: string, value: Denotable) {
    this.scope.set(name, value);
  }

  public get(name: string): Denotable {
    if (this.scope.has(name)) {
      this.logger.debug(`Found Name=(${name}) in current scope`);
      return this.scope.get(name)!;
    }

    if (this.parent) {
      this.logger.debug(`Looking for Name=(${name}) in parent scope`);
      return this.parent.get(name);
    }

    throw new UnknownSymbolError(`Undefined variable: ${name}`);
  }

  public has(name: string): boolean {
    if (this.scope.has(name)) {
      this.logger.debug(`Found Name=(${name}) in current scope`);
      return true;
    }

    if (this.parent) {
      this.logger.debug(`Found Name=(${name}) in current scope`);
      return this.parent.has(name);
    }

    this.logger.debug(`Name=(${name}) not found in any scope`);
    return false;
  }

  public createChild(): Environment {
    return new Environment(this.logger.createChild('Env'), this);
  }

  public apply(name: string, args: Denotable[]): Denotable {
    const fn = this.get(name);
    if (typeof fn.value !== 'object' || !fn.value || !('body' in fn.value)) {
      throw new InvalidType(name + ' is not a valid function');
    }

    const argTypes = args.map(arg => {
      const { type, value } = arg;
      const isFunction =
        type === 'function' &&
        typeof value === 'object' &&
        value &&
        'signatures' in value;
      if (isFunction) {
        return value.signatures;
      }
      return type;
    });

    const appliedSignature = matchSignature(argTypes, fn.value.signatures);
    if (!appliedSignature) {
      throw new InvalidType(`No matching signature for ${name}`);
    }

    this.logger.debug(
      `Applying Function=(${name}) with Args=(${JSON.stringify(args)}) with Signature=(${JSON.stringify(appliedSignature)})`,
    );

    const value = fn.value.body.apply(this, args);
    return { type: appliedSignature.return, value };
  }
}