/*
 * Copyright (C) 2007 The Android Open Source Project
 *
 * 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 android.net.http;

import android.content.Context;
import android.os.SystemClock;

import java.io.IOException;
import java.net.UnknownHostException;
import java.util.ListIterator;
import java.util.LinkedList;

import javax.net.ssl.SSLHandshakeException;

import org.apache.http.ConnectionReuseStrategy;
import org.apache.http.HttpEntity;
import org.apache.http.HttpException;
import org.apache.http.HttpHost;
import org.apache.http.HttpVersion;
import org.apache.http.ParseException;
import org.apache.http.ProtocolVersion;
import org.apache.http.protocol.ExecutionContext;
import org.apache.http.protocol.HttpContext;
import org.apache.http.protocol.BasicHttpContext;

/**
 * {@hide}
 */
abstract class Connection {

    /**
     * Allow a TCP connection 60 idle seconds before erroring out
     */
    static final int SOCKET_TIMEOUT = 60000;

    private static final int SEND = 0;
    private static final int READ = 1;
    private static final int DRAIN = 2;
    private static final int DONE = 3;
    private static final String[] states = {"SEND",  "READ", "DRAIN", "DONE"};

    Context mContext;

    /** The low level connection */
    protected AndroidHttpClientConnection mHttpClientConnection = null;

    /**
     * The server SSL certificate associated with this connection
     * (null if the connection is not secure)
     * It would be nice to store the whole certificate chain, but
     * we want to keep things as light-weight as possible
     */
    protected SslCertificate mCertificate = null;

    /**
     * The host this connection is connected to.  If using proxy,
     * this is set to the proxy address
     */
    HttpHost mHost;

    /** true if the connection can be reused for sending more requests */
    private boolean mCanPersist;

    /** context required by ConnectionReuseStrategy. */
    private HttpContext mHttpContext;

    /** set when cancelled */
    private static int STATE_NORMAL = 0;
    private static int STATE_CANCEL_REQUESTED = 1;
    private int mActive = STATE_NORMAL;

    /** The number of times to try to re-connect (if connect fails). */
    private final static int RETRY_REQUEST_LIMIT = 2;

    private static final int MIN_PIPE = 2;
    private static final int MAX_PIPE = 3;

    /**
     * Doesn't seem to exist anymore in the new HTTP client, so copied here.
     */
    private static final String HTTP_CONNECTION = "http.connection";

    RequestQueue.ConnectionManager mConnectionManager;
    RequestFeeder mRequestFeeder;

    /**
     * Buffer for feeding response blocks to webkit.  One block per
     * connection reduces memory churn.
     */
    private byte[] mBuf;

    protected Connection(Context context, HttpHost host,
                         RequestQueue.ConnectionManager connectionManager,
                         RequestFeeder requestFeeder) {
        mContext = context;
        mHost = host;
        mConnectionManager = connectionManager;
        mRequestFeeder = requestFeeder;

        mCanPersist = false;
        mHttpContext = new BasicHttpContext(null);
    }

    HttpHost getHost() {
        return mHost;
    }

    /**
     * connection factory: returns an HTTP or HTTPS connection as
     * necessary
     */
    static Connection getConnection(
            Context context, HttpHost host,
            RequestQueue.ConnectionManager connectionManager,
            RequestFeeder requestFeeder) {

        if (host.getSchemeName().equals("http")) {
            return new HttpConnection(context, host, connectionManager,
                                      requestFeeder);
        }

        // Otherwise, default to https
        return new HttpsConnection(context, host, connectionManager,
                                   requestFeeder);
    }

    /**
     * @return The server SSL certificate associated with this
     * connection (null if the connection is not secure)
     */
    /* package */ SslCertificate getCertificate() {
        return mCertificate;
    }

    /**
     * Close current network connection
     * Note: this runs in non-network thread
     */
    void cancel() {
        mActive = STATE_CANCEL_REQUESTED;
        closeConnection();
        if (HttpLog.LOGV) HttpLog.v(
            "Connection.cancel(): connection closed " + mHost);
    }

    /**
     * Process requests in queue
     * pipelines requests
     */
    void processRequests(Request firstRequest) {
        Request req = null;
        boolean empty;
        int error = EventHandler.OK;
        Exception exception = null;

        LinkedList<Request> pipe = new LinkedList<Request>();

        int minPipe = MIN_PIPE, maxPipe = MAX_PIPE;
        int state = SEND;

        while (state != DONE) {
            if (HttpLog.LOGV) HttpLog.v(
                    states[state] + " pipe " + pipe.size());

            /* If a request was cancelled, give other cancel requests
               some time to go through so we don't uselessly restart
               connections */
            if (mActive == STATE_CANCEL_REQUESTED) {
                try {
                    Thread.sleep(100);
                } catch (InterruptedException x) { /* ignore */ }
                mActive = STATE_NORMAL;
            }

            switch (state) {
                case SEND: {
                    if (pipe.size() == maxPipe) {
                        state = READ;
                        break;
                    }
                    /* get a request */
                    if (firstRequest == null) {
                        req = mRequestFeeder.getRequest(mHost);
                    } else {
                        req = firstRequest;
                        firstRequest = null;
                    }
                    if (req == null) {
                        state = DRAIN;
                        break;
                    }
                    req.setConnection(this);

                    /* Don't work on cancelled requests. */
                    if (req.mCancelled) {
                        if (HttpLog.LOGV) HttpLog.v(
                                "processRequests(): skipping cancelled request "
                                + req);
                        req.complete();
                        break;
                    }

                    if (mHttpClientConnection == null ||
                        !mHttpClientConnection.isOpen()) {
                        /* If this call fails, the address is bad or
                           the net is down.  Punt for now.

                           FIXME: blow out entire queue here on
                           connection failure if net up? */

                        if (!openHttpConnection(req)) {
                            state = DONE;
                            break;
                        }
                    }

                    try {
                        /* FIXME: don't increment failure count if old
                           connection?  There should not be a penalty for
                           attempting to reuse an old connection */
                        req.sendRequest(mHttpClientConnection);
                    } catch (HttpException e) {
                        exception = e;
                        error = EventHandler.ERROR;
                    } catch (IOException e) {
                        exception = e;
                        error = EventHandler.ERROR_IO;
                    } catch (IllegalStateException e) {
                        exception = e;
                        error = EventHandler.ERROR_IO;
                    }
                    if (exception != null) {
                        if (httpFailure(req, error, exception) &&
                            !req.mCancelled) {
                            /* retry request if not permanent failure
                               or cancelled */
                            pipe.addLast(req);
                        }
                        exception = null;
                        state = (clearPipe(pipe) ||
                                 !mConnectionManager.isNetworkConnected()) ?
                                DONE : SEND;
                        minPipe = maxPipe = 1;
                        break;
                    }

                    pipe.addLast(req);
                    if (!mCanPersist) state = READ;
                    break;

                }
                case DRAIN:
                case READ: {
                    empty = !mRequestFeeder.haveRequest(mHost);
                    int pipeSize = pipe.size();
                    if (state != DRAIN && pipeSize < minPipe &&
                        !empty && mCanPersist) {
                        state = SEND;
                        break;
                    } else if (pipeSize == 0) {
                        /* Done if no other work to do */
                        state = empty ? DONE : SEND;
                        break;
                    }

                    req = (Request)pipe.removeFirst();
                    if (HttpLog.LOGV) HttpLog.v(
                            "processRequests() reading " + req);

                    try {
                        req.readResponse(mHttpClientConnection);
                    } catch (ParseException e) {
                        exception = e;
                        error = EventHandler.ERROR_IO;
                    } catch (IOException e) {
                        exception = e;
                        error = EventHandler.ERROR_IO;
                    } catch (IllegalStateException e) {
                        exception = e;
                        error = EventHandler.ERROR_IO;
                    }
                    if (exception != null) {
                        if (httpFailure(req, error, exception) &&
                            !req.mCancelled) {
                            /* retry request if not permanent failure
                               or cancelled */
                            req.reset();
                            pipe.addFirst(req);
                        }
                        exception = null;
                        mCanPersist = false;
                    }
                    if (!mCanPersist) {
                        if (HttpLog.LOGV) HttpLog.v(
                                "processRequests(): no persist, closing " +
                                mHost);

                        closeConnection();

                        mHttpContext.removeAttribute(HTTP_CONNECTION);
                        clearPipe(pipe);
                        minPipe = maxPipe = 1;
                        /* If network active continue to service this queue */
                        state = mConnectionManager.isNetworkConnected() ?
                                SEND : DONE;
                    }
                    break;
                }
            }
        }
    }

    /**
     * After a send/receive failure, any pipelined requests must be
     * cleared back to the mRequest queue
     * @return true if mRequests is empty after pipe cleared
     */
    private boolean clearPipe(LinkedList<Request> pipe) {
        boolean empty = true;
        if (HttpLog.LOGV) HttpLog.v(
                "Connection.clearPipe(): clearing pipe " + pipe.size());
        synchronized (mRequestFeeder) {
            Request tReq;
            while (!pipe.isEmpty()) {
                tReq = (Request)pipe.removeLast();
                if (HttpLog.LOGV) HttpLog.v(
                        "clearPipe() adding back " + mHost + " " + tReq);
                mRequestFeeder.requeueRequest(tReq);
                empty = false;
            }
            if (empty) empty = mRequestFeeder.haveRequest(mHost);
        }
        return empty;
    }

    /**
     * @return true on success
     */
    private boolean openHttpConnection(Request req) {

        long now = SystemClock.uptimeMillis();
        int error = EventHandler.OK;
        Exception exception = null;

        try {
            // reset the certificate to null before opening a connection
            mCertificate = null;
            mHttpClientConnection = openConnection(req);
            if (mHttpClientConnection != null) {
                mHttpClientConnection.setSocketTimeout(SOCKET_TIMEOUT);
                mHttpContext.setAttribute(HTTP_CONNECTION,
                                          mHttpClientConnection);
            } else {
                // we tried to do SSL tunneling, failed,
                // and need to drop the request;
                // we have already informed the handler
                req.mFailCount = RETRY_REQUEST_LIMIT;
                return false;
            }
        } catch (UnknownHostException e) {
            if (HttpLog.LOGV) HttpLog.v("Failed to open connection");
            error = EventHandler.ERROR_LOOKUP;
            exception = e;
        } catch (IllegalArgumentException e) {
            if (HttpLog.LOGV) HttpLog.v("Illegal argument exception");
            error = EventHandler.ERROR_CONNECT;
            req.mFailCount = RETRY_REQUEST_LIMIT;
            exception = e;
        } catch (SSLConnectionClosedByUserException e) {
            // hack: if we have an SSL connection failure,
            // we don't want to reconnect
            req.mFailCount = RETRY_REQUEST_LIMIT;
            // no error message
            return false;
        } catch (SSLHandshakeException e) {
            // hack: if we have an SSL connection failure,
            // we don't want to reconnect
            req.mFailCount = RETRY_REQUEST_LIMIT;
            if (HttpLog.LOGV) HttpLog.v(
                    "SSL exception performing handshake");
            error = EventHandler.ERROR_FAILED_SSL_HANDSHAKE;
            exception = e;
        } catch (IOException e) {
            error = EventHandler.ERROR_CONNECT;
            exception = e;
        }

        if (HttpLog.LOGV) {
            long now2 = SystemClock.uptimeMillis();
            HttpLog.v("Connection.openHttpConnection() " +
                      (now2 - now) + " " + mHost);
        }

        if (error == EventHandler.OK) {
            return true;
        } else {
            if (mConnectionManager.isNetworkConnected() == false ||
                req.mFailCount < RETRY_REQUEST_LIMIT) {
                // requeue
                mRequestFeeder.requeueRequest(req);
                req.mFailCount++;
            } else {
                httpFailure(req, error, exception);
            }
            return error == EventHandler.OK;
        }
    }

    /**
     * Helper.  Calls the mEventHandler's error() method only if
     * request failed permanently.  Increments mFailcount on failure.
     *
     * Increments failcount only if the network is believed to be
     * connected
     *
     * @return true if request can be retried (less than
     * RETRY_REQUEST_LIMIT failures have occurred).
     */
    private boolean httpFailure(Request req, int errorId, Exception e) {
        boolean ret = true;
        boolean networkConnected = mConnectionManager.isNetworkConnected();

        // e.printStackTrace();
        if (HttpLog.LOGV) HttpLog.v(
                "httpFailure() ******* " + e + " count " + req.mFailCount +
                " networkConnected " + networkConnected + " " + mHost + " " + req.getUri());

        if (networkConnected && ++req.mFailCount >= RETRY_REQUEST_LIMIT) {
            ret = false;
            String error;
            if (errorId < 0) {
                error = mContext.getText(
                        EventHandler.errorStringResources[-errorId]).toString();
            } else {
                Throwable cause = e.getCause();
                error = cause != null ? cause.toString() : e.getMessage();
            }
            req.mEventHandler.error(errorId, error);
            req.complete();
        }

        closeConnection();
        mHttpContext.removeAttribute(HTTP_CONNECTION);

        return ret;
    }

    HttpContext getHttpContext() {
        return mHttpContext;
    }

    /**
     * Use same logic as ConnectionReuseStrategy
     * @see ConnectionReuseStrategy
     */
    private boolean keepAlive(HttpEntity entity,
            ProtocolVersion ver, int connType, final HttpContext context) {
        org.apache.http.HttpConnection conn = (org.apache.http.HttpConnection)
            context.getAttribute(ExecutionContext.HTTP_CONNECTION);

        if (conn != null && !conn.isOpen())
            return false;
        // do NOT check for stale connection, that is an expensive operation

        if (entity != null) {
            if (entity.getContentLength() < 0) {
                if (!entity.isChunked() || ver.lessEquals(HttpVersion.HTTP_1_0)) {
                    // if the content length is not known and is not chunk
                    // encoded, the connection cannot be reused
                    return false;
                }
            }
        }
        // Check for 'Connection' directive
        if (connType == Headers.CONN_CLOSE) {
            return false;
        } else if (connType == Headers.CONN_KEEP_ALIVE) {
            return true;
        }
        // Resorting to protocol version default close connection policy
        return !ver.lessEquals(HttpVersion.HTTP_1_0);
    }

    void setCanPersist(HttpEntity entity, ProtocolVersion ver, int connType) {
        mCanPersist = keepAlive(entity, ver, connType, mHttpContext);
    }

    void setCanPersist(boolean canPersist) {
        mCanPersist = canPersist;
    }

    boolean getCanPersist() {
        return mCanPersist;
    }

    /** typically http or https... set by subclass */
    abstract String getScheme();
    abstract void closeConnection();
    abstract AndroidHttpClientConnection openConnection(Request req) throws IOException;

    /**
     * Prints request queue to log, for debugging.
     * returns request count
     */
    public synchronized String toString() {
        return mHost.toString();
    }

    byte[] getBuf() {
        if (mBuf == null) mBuf = new byte[8192];
        return mBuf;
    }

}
