package org.fastcatsearch.http;

import java.net.InetSocketAddress;
import java.nio.channels.ClosedChannelException;
import java.util.concurrent.Executors;

import org.fastcatsearch.env.Environment;
import org.fastcatsearch.module.AbstractModule;
import org.fastcatsearch.module.ModuleException;
import org.fastcatsearch.settings.Settings;
import org.fastcatsearch.transport.NetworkExceptionHelper;
import org.jboss.netty.bootstrap.ServerBootstrap;
import org.jboss.netty.channel.Channel;
import org.jboss.netty.channel.ChannelHandlerContext;
import org.jboss.netty.channel.ChannelPipeline;
import org.jboss.netty.channel.ChannelPipelineFactory;
import org.jboss.netty.channel.Channels;
import org.jboss.netty.channel.ExceptionEvent;
import org.jboss.netty.channel.socket.nio.NioServerSocketChannelFactory;
import org.jboss.netty.handler.codec.http.HttpChunkAggregator;
import org.jboss.netty.handler.codec.http.HttpContentCompressor;
import org.jboss.netty.handler.codec.http.HttpContentDecompressor;
import org.jboss.netty.handler.codec.http.HttpRequest;
import org.jboss.netty.handler.codec.http.HttpRequestDecoder;
import org.jboss.netty.handler.codec.http.HttpResponseEncoder;
import org.jboss.netty.handler.timeout.ReadTimeoutException;

public class HttpTransportModule extends AbstractModule {

	private volatile Channel serverChannel;
	private volatile ServerBootstrap serverBootstrap;

	final int maxContentLength;
	final int maxInitialLineLength;
	final int maxHeaderSize;
	final int maxChunkSize;

	private final int workerCount;

	// private final boolean blockingServer;

	final boolean compression;

	private final int compressionLevel; // expected: 0-9, 0 : no compression

	final boolean resetCookies;

	private final int port;

	private final Boolean tcpNoDelay;

	private final Boolean tcpKeepAlive;

	private final Boolean reuseAddress;

	private final long tcpSendBufferSize;
	private final long tcpReceiveBufferSize;

	final int maxCumulationBufferCapacity; // Integer.MAX_VALUE가 최대이다. 넘는다면 재설정해준다.
	final int maxCompositeBufferComponents;

	private volatile HttpServerAdapter httpServerAdapter;

	public HttpTransportModule(Environment environment, Settings settings, int port) {
		super(environment, settings);
		this.port = port;
		maxContentLength = settings.getInt("max_content_length", settings.getInt("http.max_content_length", 100 * 1024 * 1024));
		this.maxChunkSize = settings.getInt("max_chunk_size", settings.getInt("http.max_chunk_size", 8 * 1024));
		this.maxHeaderSize = settings.getInt("max_header_size", settings.getInt("http.max_header_size", 8 * 1024));
		this.maxInitialLineLength = settings.getInt("max_initial_line_length", settings.getInt("http.max_initial_line_length", 4 * 1024));
		// don't reset cookies by default, since I don't think we really need to
		// note, parsing cookies was fixed in netty 3.5.1 regarding stack allocation, but still, currently, we don't need cookies
		this.resetCookies = settings.getBoolean("reset_cookies", settings.getBoolean("http.reset_cookies", false));
		this.maxCumulationBufferCapacity = settings.getInt("max_cumulation_buffer_capacity", 0);
		this.maxCompositeBufferComponents = settings.getInt("max_composite_buffer_components", -1);
		this.workerCount = settings.getInt("worker_count", Runtime.getRuntime().availableProcessors() * 2);
		
		this.tcpNoDelay = settings.getBoolean("tcp_no_delay", true);
		this.tcpKeepAlive = settings.getBoolean("tcp_keep_alive", true);
		this.reuseAddress = settings.getBoolean("reuse_address", true);
		this.tcpSendBufferSize = settings.getInt("tcp_send_buffer_size");
		this.tcpReceiveBufferSize = settings.getInt("tcp_receive_buffer_size");
		this.compression = settings.getBoolean("http.compression", false);
		this.compressionLevel = settings.getInt("http.compression_level", 6);
	}

	@Override
	protected boolean doLoad() throws ModuleException {

		serverBootstrap = new ServerBootstrap(new NioServerSocketChannelFactory(Executors.newCachedThreadPool(), Executors.newCachedThreadPool(),
				workerCount));

		serverBootstrap.setPipelineFactory(new MyChannelPipelineFactory(this));
		serverBootstrap.setOption("child.tcpNoDelay", tcpNoDelay);
		serverBootstrap.setOption("child.keepAlive", tcpKeepAlive);
		if (tcpSendBufferSize > 0) {
			serverBootstrap.setOption("child.sendBufferSize", tcpSendBufferSize);
		}
		if (tcpReceiveBufferSize > 0) {
			serverBootstrap.setOption("child.receiveBufferSize", tcpReceiveBufferSize);
		}
		serverBootstrap.setOption("reuseAddress", reuseAddress);
		serverBootstrap.setOption("child.reuseAddress", reuseAddress);

		serverChannel = serverBootstrap.bind(new InetSocketAddress(port));
		logger.debug("Bound to port [{}]", port);

		return true;
	}

	@Override
	protected boolean doUnload() throws ModuleException {
		if (serverBootstrap != null) {
			serverBootstrap.shutdown();
		}
		if (serverChannel != null) {
			serverChannel.close();
		}
		return true;
	}

	static class MyChannelPipelineFactory implements ChannelPipelineFactory {

		private final HttpRequestHandler requestHandler;
		private final HttpTransportModule transport;

		MyChannelPipelineFactory(HttpTransportModule transport) {
			this.transport = transport;
			this.requestHandler = new HttpRequestHandler(transport);
		}

		@Override
		public ChannelPipeline getPipeline() throws Exception {
			ChannelPipeline pipeline = Channels.pipeline();
			// pipeline.addLast("openChannels", transport.serverOpenChannels);
			HttpRequestDecoder requestDecoder = new HttpRequestDecoder(transport.maxInitialLineLength, transport.maxHeaderSize,
					transport.maxChunkSize);
			if (transport.maxCumulationBufferCapacity > 0) {
				// if (transport.maxCumulationBufferCapacity > Integer.MAX_VALUE) {
				// requestDecoder.setMaxCumulationBufferCapacity(Integer.MAX_VALUE);
				// } else {
				requestDecoder.setMaxCumulationBufferCapacity((int) transport.maxCumulationBufferCapacity);
				// }
			}
			if (transport.maxCompositeBufferComponents != -1) {
				requestDecoder.setMaxCumulationBufferComponents(transport.maxCompositeBufferComponents);
			}
			pipeline.addLast("decoder", requestDecoder);
			if (transport.compression) {
				pipeline.addLast("decoder_compress", new HttpContentDecompressor());
			}
			HttpChunkAggregator httpChunkAggregator = new HttpChunkAggregator(transport.maxContentLength);
			if (transport.maxCompositeBufferComponents != -1) {
				httpChunkAggregator.setMaxCumulationBufferComponents(transport.maxCompositeBufferComponents);
			}
			pipeline.addLast("aggregator", httpChunkAggregator);
			pipeline.addLast("encoder", new HttpResponseEncoder());
			if (transport.compression) {
				pipeline.addLast("encoder_compress", new HttpContentCompressor(transport.compressionLevel));
			}
			pipeline.addLast("handler", requestHandler);
			return pipeline;
		}
	}

	public void httpServerAdapter(HttpServerAdapter httpServerAdapter) {
		this.httpServerAdapter = httpServerAdapter;
	}

	public void dispatchRequest(HttpRequest request, HttpChannel httpChannel) {
		httpServerAdapter.dispatchRequest(request, httpChannel);
	}

	public void exceptionCaught(ChannelHandlerContext ctx, ExceptionEvent e) throws Exception {
//		logger.error("exceptionCaught", e.getCause());
		if (e.getCause() instanceof ReadTimeoutException) {
			if (logger.isTraceEnabled()) {
				logger.trace("Connection timeout [{}]", ctx.getChannel().getRemoteAddress());
			}
			ctx.getChannel().close();
		} else {
			if (!isLoaded) {
				// ignore
				return;
			}
			if (!NetworkExceptionHelper.isCloseConnectionException(e.getCause())) {
//				logger.warn("Caught exception while handling client http traffic, closing connection {}", e.getCause(), ctx.getChannel());
				ctx.getChannel().close();
			} else {
//				logger.debug("Caught exception while handling client http traffic, closing connection {}", e.getCause(), ctx.getChannel());
				ctx.getChannel().close();
			}
		}
		
	}

}