Source: Evaluators.js

import { formatValue, getVariable } from '../utils.js'
import { EvaluationError, GenericError } from './Errors.js'
import { VariableDeclaration } from './Declarations.js'

/**
 * @class
 */
export class ExpressionEvaluator {
	/**
	 * Evaluates each line separately while providing scope
	 * @param {object} scope
	 * @param {object} expr
	 */
	static eval(scope, expr) {
		const { type } = expr

		const evaluators = {
			VariableDeclarationEvaluator,
			ConsoleStatementEvaluator,
			IfStatementEvaluator,
			RepeatStatementEvaluator,
			FunctionDeclarationEvaluator,
			CallExpressionEvaluator
		}

		const evaluator = evaluators[`${ type }Evaluator`]

		if (evaluator) return evaluator.eval(scope, expr)
		else throw new GenericError(Nabd0001, expr)
	}
}


/**
 * @class
 */
class VariableDeclarationEvaluator {
	/**
	 * Evaluatues variable declarations
	 * @param {object} scope
	 * @param {object} expr
	 */
	static eval(scope, expr) {
		const decl = expr.declaration
		const { type, value, name } = decl

		if (type === "string" || type === "number") scope[name] = value
		else if (type === "variable") {
			if (!scope[value]) throw new EvaluationError(Nabd0002, expr)

			// Functions are stored in global scope as objects while variables are primitives
			if (typeof scope[value] == "object")
				throw new EvaluationError(Nabd0011, expr)

			scope[name] = scope[value]
		}
		else new GenericError(Nabd0001, expr)
	}
}

/**
 * @class
 */
class ConsoleStatementEvaluator {
	/**
	 * Evaluatues console statements
	 * @param {object} scope
	 * @param {object} expr
	 */
	static eval(scope, expr) {
		const arg = expr.argument
		const { type, value } = arg

		if (type === "string" || type === "number") return value
		else if (type === "variable") {
			if (!scope[value]) throw new EvaluationError(Nabd0002, expr)
			if (typeof scope[value] == "object") throw new EvaluationError(Nabd0010, expr)

			return scope[value]
		}
		else new GenericError(Nabd0001, expr)
	}
}

/**
 * @class
 */
class IfStatementEvaluator {
	/**
	 * Evaluatues if statements
	 * @param {object} scope
	 * @param {object} expr
	 */
	static eval(scope, expr) {
		const { consequent } = expr

		if (this.isTrue(expr, scope)) return ExpressionEvaluator.eval(scope, consequent)
	}

	/**
	 * Compares the two given values using the respective operator
	 * @param {object} expr
	 */
	static compare({ rightValue, leftValue, type, operator }) {
		if ((operator === "<" || operator === ">") && type !== "number")
			throw new EvaluationError(
				Nabd0004,
				`${ rightValue }${ operator }${ leftValue }`
			)

		if (operator === "<") return leftValue < rightValue
		else if (operator === ">") return leftValue > rightValue
		else return leftValue === rightValue
	}

	/**
	 * Determines whether if statement condition is truthy
	 * @param {object} expr
	 * @param {object} scope
	 */
	static isTrue(expr, scope) {
		const { right, left, type, operator, isNegated } = expr

		const rightValue = right.type === "variable"
			? getVariable(scope, right.name)
			: right.value

		const leftValue = left.type === "variable"
			? getVariable(scope, left.name)
			: left.value

		if (typeof rightValue !== typeof leftValue)
			throw new EvaluationError(Nabd0003, expr)

		// passing right.type only since both are gonna be same type anyway
		const result = this.compare({
			rightValue,
			leftValue,
			type: right.type,
			operator
		})

		return isNegated ? !result : result
	}
}

/**
 * @class
 */
class RepeatStatementEvaluator {
	/**
	 * Evaluatues if statements
	 * @param {object} scope
	 * @param {object} expr
	 */
	static eval(scope, expr) {
		const { count, consequent } = expr

		return this.repeat(scope, count, consequent)
	}

	/**
	 * Compares the two given values using the respective operator
	 * @param {object} expr
	 */
	static repeat(scope, count, consequent) {
		const result = []

		if (count >= Infinity) throw new EvaluationError(Nabd0008, consequent)

		for (let i = 0; i < count; i++) {
			result.push(ExpressionEvaluator.eval(scope, consequent))
		}

		return result
	}
}

/**
 * @class
 */
class FunctionDeclarationEvaluator {
	/**
	 * Evaluatues variable declarations
	 * @param {object} globalScope scope, but global
	 * @param {object} expr
	 */
	static eval(globalScope, expr) {
		const { raw, declaration } = expr
		const { name, param, body } = declaration
		const extant = globalScope[name]

		if (extant) {
			if (typeof extant == "object") throw new EvaluationError(Nabd0012, expr)
			else throw new EvaluationError(Nabd0013, expr)
		}
		else {
			globalScope[name] = expr
		}
	}
}

/**
 * @class
 */
class CallExpressionEvaluator {
	/**
	 * Evaluatues variable declarations
	 * @param {object} globalScope scope, but global
	 * @param {object} expr
	 */

	static prepareParams({ globalScope, functionScope, func, param, raw }) {
		const localVariableName = func.declaration.param
		const paramObject = globalScope[param.value] ||
			new VariableDeclaration(localVariableName, param.value, raw)

		if (!globalScope[param.value])
			VariableDeclarationEvaluator.eval(functionScope, paramObject)
		else functionScope[localVariableName] = paramObject
	}

	static eval(globalScope, expr) {
		const { name, param, raw } = expr
		const func = globalScope[name]

		if (!func) throw new EvaluationError(Nabd0002, expr)
		else {
			if (typeof func !== "object") throw new EvaluationError(Nabd0014, expr)
			else {
				// Create local function scope and add param value to it,
				// then pass that scope to the function body expr.
				const functionScope = {}

				if (param) this.prepareParams({
					globalScope,
					functionScope,
					func,
					param,
					raw
				})

				return ExpressionEvaluator.eval(functionScope, func.declaration.body)
			}
		}
	}
}