package org.emdev.ui.actions;

import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;

import org.emdev.common.log.LogContext;
import org.emdev.common.log.LogManager;
import org.emdev.utils.LengthUtils;
import org.emdev.utils.collections.SparseArrayEx;

public class ActionControllerMethod {

    private static final LogContext LCTX = LogManager.root().lctx("Actions");

    private static HashMap<Class<?>, SparseArrayEx<Method>> s_methods = new HashMap<Class<?>, SparseArrayEx<Method>>();

    private final IActionController<?> m_controller;

    private final ActionEx m_action;

    private Object m_target;

    private Method m_method;

    private Throwable m_errorInfo;

    /**
     * Constructor
     *
     * @param controller
     *            action controller
     * @param actionId
     *            action id
     */
    ActionControllerMethod(final IActionController<?> controller, final ActionEx action) {
        m_controller = controller;
        m_action = action;
    }

    /**
     * Invokes controller method for the given controller and action
     *
     * @param action
     *            action
     * @return execution result
     * @throws Throwable
     *             thrown by reflection API or executed method
     */
    public Object invoke(final ActionEx action) throws Throwable {
        final Method m = getMethod();
        if (m != null) {
            return m.invoke(m_target, action);
        } else {
            throw m_errorInfo;
        }
    }

    /**
     * @return <code>true</code> if method is exist
     */
    public boolean isValid() {
        return null != getMethod();
    }

    /**
     * Returns reflection error info.
     *
     * @return {@link Throwable}
     */
    public Throwable getErrorInfo() {
        getMethod();
        return m_errorInfo;
    }

    /**
     * @return {@link Method}
     */
    Method getMethod() {
        if (m_method == null && m_errorInfo == null) {
            List<String> classes = new ArrayList<String>();

            for (IActionController<?> c = m_controller; m_method == null && c != null; c = c.getParent()) {
                classes.add(c.getManagedComponent().getClass().getSimpleName());
                classes.add(c.getClass().getSimpleName());

                m_method = getMethod(c.getManagedComponent(), m_action.id);
                m_target = m_method != null ? c.getManagedComponent() : null;
                if (m_method == null) {
                    m_method = getMethod(c, m_action.id);
                    m_target = m_method != null ? c : null;
                }
            }

            if (m_method == null) {
                String text = "No appropriate method found for action " + m_action.name + " in the following classes: "
                        + classes;
                m_errorInfo = new NoSuchMethodException(text);
            } else {
                LCTX.d("Action method found for " + m_action.name + ": " + m_method);
            }
        }
        return m_method;
    }

    /**
     * Gets the method.
     *
     * @param target
     *            a possible action target
     * @param actionId
     *            the action id
     *
     * @return the method
     */
    private static synchronized Method getMethod(final Object target, final int actionId) {
        Class<? extends Object> clazz = target.getClass();

        SparseArrayEx<Method> methods = s_methods.get(clazz);
        if (methods == null) {
            methods = getActionMethods(clazz);
            s_methods.put(clazz, methods);
        }
        return methods.get(actionId);
    }

    /**
     * Gets the method.
     *
     * @param clazz
     *            an action target class
     *
     * @return the map of action methods method
     */
    private static SparseArrayEx<Method> getActionMethods(final Class<?> clazz) {
        final SparseArrayEx<Method> result = new SparseArrayEx<Method>();
        getActionMethodsFromMethodAnnotations(clazz, result);
        return result;
    }

    private static void getActionMethodsFromMethodAnnotations(final Class<?> clazz, final SparseArrayEx<Method> result) {
        final Method[] methods = clazz.getMethods();
        for (final Method method : methods) {
            final int modifiers = method.getModifiers();
            if (Modifier.isPublic(modifiers) && !Modifier.isStatic(modifiers)) {
                final Class<?>[] args = method.getParameterTypes();
                if (LengthUtils.length(args) == 1 && ActionEx.class.equals(args[0])) {
                    if (method.isAnnotationPresent(ActionMethod.class)) {
                        final ActionMethod annotation = method.getAnnotation(ActionMethod.class);
                        if (annotation != null) {
                            for (int id : annotation.ids()) {
                                result.put(id, method);
                            }
                        }
                    }
                }
            }
        }
    }

    @Override
    public String toString() {
        Method m = getMethod();
        return (m != null ? "" + m : "no method: " + m_errorInfo.getMessage());
    }
}
