package table;

import java.text.MessageFormat;
import java.util.*;

import error.CriticalLispException;
import function.*;
import function.builtin.*;
import function.builtin.cons.*;
import function.builtin.math.*;
import function.builtin.predicate.*;
import function.builtin.special.*;

public class FunctionTable {

    public static LispFunction lookupFunction(String functionName) {
        return getTable().get(functionName);
    }

    public static boolean isAlreadyDefined(String functionName) {
        return getTable().containsKey(functionName);
    }

    public static void defineFunction(String functionName, LispFunction function) {
        getTable().put(functionName, function);
    }

    public static void reset() {
        getUniqueInstance().initializeFunctionTable(allBuiltIns);
    }

    static void reset(Set<Class<? extends LispFunction>> builtIns) {
        getUniqueInstance().initializeFunctionTable(builtIns);
    }

    private static Set<Class<? extends LispFunction>> allBuiltIns = new HashSet<>();

    static {
        allBuiltIns.add(AND.class);
        allBuiltIns.add(APPLY.class);
        allBuiltIns.add(ATOM.class);
        allBuiltIns.add(COND.class);
        allBuiltIns.add(CONS.class);
        allBuiltIns.add(DEFINE_MACRO.class);
        allBuiltIns.add(DEFUN.class);
        allBuiltIns.add(DIVIDE.class);
        allBuiltIns.add(EQ.class);
        allBuiltIns.add(EQUAL.class);
        allBuiltIns.add(EQUALSP.class);
        allBuiltIns.add(EVAL.class);
        allBuiltIns.add(EXIT.class);
        allBuiltIns.add(FIRST.class);
        allBuiltIns.add(FUNCALL.class);
        allBuiltIns.add(GREATERP.class);
        allBuiltIns.add(IF.class);
        allBuiltIns.add(LAMBDA.class);
        allBuiltIns.add(LENGTH.class);
        allBuiltIns.add(LESSP.class);
        allBuiltIns.add(LET.class);
        allBuiltIns.add(LIST.class);
        allBuiltIns.add(LISTP.class);
        allBuiltIns.add(LOAD.class);
        allBuiltIns.add(MINUS.class);
        allBuiltIns.add(MULTIPLY.class);
        allBuiltIns.add(NULL.class);
        allBuiltIns.add(OR.class);
        allBuiltIns.add(PLUS.class);
        allBuiltIns.add(PRINT.class);
        allBuiltIns.add(QUOTE.class);
        allBuiltIns.add(REST.class);
        allBuiltIns.add(SET.class);
        allBuiltIns.add(SETF.class);
        allBuiltIns.add(SYMBOL_FUNCTION.class);
    }

    private static FunctionTable uniqueInstance;

    private static FunctionTable getUniqueInstance() {
        if (uniqueInstance == null) {
            uniqueInstance = new FunctionTable();
        }

        return uniqueInstance;
    }

    private static Map<String, LispFunction> getTable() {
        return getUniqueInstance().table;
    }

    private Map<String, LispFunction> table;

    private FunctionTable() {
        initializeFunctionTable(allBuiltIns);
    }

    private void initializeFunctionTable(Set<Class<? extends LispFunction>> builtIns) {
        table = new HashMap<>();

        for (Class<? extends LispFunction> function : builtIns)
            addBuiltInFunctionToTable(function);
    }

    private void addBuiltInFunctionToTable(Class<? extends LispFunction> function) {
        FunctionNames functionNames = function.getAnnotation(FunctionNames.class);

        if (functionNames != null)
            addAllFunctionNamesToTable(function, functionNames.value());
    }

    private void addAllFunctionNamesToTable(Class<? extends LispFunction> function, String[] names) {
        LispFunction instance = createInstance(function);

        for (String name : names)
            table.put(name, instance);
    }

    private LispFunction createInstance(Class<? extends LispFunction> function) {
        LispFunction instance = null;

        try {
            instance = function.getConstructor().newInstance();
        } catch (Exception e) {
            throw new LispFunctionInstantiationException(function.getName());
        }

        return instance;
    }

    public static class LispFunctionInstantiationException extends CriticalLispException {

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

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

        @Override
        public String getMessage() {
            return MessageFormat.format("Could not create an instance of ''{0}''", functionName);
        }
    }

}
