package function;

import static function.builtin.EVAL.eval;

import java.util.ArrayList;

import sexpression.*;
import table.*;

public class UserDefinedFunction extends LispFunction {

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

    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();

        for (this.formalParameters = new ArrayList<>(); lambdaList.isCons(); lambdaList = (Cons) lambdaList.getRest())
            this.formalParameters.add(lambdaList.getFirst().toString());

        this.argumentValidator = new ArgumentValidator(this.name);
        this.argumentValidator.setExactNumberOfArguments(this.formalParameters.size());
    }

    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();
        }

        return executionScope;
    }

    private SExpression evaluateBody() {
        SExpression lastEvaluation = Nil.getInstance();

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

        return lastEvaluation;
    }

    public Cons getLambdaExpression() {
        return lambdaExpression;
    }

}
