/**
 *  Copyright (c) 2009, WSO2 Inc. (http://www.wso2.org) All Rights Reserved.
 *
 *  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 org.apache.synapse.transport.passthru.util;

import org.apache.axiom.om.OMOutputFormat;
import org.apache.axis2.addressing.EndpointReference;
import org.apache.axis2.context.MessageContext;
import org.apache.axis2.context.ConfigurationContext;
import org.apache.axis2.Constants;
import org.apache.axis2.transport.TransportUtils;
import org.apache.axis2.util.JavaUtils;
import org.apache.axis2.description.AxisService;
import org.apache.axis2.description.Parameter;
import org.apache.axis2.description.AxisOperation;
import org.apache.http.nio.NHttpClientConnection;
import org.apache.http.nio.NHttpServerConnection;
import org.apache.http.protocol.HTTP;
import org.apache.http.HttpStatus;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.synapse.transport.nhttp.NhttpConstants;
import org.apache.synapse.transport.passthru.PassThroughConstants;
import org.apache.synapse.transport.passthru.config.TargetConfiguration;

import java.net.InetAddress;
import java.net.SocketException;
import java.net.NetworkInterface;
import java.util.Map;
import java.util.Iterator;
import java.util.Enumeration;
import java.util.Hashtable;

/**
 * Utility methods used by the transport.
 */
public class PassThroughTransportUtils {
    private static Log log = LogFactory.getLog(PassThroughTransportUtils.class);

    /**
     * This method tries to determine the hostname of the given InetAddress without
     * triggering a reverse DNS lookup.  {@link java.net.InetAddress#getHostName()}
     * triggers a reverse DNS lookup which can be very costly in cases where reverse
     * DNS fails. Tries to parse a symbolic hostname from {@link java.net.InetAddress#toString()},
     * which is documented to return a String of the form "hostname / literal IP address"
     * with 'hostname' blank if not already computed & stored in <code>address</code>.
     * <p/>
     * If the hostname cannot be determined from InetAddress.toString(),
     * the value of {@link java.net.InetAddress#getHostAddress()} is returned.
     *
     * @param address The InetAddress whose hostname has to be determined
     * @return hostsname, if it can be determined. hostaddress, if not.          
     */
    public static String getHostName(InetAddress address) {
        String result;
        String hostAddress = address.getHostAddress();
        String inetAddr = address.toString();
        int index1 = inetAddr.lastIndexOf('/');
        int index2 = inetAddr.indexOf(hostAddress);
        if (index2 == index1 + 1) {
            if (index1 == 0) {
                result = hostAddress;
            } else {
                result = inetAddr.substring(0, index1);
            }
        } else {
            result = hostAddress;
        }
        return result;
    }

    /**
     * Get the EPR for the message passed in
     * @param msgContext the message context
     * @return the destination EPR
     */
    public static EndpointReference getDestinationEPR(MessageContext msgContext) {

        // Trasnport URL can be different from the WSA-To
        String transportURL = (String) msgContext.getProperty(
            Constants.Configuration.TRANSPORT_URL);

        if (transportURL != null) {
            return new EndpointReference(transportURL);
        } else if (
            (msgContext.getTo() != null) && !msgContext.getTo().hasAnonymousAddress()) {
            return msgContext.getTo();
        }
        return null;
    }

    /**
     * Remove unwanted headers from the http response of outgoing request. These are headers which
     * should be dictated by the transport and not the user. We remove these as these may get
     * copied from the request messages
     *
     * @param msgContext the Axis2 Message context from which these headers should be removed
     * @param targetConfiguration configuration for the passThrough handler
     */
    public static void removeUnwantedHeaders(MessageContext msgContext, TargetConfiguration targetConfiguration) {
        Map transportHeaders = (Map) msgContext.getProperty(MessageContext.TRANSPORT_HEADERS);
        Map excessHeaders = (Map) msgContext.getProperty(NhttpConstants.EXCESS_TRANSPORT_HEADERS);

        if (transportHeaders != null && !transportHeaders.isEmpty()) {
            //a hack which takes the original content header
            if (transportHeaders.get(HTTP.CONTENT_LEN) != null) {
                msgContext.setProperty(PassThroughConstants.ORGINAL_CONTEN_LENGTH,
                                       transportHeaders.get(HTTP.CONTENT_LEN));
            }

            removeUnwantedHeadersFromHeaderMap(transportHeaders, targetConfiguration);
        }

        if (excessHeaders != null && !excessHeaders.isEmpty()) {
            removeUnwantedHeadersFromHeaderMap(excessHeaders, targetConfiguration);
        }

    }

    public static void setSourceConnectionContextAttributes(NHttpServerConnection sourceConn,
                                                            NHttpClientConnection conn) {
        sourceConn.getContext().setAttribute(PassThroughConstants.RES_HEADER_ARRIVAL_TIME,
                conn.getContext()
                        .getAttribute(PassThroughConstants.RES_HEADER_ARRIVAL_TIME)
        );
        conn.getContext().removeAttribute(PassThroughConstants.RES_HEADER_ARRIVAL_TIME);

        sourceConn.getContext().setAttribute(PassThroughConstants.REQ_DEPARTURE_TIME,
                conn.getContext()
                        .getAttribute(PassThroughConstants.REQ_DEPARTURE_TIME)
        );
        conn.getContext().removeAttribute(PassThroughConstants.REQ_DEPARTURE_TIME);
        sourceConn.getContext().setAttribute(PassThroughConstants.REQ_TO_BACKEND_WRITE_START_TIME,
                conn.getContext()
                        .getAttribute(PassThroughConstants.REQ_TO_BACKEND_WRITE_START_TIME)
        );

        conn.getContext().removeAttribute(PassThroughConstants.REQ_TO_BACKEND_WRITE_START_TIME);
        sourceConn.getContext().setAttribute(PassThroughConstants.REQ_TO_BACKEND_WRITE_END_TIME,
                conn.getContext()
                        .getAttribute(PassThroughConstants.REQ_TO_BACKEND_WRITE_END_TIME)
        );
        conn.getContext().removeAttribute(PassThroughConstants.REQ_TO_BACKEND_WRITE_END_TIME);
        sourceConn.getContext().setAttribute(PassThroughConstants.RES_FROM_BACKEND_READ_START_TIME,
                conn.getContext()
                        .getAttribute(PassThroughConstants.RES_FROM_BACKEND_READ_START_TIME)
        );
        conn.getContext().removeAttribute(PassThroughConstants.RES_FROM_BACKEND_READ_START_TIME);
    }


    /**
     * Remove unwanted headers from the given header map.
     *
     * @param headers             http header map
     * @param targetConfiguration configurations for the passThrough transporter
     */
    private static void removeUnwantedHeadersFromHeaderMap(Map headers,
                                                           TargetConfiguration targetConfiguration) {
        Iterator iter = headers.keySet().iterator();
        while (iter.hasNext()) {
            String headerName = (String) iter.next();
            if (HTTP.TRANSFER_ENCODING.equalsIgnoreCase(headerName)) {
                iter.remove();
            }

            if (HTTP.CONN_DIRECTIVE.equalsIgnoreCase(headerName)
                && !targetConfiguration.isPreserveHttpHeader(HTTP.CONN_DIRECTIVE)) {
                iter.remove();
            }

            if (HTTP.CONN_KEEP_ALIVE.equalsIgnoreCase(headerName)
                && !targetConfiguration.isPreserveHttpHeader(HTTP.CONN_KEEP_ALIVE)) {
                iter.remove();
            }

            if (HTTP.CONTENT_LEN.equalsIgnoreCase(headerName)
                && !targetConfiguration.isPreserveHttpHeader(HTTP.CONTENT_LEN)) {
                iter.remove();
            }

            if (HTTP.DATE_HEADER.equalsIgnoreCase(headerName)
                && !targetConfiguration.isPreserveHttpHeader(HTTP.DATE_HEADER)) {
                iter.remove();
            }

            if (HTTP.SERVER_HEADER.equalsIgnoreCase(headerName)
                && !targetConfiguration.isPreserveHttpHeader(HTTP.SERVER_HEADER)) {
                iter.remove();
            }

            if (HTTP.USER_AGENT.equalsIgnoreCase(headerName)
                && !targetConfiguration.isPreserveHttpHeader(HTTP.USER_AGENT)) {
                iter.remove();
            }
        }
    }

    /**
     * Determine the Http Status Code depending on the message type processed <br>
     * (normal response versus fault response) as well as Axis2 message context properties set
     * via Synapse configuration or MessageBuilders.
     *
     * @see PassThroughConstants#FAULTS_AS_HTTP_200
     * @see PassThroughConstants#HTTP_SC
     *
     * @param msgContext the Axis2 message context
     *
     * @return the HTTP status code to set in the HTTP response object
     */
    public static int determineHttpStatusCode(MessageContext msgContext) {

        int httpStatus = HttpStatus.SC_OK;

        // if this is a dummy message to handle http 202 case with non-blocking IO
        // set the status code to 202
        if (msgContext.isPropertyTrue(PassThroughConstants.SC_ACCEPTED)) {
            httpStatus = HttpStatus.SC_ACCEPTED;
        } else {
            // is this a fault message
            boolean handleFault = msgContext.getEnvelope() != null ?
                (msgContext.getEnvelope().getBody().hasFault() || msgContext.isProcessingFault()):false;
            boolean faultsAsHttp200 = false;
            if (msgContext.getProperty(PassThroughConstants.FAULTS_AS_HTTP_200) != null) {
                // shall faults be transmitted with HTTP 200
                faultsAsHttp200 =
                        PassThroughConstants.TRUE.equals(
                                msgContext.getProperty(PassThroughConstants.FAULTS_AS_HTTP_200).toString().toUpperCase());
            }
            // Set HTTP status code to 500 if this is a fault case and we shall not use HTTP 200
            if (handleFault && !faultsAsHttp200) {
                httpStatus = HttpStatus.SC_INTERNAL_SERVER_ERROR;
            }

            // Any status code previously set shall be overwritten with the value of the following
            // message context property if it is set.
            Object statusCode = msgContext.getProperty(PassThroughConstants.HTTP_SC);
            if (statusCode != null) {
                try {
                    httpStatus = Integer.parseInt(
                            msgContext.getProperty(PassThroughConstants.HTTP_SC).toString());
                } catch (NumberFormatException e) {
                    log.warn("Unable to set the HTTP status code from the property "
                            + PassThroughConstants.HTTP_SC + " with value: " + statusCode);
                }
            }
        }

        return httpStatus;
    }

    /**
     * Determine the Http Status Message depending on the message type processed <br>
     * (normal response versus fault response) as well as Axis2 message context properties set
     * via Synapse configuration or MessageBuilders.
     *
     * @see PassThroughConstants#FAULTS_AS_HTTP_200
     * @see PassThroughConstants#HTTP_SC
     *
     * @param msgContext the Axis2 message context
     *
     * @return the HTTP status message string or null
     */
    public static String determineHttpStatusLine(MessageContext msgContext) {
        Object statusLine = msgContext.getProperty(PassThroughConstants.HTTP_SC_DESC);
        if (statusLine != null) {
            return (String) statusLine;
        }
        return null;
    }

    /**
     * Whatever this method returns as the IP is ignored by the actual http/s listener when
     * its getServiceEPR is invoked. This was originally copied from axis2
     *
     * @return Returns String.
     * @throws java.net.SocketException if the socket can not be accessed
     */
    public static String getIpAddress() throws SocketException {
        Enumeration e = NetworkInterface.getNetworkInterfaces();
        String address = "127.0.0.1";

        while (e.hasMoreElements()) {
            NetworkInterface netface = (NetworkInterface) e.nextElement();
            Enumeration addresses = netface.getInetAddresses();

            while (addresses.hasMoreElements()) {
                InetAddress ip = (InetAddress) addresses.nextElement();
                if (!ip.isLoopbackAddress() && isIP(ip.getHostAddress())) {
                    return ip.getHostAddress();
                }
            }
        }
        return address;
    }

    private static boolean isIP(String hostAddress) {
        return hostAddress.split("[.]").length == 4;
    }


    /**
     * Returns the HTML text for the list of services deployed.
     * This can be delegated to another Class as well
     * where it will handle more options of GET messages.
     *
     * @param prefix to be used for the Service names
     * @param cfgCtx axis2 configuration context
     * @return the HTML to be displayed as a String
     */
    public String getServicesHTML(String prefix, ConfigurationContext cfgCtx) {

        Map services = cfgCtx.getAxisConfiguration().getServices();
        Hashtable erroneousServices = cfgCtx.getAxisConfiguration().getFaultyServices();
        boolean servicesFound = false;

        StringBuffer resultBuf = new StringBuffer();
        resultBuf.append("<html><head><title>Axis2: Services</title></head>" + "<body>");

        if ((services != null) && !services.isEmpty()) {

            servicesFound = true;
            resultBuf.append("<h2>" + "Deployed services" + "</h2>");

            for (Object service : services.values()) {

                AxisService axisService = (AxisService) service;
                Parameter parameter = axisService.getParameter(
                        PassThroughConstants.HIDDEN_SERVICE_PARAM_NAME);
                if (axisService.getName().startsWith("__") ||
                        (parameter != null && JavaUtils.isTrueExplicitly(parameter.getValue()))) {
                    continue;    // skip private services
                }

                Iterator iterator = axisService.getOperations();
                resultBuf.append("<h3><a href=\"").append(prefix).append(axisService.getName()).append(
                        "?wsdl\">").append(axisService.getName()).append("</a></h3>");

                if (iterator.hasNext()) {
                    resultBuf.append("Available operations <ul>");

                    for (; iterator.hasNext();) {
                        AxisOperation axisOperation = (AxisOperation) iterator.next();
                        resultBuf.append("<li>").append(
                                axisOperation.getName().getLocalPart()).append("</li>");
                    }
                    resultBuf.append("</ul>");
                } else {
                    resultBuf.append("No operations specified for this service");
                }
            }
        }

        if ((erroneousServices != null) && !erroneousServices.isEmpty()) {
            servicesFound = true;
            resultBuf.append("<hr><h2><font color=\"blue\">Faulty Services</font></h2>");
            Enumeration faultyservices = erroneousServices.keys();

            while (faultyservices.hasMoreElements()) {
                String faultyserviceName = (String) faultyservices.nextElement();
                resultBuf.append("<h3><font color=\"blue\">").append(
                        faultyserviceName).append("</font></h3>");
            }
        }

        if (!servicesFound) {
            resultBuf.append("<h2>There are no services deployed</h2>");
        }

        resultBuf.append("</body></html>");
        return resultBuf.toString();
    }

    public static OMOutputFormat getOMOutputFormat(MessageContext msgContext) {

    	OMOutputFormat format = null;
    	if(msgContext.getProperty(PassThroughConstants.MESSAGE_OUTPUT_FORMAT) != null){
    		format = (OMOutputFormat) msgContext.getProperty(PassThroughConstants.MESSAGE_OUTPUT_FORMAT);
    	}else{
    		format = new OMOutputFormat();
    	}
        // Keep the formatter information to prevent multipart boundary override (this will be the content writing to header)
        msgContext.setProperty(PassThroughConstants.MESSAGE_OUTPUT_FORMAT, format);
     
        msgContext.setDoingMTOM(TransportUtils.doWriteMTOM(msgContext));
        msgContext.setDoingSwA(TransportUtils.doWriteSwA(msgContext));
        msgContext.setDoingREST(TransportUtils.isDoingREST(msgContext));

        /**
               *  PassThroughConstants.INVOKED_REST set to true here if isDoingREST is true -
               *  this enables us to check whether the original request to the endpoint was a
               * REST request inside DefferedMessageBuilder (which we need to convert
               * text/xml content type into application/xml if the request was not a SOAP
               * request.
               */
        if(msgContext.isDoingREST()) {
            msgContext.setProperty(PassThroughConstants.INVOKED_REST, true);
        }
        format.setSOAP11(msgContext.isSOAP11());
        format.setDoOptimize(msgContext.isDoingMTOM());
        format.setDoingSWA(msgContext.isDoingSwA());

        format.setCharSetEncoding(TransportUtils.getCharSetEncoding(msgContext));
        Object mimeBoundaryProperty = msgContext.getProperty(Constants.Configuration.MIME_BOUNDARY);
        if (mimeBoundaryProperty != null) {
            format.setMimeBoundary((String) mimeBoundaryProperty);
        }

        return format;
    }

}
