/*
 * Copyright (C) 2008-2009 GLAD!! (ITO Yoshiichi)
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
 * either express or implied. See the License for the specific language
 * governing permissions and limitations under the License.
 */
package jp.sourceforge.glad.struts.action;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import jp.sourceforge.glad.util.StringUtils;

import org.apache.struts.action.Action;
import org.apache.struts.action.ActionForm;
import org.apache.struts.action.ActionForward;
import org.apache.struts.action.ActionMapping;
import org.apache.struts.config.ActionConfig;

/**
 * parameter の値に応じて、クラスに定義したメソッドを起動する Action。
 * <p>
 * メソッドの引数は ActionMapping, ActionForm, ServletRequest, ServletResponse
 * およびそのサブクラスから任意に(必要なら複数)選択することができます。
 * メソッドの戻り値は ActionForward, String, void から選択することができます。
 * 
 * @author GLAD!!
 */
public abstract class AbstractAction extends Action {

    public static final String SUCCESS = "success";

    public static final String INPUT = "input";

    private String defaultMethodName = "index";

    protected final Map<String, Method> methods = new HashMap<String, Method>();

    public String getDefaultMethodName() {
        return defaultMethodName;
    }

    public void setDefaultMethodName(String defaultMethodName) {
        this.defaultMethodName = defaultMethodName;
    }

    @Override
    public ActionForward execute(
            ActionMapping mapping, ActionForm form,
            HttpServletRequest request, HttpServletResponse response)
            throws Exception {

        return getActionContext(mapping, form, request, response)
                .execute();
    }

    protected ActionContext getActionContext(
            ActionMapping mapping, ActionForm form,
            HttpServletRequest request, HttpServletResponse response) {
        return new ActionContext(mapping, form, request, response);
    }

    protected class ActionContext {

        protected final ActionMapping mapping;
        protected final ActionForm form;
        protected final HttpServletRequest request;
        protected final HttpServletResponse response;

        protected Method method = null;

        protected ActionContext(
                ActionMapping mapping, ActionForm form,
                HttpServletRequest request, HttpServletResponse response) {
            this.mapping = mapping;
            this.form = form;
            this.request = request;
            this.response = response;
        }

        protected Object getAction() {
            return AbstractAction.this;
        }

        public ActionForward execute() throws Exception {
            method = getMethod(getMethodName());
            Object result = invoke(getArguments());
            return findForward(result);
        }

        protected String getMethodName() {
            String methodName = mapping.getParameter();
            if (StringUtils.isNotEmpty(methodName)) {
                return methodName;
            }
            return getDefaultMethodName();
        }

        protected Method getMethod(String methodName)
                throws NoSuchMethodException {
            Class<?> clazz = getAction().getClass();
            synchronized (methods) {
                Method method = methods.get(methodName);
                if (method != null) {
                    return method;
                }
                for (Method m : clazz.getMethods()) {
                    if (methodName.equals(m.getName())) {
                        methods.put(methodName, m);
                        return m;
                    }
                }
            }
            throw new NoSuchMethodException(clazz.getName() + '#' + methodName);
        }

        protected Object[] getArguments() {
            Class<?>[] paramTypes = method.getParameterTypes();
            int length = paramTypes.length;
            Object[] args = new Object[length];
            for (int i = 0; i < length; ++i) {
                Class<?> paramType = paramTypes[i];
                if (ActionConfig.class.isAssignableFrom(paramType)) {
                    args[i] = mapping;
                } else if (ActionForm.class.isAssignableFrom(paramType)) {
                    args[i] = form;
                } else if (ServletRequest.class.isAssignableFrom(paramType)) {
                    args[i] = request;
                } else if (ServletResponse.class.isAssignableFrom(paramType)) {
                    args[i] = response;
                }
            }
            return args;
        }

        protected Object invoke(Object... args)
                throws IllegalAccessException, InvocationTargetException {
            return method.invoke(getAction(), args);
        }

        protected ActionForward findForward(Object result) {
            if (result == null) {
                return mapping.findForward(SUCCESS);
            }
            if (result instanceof ActionForward) {
                return (ActionForward) result;
            }
            if (INPUT.equals(result)) {
                return mapping.getInputForward();
            }
            return mapping.findForward(result.toString());
        }

    }

}
