// This software is released into the Public Domain.  See copying.txt for details.
package org.openstreetmap.osmosis.replicationhttp.v0_6.impl;

import java.net.InetSocketAddress;
import java.nio.channels.ClosedChannelException;
import java.util.logging.Level;
import java.util.logging.Logger;

import org.jboss.netty.buffer.ChannelBuffer;
import org.jboss.netty.buffer.ChannelBuffers;
import org.jboss.netty.channel.ChannelFuture;
import org.jboss.netty.channel.ChannelFutureListener;
import org.jboss.netty.channel.ChannelHandlerContext;
import org.jboss.netty.channel.ChannelStateEvent;
import org.jboss.netty.channel.Channels;
import org.jboss.netty.channel.ExceptionEvent;
import org.jboss.netty.channel.MessageEvent;
import org.jboss.netty.channel.SimpleChannelHandler;
import org.jboss.netty.handler.codec.http.DefaultHttpResponse;
import org.jboss.netty.handler.codec.http.HttpHeaders;
import org.jboss.netty.handler.codec.http.HttpRequest;
import org.jboss.netty.handler.codec.http.HttpResponseStatus;
import org.jboss.netty.handler.codec.http.HttpVersion;
import org.jboss.netty.util.CharsetUtil;


/**
 * Netty handler for sending replication sequence numbers to clients.
 * 
 * @author Brett Henderson
 */
public abstract class SequenceServerHandler extends SimpleChannelHandler {

	private static final Logger LOG = Logger.getLogger(SequenceServerHandler.class.getName());

	private SequenceServerControl control;
	private long currentSequenceNumber;


	/**
	 * Creates a new instance.
	 * 
	 * @param control
	 *            Provides the Netty handlers with access to the controller.
	 */
	public SequenceServerHandler(SequenceServerControl control) {
		this.control = control;
	}


	/**
	 * Gets the central control object.
	 * 
	 * @return The controller.
	 */
	protected SequenceServerControl getControl() {
		return control;
	}


	@Override
	public void channelOpen(ChannelHandlerContext ctx, ChannelStateEvent e) {
		control.registerChannel(e.getChannel());
	}


	/**
	 * Writes a HTTP 404 response to the client.
	 * 
	 * @param ctx
	 *            The Netty context.
	 * @param requestedUri
	 *            The URI requested by the client.
	 */
	private void writeResourceNotFound(final ChannelHandlerContext ctx, String requestedUri) {
		// Write the HTTP header to the client.
		DefaultHttpResponse response = new DefaultHttpResponse(HttpVersion.HTTP_1_0, HttpResponseStatus.NOT_FOUND);
		response.headers().add("Content-Type", "text/plain");

		// Send the 404 message to the client.
		ChannelBuffer buffer = ChannelBuffers.copiedBuffer("The requested resource does not exist: " + requestedUri,
				CharsetUtil.UTF_8);
		response.setContent(buffer);

		// Write the header. Use a new future because the future we've been
		// passed is for upstream.
		ChannelFuture headerFuture = Channels.future(ctx.getChannel());
		Channels.write(ctx, headerFuture, response);

		// Wait for the previous operation to finish and then close the channel.
		headerFuture.addListener(new ChannelFutureListener() {
			@Override
			public void operationComplete(ChannelFuture future) {
				ctx.getChannel().close();
			}
		});
	}


	/**
	 * Writes a HTTP 410 response to the client.
	 * 
	 * @param ctx
	 *            The Netty context.
	 * @param requestedUri
	 *            The URI requested by the client.
	 */
	private void writeResourceGone(final ChannelHandlerContext ctx, String requestedUri) {
		// Write the HTTP header to the client.
		DefaultHttpResponse response = new DefaultHttpResponse(HttpVersion.HTTP_1_0, HttpResponseStatus.GONE);
		response.headers().add("Content-Type", "text/plain");

		// Send the 410 message to the client.
		ChannelBuffer buffer = ChannelBuffers.copiedBuffer("The requested resource is no longer available: "
				+ requestedUri, CharsetUtil.UTF_8);
		response.setContent(buffer);

		// Write the header. Use a new future because the future we've been
		// passed is for upstream.
		ChannelFuture headerFuture = Channels.future(ctx.getChannel());
		Channels.write(ctx, headerFuture, response);

		// Wait for the previous operation to finish and then close the channel.
		headerFuture.addListener(new ChannelFutureListener() {
			@Override
			public void operationComplete(ChannelFuture future) {
				ctx.getChannel().close();
			}
		});
	}


	/**
	 * Writes a HTTP 400 response to the client.
	 * 
	 * @param ctx
	 *            The Netty context.
	 * @param requestedUri
	 *            The URI requested by the client.
	 * @param errorMessage
	 *            Further information about why the request is bad.
	 */
	private void writeBadRequest(final ChannelHandlerContext ctx, String requestedUri,
			String errorMessage) {
		final String newLine = "\r\n";

		// Write the HTTP header to the client.
		DefaultHttpResponse response = new DefaultHttpResponse(HttpVersion.HTTP_1_0, HttpResponseStatus.NOT_FOUND);
		response.headers().add("Content-Type", "text/plain");

		// Send the 400 message to the client.
		StringBuilder messageBuilder = new StringBuilder();
		messageBuilder.append("Bad Request").append(newLine);
		messageBuilder.append("Message: ").append(errorMessage).append(newLine);
		messageBuilder.append("Requested URI: ").append(requestedUri).append(newLine);
		ChannelBuffer buffer = ChannelBuffers.copiedBuffer(messageBuilder.toString(), CharsetUtil.UTF_8);
		response.setContent(buffer);

		// Write the header. Use a new future because the future we've been
		// passed is for upstream.
		ChannelFuture headerFuture = Channels.future(ctx.getChannel());
		Channels.write(ctx, headerFuture, response);

		// Wait for the previous operation to finish and then close the channel.
		headerFuture.addListener(new ChannelFutureListener() {
			@Override
			public void operationComplete(ChannelFuture future) {
				ctx.getChannel().close();
			}
		});
	}


	/**
	 * Writes server statistics to the client.
	 * 
	 * @param ctx
	 *            The Netty context.
	 */
	private void writeStatistics(final ChannelHandlerContext ctx) {
		final String newLine = "\r\n";
		
		ServerStatistics statistics = control.getStatistics();

		// Write the HTTP header to the client.
		DefaultHttpResponse response = new DefaultHttpResponse(HttpVersion.HTTP_1_0, HttpResponseStatus.OK);
		response.headers().add("Content-Type", "text/plain");

		// Send the statistics message to the client.
		StringBuilder messageBuilder = new StringBuilder();
		messageBuilder.append("Server Statistics").append(newLine);
		messageBuilder.append("Total Requests: ").append(statistics.getTotalRequests()).append(newLine);
		messageBuilder.append("Active Connections: ").append(statistics.getActiveConnections()).append(newLine);
		ChannelBuffer buffer = ChannelBuffers.copiedBuffer(messageBuilder.toString(), CharsetUtil.UTF_8);
		response.setContent(buffer);

		// Write the header. Use a new future because the future we've been
		// passed is for upstream.
		ChannelFuture headerFuture = Channels.future(ctx.getChannel());
		Channels.write(ctx, headerFuture, response);

		// Wait for the previous operation to finish and then close the channel.
		headerFuture.addListener(new ChannelFutureListener() {
			@Override
			public void operationComplete(ChannelFuture future) {
				ctx.getChannel().close();
			}
		});
	}


	/**
	 * Writes sequence data to the client. If follow is set, it allows
	 * continuous updates to be streamed to the client.
	 * 
	 * @param ctx
	 *            The Netty context.
	 * @param contentType
	 *            The content type to set on the HTTP response.
	 * @param requestedSequenceNumber
	 *            The requested sequence number. Sending will start from this
	 *            number.
	 * @param follow
	 *            If true, continuous updates will be sent to the client.
	 */
	protected void initiateSequenceWriting(final ChannelHandlerContext ctx,
			String contentType, final long requestedSequenceNumber, final boolean follow) {
		// Create the HTTP header to send to the client.
		DefaultHttpResponse response = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK);
		response.headers().add("Content-Type", contentType);
		response.setChunked(true);
		response.headers().add(HttpHeaders.Names.TRANSFER_ENCODING, HttpHeaders.Values.CHUNKED);

		// Write the header. We must use a new future because the future we've
		// been passed is for upstream.
		ChannelFuture headerFuture = Channels.future(ctx.getChannel());
		Channels.write(ctx, headerFuture, response);

		// Wait for the previous operation to finish and then start sending
		// sequence numbers to this channel.
		headerFuture.addListener(new ChannelFutureListener() {
			@Override
			public void operationComplete(ChannelFuture future) throws Exception {
				if (future.isSuccess()) {
					control.determineNextChannelAction(ctx.getChannel(), requestedSequenceNumber, follow);
				}
			}
		});
	}


	/**
	 * Parses the request and initialises the response processing, typically by
	 * calling the writeSequence method.
	 * 
	 * @param ctx
	 *            The Netty context.
	 * @param request
	 *            The client request.
	 */
	protected abstract void handleRequest(ChannelHandlerContext ctx, HttpRequest request);


	@Override
	public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) throws Exception {
		// We have received a message from the client which is a HTTP request.
		HttpRequest request = (HttpRequest) e.getMessage();
		
		InetSocketAddress remoteAddress = (InetSocketAddress) ctx.getChannel().getRemoteAddress();
		if (LOG.isLoggable(Level.FINE)) {
			LOG.fine("Received new request from " + remoteAddress.getAddress().getHostAddress() + ":"
					+ remoteAddress.getPort());
		}

		// Process the HTTP request.
		try {
			// Check if this is a request to a generic URL. If it isn't
			// something we support then delegate to the specific handler.
			if (request.getUri().equals("/statistics")) {
				writeStatistics(ctx);
			} else {
				handleRequest(ctx, request);
			}
			
		} catch (ResourceNotFoundException ex) {
			writeResourceNotFound(ctx, request.getUri());
		} catch (ResourceGoneException ex) {
			writeResourceGone(ctx, request.getUri());
		} catch (BadRequestException ex) {
			writeBadRequest(ctx, request.getUri(), ex.getMessage());
		}
	}


	/**
	 * Convert the sequence number to sequence data and write to the channel.
	 * 
	 * @param ctx
	 *            The channel handler context.
	 * @param future
	 *            The future for current processing.
	 * @param sequenceNumber
	 *            The sequence number to be written.
	 */
	protected abstract void writeSequence(ChannelHandlerContext ctx, ChannelFuture future, long sequenceNumber);


	@Override
	public void writeRequested(ChannelHandlerContext ctx, MessageEvent e) throws Exception {
		// The message event is a Long containing the sequence number.
		currentSequenceNumber = (Long) e.getMessage();

		// Call the concrete implementation to convert the sequence to writable
		// data.
		writeSequence(ctx, e.getFuture(), currentSequenceNumber);
	}


	@Override
	public void exceptionCaught(ChannelHandlerContext ctx, ExceptionEvent e) throws Exception {
		// Get the cause of the exception.
		Throwable t = e.getCause();

		// A ClosedChannelException occurs if the client disconnects and is not
		// an error scenario.
		if (!(t instanceof ClosedChannelException)) {
			LOG.log(Level.SEVERE, "Error during processing for channel " + ctx.getChannel() + ".", t);
		}

		// We must stop sending to this client if any errors occur during
		// processing.
		e.getChannel().close();
	}

	/**
	 * Used during request parsing to notify that the requested URI could not be
	 * found.
	 */
	protected static class ResourceNotFoundException extends RuntimeException {
		private static final long serialVersionUID = -1L;
	}

	/**
	 * Used during request parsing to notify that the request is invalid in some
	 * way.
	 */
	protected static class BadRequestException extends RuntimeException {
		private static final long serialVersionUID = -1L;


		/**
		 * Creates a new instance.
		 * 
		 * @param message
		 *            The error message.
		 */
		public BadRequestException(String message) {
			super(message);
		}
	}

	/**
	 * Used during request parsing to notify that the requested URI is no longer
	 * available.
	 */
	protected static class ResourceGoneException extends RuntimeException {
		private static final long serialVersionUID = -1L;
	}
}
