/*
 *  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.mina.proxy.handlers.http;

import java.io.UnsupportedEncodingException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.mina.core.buffer.IoBuffer;
import org.apache.mina.core.filterchain.IoFilter.NextFilter;
import org.apache.mina.core.future.ConnectFuture;
import org.apache.mina.core.session.IoSession;
import org.apache.mina.core.session.IoSessionInitializer;
import org.apache.mina.proxy.AbstractProxyLogicHandler;
import org.apache.mina.proxy.ProxyAuthException;
import org.apache.mina.proxy.session.ProxyIoSession;
import org.apache.mina.proxy.utils.IoBufferDecoder;
import org.apache.mina.proxy.utils.StringUtilities;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * AbstractHttpLogicHandler.java - Base class for HTTP proxy {@link AbstractProxyLogicHandler} implementations. 
 * Provides HTTP request encoding/response decoding functionality.
 * 
 * @author <a href="http://mina.apache.org">Apache MINA Project</a>
 * @since MINA 2.0.0-M3
 */
public abstract class AbstractHttpLogicHandler extends AbstractProxyLogicHandler {
    private static final Logger LOGGER = LoggerFactory.getLogger(AbstractHttpLogicHandler.class);

    private static final String DECODER = AbstractHttpLogicHandler.class.getName() + ".Decoder";

    private static final byte[] HTTP_DELIMITER = new byte[] { '\r', '\n', '\r', '\n' };

    private static final byte[] CRLF_DELIMITER = new byte[] { '\r', '\n' };

    // Parsing vars

    /**
     * Temporary buffer to accumulate the HTTP response from the proxy.
     */
    private IoBuffer responseData = null;

    /**
     * The parsed http proxy response
     */
    private HttpProxyResponse parsedResponse = null;

    /**
     * The content length of the proxy response.
     */
    private int contentLength = -1;

    // HTTP/1.1 vars

    /**
     * A flag that indicates that this is a HTTP/1.1 response with chunked data.and that some chunks are missing.   
     */
    private boolean hasChunkedData;

    /**
     * A flag that indicates that some chunks of data are missing to complete the HTTP/1.1 response.   
     */
    private boolean waitingChunkedData;

    /**
     * A flag that indicates that chunked data has been read and that we're now reading the footers.   
     */
    private boolean waitingFooters;

    /**
     * Contains the position of the entity body start in the <code>responseData</code> {@link IoBuffer}.
     */
    private int entityBodyStartPosition;

    /**
     * Contains the limit of the entity body start in the <code>responseData</code> {@link IoBuffer}.
     */
    private int entityBodyLimitPosition;

    /**
     * Creates a new {@link AbstractHttpLogicHandler}.
     * 
     * @param proxyIoSession the {@link ProxyIoSession} in use.
     */
    public AbstractHttpLogicHandler(final ProxyIoSession proxyIoSession) {
        super(proxyIoSession);
    }

    /**
     * Handles incoming data during the handshake process. Should consume only the
     * handshake data from the buffer, leaving any extra data in place.
     * 
     * @param nextFilter the next filter
     * @param buf the buffer holding received data
     */
    @Override
    public synchronized void messageReceived(final NextFilter nextFilter, final IoBuffer buf) throws ProxyAuthException {
        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug(" messageReceived()");
        }

        IoBufferDecoder decoder = (IoBufferDecoder) getSession().getAttribute(DECODER);
        if (decoder == null) {
            decoder = new IoBufferDecoder(HTTP_DELIMITER);
            getSession().setAttribute(DECODER, decoder);
        }

        try {
            if (parsedResponse == null) {

                responseData = decoder.decodeFully(buf);
                if (responseData == null) {
                    return;
                }

                // Handle the response                                
                String responseHeader = responseData.getString(getProxyIoSession().getCharset().newDecoder());
                entityBodyStartPosition = responseData.position();

                if (LOGGER.isDebugEnabled()) {
                    LOGGER.debug("  response header received:\n{}",
                            responseHeader.replace("\r", "\\r").replace("\n", "\\n\n"));
                }

                // Parse the response
                parsedResponse = decodeResponse(responseHeader);

                // Is handshake complete ?
                if (parsedResponse.getStatusCode() == 200
                        || (parsedResponse.getStatusCode() >= 300 && parsedResponse.getStatusCode() <= 307)) {
                    buf.position(0);
                    setHandshakeComplete();
                    return;
                }

                String contentLengthHeader = StringUtilities.getSingleValuedHeader(parsedResponse.getHeaders(),
                        "Content-Length");

                if (contentLengthHeader == null) {
                    contentLength = 0;
                } else {
                    contentLength = Integer.parseInt(contentLengthHeader.trim());
                    decoder.setContentLength(contentLength, true);
                }
            }

            if (!hasChunkedData) {
                if (contentLength > 0) {
                    IoBuffer tmp = decoder.decodeFully(buf);
                    if (tmp == null) {
                        return;
                    }
                    responseData.setAutoExpand(true);
                    responseData.put(tmp);
                    contentLength = 0;
                }

                if ("chunked".equalsIgnoreCase(StringUtilities.getSingleValuedHeader(parsedResponse.getHeaders(),
                        "Transfer-Encoding"))) {
                    // Handle Transfer-Encoding: Chunked
                    if (LOGGER.isDebugEnabled()) {
                        LOGGER.debug("Retrieving additional http response chunks");
                    }
                    
                    hasChunkedData = true;
                    waitingChunkedData = true;
                }
            }

            if (hasChunkedData) {
                // Read chunks
                while (waitingChunkedData) {
                    if (contentLength == 0) {
                        decoder.setDelimiter(CRLF_DELIMITER, false);
                        IoBuffer tmp = decoder.decodeFully(buf);
                        if (tmp == null) {
                            return;
                        }

                        String chunkSize = tmp.getString(getProxyIoSession().getCharset().newDecoder());
                        int pos = chunkSize.indexOf(';');
                        if (pos >= 0) {
                            chunkSize = chunkSize.substring(0, pos);
                        } else {
                            chunkSize = chunkSize.substring(0, chunkSize.length() - 2);
                        }
                        contentLength = Integer.decode("0x" + chunkSize);
                        if (contentLength > 0) {
                            contentLength += 2; // also read chunk's trailing CRLF
                            decoder.setContentLength(contentLength, true);
                        }
                    }

                    if (contentLength == 0) {
                        waitingChunkedData = false;
                        waitingFooters = true;
                        entityBodyLimitPosition = responseData.position();
                        break;
                    }

                    IoBuffer tmp = decoder.decodeFully(buf);
                    if (tmp == null) {
                        return;
                    }
                    contentLength = 0;
                    responseData.put(tmp);
                    buf.position(buf.position());
                }

                // Read footers
                while (waitingFooters) {
                    decoder.setDelimiter(CRLF_DELIMITER, false);
                    IoBuffer tmp = decoder.decodeFully(buf);
                    if (tmp == null) {
                        return;
                    }

                    if (tmp.remaining() == 2) {
                        waitingFooters = false;
                        break;
                    }

                    // add footer to headers                    
                    String footer = tmp.getString(getProxyIoSession().getCharset().newDecoder());
                    String[] f = footer.split(":\\s?", 2);
                    StringUtilities.addValueToHeader(parsedResponse.getHeaders(), f[0], f[1], false);
                    responseData.put(tmp);
                    responseData.put(CRLF_DELIMITER);
                }
            }

            responseData.flip();

            if (LOGGER.isDebugEnabled()) {
                LOGGER.debug("  end of response received:\n{}",
                        responseData.getString(getProxyIoSession().getCharset().newDecoder()));
            }

            // Retrieve entity body content
            responseData.position(entityBodyStartPosition);
            responseData.limit(entityBodyLimitPosition);
            parsedResponse.setBody(responseData.getString(getProxyIoSession().getCharset().newDecoder()));

            // Free the response buffer
            responseData.free();
            responseData = null;

            handleResponse(parsedResponse);

            parsedResponse = null;
            hasChunkedData = false;
            contentLength = -1;
            decoder.setDelimiter(HTTP_DELIMITER, true);

            if (!isHandshakeComplete()) {
                doHandshake(nextFilter);
            }
        } catch (Exception ex) {
            if (ex instanceof ProxyAuthException) {
                throw (ProxyAuthException) ex;
            }

            throw new ProxyAuthException("Handshake failed", ex);
        }
    }

    /**
     * Handles a HTTP response from the proxy server.
     * 
     * @param response The response.
     * @throws ProxyAuthException If we get an error during the proxy authentication
     */
    public abstract void handleResponse(final HttpProxyResponse response) throws ProxyAuthException;

    /**
     * Calls writeRequest0(NextFilter, HttpProxyRequest) to write the request. 
     * If needed a reconnection to the proxy is done previously.
     * 
     * @param nextFilter the next filter
     * @param request the http request
     */
    public void writeRequest(final NextFilter nextFilter, final HttpProxyRequest request) {
        ProxyIoSession proxyIoSession = getProxyIoSession();

        if (proxyIoSession.isReconnectionNeeded()) {
            reconnect(nextFilter, request);
        } else {
            writeRequest0(nextFilter, request);
        }
    }

    /**
     * Encodes a HTTP request and sends it to the proxy server.
     * 
     * @param nextFilter the next filter
     * @param request the http request
     */
    private void writeRequest0(final NextFilter nextFilter, final HttpProxyRequest request) {
        try {
            String data = request.toHttpString();
            IoBuffer buf = IoBuffer.wrap(data.getBytes(getProxyIoSession().getCharsetName()));

            if (LOGGER.isDebugEnabled()) {
                LOGGER.debug("   write:\n{}", data.replace("\r", "\\r").replace("\n", "\\n\n"));
            }

            writeData(nextFilter, buf);

        } catch (UnsupportedEncodingException ex) {
            closeSession("Unable to send HTTP request: ", ex);
        }
    }

    /**
     * Method to reconnect to the proxy when it decides not to maintain the connection 
     * during handshake.
     * 
     * @param nextFilter the next filter
     * @param request the http request
     */
    private void reconnect(final NextFilter nextFilter, final HttpProxyRequest request) {
        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug("Reconnecting to proxy ...");
        }

        final ProxyIoSession proxyIoSession = getProxyIoSession();

        // Fires reconnection
        proxyIoSession.getConnector().connect(new IoSessionInitializer<ConnectFuture>() {
            @Override
            public void initializeSession(final IoSession session, ConnectFuture future) {
                if (LOGGER.isDebugEnabled()) {
                    LOGGER.debug("Initializing new session: {}", session);
                }
                
                session.setAttribute(ProxyIoSession.PROXY_SESSION, proxyIoSession);
                proxyIoSession.setSession(session);
                
                if (LOGGER.isDebugEnabled()) {
                    LOGGER.debug("  setting up proxyIoSession: {}", proxyIoSession);
                }
                
                // Reconnection is done so we send the
                // request to the proxy
                proxyIoSession.setReconnectionNeeded(false);
                writeRequest0(nextFilter, request);
            }
        });
    }

    /**
     * Parse a HTTP response from the proxy server.
     * 
     * @param response The response string.
     * @return The decoded HttpResponse
     * @throws Exception If we get an error while decoding the response
     */
    protected HttpProxyResponse decodeResponse(final String response) throws Exception {
        if (LOGGER.isDebugEnabled()) {
            LOGGER.debug("  parseResponse()");
        }

        // Break response into lines
        String[] responseLines = response.split(HttpProxyConstants.CRLF);

        // Status-Line = HTTP-Version SP Status-Code SP Reason-Phrase CRLF
        // BUG FIX : Trimed to prevent failures with some proxies that add 
        // extra space chars like "Microsoft-IIS/5.0" ...
        String[] statusLine = responseLines[0].trim().split(" ", 2);

        if (statusLine.length < 2) {
            throw new Exception("Invalid response status line (" + statusLine + "). Response: " + response);
        }

        // Status line [1] is 3 digits, space and optional error text
        if (!statusLine[1].matches("^\\d\\d\\d.*")) {
            throw new Exception("Invalid response code (" + statusLine[1] + "). Response: " + response);
        }

        Map<String, List<String>> headers = new HashMap<>();

        for (int i = 1; i < responseLines.length; i++) {
            String[] args = responseLines[i].split(":\\s?", 2);
            StringUtilities.addValueToHeader(headers, args[0], args[1], false);
        }

        return new HttpProxyResponse(statusLine[0], statusLine[1], headers);
    }
}
