/*
 * Copyright (c) 2011-2015 Pivotal Software Inc, 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 reactor.io.net.impl.netty.http;

import io.netty.channel.ChannelPipeline;
import io.netty.channel.socket.SocketChannel;
import io.netty.handler.codec.http.*;
import io.netty.handler.codec.http.websocketx.WebSocketClientHandshakerFactory;
import io.netty.handler.codec.http.websocketx.WebSocketVersion;
import io.netty.handler.logging.LoggingHandler;
import org.reactivestreams.Publisher;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import reactor.Environment;
import reactor.core.Dispatcher;
import reactor.core.support.Assert;
import reactor.fn.Supplier;
import reactor.fn.tuple.Tuple2;
import reactor.io.buffer.Buffer;
import reactor.io.codec.Codec;
import reactor.io.net.ChannelStream;
import reactor.io.net.ReactorChannelHandler;
import reactor.io.net.Reconnect;
import reactor.io.net.config.ClientSocketOptions;
import reactor.io.net.config.SslOptions;
import reactor.io.net.http.HttpChannel;
import reactor.io.net.http.HttpClient;
import reactor.io.net.http.model.Method;
import reactor.io.net.impl.netty.NettyChannelStream;
import reactor.io.net.impl.netty.tcp.NettyTcpClient;
import reactor.rx.Promise;
import reactor.rx.Promises;
import reactor.rx.Stream;
import reactor.rx.Streams;

import java.net.InetSocketAddress;
import java.net.URI;

/**
 * A Netty-based {@code TcpClient}.
 *
 * @param <IN>  The type that will be received by this client
 * @param <OUT> The type that will be sent by this client
 * @author Jon Brisbin
 * @author Stephane Maldini
 */
public class NettyHttpClient<IN, OUT> extends HttpClient<IN, OUT> {

	private final static Logger log = LoggerFactory.getLogger(NettyHttpClient.class);

	private final NettyTcpClient<IN, OUT>            client;
	private final Promise<NettyHttpChannel<IN, OUT>> reply;

	private URI lastURI = null;

	/**
	 * Creates a new NettyTcpClient that will use the given {@code env} for configuration and the given {@code
	 * reactor} to
	 * send events. The number of IO threads used by the client is configured by the environment's {@code
	 * reactor.tcp.ioThreadCount} property. In its absence the number of IO threads will be equal to the {@link
	 * reactor.Environment#PROCESSORS number of available processors}. </p> The client will connect to the given {@code
	 * connectAddress}, configuring its socket using the given {@code opts}. The given {@code codec} will be used for
	 * encoding and decoding of data.
	 *
	 * @param env            The configuration environment
	 * @param dispatcher     The dispatcher used to send events
	 * @param connectAddress The root host and port to connect relatively from in http handlers
	 * @param options        The configuration options for the client's socket
	 * @param sslOptions     The SSL configuration options for the client's socket
	 * @param codec          The codec used to encode and decode data
	 */
	public NettyHttpClient(final Environment env,
	                       final Dispatcher dispatcher,
	                       final Supplier<InetSocketAddress> connectAddress,
	                       final ClientSocketOptions options,
	                       final SslOptions sslOptions,
	                       final Codec<Buffer, IN, OUT> codec) {
		super(env, dispatcher, codec, options);

		Supplier<InetSocketAddress> _connectAddress = connectAddress == null ? new Supplier<InetSocketAddress>() {
			@Override
			public InetSocketAddress get() {
				try {
					URI url = lastURI;
					String host = url != null && url.getHost() != null ? url.getHost() : "localhost";
					int port = url != null ? url.getPort() : -1;
					if (port == -1) {
						if (url != null && url.getScheme() != null && (
								url.getScheme().toLowerCase().equals(HttpChannel.HTTPS_SCHEME) ||
										url.getScheme().toLowerCase().equals(HttpChannel.WSS_SCHEME)
						)) {
							port = 443;
						} else {
							port = 80;
						}
					}
					return new InetSocketAddress(host, port);
				} catch (Exception e) {
					throw new IllegalArgumentException(e);
				}
			}
		} : connectAddress;

		this.client = new NettyTcpClient<IN, OUT>(
				env,
				dispatcher,
				_connectAddress,
				options,
				sslOptions,
				codec
		) {
			@Override
			protected void bindChannel(ReactorChannelHandler<IN, OUT, ChannelStream<IN, OUT>> handler, SocketChannel
					nativeChannel) {

				URI currentURI = lastURI;
				try {
					if (currentURI.getScheme() != null
							&& (currentURI.getScheme().toLowerCase().equals(HttpChannel.HTTPS_SCHEME) ||
							currentURI.getScheme().toLowerCase().equals(HttpChannel.WSS_SCHEME))) {
						addSecureHandler(nativeChannel);
					}
				} catch (Exception e) {
					nativeChannel.pipeline().fireExceptionCaught(e);
				}

				NettyHttpClient.this.bindChannel(handler, nativeChannel);
			}
		};

		this.reply = Promises.prepare();
	}

	@Override
	protected Promise<Void> doStart(final ReactorChannelHandler<IN, OUT, HttpChannel<IN, OUT>> handler) {
		return client.start(new ReactorChannelHandler<IN, OUT, ChannelStream<IN, OUT>>() {
			@Override
			public Publisher<Void> apply(ChannelStream<IN, OUT> inoutChannelStream) {
				final NettyHttpChannel<IN, OUT> ch = ((NettyHttpChannel<IN, OUT>) inoutChannelStream);
				return handler.apply(ch);
			}
		});
	}

	@Override
	protected Stream<Tuple2<InetSocketAddress, Integer>> doStart(final ReactorChannelHandler<IN, OUT, HttpChannel<IN,
			OUT>> handler,
	                                                             final Reconnect reconnect) {
		return client.start(new ReactorChannelHandler<IN, OUT, ChannelStream<IN, OUT>>() {
			@Override
			public Publisher<Void> apply(ChannelStream<IN, OUT> inoutChannelStream) {
				final NettyHttpChannel<IN, OUT> ch = ((NettyHttpChannel<IN, OUT>) inoutChannelStream);
				return handler.apply(ch);
			}
		}, reconnect);
	}

	@Override
	public Promise<? extends HttpChannel<IN, OUT>> request(final Method method, final String url,
	                                                       final ReactorChannelHandler<IN, OUT, HttpChannel<IN, OUT>>
			                                                       handler) {
		final URI currentURI;
		try{
			Assert.isTrue(method != null && url != null);
			currentURI = parseURL(method, url);
			lastURI = currentURI;
		}catch(Exception e){
			return Promises.error(e);
		}

		start(new ReactorChannelHandler<IN, OUT, HttpChannel<IN, OUT>>() {
			@Override
			public Publisher<Void> apply(HttpChannel<IN, OUT> inoutHttpChannel) {
				try {
					final NettyHttpChannel<IN, OUT> ch = ((NettyHttpChannel<IN, OUT>) inoutHttpChannel);
					ch.getNettyRequest()
							.setUri(currentURI.getPath() + (currentURI.getQuery() == null ? "" : "?" + currentURI.getQuery()))
							.setMethod(new HttpMethod(method.getName()))
							.headers()
								.add(HttpHeaders.Names.HOST, currentURI.getHost())
								.add(HttpHeaders.Names.ACCEPT, "*/*");

					if (handler != null) {
						Publisher<Void> p = handler.apply(ch);
						reply.onNext(ch);
						return p;
					} else {
						reply.onNext(ch);
						return Streams.empty();
					}
				} catch (Throwable t) {
					reply.onError(t);
					return Promises.error(t);
				}
			}

		});
		return reply;
	}

	private URI parseURL(Method method, String url) throws Exception{
		if(!url.startsWith(HttpChannel.HTTP_SCHEME) && !url.startsWith(HttpChannel.WS_SCHEME)){
			final String parsedUrl = (method.equals(Method.WS) ? HttpChannel.WS_SCHEME : HttpChannel.HTTP_SCHEME) + "://";
			if(url.startsWith("/")){
				return new URI(parsedUrl
						+ (lastURI != null && lastURI.getHost() != null ? lastURI.getHost() : "localhost")
						+ url);
			}else {
				return new URI(parsedUrl + url);
			}
		}else{
			return new URI(url);
		}
	}

	@Override
	protected final Promise<Void> doShutdown() {
		return client.shutdown();
	}

	protected void bindChannel(ReactorChannelHandler<IN, OUT, ChannelStream<IN, OUT>> handler, Object nativeChannel) {
		SocketChannel ch = (SocketChannel) nativeChannel;

		NettyChannelStream<IN, OUT> netChannel = new NettyChannelStream<IN, OUT>(
				getDefaultEnvironment(),
				getDefaultCodec(),
				getDefaultPrefetchSize(),
				getDefaultDispatcher(),
				ch
		);


		ChannelPipeline pipeline = ch.pipeline();
		if (log.isDebugEnabled()) {
			pipeline.addLast(new LoggingHandler(NettyHttpClient.class));
		}

		pipeline
				.addLast(new HttpClientCodec());

		URI currentURI = lastURI;
		if(currentURI.getScheme() != null &&
				currentURI.getScheme().toLowerCase().startsWith(HttpChannel.WS_SCHEME)){
			pipeline
					.addLast(new HttpObjectAggregator(8192))
					.addLast(
							new NettyHttpWSClientHandler<IN, OUT>(
									handler,
									netChannel,
									WebSocketClientHandshakerFactory.newHandshaker(
											lastURI, WebSocketVersion.V13, null, false, new DefaultHttpHeaders()))
					);
		}else {
			pipeline
					.addLast(new NettyHttpClientHandler<IN, OUT>(handler, netChannel));
		}
	}
}
