/*
 * 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 org.apache.axis2.engine;

import org.apache.axiom.om.OMElement;
import org.apache.axiom.soap.SOAPHeader;
import org.apache.axis2.AxisFault;
import org.apache.axis2.Constants;
import org.apache.axis2.addressing.AddressingHelper;
import org.apache.axis2.addressing.EndpointReference;
import org.apache.axis2.context.ConfigurationContext;
import org.apache.axis2.context.MessageContext;
import org.apache.axis2.context.OperationContext;
import org.apache.axis2.context.ServiceContext;
import org.apache.axis2.context.ServiceGroupContext;
import org.apache.axis2.context.SessionContext;
import org.apache.axis2.description.AxisOperation;
import org.apache.axis2.description.AxisService;
import org.apache.axis2.description.AxisServiceGroup;
import org.apache.axis2.description.Parameter;
import org.apache.axis2.description.WSDL2Constants;
import org.apache.axis2.i18n.Messages;
import org.apache.axis2.transport.RequestResponseTransport;
import org.apache.axis2.transport.TransportListener;
import org.apache.axis2.transport.http.HTTPConstants;
import org.apache.axis2.util.JavaUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import javax.xml.namespace.QName;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

public class DispatchPhase extends Phase {
    private static final Log log = LogFactory.getLog(DispatchPhase.class);

    public DispatchPhase() {
    }

    public DispatchPhase(String phaseName) {
        super(phaseName);
    }
    
    
    private Boolean getDisableAck(MessageContext msgContext) throws AxisFault {
    
       // We should send an early ack to the transport whever possible, but some modules need
       // to use the backchannel, so we need to check if they have disabled this code.
       Boolean disableAck = (Boolean) msgContext.getProperty(Constants.Configuration.DISABLE_RESPONSE_ACK);
       if(disableAck == null) {
          disableAck = (Boolean) (msgContext.getAxisService() != null ? msgContext.getAxisService().getParameterValue(Constants.Configuration.DISABLE_RESPONSE_ACK) : null);
       }    
       
       return disableAck;
    }

    public void checkPostConditions(MessageContext msgContext) throws AxisFault {
        EndpointReference toEPR = msgContext.getTo();

        if (msgContext.getAxisService() == null) {
            AxisFault fault = new AxisFault(Messages.getMessage("servicenotfoundforepr",
                                                    ((toEPR != null) ? toEPR.getAddress() : "")));
            fault.setFaultCode(org.apache.axis2.namespace.Constants.FAULT_CLIENT);
            throw fault;
        }

        AxisService service = msgContext.getAxisService();
        AxisOperation operation = msgContext.getAxisOperation();
        
        // If operation is an excluded operation, throw an exception.
        // This code is needed to enable exclude operations for static WSDL files. 
        // Without this code, if one specifies excludeOperations in services.xml
        // file and a static WSDL is used that contains the operation, 
        // the operation would succeed.
        if (operation != null 
                && service.isExcludedOperation(operation.getName().getLocalPart())) {
            AxisFault fault = new AxisFault(Messages.getMessage("operationnotfoundforepr2",
                    ((toEPR != null) ? toEPR.getAddress()
                            : ""), msgContext.getWSAAction()));
            fault.setFaultCode(org.apache.axis2.namespace.Constants.FAULT_CLIENT);
            throw fault;
        }

        // If we're configured to do so, check the service for a single op...
        if (operation == null &&
                JavaUtils.isTrue(service.getParameterValue(AxisService.SUPPORT_SINGLE_OP))) {
            Iterator<AxisOperation> ops = service.getOperations();
            // If there's exactly one, that's the one we want.  If there's more, forget it.
            if (ops.hasNext()) {
                operation = (AxisOperation)ops.next();
                if (ops.hasNext()) {
                    operation = null;
                }
            }
            msgContext.setAxisOperation(operation);
        }

        // If we still don't have an operation, fault.
        if (operation == null) {
            AxisFault fault = new AxisFault(Messages.getMessage("operationnotfoundforepr2",
                                                    ((toEPR != null) ? toEPR.getAddress()
                                                            : ""), msgContext.getWSAAction()));
            fault.setFaultCode(org.apache.axis2.namespace.Constants.FAULT_CLIENT);
            throw fault;
        }

        validateTransport(msgContext);
        
        validateBindings(msgContext);

        loadContexts(service, msgContext);

        if (msgContext.getOperationContext() == null) {
            throw new AxisFault(Messages.getMessage("cannotBeNullOperationContext"));
        }

        if (msgContext.getServiceContext() == null) {
            throw new AxisFault(Messages.getMessage("cannotBeNullServiceContext"));
        }

        // TODO - review this
        if ((msgContext.getAxisOperation() == null) && (msgContext.getOperationContext() != null)) {
            msgContext.setAxisOperation(msgContext.getOperationContext().getAxisOperation());
        }

        if ((msgContext.getAxisService() == null) && (msgContext.getServiceContext() != null)) {
            msgContext.setAxisService(msgContext.getServiceContext().getAxisService());
        }

        // We should send an early ack to the transport whever possible, but some modules need
        // to use the backchannel, so we need to check if they have disabled this code.
        String mepString = msgContext.getAxisOperation().getMessageExchangePattern();

        if (isOneway(mepString)) {
            Object requestResponseTransport = msgContext.getProperty(RequestResponseTransport.TRANSPORT_CONTROL);
            if (requestResponseTransport != null) {

                Boolean disableAck = getDisableAck(msgContext);                    
                if (disableAck == null || disableAck.booleanValue() == false) {
                    ((RequestResponseTransport) requestResponseTransport).acknowledgeMessage(msgContext);
                }
            }
        } else if (AddressingHelper.isReplyRedirected(msgContext) && AddressingHelper.isFaultRedirected(msgContext)) {


            if (mepString.equals(WSDL2Constants.MEP_URI_IN_OUT)
                    || mepString.equals(WSDL2Constants.MEP_URI_IN_OUT)
                    || mepString.equals(WSDL2Constants.MEP_URI_IN_OUT)) { 
                // OR, if 2 way operation but the response is intended to not use the response channel of a 2-way transport
                // then we don't need to keep the transport waiting.

                Object requestResponseTransport = msgContext.getProperty(RequestResponseTransport.TRANSPORT_CONTROL);
                if (requestResponseTransport != null) {

                    // We should send an early ack to the transport whever possible, but some modules need
                    // to use the backchannel, so we need to check if they have disabled this code.
                    Boolean disableAck = getDisableAck(msgContext);

                    if (disableAck == null || disableAck.booleanValue() == false) {
                        ((RequestResponseTransport) requestResponseTransport).acknowledgeMessage(msgContext);
                    }
                    
                }
            }        
        }
        

        ArrayList operationChain = msgContext.getAxisOperation().getRemainingPhasesInFlow();
        msgContext.setExecutionChain((ArrayList) operationChain.clone());
    }

    private void loadContexts(AxisService service, MessageContext msgContext) throws AxisFault {
        String scope = service == null ? null : service.getScope();
        ServiceContext serviceContext = msgContext.getServiceContext();

        if ((msgContext.getOperationContext() != null)
                && (serviceContext != null)) {
            msgContext.setServiceGroupContextId(
                    ((ServiceGroupContext)serviceContext.getParent()).getId());
            return;
        }
        if (Constants.SCOPE_TRANSPORT_SESSION.equals(scope)) {
            fillContextsFromSessionContext(msgContext);
        } else if (Constants.SCOPE_SOAP_SESSION.equals(scope)) {
            extractServiceGroupContextId(msgContext);
        }

        AxisOperation axisOperation = msgContext.getAxisOperation();
//        if (axisOperation == null) {
//            return;
//        }
        OperationContext operationContext =
                axisOperation.findForExistingOperationContext(msgContext);

        if (operationContext != null) {
            // register operation context and message context
//            axisOperation.registerOperationContext(msgContext, operationContext);
            axisOperation.registerMessageContext(msgContext, operationContext);

            serviceContext = (ServiceContext) operationContext.getParent();
            ServiceGroupContext serviceGroupContext =
                    (ServiceGroupContext) serviceContext.getParent();

            msgContext.setServiceContext(serviceContext);
            msgContext.setServiceGroupContext(serviceGroupContext);
            msgContext.setServiceGroupContextId(serviceGroupContext.getId());
        } else {    // 2. if null, create new opCtxt
            if (serviceContext == null) {
                // fill the service group context and service context info
                msgContext.getConfigurationContext().
                        fillServiceContextAndServiceGroupContext(msgContext);
                serviceContext = msgContext.getServiceContext();
            }
            operationContext = serviceContext.createOperationContext(axisOperation);
            axisOperation.registerMessageContext(msgContext, operationContext);
        }

        serviceContext.setMyEPR(msgContext.getTo());
    }

    /**
     * To check whether the incoming request has come in valid transport , simply the transports
     * that service author wants to expose
     *
     * @param msgctx the current MessageContext
     * @throws AxisFault in case of error
     */
    private void validateTransport(MessageContext msgctx) throws AxisFault {
        AxisService service = msgctx.getAxisService();
        if (service.isEnableAllTransports()) {
            return;
        } else {
            List trs = service.getExposedTransports();
            String incomingTrs = msgctx.getIncomingTransportName();

            //local transport is a special case, it need not be exposed.
            if (Constants.TRANSPORT_LOCAL.equals(incomingTrs)) {
                return;
            }

            if (log.isDebugEnabled()) {
                log.debug("Incoming transport: " + incomingTrs + "; allowed transports: " + trs);
            }
            
            for (int i = 0; i < trs.size(); i++) {
                String tr = (String) trs.get(i);
                if (incomingTrs != null && incomingTrs.equals(tr)) {
                    return;
                }
            }
        }
        EndpointReference toEPR = msgctx.getTo();
        throw new AxisFault(Messages.getMessage("servicenotfoundforepr",
                                                ((toEPR != null) ? toEPR.getAddress() : "")));
    }
    
    /**
     * To check whether the incoming request has come in valid binding , we check whether service
     * author has disabled any binding using parameters
     * <code>org.apache.axis2.Constants.Configuration.DISABLE_SOAP12</code>
     * <code>org.apache.axis2.Constants.Configuration.DISABLE_SOAP11</code>
     * <code>org.apache.axis2.Constants.Configuration.DISABLE_REST</code>
     * @param service msgctx the current MessageContext
     * @throws AxisFault in case of error
     */
    private void validateBindings(MessageContext msgctx) throws AxisFault {
        
        AxisService service = msgctx.getAxisService();        
   
        if (msgctx.isDoingREST()) {
            
            boolean disableREST = false;
            Parameter disableRESTParameter = service
                            .getParameter(org.apache.axis2.Constants.Configuration.DISABLE_REST);
            if (disableRESTParameter != null
                            && JavaUtils.isTrueExplicitly(disableRESTParameter.getValue())) {
                    disableREST = true;
            }         
            
            if (disableREST) {
                throw new AxisFault(Messages.getMessage("bindingDisabled","Http"));
            }
        } else if (msgctx.isSOAP11()) {

            boolean disableSOAP11 = false;
            Parameter disableSOAP11Parameter = service
                            .getParameter(org.apache.axis2.Constants.Configuration.DISABLE_SOAP11);
            if (disableSOAP11Parameter != null
                            && JavaUtils.isTrueExplicitly(disableSOAP11Parameter.getValue())) {
                    disableSOAP11 = true;
            }         
         
            if (disableSOAP11) {
                throw new AxisFault(Messages.getMessage("bindingDisabled","SOAP11"));
            }
        } else {
         
            boolean disableSOAP12 = false;
            Parameter disableSOAP12Parameter = service
                            .getParameter(org.apache.axis2.Constants.Configuration.DISABLE_SOAP12);
            if (disableSOAP12Parameter != null
                            && JavaUtils
                                            .isTrueExplicitly(disableSOAP12Parameter.getValue())) {
                    disableSOAP12 = true;
            }         
         
            if(disableSOAP12) {
                throw new AxisFault(Messages.getMessage("bindingDisabled","SOAP12"));  
            }
        }
    }

    private void fillContextsFromSessionContext(MessageContext msgContext) throws AxisFault {
        AxisService service = msgContext.getAxisService();
        if (service == null) {
            throw new AxisFault(Messages.getMessage("unabletofindservice"));
        }
        SessionContext sessionContext = msgContext.getSessionContext();
        if (sessionContext == null) {
            TransportListener listener = msgContext.getTransportIn().getReceiver();
            sessionContext = listener.getSessionContext(msgContext);
            if (sessionContext == null) {
                createAndFillContexts(service, msgContext, sessionContext);
                return;
            }
        }
        String serviceGroupName = msgContext.getAxisServiceGroup().getServiceGroupName();
        ServiceGroupContext serviceGroupContext =
                sessionContext.getServiceGroupContext(serviceGroupName);
        if (serviceGroupContext != null) {
            //setting service group context
            msgContext.setServiceGroupContext(serviceGroupContext);
            // setting Service context
            msgContext.setServiceContext(serviceGroupContext.getServiceContext(service));
        } else {
            createAndFillContexts(service, msgContext, sessionContext);
        }
        ServiceContext serviceContext = sessionContext.getServiceContext(service);
        //found the serviceContext from session context , so adding that into msgContext
        if (serviceContext != null) {
            msgContext.setServiceContext(serviceContext);
            serviceContext.setProperty(HTTPConstants.COOKIE_STRING, sessionContext.getCookieID());
        }
    }

    private void createAndFillContexts(AxisService service,
                                       MessageContext msgContext,
                                       SessionContext sessionContext) throws AxisFault {
        ServiceGroupContext serviceGroupContext;
        AxisServiceGroup axisServiceGroup = service.getAxisServiceGroup();
        ConfigurationContext configCtx = msgContext.getConfigurationContext();
        serviceGroupContext = configCtx.createServiceGroupContext(axisServiceGroup);

        msgContext.setServiceGroupContext(serviceGroupContext);
        ServiceContext serviceContext = serviceGroupContext.getServiceContext(service);
        msgContext.setServiceContext(serviceContext);
        if (sessionContext != null) {
            sessionContext.addServiceContext(serviceContext);
            sessionContext.addServiceGroupContext(serviceGroupContext);
        }
    }

    private static final QName SERVICE_GROUP_QNAME = new QName(Constants.AXIS2_NAMESPACE_URI,
                                                               Constants.SERVICE_GROUP_ID,
                                                               Constants.AXIS2_NAMESPACE_PREFIX);

    private void extractServiceGroupContextId(MessageContext msgContext) throws AxisFault {
        SOAPHeader soapHeader = msgContext.getEnvelope().getHeader();
        if (soapHeader != null) {
            OMElement serviceGroupId = soapHeader.getFirstChildWithName(SERVICE_GROUP_QNAME);
            if (serviceGroupId != null) {
                msgContext.setServiceGroupContextId(serviceGroupId.getText());
            }
        }
    }
    
    /**
     * This method will determine if the MEP indicated by 'mepString' specifies
     * a oneway MEP.
     */
    @SuppressWarnings("deprecation")
    boolean isOneway(String mepString) {
        return (mepString.equals(WSDL2Constants.MEP_URI_IN_ONLY)
                || mepString.equals(WSDL2Constants.MEP_URI_IN_ONLY)
                || mepString.equals(WSDL2Constants.MEP_URI_IN_ONLY));
    }

}