/**
 * Copyright 2007-2016, Kaazing Corporation. 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.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.future.IoFutureListener;
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.
     * @param request the requested url to negotiate with the proxy.
     */
    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
     */
    public synchronized void messageReceived(final NextFilter nextFilter,
            final IoBuffer buf) throws ProxyAuthException {
        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();

                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
                    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();

            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.
     */
    public abstract void handleResponse(final HttpProxyResponse response)
            throws ProxyAuthException;

    /**
     * Calls{@link #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()));

            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) {
        LOGGER.debug("Reconnecting to proxy ...");

        final ProxyIoSession proxyIoSession = getProxyIoSession();

        // Fires reconnection
        proxyIoSession.getConnector().connect(
                new IoSessionInitializer<ConnectFuture>() {
                    public void initializeSession(final IoSession session,
                            ConnectFuture future) {
                        LOGGER.debug("Initializing new session: {}", session);
                        session.setAttribute(ProxyIoSession.PROXY_SESSION,
                                proxyIoSession);
                        proxyIoSession.setSession(session);
                        LOGGER.debug("  setting up proxyIoSession: {}", proxyIoSession);
                        future
                                .addListener(new IoFutureListener<ConnectFuture>() {
                                    public void operationComplete(
                                            ConnectFuture future) {
                                        // 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.
     */
    protected HttpProxyResponse decodeResponse(final String response)
            throws Exception {
        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 code is 3 digits
        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);
    }
}
