/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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 com.opensymphony.xwork2;

import com.opensymphony.xwork2.config.ConfigurationException;
import com.opensymphony.xwork2.config.entities.ActionConfig;
import com.opensymphony.xwork2.config.entities.InterceptorMapping;
import com.opensymphony.xwork2.config.entities.ResultConfig;
import com.opensymphony.xwork2.inject.Container;
import com.opensymphony.xwork2.inject.Inject;
import com.opensymphony.xwork2.interceptor.Interceptor;
import com.opensymphony.xwork2.interceptor.PreResultListener;
import com.opensymphony.xwork2.interceptor.WithLazyParams;
import com.opensymphony.xwork2.ognl.OgnlUtil;
import com.opensymphony.xwork2.util.ValueStack;
import com.opensymphony.xwork2.util.ValueStackFactory;
import ognl.MethodFailedException;
import ognl.NoSuchPropertyException;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.struts2.StrutsException;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Callable;

/**
 * The Default ActionInvocation implementation
 *
 * @author Rainer Hermanns
 * @author tmjee
 * @version $Date$ $Id$
 * @see com.opensymphony.xwork2.DefaultActionProxy
 */
public class DefaultActionInvocation implements ActionInvocation {

    private static final Logger LOG = LogManager.getLogger(DefaultActionInvocation.class);

    protected Object action;
    protected ActionProxy proxy;
    protected List<PreResultListener> preResultListeners;
    protected Map<String, Object> extraContext;
    protected ActionContext invocationContext;
    protected Iterator<InterceptorMapping> interceptors;
    protected ValueStack stack;
    protected Result result;
    protected Result explicitResult;
    protected String resultCode;
    protected boolean executed = false;
    protected boolean pushAction;
    protected ObjectFactory objectFactory;
    protected ActionEventListener actionEventListener;
    protected ValueStackFactory valueStackFactory;
    protected Container container;
    protected UnknownHandlerManager unknownHandlerManager;
    protected OgnlUtil ognlUtil;
    protected AsyncManager asyncManager;
    protected Callable asyncAction;
    protected WithLazyParams.LazyParamInjector lazyParamInjector;

    public DefaultActionInvocation(final Map<String, Object> extraContext, final boolean pushAction) {
        this.extraContext = extraContext;
        this.pushAction = pushAction;
    }

    @Inject
    public void setUnknownHandlerManager(UnknownHandlerManager unknownHandlerManager) {
        this.unknownHandlerManager = unknownHandlerManager;
    }

    @Inject
    public void setValueStackFactory(ValueStackFactory fac) {
        this.valueStackFactory = fac;
    }

    @Inject
    public void setObjectFactory(ObjectFactory fac) {
        this.objectFactory = fac;
    }

    @Inject
    public void setContainer(Container cont) {
        this.container = cont;
    }

    @Inject(required=false)
    public void setActionEventListener(ActionEventListener listener) {
        this.actionEventListener = listener;
    }

    @Inject
    public void setOgnlUtil(OgnlUtil ognlUtil) {
        this.ognlUtil = ognlUtil;
    }

    @Inject(required=false)
    public void setAsyncManager(AsyncManager asyncManager) {
        this.asyncManager = asyncManager;
    }

    public Object getAction() {
        return action;
    }

    public boolean isExecuted() {
        return executed;
    }

    public ActionContext getInvocationContext() {
        return invocationContext;
    }

    public ActionProxy getProxy() {
        return proxy;
    }

    /**
     * If the DefaultActionInvocation has been executed before and the Result is an instance of ActionChainResult, this method
     * will walk down the chain of ActionChainResults until it finds a non-chain result, which will be returned. If the
     * DefaultActionInvocation's result has not been executed before, the Result instance will be created and populated with
     * the result params.
     *
     * @return a Result instance
     * @throws Exception in case of any error
     */
    public Result getResult() throws Exception {
        Result returnResult = result;

        // If we've chained to other Actions, we need to find the last result
        while (returnResult instanceof ActionChainResult) {
            ActionProxy aProxy = ((ActionChainResult) returnResult).getProxy();

            if (aProxy != null) {
                Result proxyResult = aProxy.getInvocation().getResult();

                if ((proxyResult != null) && (aProxy.getExecuteResult())) {
                    returnResult = proxyResult;
                } else {
                    break;
                }
            } else {
                break;
            }
        }

        return returnResult;
    }

    public String getResultCode() {
        return resultCode;
    }

    public void setResultCode(String resultCode) {
        if (isExecuted()) {
            throw new IllegalStateException("Result has already been executed.");
        }
        this.resultCode = resultCode;
    }


    public ValueStack getStack() {
        return stack;
    }

    /**
     * Register a com.opensymphony.xwork2.interceptor.PreResultListener to be notified after the Action is executed and before the
     * Result is executed. The ActionInvocation implementation must guarantee that listeners will be called in the order
     * in which they are registered. Listener registration and execution does not need to be thread-safe.
     *
     * @param listener to register
     */
    public void addPreResultListener(PreResultListener listener) {
        if (preResultListeners == null) {
            preResultListeners = new ArrayList<>(1);
        }

        preResultListeners.add(listener);
    }

    public Result createResult() throws Exception {
        LOG.trace("Creating result related to resultCode [{}]", resultCode);

        if (explicitResult != null) {
            Result ret = explicitResult;
            explicitResult = null;

            return ret;
        }
        ActionConfig config = proxy.getConfig();
        Map<String, ResultConfig> results = config.getResults();

        ResultConfig resultConfig = null;

        try {
            resultConfig = results.get(resultCode);
        } catch (NullPointerException e) {
            LOG.debug("Got NPE trying to read result configuration for resultCode [{}]", resultCode);
        }
        
        if (resultConfig == null) {
            // If no result is found for the given resultCode, try to get a wildcard '*' match.
            resultConfig = results.get("*");
        }

        if (resultConfig != null) {
            try {
                return objectFactory.buildResult(resultConfig, invocationContext.getContextMap());
            } catch (Exception e) {
                LOG.error("There was an exception while instantiating the result of type {}", resultConfig.getClassName(), e);
                throw new StrutsException(e, resultConfig);
            }
        } else if (resultCode != null && !Action.NONE.equals(resultCode) && unknownHandlerManager.hasUnknownHandlers()) {
            return unknownHandlerManager.handleUnknownResult(invocationContext, proxy.getActionName(), proxy.getConfig(), resultCode);
        }
        return null;
    }

    /**
     * @throws ConfigurationException If no result can be found with the returned code
     */
    public String invoke() throws Exception {
        if (executed) {
            throw new IllegalStateException("Action has already executed");
        }

        if (asyncManager == null || !asyncManager.hasAsyncActionResult()) {
            if (interceptors.hasNext()) {
                final InterceptorMapping interceptorMapping = interceptors.next();
                Interceptor interceptor = interceptorMapping.getInterceptor();
                if (interceptor instanceof WithLazyParams) {
                    interceptor = lazyParamInjector.injectParams(interceptor, interceptorMapping.getParams(), invocationContext);
                }
                resultCode = interceptor.intercept(DefaultActionInvocation.this);
            } else {
                resultCode = invokeActionOnly();
            }
        } else {
            Object asyncActionResult = asyncManager.getAsyncActionResult();
            if (asyncActionResult instanceof Throwable) {
                throw new Exception((Throwable) asyncActionResult);
            }
            asyncAction = null;
            resultCode = saveResult(proxy.getConfig(), asyncActionResult);
        }

        if (asyncManager == null || asyncAction == null) {
            // this is needed because the result will be executed, then control will return to the Interceptor, which will
            // return above and flow through again
            if (!executed) {
                if (preResultListeners != null) {
                    LOG.trace("Executing PreResultListeners for result [{}]", result);

                    for (Object preResultListener : preResultListeners) {
                        PreResultListener listener = (PreResultListener) preResultListener;

                        listener.beforeResult(this, resultCode);
                    }
                }

                // now execute the result, if we're supposed to
                if (proxy.getExecuteResult()) {
                    executeResult();
                }

                executed = true;
            }
        } else {
            asyncManager.invokeAsyncAction(asyncAction);
        }

        return resultCode;
    }

    public String invokeActionOnly() throws Exception {
        return invokeAction(getAction(), proxy.getConfig());
    }

    protected void createAction(Map<String, Object> contextMap) {
        // load action
        try {
            action = objectFactory.buildAction(proxy.getActionName(), proxy.getNamespace(), proxy.getConfig(), contextMap);
        } catch (InstantiationException e) {
            throw new StrutsException("Unable to instantiate Action!", e, proxy.getConfig());
        } catch (IllegalAccessException e) {
            throw new StrutsException("Illegal access to constructor, is it public?", e, proxy.getConfig());
        } catch (Exception e) {
            String gripe;

            if (proxy == null) {
                gripe = "Whoa!  No ActionProxy instance found in current ActionInvocation.  This is bad ... very bad";
            } else if (proxy.getConfig() == null) {
                gripe = "Sheesh.  Where'd that ActionProxy get to?  I can't find it in the current ActionInvocation!?";
            } else if (proxy.getConfig().getClassName() == null) {
                gripe = "No Action defined for '" + proxy.getActionName() + "' in namespace '" + proxy.getNamespace() + "'";
            } else {
                gripe = "Unable to instantiate Action, " + proxy.getConfig().getClassName() + ",  defined for '" + proxy.getActionName() + "' in namespace '" + proxy.getNamespace() + "'";
            }

            gripe += (((" -- " + e.getMessage()) != null) ? e.getMessage() : " [no message in exception]");
            throw new StrutsException(gripe, e, proxy.getConfig());
        }

        if (actionEventListener != null) {
            action = actionEventListener.prepare(action, stack);
        }
    }

    protected Map<String, Object> createContextMap() {
        ActionContext actionContext;

        if (ActionContext.containsValueStack(extraContext)) {
            // In case the ValueStack was passed in
            stack = ActionContext.of(extraContext).getValueStack();

            if (stack == null) {
                throw new IllegalStateException("There was a null Stack set into the extra params.");
            }

            actionContext = stack.getActionContext();
        } else {
            // create the value stack
            // this also adds the ValueStack to its context
            stack = valueStackFactory.createValueStack();

            // create the action context
            actionContext = stack.getActionContext();
        }

        return actionContext
            .withExtraContext(extraContext)
            .withActionInvocation(this)
            .withContainer(container)
            .getContextMap();
    }

    /**
     * Uses getResult to get the final Result and executes it
     *
     * @throws ConfigurationException If not result can be found with the returned code
     */
    private void executeResult() throws Exception {
        result = createResult();

        if (result != null) {
            result.execute(this);
        } else if (resultCode != null && !Action.NONE.equals(resultCode)) {
            throw new ConfigurationException("No result defined for action " + getAction().getClass().getName()
                    + " and result " + getResultCode(), proxy.getConfig());
        } else {
            if (LOG.isDebugEnabled()) {
                LOG.debug("No result returned for action {} at {}", getAction().getClass().getName(), proxy.getConfig().getLocation());
            }
        }
    }

    public void init(ActionProxy proxy) {
        this.proxy = proxy;
        Map<String, Object> contextMap = createContextMap();

        // Setting this so that other classes, like object factories, can use the ActionProxy and other
        // contextual information to operate
        ActionContext actionContext = ActionContext.getContext();

        if (actionContext != null) {
            actionContext.withActionInvocation(this);
        }

        createAction(contextMap);

        if (pushAction) {
            stack.push(action);
            contextMap.put("action", action);
        }

        invocationContext = ActionContext.of(contextMap)
            .withActionName(proxy.getActionName());

        createInterceptors(proxy);

        prepareLazyParamInjector(invocationContext.getValueStack());
    }

    protected void prepareLazyParamInjector(ValueStack valueStack) {
        lazyParamInjector = new WithLazyParams.LazyParamInjector(valueStack);
        container.inject(lazyParamInjector);
    }

    protected void createInterceptors(ActionProxy proxy) {
        // Get a new List so we don't get problems with the iterator if someone changes the original list
        List<InterceptorMapping> interceptorList = new ArrayList<>(proxy.getConfig().getInterceptors());
        interceptors = interceptorList.iterator();
    }

    protected String invokeAction(Object action, ActionConfig actionConfig) throws Exception {
        String methodName = proxy.getMethod();

        LOG.debug("Executing action method = {}", methodName);

        try {
            Object methodResult;
            try {
                methodResult = ognlUtil.callMethod(methodName + "()", getStack().getContext(), action);
            } catch (MethodFailedException e) {
                // if reason is missing method,  try checking UnknownHandlers
                if (e.getReason() instanceof NoSuchMethodException) {
                    if (unknownHandlerManager.hasUnknownHandlers()) {
                        try {
                            methodResult = unknownHandlerManager.handleUnknownMethod(action, methodName);
                        } catch (NoSuchMethodException ignore) {
                            // throw the original one
                            throw e;
                        }
                    } else {
                        // throw the original one
                        throw e;
                    }
                    // throw the original exception as UnknownHandlers weren't able to handle invocation as well
                    if (methodResult == null) {
                        throw e;
                    }
                } else {
                    // exception isn't related to missing action method, throw it
                    throw e;
                }
            }
            return saveResult(actionConfig, methodResult);
        } catch (NoSuchPropertyException e) {
            throw new IllegalArgumentException("The " + methodName + "() is not defined in action " + getAction().getClass() + "");
        } catch (MethodFailedException e) {
            // We try to return the source exception.
            Throwable t = e.getCause();

            if (actionEventListener != null) {
                String result = actionEventListener.handleException(t, getStack());
                if (result != null) {
                    return result;
                }
            }
            if (t instanceof Exception) {
                throw (Exception) t;
            } else {
                throw e;
            }
        }
    }

    /**
     * Save the result to be used later.
     * @param actionConfig current ActionConfig
     * @param methodResult the result of the action.
     * @return the result code to process.
     */
    protected String saveResult(ActionConfig actionConfig, Object methodResult) {
        if (methodResult instanceof Result) {
            this.explicitResult = (Result) methodResult;

            // Wire the result automatically
            container.inject(explicitResult);
            return null;
        } else if (methodResult instanceof Callable) {
            asyncAction = (Callable) methodResult;
            return null;
        } else {
            return (String) methodResult;
        }
    }

}
