/*
 *  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.ahc.codec;

import org.apache.ahc.util.NeedMoreDataException;
import org.apache.mina.common.ByteBuffer;
import org.apache.mina.common.IoSession;
import org.apache.mina.filter.codec.CumulativeProtocolDecoder;
import org.apache.mina.filter.codec.ProtocolDecoderOutput;

/**
 * The Class HttpResponseDecoder. This handles the decoding of raw bytes into a
 * {@link HttpResponseMessage} object.
 */
public class HttpResponseDecoder extends CumulativeProtocolDecoder {

    /** The http decoder. */
    private HttpDecoder httpDecoder = new HttpDecoder();

    /**
     * Decodes the raw HTTP response from a server into a {@link HttpResponseMessage} object.
     * 
     * @param ioSession the {@link org.apache.mina.common.IoSession} representing the connection to the server.
     * @param in the <code>ByteBuffer</code> that contains the raw bytes from the server
     * @param out {@link org.apache.mina.filter.codec.ProtocolDecoderOutput} used for output
     * 
     * @return <code>true</code> if it can read all of the data, or <code>false</code> if not.  A false tells the API to fetch more data.
     * 
     * @see org.apache.mina.filter.codec.CumulativeProtocolDecoder#doDecode(org.apache.mina.common.IoSession, org.apache.mina.common.ByteBuffer, org.apache.mina.filter.codec.ProtocolDecoderOutput)
     */
    protected boolean doDecode(IoSession ioSession, ByteBuffer in, ProtocolDecoderOutput out)
        throws Exception {

        try {
            HttpResponseMessage response = (HttpResponseMessage)ioSession.getAttribute(HttpIoHandler.CURRENT_RESPONSE);
            if (response == null) {
                HttpRequestMessage request = (HttpRequestMessage)ioSession.getAttribute(HttpIoHandler.CURRENT_REQUEST);
                response = new HttpResponseMessage(request.getUrl());
                ioSession.setAttribute(HttpIoHandler.CURRENT_RESPONSE, response);
            }

            //Test if we need the response...
            if (response.getState() == HttpResponseMessage.STATE_START) {

                if (!processStatus(response, in)) {
                    throw new NeedMoreDataException();
                }

                //Handle HTTP/1.1 100 Continue
                if (response.getStatusCode() == 100) {
                    response.setState(HttpResponseMessage.STATE_STATUS_CONTINUE);
                } else {
                    response.setState(HttpResponseMessage.STATE_STATUS_READ);
                }
            }

            //If we are in a 100 Continue, read until we get the real header
            if (response.getState() == HttpResponseMessage.STATE_STATUS_CONTINUE) {
                //Continue reading until we get a blank line
                while (true) {
                    String line = httpDecoder.decodeLine(in);

                    //Check if the entire response has been read
                    if (line == null) {
                        throw new NeedMoreDataException();
                    }

                    //Check if the entire response headers have been read
                    if (line.length() == 0) {
                        response.setState(HttpResponseMessage.STATE_STATUS_READ);

                        //The next line should be a header
                        if (!processStatus(response, in)) {
                            throw new NeedMoreDataException();
                        }
                        break;
                    }
                }
            }

            //Are we reading headers?
            if (response.getState() == HttpResponseMessage.STATE_STATUS_READ) {
                if (!processHeaders(response, in)) {
                    throw new NeedMoreDataException();
                }
            }

            //Are we reading content?
            if (response.getState() == HttpResponseMessage.STATE_HEADERS_READ) {
                if (!processContent(response, ioSession, in)) {
                    throw new NeedMoreDataException();
                }
            }

            //If we are chunked and we have read all the content, then read the footers if there are any
            if (response.isChunked() && response.getState() == HttpResponseMessage.STATE_CONTENT_READ) {
                if (!processFooters(response, in)) {
                    throw new NeedMoreDataException();
                }
            }

            completeResponse(ioSession, out, response);

            return true;
        } catch (NeedMoreDataException e) {
            return false;
        }
    }

    @Override
    public void finishDecode(IoSession session, ProtocolDecoderOutput out) throws Exception {
        // if the response was still being decoded when the session is getting
        // closed, we finish decoding and hand off the response
        HttpResponseMessage response = 
                (HttpResponseMessage)session.getAttribute(HttpIoHandler.CURRENT_RESPONSE);
        // we're only interested in non-chunked responses with no content length
        // specified, in which case connection close marks the end of the body
        if (response != null &&
                !response.isChunked() &&
                response.getContentLength() < 0 &&
                response.getState() == HttpResponseMessage.STATE_HEADERS_READ) {
            completeResponse(session, out, response);
        }
    }

    private void completeResponse(IoSession ioSession, ProtocolDecoderOutput out, HttpResponseMessage response) {
        response.setState(HttpResponseMessage.STATE_FINISHED);
        out.write(response);
        ioSession.removeAttribute(HttpIoHandler.CURRENT_RESPONSE);
    }

    /**
     * Reads the headers and processes them as header objects in the {@link HttpResponseMessage} object.
     * 
     * @param response the {@link HttpResponseMessage} response object
     * @param in the <code>ByteBuffer</code> that contains the raw bytes from the server
     * 
     * @return <code>true</code> if it can read all of the data, or <code>false</code> if not.
     * 
     * @throws Exception if any exception occurs
     */
    private boolean processHeaders(HttpResponseMessage response, ByteBuffer in) throws Exception {
        if (!findHeaders(response, in)) {
            return false;
        }

        response.setState(HttpResponseMessage.STATE_HEADERS_READ);
        return true;
    }

    /**
     * Reads the footers and processes them as header objects in the {@link HttpResponseMessage} 
     * object.  This is only used in chunked transcoding.
     * 
     * @param response the {@link HttpResponseMessage} response object
     * @param in the <code>ByteBuffer</code> that contains the raw bytes from the server
     * 
     * @return <code>true</code> if it can read all of the data, or <code>false</code> if not.
     * 
     * @throws Exception if any exception occurs
     */
    private boolean processFooters(HttpResponseMessage response, ByteBuffer in) throws Exception {
        if (!findHeaders(response, in)) {
            return false;
        }

        response.setState(HttpResponseMessage.STATE_FOOTERS_READ);
        return true;
    }

    /**
     * Decodes the raw headers/footers.
     * 
     * @param response the {@link HttpResponseMessage} response object
     * @param in the <code>ByteBuffer</code> that contains the raw bytes from the server
     * 
     * @return <code>true</code> if it can read all of the data, or <code>false</code> if not.
     * 
     * @throws Exception if any exception occurs
     */
    private boolean findHeaders(HttpResponseMessage response, ByteBuffer in) throws Exception {
        //Read the headers and process them
        while (true) {
            String line = httpDecoder.decodeHeaderLine(in);

            //Check if the entire response has been read
            if (line == null) {
                return false;
            }

            //Check if the entire response headers have been read
            if (line.length() == 0) {
                break;
            }

            httpDecoder.decodeHeader(line, response);
        }
        return true;
    }

    /**
     * Process and read the content.
     * 
     * @param response the {@link HttpResponseMessage} response object
     * @param in the <code>ByteBuffer</code> that contains the raw bytes from the server
     * @return <code>true</code> if it can read all of the data, or <code>false</code> if not.
     * 
     * @throws Exception if any exception occurs
     */
    private boolean processContent(HttpResponseMessage response, IoSession session, ByteBuffer in) 
            throws Exception {
        if (response.isChunked()) {
            while (true) {
                //Check what kind of record we are reading (content or size)
                if (response.getExpectedToRead() == HttpResponseMessage.EXPECTED_NOT_READ) {
                    //We haven't read the size, so we are expecting a size
                    String line = httpDecoder.decodeLine(in);

                    //Check if the entire line has been read
                    if (line == null) {
                        return false;
                    }

                    response.setExpectedToRead(httpDecoder.decodeSize(line));

                    //Are we done reading the chunked content? (A zero means we are done)
                    if (response.getExpectedToRead() == 0) {
                        break;
                    }
                }

                //Now read the content chunk

                //Be sure all of the data is there for us to retrieve + the CRLF...
                if (response.getExpectedToRead() + 2 > in.remaining()) {
                    //Need more data
                    return false;
                }

                //Read the content
                httpDecoder.decodeChunkedContent(in, response);

                //Flag that it's time to read a size record
                response.setExpectedToRead(HttpResponseMessage.EXPECTED_NOT_READ);

            }

        } else if (response.getContentLength() > 0) {
            //Do we have enough data?
            if ((response.getContentLength()) > in.remaining()) {
                return false;
            }
            httpDecoder.decodeContent(in, response);
        } else if (!noContentExpected(session, response)) {
            // it neither is chunked nor has content length; read until the
            // session closes
            httpDecoder.decodeRemainingContent(in, response);
            // keep looping until the session closes
            return false;
        }

        response.setState(HttpResponseMessage.STATE_CONTENT_READ);

        return true;
    }
    
    /**
     * Returns whether content is not expected for this response.  Responses
     * with certain status codes must not have contents, as well as responses to
     * proxy connect requests.
     */
    private boolean noContentExpected(IoSession session, HttpResponseMessage response) {
        int status = response.getStatusCode();
        // if status is no content or not modified, content is not allowed
        if (status == 204 || status == 304) {
            return true;
        }
        
        // explicit 0 content length
        if (response.getContentLength() == 0) {
            return true;
        }
        
        // proxy connect handshake is in progress, in which case the connection
        // will be persistent but no content is expected
        if (session.getAttribute(HttpIoHandler.PROXY_CONNECT_IN_PROGRESS) != null) {
            return true;
        }
        
        return false;
    }

    /**
     * Process  and read the status header.
     * 
     * @param response the {@link HttpResponseMessage} response object
     * @param in the <code>ByteBuffer</code> that contains the raw bytes from the server
     * 
     * @return <code>true</code> if it can read all of the data, or <code>false</code> if not.
     * 
     * @throws Exception if any exception occurs
     */
    private boolean processStatus(HttpResponseMessage response, ByteBuffer in) throws Exception {
        //Read the status header
        String header = httpDecoder.decodeLine(in);
        if (header == null) {
            return false;
        }

        httpDecoder.decodeStatus(header, response);

        return true;
    }
}
