package function.builtin.special;

import static function.builtin.cons.LIST.makeList;
import static java.text.MessageFormat.format;
import static table.FunctionTable.defineFunction;
import static table.FunctionTable.isAlreadyDefined;

import environment.RuntimeEnvironment;
import error.LispWarning;
import function.ArgumentValidator;
import function.LispSpecialFunction;
import function.UserDefinedFunction;
import sexpression.Cons;
import sexpression.SExpression;
import sexpression.Symbol;

public abstract class Define extends LispSpecialFunction {

    private ArgumentValidator argumentValidator;
    private ArgumentValidator lambdaListIsListValidator;
    private ArgumentValidator lambdaListValidator;
    private RuntimeEnvironment environment;

    public Define(String functionName) {
        this.argumentValidator = new ArgumentValidator(functionName);
        this.argumentValidator.setMinimumNumberOfArguments(2);
        this.argumentValidator.setFirstArgumentExpectedType(Symbol.class);

        this.lambdaListIsListValidator = new ArgumentValidator(functionName + "|lambda-list|");
        this.lambdaListIsListValidator.setEveryArgumentExpectedType(Cons.class);

        this.lambdaListValidator = new ArgumentValidator(functionName + "|parameter|");
        this.lambdaListValidator.setEveryArgumentExpectedType(Symbol.class);

        this.environment = RuntimeEnvironment.getInstance();
    }

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

        Cons remainingArguments = (Cons) argumentList.getRest();
        SExpression functionName = argumentList.getFirst();
        SExpression secondArgument = remainingArguments.getFirst();
        lambdaListIsListValidator.validate(makeList(secondArgument));

        Cons lambdaList = (Cons) secondArgument;
        lambdaListValidator.validate(lambdaList);

        Cons functionBody = (Cons) remainingArguments.getRest();
        UserDefinedFunction function = createFunction(functionName, lambdaList, functionBody);

        if (isAlreadyDefined(functionName.toString()))
            environment.getErrorManager().handle(new RedefiningFunctionWarning(functionName.toString()));

        defineFunction(functionName.toString(), function);

        return functionName;
    }

    protected abstract UserDefinedFunction createFunction(SExpression functionName, Cons lambdaList, Cons functionBody);

    public class RedefiningFunctionWarning extends LispWarning {

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

        public RedefiningFunctionWarning(String functionName) {
            this.functionName = functionName;
        }

        @Override
        public String getMessage() {
            return format("redefining function {0}", functionName);
        }
    }

}
