//
//  ========================================================================
//  Copyright (c) 1995-2020 Mort Bay Consulting Pty Ltd and others.
//  ------------------------------------------------------------------------
//  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.spdy.server.proxy;

import java.nio.ByteBuffer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.eclipse.jetty.http.HttpField;
import org.eclipse.jetty.http.HttpFields;
import org.eclipse.jetty.http.HttpGenerator;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpHeaderValue;
import org.eclipse.jetty.http.HttpMethod;
import org.eclipse.jetty.http.HttpParser;
import org.eclipse.jetty.http.HttpVersion;
import org.eclipse.jetty.io.EndPoint;
import org.eclipse.jetty.server.Connector;
import org.eclipse.jetty.server.HttpConfiguration;
import org.eclipse.jetty.server.HttpConnection;
import org.eclipse.jetty.server.SslConnectionFactory;
import org.eclipse.jetty.spdy.ISession;
import org.eclipse.jetty.spdy.IStream;
import org.eclipse.jetty.spdy.StandardSession;
import org.eclipse.jetty.spdy.StandardStream;
import org.eclipse.jetty.spdy.api.ByteBufferDataInfo;
import org.eclipse.jetty.spdy.api.DataInfo;
import org.eclipse.jetty.spdy.api.GoAwayInfo;
import org.eclipse.jetty.spdy.api.GoAwayResultInfo;
import org.eclipse.jetty.spdy.api.HeadersInfo;
import org.eclipse.jetty.spdy.api.PushInfo;
import org.eclipse.jetty.spdy.api.ReplyInfo;
import org.eclipse.jetty.spdy.api.RstInfo;
import org.eclipse.jetty.spdy.api.SessionStatus;
import org.eclipse.jetty.spdy.api.Stream;
import org.eclipse.jetty.spdy.api.StreamFrameListener;
import org.eclipse.jetty.spdy.api.SynInfo;
import org.eclipse.jetty.spdy.http.HTTPSPDYHeader;
import org.eclipse.jetty.util.BufferUtil;
import org.eclipse.jetty.util.Callback;
import org.eclipse.jetty.util.Fields;
import org.eclipse.jetty.util.Promise;

public class ProxyHTTPSPDYConnection extends HttpConnection implements HttpParser.RequestHandler<ByteBuffer>
{
    private final short version;
    private final Fields headers = new Fields();
    private final ProxyEngineSelector proxyEngineSelector;
    private final ISession session;
    private HTTPStream stream;
    private ByteBuffer content;

    public ProxyHTTPSPDYConnection(Connector connector, HttpConfiguration config, EndPoint endPoint, short version, ProxyEngineSelector proxyEngineSelector)
    {
        super(config, connector, endPoint);
        this.version = version;
        this.proxyEngineSelector = proxyEngineSelector;
        this.session = new HTTPSession(version, connector);
    }

    @Override
    protected HttpParser.RequestHandler<ByteBuffer> newRequestHandler()
    {
        return this;
    }

    @Override
    public boolean startRequest(HttpMethod method, String methodString, ByteBuffer uri, HttpVersion httpVersion)
    {
        Connector connector = getConnector();
        String scheme = connector.getConnectionFactory(SslConnectionFactory.class) != null ? "https" : "http";
        headers.put(HTTPSPDYHeader.SCHEME.name(version), scheme);
        headers.put(HTTPSPDYHeader.METHOD.name(version), methodString);
        headers.put(HTTPSPDYHeader.URI.name(version), BufferUtil.toUTF8String(uri)); // TODO handle bad encodings
        headers.put(HTTPSPDYHeader.VERSION.name(version), httpVersion.asString());
        return false;
    }

    @Override
    public boolean parsedHeader(HttpField field)
    {
        if (field.getHeader() == HttpHeader.HOST)
            headers.put(HTTPSPDYHeader.HOST.name(version), field.getValue());
        else
            headers.put(field.getName(), field.getValue());
        return false;
    }

    @Override
    public boolean parsedHostHeader(String host, int port)
    {
        return false;
    }

    @Override
    public boolean headerComplete()
    {
        return false;
    }

    @Override
    public boolean content(ByteBuffer item)
    {
        if (content == null)
        {
            stream = syn(false);
            content = item;
        }
        else
        {
            stream.getStreamFrameListener().onData(stream, toDataInfo(item, false));
        }
        return false;
    }

    @Override
    public boolean messageComplete()
    {
        if (stream == null)
        {
            assert content == null;
            if (headers.isEmpty())
                proxyEngineSelector.onGoAway(session, new GoAwayResultInfo(0, SessionStatus.OK));
            else
                syn(true);
        }
        else
        {
            stream.getStreamFrameListener().onData(stream, toDataInfo(content, true));
        }
        return false;
    }

    @Override
    public void completed()
    {
        headers.clear();
        stream = null;
        content = null;
        super.completed();
    }

    @Override
    public int getHeaderCacheSize()
    {
        // TODO get from configuration
        return 256;
    }

    @Override
    public void earlyEOF()
    {
        // TODO
    }

    @Override
    public void badMessage(int status, String reason)
    {
        // TODO
    }

    private HTTPStream syn(boolean close)
    {
        HTTPStream stream = new HTTPStream(1, (byte)0, session, null);
        StreamFrameListener streamFrameListener = proxyEngineSelector.onSyn(stream, new SynInfo(headers, close));
        stream.setStreamFrameListener(streamFrameListener);
        return stream;
    }

    private DataInfo toDataInfo(ByteBuffer buffer, boolean close)
    {
        return new ByteBufferDataInfo(buffer, close);
    }

    private class HTTPSession extends StandardSession
    {
        private HTTPSession(short version, Connector connector)
        {
            super(version, connector.getByteBufferPool(), connector.getScheduler(), null,
                    getEndPoint(), null, 1, proxyEngineSelector, null, null);
        }

        @Override
        public void rst(RstInfo rstInfo, Callback handler)
        {
            HttpGenerator.ResponseInfo info = new HttpGenerator.ResponseInfo(HttpVersion.fromString(headers.get
                    ("version").getValue()), null, 0, 502, "SPDY reset received from upstream server", false);
            send(info, null, true, Callback.Adapter.INSTANCE);
        }

        @Override
        public void goAway(GoAwayInfo goAwayInfo, Callback handler)
        {
            ProxyHTTPSPDYConnection.this.close();
            handler.succeeded();
        }
    }

    /**
     * <p>This stream will convert the SPDY invocations performed by the proxy into HTTP to be sent to the client.</p>
     */
    private class HTTPStream extends StandardStream
    {
        private final Pattern statusRegexp = Pattern.compile("(\\d{3})\\s+(.*)");

        private HTTPStream(int id, byte priority, ISession session, IStream associatedStream)
        {
            super(id, priority, session, associatedStream, getHttpChannel().getScheduler(), null);
        }

        @Override
        public void push(PushInfo pushInfo, Promise<Stream> handler)
        {
            // HTTP does not support pushed streams
            handler.succeeded(new HTTPPushStream(2, getPriority(), getSession(), this));
        }

        @Override
        public void headers(HeadersInfo headersInfo, Callback handler)
        {
            // TODO
            throw new UnsupportedOperationException("Not Yet Implemented");
        }

        @Override
        public void reply(ReplyInfo replyInfo, final Callback handler)
        {
            Fields headers = new Fields(replyInfo.getHeaders(), false);

                addPersistenceHeader(headers);

            headers.remove(HTTPSPDYHeader.SCHEME.name(version));

            String status = headers.remove(HTTPSPDYHeader.STATUS.name(version)).getValue();
            Matcher matcher = statusRegexp.matcher(status);
            matcher.matches();
            int code = Integer.parseInt(matcher.group(1));
            String reason = matcher.group(2).trim();

            HttpVersion httpVersion = HttpVersion.fromString(headers.remove(HTTPSPDYHeader.VERSION.name(version)).getValue());

            // Convert the Host header from a SPDY special header to a normal header
            Fields.Field host = headers.remove(HTTPSPDYHeader.HOST.name(version));
            if (host != null)
                headers.put("host", host.getValue());

            HttpFields fields = new HttpFields();
            for (Fields.Field header : headers)
            {
                String name = camelize(header.getName());
                fields.put(name, header.getValue());
            }

            // TODO: handle better the HEAD last parameter
            long contentLength = fields.getLongField(HttpHeader.CONTENT_LENGTH.asString());
            HttpGenerator.ResponseInfo info = new HttpGenerator.ResponseInfo(httpVersion, fields, contentLength, code,
                    reason, false);

            send(info, null, replyInfo.isClose(), new Adapter()
            {
                @Override
                public void failed(Throwable x)
                {
                    handler.failed(x);
                }
            });

            if (replyInfo.isClose())
                completed();

            handler.succeeded();
        }

        private String camelize(String name)
        {
            char[] chars = name.toCharArray();
            chars[0] = Character.toUpperCase(chars[0]);

            for (int i = 0; i < chars.length; ++i)
            {
                char c = chars[i];
                int j = i + 1;
                if (c == '-' && j < chars.length)
                    chars[j] = Character.toUpperCase(chars[j]);
            }
            return new String(chars);
        }

        @Override
        public void data(DataInfo dataInfo, final Callback handler)
        {
            // Data buffer must be copied, as the ByteBuffer is pooled
            ByteBuffer byteBuffer = dataInfo.asByteBuffer(false);

            send(null, byteBuffer, dataInfo.isClose(), new Adapter()
            {
                @Override
                public void failed(Throwable x)
                {
                    handler.failed(x);
                }
            });

            if (dataInfo.isClose())
                completed();

            handler.succeeded();
        }
    }

    private void addPersistenceHeader(Fields headersToAddTo)
    {
        HttpVersion httpVersion = HttpVersion.fromString(headers.get("version").getValue());
        boolean persistent = false;
        switch (httpVersion)
        {
            case HTTP_1_0:
            {
                Fields.Field keepAliveHeader = headers.get(HttpHeader.KEEP_ALIVE.asString());
                if(keepAliveHeader!=null)
                    persistent = HttpHeaderValue.KEEP_ALIVE.asString().equals(keepAliveHeader.getValue());
                if (!persistent)
                    persistent = HttpMethod.CONNECT.is(headers.get("method").getValue());
                if (persistent)
                    headersToAddTo.add(HttpHeader.CONNECTION.asString(), HttpHeaderValue.KEEP_ALIVE.asString());
                break;
            }
            case HTTP_1_1:
            {
                Fields.Field connectionHeader = headers.get(HttpHeader.CONNECTION.asString());
                if(connectionHeader != null)
                    persistent = !HttpHeaderValue.CLOSE.asString().equals(connectionHeader.getValue());
                else
                    persistent = true;
                if (!persistent)
                    persistent = HttpMethod.CONNECT.is(headers.get("method").getValue());
                if (!persistent)
                    headersToAddTo.add(HttpHeader.CONNECTION.asString(), HttpHeaderValue.CLOSE.asString());
                break;
            }
            default:
            {
                throw new IllegalStateException();
            }
        }
    }

    private class HTTPPushStream extends StandardStream
    {
        private HTTPPushStream(int id, byte priority, ISession session, IStream associatedStream)
        {
            super(id, priority, session, associatedStream, getHttpChannel().getScheduler(), null);
        }

        @Override
        public void headers(HeadersInfo headersInfo, Callback handler)
        {
            // Ignore pushed headers
            handler.succeeded();
        }

        @Override
        public void data(DataInfo dataInfo, Callback handler)
        {
            // Ignore pushed data
            handler.succeeded();
        }
    }
}
