// ========================================================================
// Copyright (c) 2006-2011 Mort Bay Consulting Pty. Ltd.
// ------------------------------------------------------------------------
// All rights reserved. This program and the accompanying materials
// are made available under the terms of the Eclipse Public License v1.0
// and Apache License v2.0 which accompanies this distribution.
// The Eclipse Public License is available at
// http://www.eclipse.org/legal/epl-v10.html
// The Apache License v2.0 is available at
// http://www.opensource.org/licenses/apache2.0.php
// You may elect to redistribute this code under either of these licenses.
// ========================================================================

package org.eclipse.jetty.client;

import java.io.IOException;
import java.io.InputStream;
import java.util.Collections;
import java.util.concurrent.atomic.AtomicBoolean;

import org.eclipse.jetty.client.security.Authentication;
import org.eclipse.jetty.http.HttpFields;
import org.eclipse.jetty.http.HttpGenerator;
import org.eclipse.jetty.http.HttpHeaderValues;
import org.eclipse.jetty.http.HttpHeaders;
import org.eclipse.jetty.http.HttpMethods;
import org.eclipse.jetty.http.HttpParser;
import org.eclipse.jetty.http.HttpSchemes;
import org.eclipse.jetty.http.HttpStatus;
import org.eclipse.jetty.http.HttpVersions;
import org.eclipse.jetty.io.AbstractConnection;
import org.eclipse.jetty.io.Buffer;
import org.eclipse.jetty.io.Buffers;
import org.eclipse.jetty.io.ByteArrayBuffer;
import org.eclipse.jetty.io.Connection;
import org.eclipse.jetty.io.EndPoint;
import org.eclipse.jetty.io.EofException;
import org.eclipse.jetty.io.View;
import org.eclipse.jetty.util.component.AggregateLifeCycle;
import org.eclipse.jetty.util.component.Dumpable;
import org.eclipse.jetty.util.log.Log;
import org.eclipse.jetty.util.log.Logger;
import org.eclipse.jetty.util.thread.Timeout;

/**
 *
 * @version $Revision: 879 $ $Date: 2009-09-11 16:13:28 +0200 (Fri, 11 Sep 2009) $
 */
public abstract class AbstractHttpConnection extends AbstractConnection implements Dumpable
{
    private static final Logger LOG = Log.getLogger(AbstractHttpConnection.class);

    protected HttpDestination _destination;
    protected HttpGenerator _generator;
    protected HttpParser _parser;
    protected boolean _http11 = true;
    protected int _status;
    protected Buffer _connectionHeader;
    protected boolean _reserved;

    // The current exchange waiting for a response
    protected volatile HttpExchange _exchange;
    protected HttpExchange _pipeline;
    private final Timeout.Task _idleTimeout = new ConnectionIdleTask();
    private AtomicBoolean _idle = new AtomicBoolean(false);


    AbstractHttpConnection(Buffers requestBuffers, Buffers responseBuffers, EndPoint endp)
    {
        super(endp);

        _generator = new HttpGenerator(requestBuffers,endp);
        _parser = new HttpParser(responseBuffers,endp,new Handler());
    }

    public void setReserved (boolean reserved)
    {
        _reserved = reserved;
    }

    public boolean isReserved()
    {
        return _reserved;
    }

    public HttpDestination getDestination()
    {
        return _destination;
    }

    public void setDestination(HttpDestination destination)
    {
        _destination = destination;
    }

    public boolean send(HttpExchange ex) throws IOException
    {
        LOG.debug("Send {} on {}",ex,this);
        synchronized (this)
        {
            if (_exchange != null)
            {
                if (_pipeline != null)
                    throw new IllegalStateException(this + " PIPELINED!!!  _exchange=" + _exchange);
                _pipeline = ex;
                return true;
            }

            _exchange = ex;
            _exchange.associate(this);

            // The call to associate() may have closed the connection, check if it's the case
            if (!_endp.isOpen())
            {
                _exchange.disassociate();
                _exchange = null;
                return false;
            }

            _exchange.setStatus(HttpExchange.STATUS_WAITING_FOR_COMMIT);

            adjustIdleTimeout();

            return true;
        }
    }

    private void adjustIdleTimeout() throws IOException
    {
        // Adjusts the idle timeout in case the default or exchange timeout
        // are greater. This is needed for long polls, where one wants an
        // aggressive releasing of idle connections (so idle timeout is small)
        // but still allow long polls to complete normally

        long timeout = _exchange.getTimeout();
        if (timeout <= 0)
            timeout = _destination.getHttpClient().getTimeout();

        long endPointTimeout = _endp.getMaxIdleTime();

        if (timeout > 0 && timeout > endPointTimeout)
        {
            // Make it larger than the exchange timeout so that there are
            // no races between the idle timeout and the exchange timeout
            // when trying to close the endpoint
            _endp.setMaxIdleTime(2 * (int)timeout);
        }
    }

    public abstract Connection handle() throws IOException;


    public boolean isIdle()
    {
        synchronized (this)
        {
            return _exchange == null;
        }
    }

    public boolean isSuspended()
    {
        return false;
    }

    public void onClose()
    {
    }

    protected void commitRequest() throws IOException
    {
        synchronized (this)
        {
            _status=0;
            if (_exchange.getStatus() != HttpExchange.STATUS_WAITING_FOR_COMMIT)
                throw new IllegalStateException();

            _exchange.setStatus(HttpExchange.STATUS_SENDING_REQUEST);
            _generator.setVersion(_exchange.getVersion());

            String method=_exchange.getMethod();
            String uri = _exchange.getRequestURI();
            if (_destination.isProxied())
            {
                if (!HttpMethods.CONNECT.equals(method) && uri.startsWith("/"))
                {
                    boolean secure = _destination.isSecure();
                    String host = _destination.getAddress().getHost();
                    int port = _destination.getAddress().getPort();
                    StringBuilder absoluteURI = new StringBuilder();
                    absoluteURI.append(secure ? HttpSchemes.HTTPS : HttpSchemes.HTTP);
                    absoluteURI.append("://");
                    absoluteURI.append(host);
                    // Avoid adding default ports
                    if (!(secure && port == 443 || !secure && port == 80))
                        absoluteURI.append(":").append(port);
                    absoluteURI.append(uri);
                    uri = absoluteURI.toString();
                }
                Authentication auth = _destination.getProxyAuthentication();
                if (auth != null)
                    auth.setCredentials(_exchange);
            }

            _generator.setRequest(method, uri);
            _parser.setHeadResponse(HttpMethods.HEAD.equalsIgnoreCase(method));

            HttpFields requestHeaders = _exchange.getRequestFields();
            if (_exchange.getVersion() >= HttpVersions.HTTP_1_1_ORDINAL)
            {
                if (!requestHeaders.containsKey(HttpHeaders.HOST_BUFFER))
                    requestHeaders.add(HttpHeaders.HOST_BUFFER,_destination.getHostHeader());
            }

            Buffer requestContent = _exchange.getRequestContent();
            if (requestContent != null)
            {
                requestHeaders.putLongField(HttpHeaders.CONTENT_LENGTH, requestContent.length());
                _generator.completeHeader(requestHeaders,false);
                _generator.addContent(new View(requestContent),true);
            }
            else
            {
                InputStream requestContentStream = _exchange.getRequestContentSource();
                if (requestContentStream != null)
                {
                    _generator.completeHeader(requestHeaders, false);
                    int available = requestContentStream.available();
                    if (available > 0)
                    {
                        // TODO deal with any known content length
                        // TODO reuse this buffer!
                        byte[] buf = new byte[available];
                        int length = requestContentStream.read(buf);
                        _generator.addContent(new ByteArrayBuffer(buf, 0, length), false);
                    }
                }
                else
                {
                    requestHeaders.remove(HttpHeaders.CONTENT_LENGTH);
                    _generator.completeHeader(requestHeaders, true);
                }
            }

            _exchange.setStatus(HttpExchange.STATUS_WAITING_FOR_RESPONSE);
        }
    }

    protected void reset() throws IOException
    {
        _connectionHeader = null;
        _parser.reset();
        _generator.reset();
        _http11 = true;
    }


    private class Handler extends HttpParser.EventHandler
    {
        @Override
        public void startRequest(Buffer method, Buffer url, Buffer version) throws IOException
        {
            // System.out.println( method.toString() + "///" + url.toString() +
            // "///" + version.toString() );
            // TODO validate this is acceptable, the <!DOCTYPE goop was coming
            // out here
            // throw new IllegalStateException();
        }

        @Override
        public void startResponse(Buffer version, int status, Buffer reason) throws IOException
        {
            HttpExchange exchange = _exchange;
            if (exchange==null)
            {
                LOG.warn("No exchange for response");
                _endp.close();
                return;
            }

            switch(status)
            {
                case HttpStatus.CONTINUE_100:
                case HttpStatus.PROCESSING_102:
                    // TODO check if appropriate expect was sent in the request.
                    exchange.setEventListener(new NonFinalResponseListener(exchange));
                    break;

                case HttpStatus.OK_200:
                    // handle special case for CONNECT 200 responses
                    if (HttpMethods.CONNECT.equalsIgnoreCase(exchange.getMethod()))
                        _parser.setHeadResponse(true);
                    break;
            }

            _http11 = HttpVersions.HTTP_1_1_BUFFER.equals(version);
            _status=status;
            exchange.getEventListener().onResponseStatus(version,status,reason);
            exchange.setStatus(HttpExchange.STATUS_PARSING_HEADERS);

        }

        @Override
        public void parsedHeader(Buffer name, Buffer value) throws IOException
        {
            HttpExchange exchange = _exchange;
            if (exchange!=null)
            {
                if (HttpHeaders.CACHE.getOrdinal(name) == HttpHeaders.CONNECTION_ORDINAL)
                {
                    _connectionHeader = HttpHeaderValues.CACHE.lookup(value);
                }
                exchange.getEventListener().onResponseHeader(name,value);
            }
        }

        @Override
        public void headerComplete() throws IOException
        {
            HttpExchange exchange = _exchange;
            if (exchange!=null)
                exchange.setStatus(HttpExchange.STATUS_PARSING_CONTENT);
        }

        @Override
        public void content(Buffer ref) throws IOException
        {
            HttpExchange exchange = _exchange;
            if (exchange!=null)
                exchange.getEventListener().onResponseContent(ref);
        }

        @Override
        public void messageComplete(long contextLength) throws IOException
        {
            HttpExchange exchange = _exchange;
            if (exchange!=null)
                exchange.setStatus(HttpExchange.STATUS_COMPLETED);
        }

        @Override
        public void earlyEOF()
        {
            HttpExchange exchange = _exchange;
            if (exchange!=null)
            {
                if (!exchange.isDone())
                {
                    if (exchange.setStatus(HttpExchange.STATUS_EXCEPTED))
                        exchange.getEventListener().onException(new EofException("early EOF"));
                }
            }
        }


    }

    @Override
    public String toString()
    {
        return String.format("%s %s g=%s p=%s",
                super.toString(),
                _destination == null ? "?.?.?.?:??" : _destination.getAddress(),
                _generator,
                _parser);
    }

    public String toDetailString()
    {
        return toString() + " ex=" + _exchange + " idle for " + _idleTimeout.getAge();
    }

    public void close() throws IOException
    {
        //if there is a live, unfinished exchange, set its status to be
        //excepted and wake up anyone waiting on waitForDone()

        HttpExchange exchange = _exchange;
        if (exchange != null && !exchange.isDone())
        {
            switch (exchange.getStatus())
            {
                case HttpExchange.STATUS_CANCELLED:
                case HttpExchange.STATUS_CANCELLING:
                case HttpExchange.STATUS_COMPLETED:
                case HttpExchange.STATUS_EXCEPTED:
                case HttpExchange.STATUS_EXPIRED:
                    break;
                case HttpExchange.STATUS_PARSING_CONTENT:
                    if (_endp.isInputShutdown() && _parser.isState(HttpParser.STATE_EOF_CONTENT))
                        break;
                default:
                    String exch= exchange.toString();
                    String reason = _endp.isOpen()?(_endp.isInputShutdown()?"half closed: ":"local close: "):"closed: ";
                    if (exchange.setStatus(HttpExchange.STATUS_EXCEPTED))
                        exchange.getEventListener().onException(new EofException(reason+exch));
            }
        }

        if (_endp.isOpen())
        {
            _endp.close();
            _destination.returnConnection(this, true);
        }
    }

    public void setIdleTimeout()
    {
        synchronized (this)
        {
            if (_idle.compareAndSet(false, true))
                _destination.getHttpClient().scheduleIdle(_idleTimeout);
            else
                throw new IllegalStateException();
        }
    }

    public boolean cancelIdleTimeout()
    {
        synchronized (this)
        {
            if (_idle.compareAndSet(true, false))
            {
                _destination.getHttpClient().cancel(_idleTimeout);
                return true;
            }
        }

        return false;
    }

    protected void exchangeExpired(HttpExchange exchange)
    {
        synchronized (this)
        {
            // We are expiring an exchange, but the exchange is pending
            // Cannot reuse the connection because the reply may arrive, so close it
            if (_exchange == exchange)
            {
                try
                {
                    _destination.returnConnection(this, true);
                }
                catch (IOException x)
                {
                    LOG.ignore(x);
                }
            }
        }
    }

    /* ------------------------------------------------------------ */
    /**
     * @see org.eclipse.jetty.util.component.Dumpable#dump()
     */
    public String dump()
    {
        return AggregateLifeCycle.dump(this);
    }

    /* ------------------------------------------------------------ */
    /**
     * @see org.eclipse.jetty.util.component.Dumpable#dump(java.lang.Appendable, java.lang.String)
     */
    public void dump(Appendable out, String indent) throws IOException
    {
        synchronized (this)
        {
            out.append(String.valueOf(this)).append("\n");
            AggregateLifeCycle.dump(out,indent,Collections.singletonList(_endp));
        }
    }

    /* ------------------------------------------------------------ */
    private class ConnectionIdleTask extends Timeout.Task
    {
        /* ------------------------------------------------------------ */
        @Override
        public void expired()
        {
            // Connection idle, close it
            if (_idle.compareAndSet(true, false))
            {
                _destination.returnIdleConnection(AbstractHttpConnection.this);
            }
        }
    }


    /* ------------------------------------------------------------ */
    private class NonFinalResponseListener implements HttpEventListener
    {
        final HttpExchange _exchange;
        final HttpEventListener _next;

        /* ------------------------------------------------------------ */
        public NonFinalResponseListener(HttpExchange exchange)
        {
            _exchange=exchange;
            _next=exchange.getEventListener();
        }

        /* ------------------------------------------------------------ */
        public void onRequestCommitted() throws IOException
        {
        }

        /* ------------------------------------------------------------ */
        public void onRequestComplete() throws IOException
        {
        }

        /* ------------------------------------------------------------ */
        public void onResponseStatus(Buffer version, int status, Buffer reason) throws IOException
        {
        }

        /* ------------------------------------------------------------ */
        public void onResponseHeader(Buffer name, Buffer value) throws IOException
        {
            _next.onResponseHeader(name,value);
        }

        /* ------------------------------------------------------------ */
        public void onResponseHeaderComplete() throws IOException
        {
            _next.onResponseHeaderComplete();
        }

        /* ------------------------------------------------------------ */
        public void onResponseContent(Buffer content) throws IOException
        {
        }

        /* ------------------------------------------------------------ */
        public void onResponseComplete() throws IOException
        {
            _exchange.setEventListener(_next);
            _exchange.setStatus(HttpExchange.STATUS_WAITING_FOR_RESPONSE);
            _parser.reset();
        }

        /* ------------------------------------------------------------ */
        public void onConnectionFailed(Throwable ex)
        {
            _exchange.setEventListener(_next);
            _next.onConnectionFailed(ex);
        }

        /* ------------------------------------------------------------ */
        public void onException(Throwable ex)
        {
            _exchange.setEventListener(_next);
            _next.onException(ex);
        }

        /* ------------------------------------------------------------ */
        public void onExpire()
        {
            _exchange.setEventListener(_next);
            _next.onExpire();
        }

        /* ------------------------------------------------------------ */
        public void onRetry()
        {
            _exchange.setEventListener(_next);
            _next.onRetry();
        }
    }
}
