/*
*  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.transport.base;

import org.apache.axis2.context.MessageContext;
import org.apache.axis2.context.ConfigurationContext;
import org.apache.axis2.AxisFault;
import org.apache.axis2.Constants;
import org.apache.axis2.wsdl.WSDLConstants;
import org.apache.axis2.util.MessageContextBuilder;
import org.apache.axis2.handlers.AbstractHandler;
import org.apache.axis2.engine.AxisEngine;
import org.apache.axis2.transport.TransportSender;
import org.apache.axis2.transport.OutTransportInfo;
import org.apache.axis2.description.TransportOutDescription;
import org.apache.axis2.description.TransportInDescription;
import org.apache.axis2.description.WSDL2Constants;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.axiom.util.UIDGenerator;

import javax.management.MBeanServer;
import javax.management.ObjectName;
import java.util.Map;
import java.util.Set;

public abstract class AbstractTransportSender extends AbstractHandler implements TransportSender {

    /** the reference to the actual commons logger to be used for log messages */
    protected Log log = null;

    /** the axis2 configuration context */
    protected ConfigurationContext cfgCtx = null;
    /** transport in description */
    private TransportInDescription transportIn  = null;
    /** transport out description */
    private TransportOutDescription transportOut = null;
    /** JMX support */
    private TransportMBeanSupport mbeanSupport;
    /** Metrics collector for the sender */
    protected MetricsCollector metrics = new MetricsCollector();
    /** state of the listener */
    private int state = BaseConstants.STOPPED;

    /**
     * A constructor that makes subclasses pick up the correct logger
     */
    protected AbstractTransportSender() {
        log = LogFactory.getLog(this.getClass());
    }

    /**
     * Initialize the generic transport sender.
     *
     * @param cfgCtx the axis configuration context
     * @param transportOut the transport-out description
     * @throws AxisFault on error
     */
    public void init(ConfigurationContext cfgCtx, TransportOutDescription transportOut)
        throws AxisFault {
        this.cfgCtx = cfgCtx;
        this.transportOut = transportOut;
        this.transportIn  = cfgCtx.getAxisConfiguration().getTransportIn(getTransportName());
        this.state = BaseConstants.STARTED;

        // register with JMX
        mbeanSupport = new TransportMBeanSupport(this, getTransportName());
        mbeanSupport.register();
        log.info(getTransportName().toUpperCase() + " Sender started");
    }

    public void stop() {
        if (state != BaseConstants.STARTED) return;
        state = BaseConstants.STOPPED;
        mbeanSupport.unregister();
        log.info(getTransportName().toUpperCase() + " Sender Shutdown");
    }

    public void cleanup(MessageContext msgContext) throws AxisFault {}

    public abstract void sendMessage(MessageContext msgCtx, String targetEPR,
        OutTransportInfo outTransportInfo) throws AxisFault;

    public InvocationResponse invoke(MessageContext msgContext) throws AxisFault {

        // is there a transport url which may be different to the WS-A To but has higher precedence
        String targetAddress = (String) msgContext.getProperty(
            Constants.Configuration.TRANSPORT_URL);

        if (targetAddress != null) {
            sendMessage(msgContext, targetAddress, null);
        } else if (msgContext.getTo() != null && !msgContext.getTo().hasAnonymousAddress()) {
            targetAddress = msgContext.getTo().getAddress();

            if (!msgContext.getTo().hasNoneAddress()) {
                sendMessage(msgContext, targetAddress, null);
            } else {
                //Don't send the message.
                return InvocationResponse.CONTINUE;
            }
        } else if (msgContext.isServerSide()) {
            // get the out transport info for server side when target EPR is unknown
            sendMessage(msgContext, null,
                (OutTransportInfo) msgContext.getProperty(Constants.OUT_TRANSPORT_INFO));
        }

        return InvocationResponse.CONTINUE;
    }

    /**
     * Process a new incoming message (Response) through the axis engine
     * @param msgCtx the axis MessageContext
     * @param trpHeaders the map containing transport level message headers
     * @param soapAction the optional soap action or null
     * @param contentType the optional content-type for the message
     */
    public void handleIncomingMessage(
        MessageContext msgCtx, Map trpHeaders,
        String soapAction, String contentType) {

        // set the soapaction if one is available via a transport header
        if (soapAction != null) {
            msgCtx.setSoapAction(soapAction);
        }

        // set the transport headers to the message context
        msgCtx.setProperty(MessageContext.TRANSPORT_HEADERS, trpHeaders);
        
        // send the message context through the axis engine
        try {
                try {
                    AxisEngine.receive(msgCtx);
                } catch (AxisFault e) {
                    if (log.isDebugEnabled()) {
                        log.debug("Error receiving message", e);
                    }
                    if (msgCtx.isServerSide()) {
                        AxisEngine.sendFault(MessageContextBuilder.createFaultMessageContext(msgCtx, e));
                    }
                }
        } catch (AxisFault axisFault) {
            logException("Error processing response message", axisFault);
        }
    }

    /**
     * Create a new axis MessageContext for an incoming response message
     * through this transport, for the given outgoing message
     *
     * @param outMsgCtx the outgoing message
     * @return the newly created message context
     */
    public MessageContext createResponseMessageContext(MessageContext outMsgCtx) {

        MessageContext responseMsgCtx = null;
        try {
            responseMsgCtx = outMsgCtx.getOperationContext().
                getMessageContext(WSDL2Constants.MESSAGE_LABEL_IN);
        } catch (AxisFault af) {
            log.error("Error getting IN message context from the operation context", af);
        }

        if (responseMsgCtx == null) {
            responseMsgCtx = new MessageContext();
            responseMsgCtx.setOperationContext(outMsgCtx.getOperationContext());
        }

        responseMsgCtx.setIncomingTransportName(getTransportName());
        responseMsgCtx.setTransportOut(transportOut);
        responseMsgCtx.setTransportIn(transportIn);

        responseMsgCtx.setMessageID(UIDGenerator.generateURNString());

        responseMsgCtx.setDoingREST(outMsgCtx.isDoingREST());
        responseMsgCtx.setProperty(
            MessageContext.TRANSPORT_IN, outMsgCtx.getProperty(MessageContext.TRANSPORT_IN));
        responseMsgCtx.setAxisMessage(outMsgCtx.getOperationContext().getAxisOperation().
            getMessage(WSDLConstants.MESSAGE_LABEL_IN_VALUE));
        responseMsgCtx.setTo(null);
        //msgCtx.setProperty(MessageContext.TRANSPORT_NON_BLOCKING, isNonBlocking);


        // are these relevant?
        //msgCtx.setServiceGroupContextId(UUIDGenerator.getUUID());
        // this is required to support Sandesha 2
        //msgContext.setProperty(RequestResponseTransport.TRANSPORT_CONTROL,
        //        new HttpCoreRequestResponseTransport(msgContext));

        return responseMsgCtx;
    }

    /**
     * Should the transport sender wait for a synchronous response to be received?
     * @param msgCtx the outgoing message context
     * @return true if a sync response is expected
     */
    protected boolean waitForSynchronousResponse(MessageContext msgCtx) {
        return
            msgCtx.getOperationContext() != null &&
            WSDL2Constants.MEP_URI_OUT_IN.equals(
                msgCtx.getOperationContext().getAxisOperation().getMessageExchangePattern());
    }

    public String getTransportName() {
        return transportOut.getName();
    }

    protected void handleException(String msg, Exception e) throws AxisFault {
        log.error(msg, e);
        throw new AxisFault(msg, e);
    }

    protected void handleException(String msg) throws AxisFault {
        log.error(msg);
        throw new AxisFault(msg);
    }

    protected void logException(String msg, Exception e) {
        log.error(msg, e);
    }

    //--- jmx/management methods ---
    public void pause() throws AxisFault {
        if (state != BaseConstants.STARTED) return;
        state = BaseConstants.PAUSED;
        log.info("Sender paused");
    }

    public void resume() throws AxisFault {
        if (state != BaseConstants.PAUSED) return;
        state = BaseConstants.STARTED;
        log.info("Sender resumed");
    }

    public void maintenenceShutdown(long millis) throws AxisFault {
        if (state != BaseConstants.STARTED) return;
        long start = System.currentTimeMillis();
        stop();
        state = BaseConstants.STOPPED;
        log.info("Sender shutdown in : " + (System.currentTimeMillis() - start) / 1000 + "s");
    }

    /**
     * Returns the number of active threads processing messages
     * @return number of active threads processing messages
     */
    public int getActiveThreadCount() {
        return 0;
    }

    /**
     * Return the number of requests queued in the thread pool
     * @return queue size
     */
    public int getQueueSize() {
        return 0;
    }

    // -- jmx/management methods--
    public long getMessagesReceived() {
        if (metrics != null) {
            return metrics.getMessagesReceived();
        }
        return -1;
    }

    public long getFaultsReceiving() {
        if (metrics != null) {
            return metrics.getFaultsReceiving();
        }
        return -1;
    }

    public long getBytesReceived() {
        if (metrics != null) {
            return metrics.getBytesReceived();
        }
        return -1;
    }

    public long getMessagesSent() {
        if (metrics != null) {
            return metrics.getMessagesSent();
        }
        return -1;
    }

    public long getFaultsSending() {
        if (metrics != null) {
            return metrics.getFaultsSending();
        }
        return -1;
    }

    public long getBytesSent() {
        if (metrics != null) {
            return metrics.getBytesSent();
        }
        return -1;
    }

    public long getTimeoutsReceiving() {
        if (metrics != null) {
            return metrics.getTimeoutsReceiving();
        }
        return -1;
    }

    public long getTimeoutsSending() {
        if (metrics != null) {
            return metrics.getTimeoutsSending();
        }
        return -1;
    }

    public long getMinSizeReceived() {
        if (metrics != null) {
            return metrics.getMinSizeReceived();
        }
        return -1;
    }

    public long getMaxSizeReceived() {
        if (metrics != null) {
            return metrics.getMaxSizeReceived();
        }
        return -1;
    }

    public double getAvgSizeReceived() {
        if (metrics != null) {
            return metrics.getAvgSizeReceived();
        }
        return -1;
    }

    public long getMinSizeSent() {
        if (metrics != null) {
            return metrics.getMinSizeSent();
        }
        return -1;
    }

    public long getMaxSizeSent() {
        if (metrics != null) {
            return metrics.getMaxSizeSent();
        }
        return -1;
    }

    public double getAvgSizeSent() {
        if (metrics != null) {
            return metrics.getAvgSizeSent();
        }
        return -1;
    }

    public Map getResponseCodeTable() {
        if (metrics != null) {
            return metrics.getResponseCodeTable();
        }
        return null;
    }

    public void resetStatistics() {
        if (metrics != null) {
            metrics.reset();
        }
    }

    public long getLastResetTime() {
        if (metrics != null) {
            return metrics.getLastResetTime();
        }
        return -1;
    }

    public long getMetricsWindow() {
        if (metrics != null) {
            return System.currentTimeMillis() - metrics.getLastResetTime();
        }
        return -1;
    } 

    private void registerMBean(MBeanServer mbs, Object mbeanInstance, String objectName) {
        try {
            ObjectName name = new ObjectName(objectName);
            Set set = mbs.queryNames(name, null);
            if (set != null && set.isEmpty()) {
                mbs.registerMBean(mbeanInstance, name);
            } else {
                mbs.unregisterMBean(name);
                mbs.registerMBean(mbeanInstance, name);
            }
        } catch (Exception e) {
            log.warn("Error registering a MBean with objectname ' " + objectName +
                " ' for JMX management", e);
        }
    }

}
