package function.builtin;

import static function.builtin.cons.LIST.makeList;
import static function.builtin.special.LAMBDA.createFunction;
import static function.builtin.special.LAMBDA.isLambdaExpression;
import static java.text.MessageFormat.format;
import static sexpression.Nil.NIL;
import static sexpression.Symbol.T;
import static table.FunctionTable.lookupFunction;

import error.LispException;
import function.ArgumentValidator;
import function.FunctionNames;
import function.LispFunction;
import sexpression.BackquoteExpression;
import sexpression.Cons;
import sexpression.LambdaExpression;
import sexpression.SExpression;
import sexpression.Symbol;
import table.ExecutionContext;

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

    public static SExpression eval(SExpression sExpression) {
        Cons argumentList = makeList(sExpression);

        return lookupFunction("EVAL").call(argumentList);
    }

    public static LispFunction lookupFunctionOrLambda(SExpression functionExpression) {
        LispFunction function = 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 (isLambdaExpression(lambdaExpression))
            return createFunction((Cons) lambdaExpression);
        else
            throw new UndefinedFunctionException(lambdaExpression);
    }

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

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

    private ArgumentValidator argumentValidator;

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

    @Override
    public SExpression call(Cons argumentList) {
        argumentValidator.validate(argumentList);
        SExpression argument = argumentList.getFirst();

        return evaluateExpression(argument);
    }

    private SExpression evaluateExpression(SExpression argument) {
        if (argument.isList())
            return evaluateList(argument);
        else if (argument.isSymbol())
            return evaluateSymbol(argument);
        else if (argument.isBackquote())
            return evaluateBackTick(argument);
        else if (argument.isComma())
            throw new UnmatchedCommaException();
        else if (argument.isAtSign())
            throw new UnmatchedAtSignException();

        return argument; // NUMBER or STRING
    }

    private SExpression evaluateList(SExpression argument) {
        if (argument.isCons())
            return evaluateFunction((Cons) argument);

        return argument; // NIL
    }

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

        return callFunction(function, (Cons) arguments);
    }

    private void validateFunctionList(Cons list, SExpression functionName) {
        ArgumentValidator functionListValidator = new ArgumentValidator(functionName.toString());
        functionListValidator.validate(list);
    }

    private SExpression callFunction(LispFunction function, Cons argumentList) {
        if (function.isArgumentListEvaluated())
            argumentList = evaluateArgumentList(argumentList);

        SExpression result = function.call(argumentList);

        if (function.isMacro())
            result = eval(result);

        return result;
    }

    private Cons evaluateArgumentList(Cons arguments) {
        if (arguments.isNull())
            return NIL;

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

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

    private SExpression evaluateSymbol(SExpression argument) {
        SExpression symbolValue = lookupSymbol(argument.toString());

        if (symbolValue != null)
            return symbolValue;

        throw new UndefinedSymbolException(argument);
    }

    private SExpression evaluateBackTick(SExpression argument) {
        BackquoteEvaluator evaluator = new BackquoteEvaluator((BackquoteExpression) argument);

        return evaluator.evaluate();
    }

    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 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 format("symbol {0} has no value", symbol);
        }
    }

    public static class UnmatchedCommaException extends LispException {

        private static final long serialVersionUID = 1L;

        @Override
        public String getMessage() {
            return "unmatched comma";
        }
    }

    public static class UnmatchedAtSignException extends LispException {

        private static final long serialVersionUID = 1L;

        @Override
        public String getMessage() {
            return "unmatched at sign";
        }
    }

}
