// @ts-check
/**
 * @description The file with the classes representing the different
 *     node types of the AST
 * @author Daniel del Castillo de la Rosa <alu0101225548@ull.edu.es>
 * @since 27/04/2021
 * @module PleaseLangAST
 */

'use strict';

/**
 * An object with the different keywords of the language
 */
const keywords = Object.create(null);

/**
 * A class representing a value. Can be a String or a Number
 */
class Value {
  /**
   * The constructor
   * @param {object} token A single token of type string or number
   */
  constructor(token) {
    this.type = 'Value';
    this.value = token.value;
  }

  /**
   * Evaluate the node
   * @return {string|number} The result of the evaluation
   */
  evaluate() {
    return this.value;
  }
}

/**
 * A class representing a word
 */
class Word {
  /**
   * The constructor
   * @param {object} token A single token of type word
   */
  constructor(token) {
    this.type = 'Word';
    this.name = token.name;
  }

  /**
   * Evaluate the node
   * @param {object} scope The scope in which the node will be evaluated
   * @return {*} The result of the evaluation
   */
  evaluate(scope) {
    if (this.name in scope) {
      return scope[this.name];
    } else {
      throw new ReferenceError(`Undefined binding: ${this.name}`);
    }
  }

  /**
   * Checks if the word is a keyword
   * @param {object} keywords The set of keywords
   * @return {boolean} Whether this instance is a keyword or not
   */
  isKeyword(keywords) {
    return this.name in keywords;
  }

  /**
   * Getter
   * @return {string} The inner value of the word
   */
  getName() {
    return this.name;
  }
}

/**
 * A class representing a call
 */
class Call {
  /**
   * The constructor
   * @param {object} operator A node representing the operator. Should
   *     evaluate to a function or a Word with the value of a keyword
   * @param {Array} args An array of nodes representing the arguments.
   *     Should be filled with nodes that evaluate to arbitrary values
   */
  constructor(operator, args) {
    this.type = 'Call';
    this.operator = operator;
    this.args = args;
  }

  /**
   * Evaluate the node
   * @param {object} scope The scope in which the node will be evaluated
   * @return {*} The result of the evaluation
   */
  evaluate(scope) {
    if (this.operator instanceof Word &&
        this.operator.isKeyword(keywords)) {
      return keywords[this.operator.getName()](this.args, scope);
    } else {
      const op = this.operator.evaluate(scope);
      if (typeof op === 'function') {
        return op(...this.args.map((arg) => arg.evaluate(scope)));
      } else {
        throw new TypeError('Calling a non-function.');
      }
    }
  }
}

module.exports = {Value, Word, Call, keywords};