/*
 * ====================================================================
 * 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.
 * ====================================================================
 *
 * This software consists of voluntary contributions made by many
 * individuals on behalf of the Apache Software Foundation.  For more
 * information on the Apache Software Foundation, please see
 * <http://www.apache.org/>.
 *
 */

package org.apache.hc.core5.http.examples;

import java.io.IOException;
import java.net.SocketException;
import java.net.SocketTimeoutException;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;

import org.apache.hc.core5.http.ClassicHttpRequest;
import org.apache.hc.core5.http.ClassicHttpResponse;
import org.apache.hc.core5.http.ConnectionClosedException;
import org.apache.hc.core5.http.ExceptionListener;
import org.apache.hc.core5.http.Header;
import org.apache.hc.core5.http.HttpConnection;
import org.apache.hc.core5.http.HttpException;
import org.apache.hc.core5.http.HttpHeaders;
import org.apache.hc.core5.http.HttpHost;
import org.apache.hc.core5.http.HttpRequest;
import org.apache.hc.core5.http.HttpResponse;
import org.apache.hc.core5.http.impl.Http1StreamListener;
import org.apache.hc.core5.http.impl.bootstrap.HttpRequester;
import org.apache.hc.core5.http.impl.bootstrap.HttpServer;
import org.apache.hc.core5.http.impl.bootstrap.RequesterBootstrap;
import org.apache.hc.core5.http.impl.bootstrap.ServerBootstrap;
import org.apache.hc.core5.http.io.HttpRequestHandler;
import org.apache.hc.core5.http.message.BasicClassicHttpRequest;
import org.apache.hc.core5.http.protocol.HttpContext;
import org.apache.hc.core5.http.protocol.HttpCoreContext;
import org.apache.hc.core5.io.CloseMode;
import org.apache.hc.core5.pool.ConnPoolListener;
import org.apache.hc.core5.pool.ConnPoolStats;
import org.apache.hc.core5.pool.PoolStats;
import org.apache.hc.core5.util.TextUtils;
import org.apache.hc.core5.util.TimeValue;
import org.apache.hc.core5.util.Timeout;

/**
 * Example of embedded HTTP/1.1 reverse proxy using classic I/O.
 */
public class ClassicReverseProxyExample {

    public static void main(final String[] args) throws Exception {
        if (args.length < 1) {
            System.out.println("Usage: <hostname[:port]> [listener port]");
            System.exit(1);
        }
        final HttpHost targetHost = HttpHost.create(args[0]);
        int port = 8080;
        if (args.length > 1) {
            port = Integer.parseInt(args[1]);
        }

        System.out.println("Reverse proxy to " + targetHost);

        final HttpRequester requester = RequesterBootstrap.bootstrap()
                .setStreamListener(new Http1StreamListener() {

                    @Override
                    public void onRequestHead(final HttpConnection connection, final HttpRequest request) {
                        System.out.println("[proxy->origin] " + Thread.currentThread()  + " " +
                                request.getMethod() + " " + request.getRequestUri());
                    }

                    @Override
                    public void onResponseHead(final HttpConnection connection, final HttpResponse response) {
                        System.out.println("[proxy<-origin] " + Thread.currentThread()  + " status " + response.getCode());
                    }

                    @Override
                    public void onExchangeComplete(final HttpConnection connection, final boolean keepAlive) {
                        System.out.println("[proxy<-origin] " + Thread.currentThread() + " exchange completed; " +
                                "connection " + (keepAlive ? "kept alive" : "cannot be kept alive"));
                    }

                })
                .setConnPoolListener(new ConnPoolListener<HttpHost>() {

                    @Override
                    public void onLease(final HttpHost route, final ConnPoolStats<HttpHost> connPoolStats) {
                        final StringBuilder buf = new StringBuilder();
                        buf.append("[proxy->origin] ").append(Thread.currentThread()).append(" connection leased ").append(route);
                        System.out.println(buf);
                    }

                    @Override
                    public void onRelease(final HttpHost route, final ConnPoolStats<HttpHost> connPoolStats) {
                        final StringBuilder buf = new StringBuilder();
                        buf.append("[proxy->origin] ").append(Thread.currentThread()).append(" connection released ").append(route);
                        final PoolStats totals = connPoolStats.getTotalStats();
                        buf.append("; total kept alive: ").append(totals.getAvailable()).append("; ");
                        buf.append("total allocated: ").append(totals.getLeased() + totals.getAvailable());
                        buf.append(" of ").append(totals.getMax());
                        System.out.println(buf);
                    }

                })
                .create();

        final HttpServer server = ServerBootstrap.bootstrap()
                .setListenerPort(port)
                .setStreamListener(new Http1StreamListener() {

                    @Override
                    public void onRequestHead(final HttpConnection connection, final HttpRequest request) {
                        System.out.println("[client->proxy] " + Thread.currentThread() + " " +
                                request.getMethod() + " " + request.getRequestUri());
                    }

                    @Override
                    public void onResponseHead(final HttpConnection connection, final HttpResponse response) {
                        System.out.println("[client<-proxy] " + Thread.currentThread() + " status " + response.getCode());
                    }

                    @Override
                    public void onExchangeComplete(final HttpConnection connection, final boolean keepAlive) {
                        System.out.println("[client<-proxy] " + Thread.currentThread() + " exchange completed; " +
                                "connection " + (keepAlive ? "kept alive" : "cannot be kept alive"));
                    }

                })
                .setExceptionListener(new ExceptionListener() {

                    @Override
                    public void onError(final Exception ex) {
                        if (ex instanceof SocketException) {
                            System.out.println("[client->proxy] " + Thread.currentThread() + " " + ex.getMessage());
                        } else {
                            System.out.println("[client->proxy] " + Thread.currentThread()  + " " + ex.getMessage());
                            ex.printStackTrace(System.out);
                        }
                    }

                    @Override
                    public void onError(final HttpConnection connection, final Exception ex) {
                        if (ex instanceof SocketTimeoutException) {
                            System.out.println("[client->proxy] " + Thread.currentThread() + " time out");
                        } else if (ex instanceof SocketException || ex instanceof ConnectionClosedException) {
                            System.out.println("[client->proxy] " + Thread.currentThread() + " " + ex.getMessage());
                        } else {
                            System.out.println("[client->proxy] " + Thread.currentThread() + " " + ex.getMessage());
                            ex.printStackTrace(System.out);
                        }
                    }

                })
                .register("*", new ProxyHandler(targetHost, requester))
                .create();

        server.start();
        Runtime.getRuntime().addShutdownHook(new Thread(() -> {
            server.close(CloseMode.GRACEFUL);
            requester.close(CloseMode.GRACEFUL);
        }));

        System.out.println("Listening on port " + port);
        server.awaitTermination(TimeValue.MAX_VALUE);
    }

    private final static Set<String> HOP_BY_HOP = Collections.unmodifiableSet(new HashSet<>(Arrays.asList(
            TextUtils.toLowerCase(HttpHeaders.HOST),
            TextUtils.toLowerCase(HttpHeaders.CONTENT_LENGTH),
            TextUtils.toLowerCase(HttpHeaders.TRANSFER_ENCODING),
            TextUtils.toLowerCase(HttpHeaders.CONNECTION),
            TextUtils.toLowerCase(HttpHeaders.KEEP_ALIVE),
            TextUtils.toLowerCase(HttpHeaders.PROXY_AUTHENTICATE),
            TextUtils.toLowerCase(HttpHeaders.TE),
            TextUtils.toLowerCase(HttpHeaders.TRAILER),
            TextUtils.toLowerCase(HttpHeaders.UPGRADE))));


    static class ProxyHandler implements HttpRequestHandler  {

        private final HttpHost targetHost;
        private final HttpRequester requester;

        public ProxyHandler(
                final HttpHost targetHost,
                final HttpRequester requester) {
            super();
            this.targetHost = targetHost;
            this.requester = requester;
        }

        @Override
        public void handle(
                final ClassicHttpRequest incomingRequest,
                final ClassicHttpResponse outgoingResponse,
                final HttpContext serverContext) throws HttpException, IOException {

            final HttpCoreContext clientContext = HttpCoreContext.create();
            final ClassicHttpRequest outgoingRequest = new BasicClassicHttpRequest(
                    incomingRequest.getMethod(),
                    targetHost,
                    incomingRequest.getPath());
            for (final Iterator<Header> it = incomingRequest.headerIterator(); it.hasNext(); ) {
                final Header header = it.next();
                if (!HOP_BY_HOP.contains(TextUtils.toLowerCase(header.getName()))) {
                    outgoingRequest.addHeader(header);
                }
            }
            outgoingRequest.setEntity(incomingRequest.getEntity());
            final ClassicHttpResponse incomingResponse = requester.execute(
                    targetHost, outgoingRequest, Timeout.ofMinutes(1), clientContext);
            outgoingResponse.setCode(incomingResponse.getCode());
            for (final Iterator<Header> it = incomingResponse.headerIterator(); it.hasNext(); ) {
                final Header header = it.next();
                if (!HOP_BY_HOP.contains(TextUtils.toLowerCase(header.getName()))) {
                    outgoingResponse.addHeader(header);
                }
            }
            outgoingResponse.setEntity(incomingResponse.getEntity());
        }
    }

}
