package function.builtin.special;

import static function.builtin.EVAL.eval;
import static sexpression.Nil.NIL;

import function.ArgumentValidator;
import function.FunctionNames;
import function.LispSpecialFunction;
import sexpression.Cons;
import sexpression.SExpression;
import sexpression.Symbol;
import table.ExecutionContext;
import table.SymbolTable;

@FunctionNames({ "LET" })
public class LET extends LispSpecialFunction {

    private ArgumentValidator argumentValidator;
    private ArgumentValidator variableDefinitionListValidator;
    private ArgumentValidator pairValidator;
    protected ExecutionContext executionContext;

    public LET(String name) {
        this.argumentValidator = new ArgumentValidator(name);
        this.argumentValidator.setMinimumNumberOfArguments(1);
        this.argumentValidator.setFirstArgumentExpectedType(Cons.class);

        this.variableDefinitionListValidator = new ArgumentValidator(name + "|pair-list|");
        this.variableDefinitionListValidator.setEveryArgumentExpectedType(Cons.class);

        this.pairValidator = new ArgumentValidator(name + "|pair|");
        this.pairValidator.setMinimumNumberOfArguments(1);
        this.pairValidator.setMaximumNumberOfArguments(2);
        this.pairValidator.setFirstArgumentExpectedType(Symbol.class);

        this.executionContext = ExecutionContext.getInstance();
    }

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

        return evaluateInScope(variableDefinitions, body);
    }

    private SExpression evaluateInScope(Cons variableDefinitions, Cons body) {
        SymbolTable localScope = createLocalScope(variableDefinitions);
        SExpression lastEvaluation = evaluateBody(body);
        restorePreviousScope(localScope);

        return lastEvaluation;
    }

    protected SymbolTable createLocalScope(Cons variableDefinitions) {
        SymbolTable localScope = new SymbolTable(executionContext.getScope());
        addVariablesToScope(localScope, variableDefinitions);
        executionContext.setScope(localScope);

        return localScope;
    }

    protected void addVariablesToScope(SymbolTable scope, Cons variableDefinitions) {
        variableDefinitionListValidator.validate(variableDefinitions);

        for (; variableDefinitions.isCons(); variableDefinitions = (Cons) variableDefinitions.getRest())
            addPairToScope((Cons) variableDefinitions.getFirst(), scope);
    }

    private void addPairToScope(Cons symbolValuePair, SymbolTable scope) {
        pairValidator.validate(symbolValuePair);

        Cons restOfPair = (Cons) symbolValuePair.getRest();
        SExpression symbol = symbolValuePair.getFirst();
        SExpression value = restOfPair.getFirst();

        scope.put(symbol.toString(), eval(value));
    }

    private SExpression evaluateBody(Cons body) {
        SExpression lastEvaluation = NIL;

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

        return lastEvaluation;
    }

    private void restorePreviousScope(SymbolTable localScope) {
        executionContext.setScope(localScope.getParent());
    }

}
