Source: eggmv.js

const fileSystem = require('fs');
const { parse, parseApply, parseExpression } = require('./parse.js');
//para que specialForms no tenga ningun metodo heredado
const specialForms = Object.create(null);

specialForms.if = (args, scope) => {
  if (args.length !== 3) {
    throw new SyntaxError('Wrong number of args to if');
  } else if (evaluate(args[0], scope) !== false) {
    return evaluate(args[1], scope);
  } else {
    return evaluate(args[2], scope);
  }
};

specialForms.while = (args, scope) => {
  debugger;
  if (args.length !== 2) {
    throw new SyntaxError('Wrong number of args to while');
  }
  // args[0] es la condicion, args[1] el codigo
  while (evaluate(args[0], scope) !== false) {
    evaluate(args[1], scope);
  }
  //egg no conoce undefined
  return false;
};

// ejecuta las sentencias que le pasemos una a una 
specialForms.do = (args, scope) => {
  let value = false;
  for (const arg of args) {
    value = evaluate(arg, scope);
  }
  return value;
};

/**
 * 
 * @param {Array<Object>} args array de objetos nodo a los que le asignaremos el valor
 * el valor se considera que es el ultimo de los argumentos
 * @param {Object} scope actual scope
 * @returns El valor asignado
 * @description recibe un array de nodos de los cuales, el ultimo de ellos se tratara
 * como el valor a asignar y el resto de los nodos se tratar como variables a las que
 * se le asignara ese valor
 */
specialForms.define = (args, scope) => {
  /*if (args.length !== 2) {
    throw new SyntaxError('Incorrect use of define');
  }
  if (args[0].type !== 'word') {
    throw new SyntaxError(`Incorrect use of define, ${args[0].name} is not a correct variable name`);
  }

  let value = evaluate(args[1], scope);
  scope[args[0].name] = value;
  return value;*/
  if (args.length < 2) {
    throw new SyntaxError('Incorrect use of define, expected more arguments');
  }
  const value = evaluate(args[args.length - 1], scope);
  for (let i = 0; i < args.length - 1; i++) {
    if (args[i].type !== 'word') {
      throw new SyntaxError(`Incorrect use of define, ${args[i].name} is not a correct variable name`);
    }
    //scope.hasOwnProperty = Object.hasOwnProperty;
    // como no hereda de object, scope no tiene hasOwnProperty
    //if (scope.hasOwnProperty(variable.name)) {
    if (Object.prototype.hasOwnProperty.call(scope, args[i].name)) {
      throw new SyntaxError(`Redefinition of ${args[i].name} variable.`);
    } else {
      scope[args[i].name] = value;
    }
  }
  return value;
};

/**
 * 
 * @param {Array<Object>} args array de objetos nodo a los que le asignaremos el valor
 * el valor se considera que es el ultimo de los argumentos
 * @param {Object} scope actual scope
 * @returns El valor asignado
 * @description recibe un array de nodos de los cuales, el ultimo de ellos se tratara
 * como el valor a asignar y el resto de los nodos se tratar como variables ya existentes
 * a las que se le asignara ese valor
 */
specialForms.set = (args, scope) => {
  if (args.length < 2) {
    throw new SyntaxError(`Incorrect use of set, more arguments expected`);
  }
  const value = evaluate(args[args.length - 1], scope);
  for (let i = 0; i < args.length - 1; i++) {
    if (args[i].type !== 'word') {
      throw new SyntaxError(`Incorrect use of set, ${args[i].name} is not a correct variable name`);
    }
    //if (scope[variable.name] !== undefined) {
    if (args[i].name in scope) {
      scope[args[i].name] = value;
    } else {
      throw new SyntaxError(`Incorrect use of set, ${args[i].name} is not defined on this scope`);
    }
  }
  return value;
};

/**
 * @param {Array<Object>} args the AST of the function body 
 * @param {Object} scope the actual enviroment, the function can access
 * external scope, but it has his own scope.
 * @returns {Function} the JavaScript function version of the egg funcion
 * definition
 */
specialForms['->'] = specialForms['fun'] = (args, scope) => {
  if (!args.length) {
    throw new SyntaxError('Functions need a body');
  }
  let body = args[args.length - 1];

  // params contiene solo el nombre de los argumentos
  let params = args.slice(0, args.length - 1).map(expr => {
    if (expr.type !== 'word') {
      throw new SyntaxError('Parameter names on functions must be words');
    }
    return expr.name;
  });

  return function () {
    if (arguments.length !== params.length) {
      throw new TypeError('Wrong number of arguments');
    }
    // el scope de la funcion es el scope actual + los argumentos
    let localScope = Object.create(scope);
    for (let i = 0; i < arguments.length; i++) {
      localScope[params[i]] = arguments[i];
    }
    return evaluate(body, localScope);
  };
};

/**
 * @param {Object} expr Abstract Syntax Tree to evaluate
 * @param {Object} scope Actual enviroment 
 * @returns The value of the expression
 * @description evaluate the given expression on the given enviroment/scope
 * and return his value
 */
function evaluate(expr, scope) {
  if (expr.type === 'value') {
    return expr.value;
  } else if (expr.type === 'word') {
    //si es una variable declarada, su valor debe estar en el scope
    if (expr.name in scope) {
      return scope[expr.name];
    }
    throw new ReferenceError(`Undefined binding: ${expr.name}`);

  } else if (expr.type === 'apply') {//es una funcion
    let { operator, args } = expr;
    //special forms son if, while...
    if (operator.type === 'word' && operator.name in specialForms) {
      return specialForms[operator.name](args, scope);
    } else {
      let op = evaluate(operator, scope);
      if (typeof op === 'function') {
        // ... pasa como argumentos cada uno de los elementos del vector
        // que devuelve map. 
        // map aplica la funcion pasada a cada elemento de un vector y almacena
        // lo que retornen esas funciones en un nuevo vector
        return op(...args.map(arg => evaluate(arg, scope)));
      }
      throw new TypeError('Applying a non-function');
    }
  }
}

const topScope = Object.create(null);

topScope.true = true;
topScope.false = false;

for (const operator of ['+', '-', '*', '/', '<', '>', '>=', '<=', '==', '===']) {
  topScope[operator] = Function('a, b', `return a ${operator} b;`);
}

/**
 * @param  {...any} values Values to print
 * @returns Last printed value
 * @description print one by one the given arguments with console.log
 */
topScope.print = function (...values) {
  for (const value of values) {
    console.log(value);
  }
  return values[values.length - 1];
};


/**
 * @param {String} program String that contains an egg program
 * @returns the last evaluated value of the egg program
 * @description evaluate the egg program that is inside of the given string
 */
function run(program) {
  //utiliza Object.create para crear una copia y no modificar el scope global
  return evaluate(parse(program), Object.create(topScope));
}

/**
 * @param {Object} program compiled egg program (JSON) evm format
 * @returns the last evaluated value of the egg program
 * @description evaluate an egg program, the program must be on evm (JSON) format
 */
function runEVM(program) {
  return evaluate(JSON.parse(program), Object.create(topScope));
}

/**
 * @param {String} fileRoute to the .egg program
 * @returns the last evaluated value of the egg program
 * @description open a .egg file and run it with the run function
 */
function runFromFile(fileRoute) {
  let output;
  try {
    output = run(fileSystem.readFileSync(fileRoute));
  } catch (error) {
    console.log('Error en runFromFile:', error);
  }
  return output;
}

/**
 * @param {String} fileRoute to the file that contains a compiled egg program
 * @returns the last evaluated value of the egg program
 * @description read the file and run it with runEVM
 */
function runFromEVM(fileRoute) {
  let output;
  try {
    output = runEVM(fileSystem.readFileSync(fileRoute, 'utf-8'));
  } catch (error) {
    console.log('Error en runFromEVM:', error);
  }
  return output;
}


module.exports = { run, topScope, specialForms, parse, evaluate, runFromFile, runFromEVM };