/**
 *  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;

import org.apache.commons.collections.map.MultiValueMap;
import org.apache.http.Header;
import org.apache.http.HttpEntityEnclosingRequest;
import org.apache.http.HttpException;
import org.apache.http.HttpRequest;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.HttpVersion;
import org.apache.http.ProtocolVersion;
import org.apache.http.message.BasicHttpResponse;
import org.apache.http.nio.ContentDecoder;
import org.apache.http.nio.NHttpServerConnection;
import org.apache.synapse.transport.passthru.config.SourceConfiguration;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.Comparator;
import java.util.Map;
import java.util.TreeMap;

/**
 * Represents a Http Request.
 */
public class SourceRequest {
    // private Log log = LogFactory.getLog(SourceRequest.class);

    private Pipe pipe = null;
    /** HTTP Headers */
    private Map<String, String> headers =  new TreeMap<String, String>(new Comparator<String>() {
        public int compare(String o1, String o2) {
            return o1.compareToIgnoreCase(o2);
        }
    });
    /** HTTP URL */
    private String url;
    /** HTTP Method */
    private String method;
    /** Weather reqyest has a body */
    private boolean entityEnclosing;
    /** The http request */
    private HttpRequest request = null;
    /** Configuration of the receiver */
    private SourceConfiguration sourceConfiguration;
    /** HTTP Version */
    private ProtocolVersion version = null;
    /** The connection from the client */
    private NHttpServerConnection connection = null;
   
    /** Excess headers of the request */
    private Map excessHeaders = new MultiValueMap();


    public SourceRequest(SourceConfiguration sourceConfiguration,
                         HttpRequest request,
                         NHttpServerConnection conn) {
        this.sourceConfiguration = sourceConfiguration;
        this.request = request;
        this.connection = conn;

        this.url = request.getRequestLine().getUri();
        this.method = request.getRequestLine().getMethod();
        this.entityEnclosing = request instanceof HttpEntityEnclosingRequest;
        this.version = request.getProtocolVersion();

        this.version = request.getRequestLine().getProtocolVersion();
        if (!version.lessEquals(HttpVersion.HTTP_1_1)) {
            // Downgrade protocol version if greater than HTTP/1.1
            this.version = HttpVersion.HTTP_1_1;
        }

        Header[] headers = request.getAllHeaders();
        if (headers != null) {
            for (Header header : headers) {
                if(this.headers.containsKey(header.getName())) {
	                 addExcessHeader(header);
	            } else {
	                 this.headers.put(header.getName(), header.getValue());
	            }
	        }
	    }
    }

    /**
     * Start processing the request by connecting the pipe if this request has an entity body.
     * @param conn connection
     * @throws IOException if an error occurs
     * @throws HttpException if an error occurs
     */
    public void start(NHttpServerConnection conn) throws IOException, HttpException {
        if (entityEnclosing) {
            pipe = new Pipe(conn, sourceConfiguration.getBufferFactory().getBuffer(), "source", sourceConfiguration);

            SourceContext.get(conn).setReader(pipe);

            // See if the client expects a 100-Continue
            if (((HttpEntityEnclosingRequest) request).expectContinue()) {
                HttpResponse ack = new BasicHttpResponse(version, HttpStatus.SC_CONTINUE, "Continue");
                conn.submitResponse(ack);
            }
        } else {
            // this request is completed, there is nothing more to read
            SourceContext.updateState(conn, ProtocolState.REQUEST_DONE);
            // No httpRequest content expected. Suspend client input
            conn.suspendInput();
        }
    }

    /**
     * Produce the content in to the pipe.
     *
     * @param conn    the connection
     * @param decoder content decoder
     * @return number of bytes read
     * @throws java.io.IOException if an error occurs
     */
    public int read(NHttpServerConnection conn, ContentDecoder decoder) throws IOException {
        if (pipe == null) {
            throw new IllegalStateException("A Pipe must be connected before calling read");
        }
        if (entityEnclosing) {
            int bytes = pipe.produce(decoder);
            readPostActions(conn, decoder);
            return bytes;
        } else {
            throw new IllegalStateException("Only Entity Enclosing Requests can read content in to the pipe");
        }
    }

    /**
     * Same as
     * {@link SourceRequest#read(org.apache.http.nio.NHttpServerConnection, org.apache.http.nio.ContentDecoder)}
     * but gives the copy of content produced in to the pipe.
     *
     * @param conn    the connection
     * @param decoder content decoder
     * @return copy of data produced to the pipe
     * @throws java.io.IOException if an error occurs
     */
    public ByteBuffer copyAndRead(NHttpServerConnection conn, ContentDecoder decoder) throws IOException {

        if (pipe == null) {
            throw new IllegalStateException("A Pipe must be connected before calling read");
        }
        if (entityEnclosing) {
            ByteBuffer bufferCopy = pipe.copyAndProduce(decoder);
            readPostActions(conn, decoder);
            return bufferCopy;
        } else {
            throw new IllegalStateException("Only Entity Enclosing Requests can read content in to the pipe");
        }
    }

    private void readPostActions(NHttpServerConnection conn, ContentDecoder decoder) {

        if (decoder.isCompleted()) {
            conn.getContext().setAttribute(PassThroughConstants.REQ_FROM_CLIENT_READ_END_TIME,
                                           System.currentTimeMillis());
            sourceConfiguration.getMetrics().
                    notifyReceivedMessageSize(conn.getMetrics().getReceivedBytesCount());
            // Update connection state
            SourceContext.updateState(conn, ProtocolState.REQUEST_DONE);
            // Suspend client input
            conn.suspendInput();
        }
    }

    public Map<String, String> getHeaders() {
        return headers;
    }

    public String getUri() {
        return url;
    }

    public String getMethod() {
        return method;
    }
    
    public Map getExcessHeaders() {
         return this.excessHeaders;
    }
  
    public void addExcessHeader(Header h) {
         this.excessHeaders.put(h.getName(), h.getValue());
    }

    public Pipe getPipe() {
        return pipe;
    }

    public NHttpServerConnection getConnection() {
        return connection;
    }

    public ProtocolVersion getVersion() {
        return version;
    }

    public HttpRequest getRequest() {
        return request;
    }

    public boolean isEntityEnclosing() {
        return entityEnclosing;
    }
}
