package function.builtin;

import java.text.MessageFormat;

import error.LispException;
import function.*;
import function.builtin.cons.LIST;
import function.builtin.special.LAMBDA;
import sexpression.*;
import table.*;

@FunctionNames({ "EVAL" })
public class EVAL extends LispFunction {

    public static LispFunction lookupFunctionOrLambda(SExpression functionExpression) {
        LispFunction function = FunctionTable.lookupFunction(functionExpression.toString());

        if (function == null)
            function = createLambdaFunction(functionExpression);

        return function;
    }

    private static LispFunction createLambdaFunction(SExpression lambdaExpression) {
        if (lambdaExpression.isFunction())
            return ((LambdaExpression) lambdaExpression).getFunction();
        else if (LAMBDA.isLambdaExpression(lambdaExpression))
            return LAMBDA.createFunction((Cons) lambdaExpression);
        else
            throw new UndefinedFunctionException(lambdaExpression);
    }

    public static SExpression lookupSymbol(String symbolName) {
        if (symbolName.equals("NIL"))
            return Nil.getInstance();
        else if (symbolName.equals("T"))
            return Symbol.T;
        else if (symbolName.startsWith(":"))
            return new Symbol(symbolName);

        return ExecutionContext.getInstance().lookupSymbolValue(symbolName);
    }

    public static SExpression eval(SExpression sExpression) {
        Cons argumentList = LIST.makeList(sExpression);
        EVAL eval = new EVAL();

        return eval.call(argumentList);
    }

    private ArgumentValidator argumentValidator;

    public EVAL() {
        this.argumentValidator = new ArgumentValidator("EVAL");
        this.argumentValidator.setExactNumberOfArguments(1);
    }

    public SExpression call(Cons argumentList) {
        argumentValidator.validate(argumentList);

        SExpression argument = argumentList.getFirst();

        if (argument.isList()) {
            if (argument.isCons())
                return evaluateList((Cons) argument);

            return argument; // NIL
        }

        if (argument.isSymbol()) {
            SExpression symbolValue = lookupSymbol(argument.toString());

            if (symbolValue != null)
                return symbolValue;

            throw new UndefinedSymbolException(argument);
        }

        return argument; // NUMBER or STRING
    }

    private SExpression evaluateList(Cons list) {
        SExpression functionName = list.getFirst();
        SExpression arguments = list.getRest();
        LispFunction function = lookupFunctionOrLambda(functionName);

        ArgumentValidator functionListValidator = new ArgumentValidator(functionName.toString());
        functionListValidator.validate(list);

        Cons argumentList = (Cons) arguments;

        if (function.evaluateArguments())
            argumentList = evaluateArgList(argumentList);

        return function.call(argumentList);
    }

    private Cons evaluateArgList(Cons arguments) {
        if (arguments.isNull())
            return Nil.getInstance();

        SExpression first = eval(arguments.getFirst());
        SExpression rest = arguments.getRest();

        return new Cons(first, evaluateArgList((Cons) rest));
    }

    public static class UndefinedFunctionException extends LispException {

        private static final long serialVersionUID = 1L;
        private SExpression function;

        public UndefinedFunctionException(SExpression function) {
            this.function = function;
        }

        @Override
        public String getMessage() {
            return MessageFormat.format("undefined function: {0}", function);
        }
    }

    public static class UndefinedSymbolException extends LispException {

        private static final long serialVersionUID = 1L;
        private SExpression symbol;

        public UndefinedSymbolException(SExpression symbol) {
            this.symbol = symbol;
        }

        @Override
        public String getMessage() {
            return MessageFormat.format("symbol {0} has no value", symbol);
        }
    }

}
