/*
 * Copyright (c) 2008 Cameron Zemek
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to
 * deal in the Software without restriction, including without limitation the
 * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
 * sell copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
 * IN THE SOFTWARE.
 */
package net.zeminvaders.lang;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.io.Reader;
import java.io.StringReader;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import net.zeminvaders.lang.ast.RootNode;
import net.zeminvaders.lang.runtime.ArrayPushFunction;
import net.zeminvaders.lang.runtime.Function;
import net.zeminvaders.lang.runtime.LenFunction;
import net.zeminvaders.lang.runtime.PrintFunction;
import net.zeminvaders.lang.runtime.PrintLineFunction;
import net.zeminvaders.lang.runtime.ZemObject;

/**
 * Interpreter directly evaluates the abstract syntax tree generated by the
 * Parser. It checks the semantics as it process the tree.
 *
 * @author <a href="mailto:grom@zeminvaders.net">Cameron Zemek</a>
 */
public class Interpreter {
    /**
     * Map of variables to their objects
     */
    private Map<String, ZemObject> symbolTable = new HashMap<String, ZemObject>();

    /**
     * Setup interpreter with empty symbol table
     * and register built-in functions.
     */
    public Interpreter() {
        // Register built-in functions
        symbolTable.put("print", new PrintFunction());
        symbolTable.put("println", new PrintLineFunction());
        symbolTable.put("len", new LenFunction());
        symbolTable.put("array_push", new ArrayPushFunction());
    }

    /**
     * Get the current value of a variable.
     *
     * @param name Variable name
     * @param pos  Source position of where the variable is being requested
     * @return The value of the variable
     */
    public ZemObject getVariable(String name, SourcePosition pos) {
        if (!symbolTable.containsKey(name)) {
            throw new UnsetVariableException(name, pos);
        }
        return symbolTable.get(name);
    }

    /**
     * Set the value of a variable
     *
     * @param name  Variable name
     * @param value New value for the variable
     */
    public void setVariable(String name, ZemObject value) {
        symbolTable.put(name, value);
    }

    /**
     * Check that a function exists.
     *
     * @param functionName Function to check.
     */
    public void checkFunctionExists(String functionName, SourcePosition pos) {
        ZemObject symbol = getVariable(functionName, pos);
        if (!(symbol instanceof Function)) {
            throw new InvalidTypeException(functionName + " is not a function", pos);
        }
    }

    /**
     * Call a function.
     *
     * @param function The function to call
     * @param args List of arguments to pass to function
     * @param pos  Position in source code of the function call
     * @param functionName Name of the function. Null is passed for anonymous functions.
     * @return Return value from function
     */
    public ZemObject callFunction(Function function, List<ZemObject> args, SourcePosition pos, String functionName) {
        // Save the symbolTable
        Map<String, ZemObject> savedSymbolTable =
            new HashMap<String, ZemObject>(symbolTable);
        // Setup symbolTable for function
        int noMissingArgs = 0;
        int noRequiredArgs = 0;
        for (int paramIndex = 0;
                paramIndex < function.getParameterCount(); paramIndex++) {
            String parameterName = function.getParameterName(paramIndex);
            ZemObject value = function.getDefaultValue(paramIndex);
            if (value == null) {
                noRequiredArgs++;
            }
            if (paramIndex < args.size()) {
                // Value provided in function call overrides the default value
                value = args.get(paramIndex);
            }
            if (value == null) {
                noMissingArgs++;
            }
            setVariable(parameterName, value);
        }
        if (noMissingArgs > 0) {
            throw new TooFewArgumentsException(functionName, noRequiredArgs,
                    args.size(), pos);
        }
        ZemObject ret = function.eval(this, pos);
        // Restore symbolTable
        symbolTable = savedSymbolTable;

        return ret;
    }

    /**
     * Evaluate script
     *
     * @param script The script to evaluate
     * @return The exit status
     * @throws IOException
     */
    public ZemObject eval(String script) throws IOException {
        return eval(new StringReader(script));
    }

    /**
     * Evaluate script
     *
     * @param file The file that contains the script
     * @return The exit status
     * @throws IOException
     */
    public ZemObject eval(File file) throws IOException {
        return eval(new BufferedReader(new FileReader(file)));
    }

    /**
     * Evaluate script
     *
     * @param reader The reader that contains the script
     * @return The exit status
     * @throws IOException
     */
    public ZemObject eval(Reader reader) throws IOException {
        Lexer lexer = new Lexer(reader);
        Parser parser = new Parser(lexer);
        RootNode program = parser.program();
        return program.eval(this);
    }
}
