package reactor.net.tcp.spec;

import reactor.core.Environment;
import reactor.core.Reactor;
import reactor.core.spec.support.EventRoutingComponentSpec;
import reactor.function.Consumer;
import reactor.io.Buffer;
import reactor.io.encoding.Codec;
import reactor.net.NetChannel;
import reactor.net.config.ClientSocketOptions;
import reactor.net.config.SslOptions;
import reactor.net.tcp.TcpClient;
import reactor.util.Assert;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.lang.reflect.Constructor;
import java.net.InetSocketAddress;
import java.util.*;

/**
 * A helper class for specifying a {@code TcpClient}
 *
 * @param <IN>
 * 		The type that will be received by the client
 * @param <OUT>
 * 		The type that will be sent by the client
 *
 * @author Jon Brisbin
 */
public class TcpClientSpec<IN, OUT> extends EventRoutingComponentSpec<TcpClientSpec<IN, OUT>, TcpClient<IN, OUT>> {

	private final Constructor<TcpClient<IN, OUT>> clientImplConstructor;

	private InetSocketAddress connectAddress;
	private ClientSocketOptions      options    = new ClientSocketOptions();
	private Collection<Consumer<IN>> consumers  = Collections.emptyList();
	private SslOptions               sslOptions = null;
	private Codec<Buffer, IN, OUT> codec;

	/**
	 * Create a {@code TcpClient.Spec} using the given implementation class.
	 *
	 * @param clientImpl
	 * 		The concrete implementation of {@link TcpClient} to instantiate.
	 */
	@SuppressWarnings({"unchecked", "rawtypes"})
	public TcpClientSpec(@Nonnull Class<? extends TcpClient> clientImpl) {
		Assert.notNull(clientImpl, "TcpClient implementation class cannot be null.");
		try {
			this.clientImplConstructor = (Constructor<TcpClient<IN, OUT>>)clientImpl.getDeclaredConstructor(
					Environment.class,
					Reactor.class,
					InetSocketAddress.class,
					ClientSocketOptions.class,
					SslOptions.class,
					Codec.class,
					Collection.class
			);
			this.clientImplConstructor.setAccessible(true);
		} catch(NoSuchMethodException e) {
			throw new IllegalArgumentException(
					"No public constructor found that matches the signature of the one found in the TcpClient class.");
		}
	}

	/**
	 * Set the common {@link ClientSocketOptions} for connections made in this client.
	 *
	 * @param options
	 * 		The socket options to apply to new connections.
	 *
	 * @return {@literal this}
	 */
	public TcpClientSpec<IN, OUT> options(ClientSocketOptions options) {
		this.options = options;
		return this;
	}

	/**
	 * Set the options to use for configuring SSL. Setting this to {@code null} means don't use SSL at all (the default).
	 *
	 * @param sslOptions
	 * 		The options to set when configuring SSL
	 *
	 * @return {@literal this}
	 */
	public TcpClientSpec<IN, OUT> ssl(@Nullable SslOptions sslOptions) {
		this.sslOptions = sslOptions;
		return this;
	}

	/**
	 * The host and port to which this client should connect.
	 *
	 * @param host
	 * 		The host to connect to.
	 * @param port
	 * 		The port to connect to.
	 *
	 * @return {@literal this}
	 */
	public TcpClientSpec<IN, OUT> connect(@Nonnull String host, int port) {
		return connect(new InetSocketAddress(host, port));
	}

	/**
	 * The address to which this client should connect.
	 *
	 * @param connectAddress
	 * 		The address to connect to.
	 *
	 * @return {@literal this}
	 */
	public TcpClientSpec<IN, OUT> connect(@Nonnull InetSocketAddress connectAddress) {
		Assert.isNull(this.connectAddress, "Connect address is already set.");
		this.connectAddress = connectAddress;
		return this;
	}

	/**
	 * The {@link Codec} to use to encode and decode data.
	 *
	 * @param codec
	 * 		The codec to use.
	 *
	 * @return {@literal this}
	 */
	public TcpClientSpec<IN, OUT> codec(@Nullable Codec<Buffer, IN, OUT> codec) {
		Assert.isNull(this.codec, "Codec has already been set.");
		this.codec = codec;
		return this;
	}

	/**
	 * The {@link reactor.function.Consumer} to use to ingest incoming data.
	 *
	 * @param consumer
	 * 		the incoming data {@link reactor.function.Consumer}
	 *
	 * @return {@literal this}
	 */
	@SuppressWarnings("unchecked")
	public TcpClientSpec<IN, OUT> consume(Consumer<IN> consumer) {
		consume(Arrays.asList(consumer));
		return this;
	}

	/**
	 * The {@link reactor.function.Consumer Consumers} to use to ingest the incoming data.
	 *
	 * @param consumers
	 * 		the incoming data {@link reactor.function.Consumer Consumers}
	 *
	 * @return {@literal this}
	 */
	public TcpClientSpec<IN, OUT> consume(Collection<Consumer<IN>> consumers) {
		Assert.notNull(consumers, "Consumers cannot be null");
		this.consumers = consumers;
		return this;
	}

	@Override
	protected TcpClient<IN, OUT> configure(Reactor reactor, Environment env) {
		List<Consumer<NetChannel<IN, OUT>>> channelConsumers = new ArrayList<Consumer<NetChannel<IN, OUT>>>();
		channelConsumers.add(new Consumer<NetChannel<IN, OUT>>() {
			@Override
			public void accept(NetChannel<IN, OUT> netChannel) {
				for(Consumer<IN> consumer : consumers) {
					netChannel.consume(consumer);
				}
			}
		});
		try {
			return clientImplConstructor.newInstance(
					env,
					reactor,
					connectAddress,
					options,
					sslOptions,
					codec,
					channelConsumers
			);
		} catch(Throwable t) {
			throw new IllegalStateException(t);
		}
	}

}
