package function.builtin;

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

import error.LispException;
import function.ArgumentValidator;
import sexpression.AtSignExpression;
import sexpression.BackquoteExpression;
import sexpression.CommaExpression;
import sexpression.Cons;
import sexpression.SExpression;

class BackquoteEvaluator {

    private ArgumentValidator listValidator;
    private ArgumentValidator atSignValidator;
    private BackquoteExpression backTick;
    private Cons resolvedList;
    private Cons leader;
    private Cons follower;

    public BackquoteEvaluator(BackquoteExpression backTick) {
        this.listValidator = new ArgumentValidator("`|list|");
        this.atSignValidator = new ArgumentValidator("@|list|");
        this.backTick = backTick;
        this.resolvedList = new Cons(NIL, NIL);
        this.leader = resolvedList;
        this.follower = resolvedList;
    }

    public SExpression evaluate() {
        SExpression expression = backTick.getExpression();

        if (expression.isCons())
            expression = resolveList((Cons) expression);
        else if (expression.isComma())
            expression = eval(((CommaExpression) expression).getExpression());
        else if (expression.isAtSign())
            throw new AtSignNotInCommaException();

        return expression;
    }

    public SExpression resolveList(Cons list) {
        listValidator.validate(list);
        createResolvedList(list);

        return resolvedList;
    }

    private void createResolvedList(Cons list) {
        for (; list.isCons(); list = (Cons) list.getRest())
            resolveExpression(list.getFirst());

        follower.setRest(NIL);
    }

    private void resolveExpression(SExpression expression) {
        if (expression.isAtSign())
            throw new AtSignNotInCommaException();
        else if (expression.isComma())
            resolveCommaExpression(expression);
        else if (expression.isList())
            resolveListExpression(expression);
        else
            addResolvedExpression(expression);
    }

    private void resolveCommaExpression(SExpression expression) {
        CommaEvaluationResult result = evaluateComma((CommaExpression) expression);

        if (result.isAtSign())
            unpackResolvedList((Cons) result.getResult());
        else
            addResolvedExpression(result.getResult());
    }

    private CommaEvaluationResult evaluateComma(CommaExpression comma) {
        SExpression expression = comma.getExpression();
        validateCommaExpression(expression);

        if (expression.isAtSign())
            return new CommaEvaluationAtSignResult(evaluateAtSign((AtSignExpression) expression));
        else
            return new CommaEvaluationResult(eval(expression));
    }

    private void validateCommaExpression(SExpression expression) {
        if (expression.isComma())
            throw new NestedCommaException();
    }

    private Cons evaluateAtSign(AtSignExpression atSign) {
        SExpression expression = atSign.getExpression();
        validateAtSignUnevaluatedExpression(expression);
        SExpression evaluation = eval(expression);

        return getValidatedList(evaluation);
    }

    private void validateAtSignUnevaluatedExpression(SExpression expression) {
        if (expression.isComma())
            throw new NestedCommaException();
        else if (expression.isAtSign())
            throw new NestedAtSignException();
    }

    private Cons getValidatedList(SExpression evaluation) {
        if (!evaluation.isList())
            throw new AtSignNotListException();

        Cons evaluatedList = (Cons) evaluation;
        atSignValidator.validate(evaluatedList);

        return evaluatedList;
    }

    private void unpackResolvedList(Cons list) {
        for (; list.isCons(); list = (Cons) list.getRest())
            addResolvedExpression(list.getFirst());
    }

    private void addResolvedExpression(SExpression expression) {
        leader.setFirst(expression);
        leader.setRest(new Cons(NIL, NIL));
        follower = leader;
        leader = (Cons) leader.getRest();
    }

    private void resolveListExpression(SExpression expression) {
        BackquoteEvaluator evaluator = new BackquoteEvaluator(new BackquoteExpression(expression));
        addResolvedExpression(evaluator.evaluate());
    }

    private static class CommaEvaluationResult {

        private SExpression result;

        public CommaEvaluationResult(SExpression result) {
            this.result = result;
        }

        public SExpression getResult() {
            return result;
        }

        public boolean isAtSign() {
            return false;
        }
    }

    private static class CommaEvaluationAtSignResult extends CommaEvaluationResult {

        public CommaEvaluationAtSignResult(SExpression result) {
            super(result);
        }

        @Override
        public boolean isAtSign() {
            return true;
        }
    }

    public static class NestedCommaException extends LispException {

        private static final long serialVersionUID = 1L;

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

    public static class NestedAtSignException extends LispException {

        private static final long serialVersionUID = 1L;

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

    public static class AtSignNotInCommaException extends LispException {

        private static final long serialVersionUID = 1L;

        @Override
        public String getMessage() {
            return "at sign not in comma";
        }
    }

    public static class AtSignNotListException extends LispException {

        private static final long serialVersionUID = 1L;

        @Override
        public String getMessage() {
            return "at sign did not evaluate to a list";
        }
    }

}
