package function;

import static function.builtin.EVAL.eval;
import static java.text.MessageFormat.format;
import static sexpression.Nil.NIL;

import java.util.ArrayList;

import error.LispException;
import sexpression.Cons;
import sexpression.SExpression;
import sexpression.Symbol;
import table.ExecutionContext;
import table.SymbolTable;

public class UserDefinedFunction extends LispFunction {

    private static final String KEYWORD_REST = "&REST";

    private String name;
    private Cons body;
    private Cons lambdaExpression;
    private ExecutionContext executionContext;
    private SymbolTable functionScope;
    private ArrayList<String> formalParameters;
    private ArgumentValidator argumentValidator;
    private String keywordRestParameter;
    private boolean isKeywordRest;

    public UserDefinedFunction(String name, Cons lambdaList, Cons body) {
        this.name = name;
        this.body = body;
        this.lambdaExpression = new Cons(new Symbol(name), new Cons(lambdaList, body));
        this.executionContext = ExecutionContext.getInstance();
        this.functionScope = executionContext.getScope();
        this.keywordRestParameter = null;
        this.isKeywordRest = false;

        createFormalParameters(lambdaList);
        setupArgumentValidator();
    }

    private void createFormalParameters(Cons lambdaList) {
        for (formalParameters = new ArrayList<>(); lambdaList.isCons(); lambdaList = advanceCons(lambdaList)) {
            String parameter = lambdaList.getFirst().toString();

            if (isKeywordRest(parameter))
                lambdaList = extractKeywordRestParameter(lambdaList);
            else
                formalParameters.add(parameter);
        }
    }

    private Cons advanceCons(Cons cons) {
        return (Cons) cons.getRest();
    }

    private boolean isKeywordRest(String parameter) {
        return KEYWORD_REST.equals(parameter);
    }

    private Cons extractKeywordRestParameter(Cons lambdaList) {
        isKeywordRest = true;
        lambdaList = advanceCons(lambdaList);
        keywordRestParameter = lambdaList.getFirst().toString();
        lambdaList = advanceCons(lambdaList);

        if (containsMoreParameters(lambdaList))
            throw new IllegalKeywordRestPositionException(this.name, lambdaList);

        return lambdaList;
    }

    private boolean containsMoreParameters(Cons lambdaList) {
        return lambdaList.isCons();
    }

    private void setupArgumentValidator() {
        argumentValidator = new ArgumentValidator(this.name);

        if (isKeywordRest)
            argumentValidator.setMinimumNumberOfArguments(this.formalParameters.size());
        else
            argumentValidator.setExactNumberOfArguments(this.formalParameters.size());
    }

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

        return evaluateInFunctionScope(argumentList);
    }

    private SExpression evaluateInFunctionScope(Cons argumentList) {
        SymbolTable callingScope = executionContext.getScope();
        SymbolTable executionScope = bindParameterValuesToFunctionScope(argumentList);

        executionContext.setScope(executionScope);
        SExpression lastEvaluation = evaluateBody();
        executionContext.setScope(callingScope);

        return lastEvaluation;
    }

    private SymbolTable bindParameterValuesToFunctionScope(Cons argumentList) {
        SymbolTable executionScope = new SymbolTable(functionScope);

        for (String parameter : formalParameters) {
            SExpression currentArg = argumentList.getFirst();
            executionScope.put(parameter, currentArg);
            argumentList = (Cons) argumentList.getRest();
        }

        if (isKeywordRest)
            executionScope.put(keywordRestParameter, argumentList);

        return executionScope;
    }

    private SExpression evaluateBody() {
        SExpression lastEvaluation = NIL;

        for (Cons expression = body; expression.isCons(); expression = (Cons) expression.getRest())
            lastEvaluation = eval(expression.getFirst());

        return lastEvaluation;
    }

    public Cons getLambdaExpression() {
        return lambdaExpression;
    }

    public static class IllegalKeywordRestPositionException extends LispException {

        private static final long serialVersionUID = 1L;
        private String functionName;
        private Cons parameters;

        public IllegalKeywordRestPositionException(String functionName, Cons parameters) {
            this.functionName = functionName;
            this.parameters = parameters;
        }

        @Override
        public String getMessage() {
            return format("unexpected parameters following ''&rest'' in definition of {0}: {1}", functionName,
                          parameters);
        }
    }

}
