package common.network;

import java.io.IOException;
import java.net.SocketAddress;
import java.nio.channels.ClosedChannelException;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.regex.Pattern;

import javax.crypto.Cipher;
import javax.crypto.SecretKey;

import common.log.Log;
import common.net.channel.Channel;
import common.net.channel.ChannelFuture;
import common.net.channel.ChannelFutureListener;
import common.net.channel.ChannelHandlerContext;
import common.net.channel.SimpleChannelInboundHandler;
import common.net.handler.timeout.TimeoutException;
import common.net.util.AttributeKey;
import common.net.util.concurrent.Future;
import common.net.util.concurrent.GenericFutureListener;
import common.network.NetHandler.ThreadQuickExitException;
import common.util.EncryptUtil;

public class NetConnection extends SimpleChannelInboundHandler<Packet> {
	private static final boolean DEBUG = System.getProperty("network.debug") != null;
	private static final Pattern IP_REPLACER = Pattern.compile("([0-9]*)\\.([0-9]*)\\.[0-9]*\\.[0-9]*");
	public static final AttributeKey<PacketRegistry> ATTR_STATE = AttributeKey.<PacketRegistry>valueOf("protocol");

	private final Queue<NetConnection.InboundHandlerTuplePacketListener> queue = new ConcurrentLinkedQueue<InboundHandlerTuplePacketListener>();
	private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();

	private Channel channel;
	private SocketAddress address;
	private NetHandler handler;
	private String exitReason;
	private boolean disconnected;

	public void channelActive(ChannelHandlerContext context) throws Exception {
		super.channelActive(context);
		this.channel = context.channel();
		this.address = this.channel.remoteAddress();

		try {
			this.setConnectionState(PacketRegistry.HANDSHAKE);
		}
		catch(Throwable t) {
			Log.NETWORK.error(t, "Fehler beim Aufbauen der Verbindung für Handshake");
		}
	}

	public void setConnectionState(PacketRegistry newState) {
		this.channel.attr(ATTR_STATE).set(newState);
		this.channel.config().setAutoRead(true);
		if(DEBUG)
			Log.NETWORK.info("Automatisches Lesen eingeschaltet");
	}

	public void channelInactive(ChannelHandlerContext context) throws Exception {
		this.closeChannel("Ende der Datenübertragung");
	}

	public void exceptionCaught(ChannelHandlerContext context, Throwable throwable) throws Exception {
		String comp;

		if(throwable instanceof TimeoutException) {
			comp = "Zeitüberschreitung";
		}
		else {
			comp = "Interner Fehler: " + throwable;
			if(!(throwable instanceof ClosedChannelException || throwable instanceof IOException))
				Log.NETWORK.error(throwable, "Fehler in der Verbindung mit %s", this.getCutAddress());
		}

		this.closeChannel(comp);
	}

	protected void channelRead0(ChannelHandlerContext context, Packet packet) throws Exception {
		if(DEBUG)
			Log.NETWORK.info("EIN: [" + context.channel().attr(NetConnection.ATTR_STATE).get() + "] " + packet.getClass().getName());
		if(this.channel.isOpen()) {
			try {
				packet.processPacket(this.handler);
			}
			catch(ThreadQuickExitException e) {
				;
			}
		}
	}

	public void setNetHandler(NetHandler handler) {
		if(handler == null) {
			throw new NullPointerException("Handler ist Null");
		}
//        Log.debug("Setze Handler von " + this + " auf " + handler);
		this.handler = handler;
	}

	public void sendPacket(Packet packet) {
		if(this.isChannelOpen()) {
			this.flushOutboundQueue();
			this.dispatchPacket(packet, null);
		}
		else {
			this.lock.writeLock().lock();

			try {
				this.queue.add(new NetConnection.InboundHandlerTuplePacketListener(packet, null));
			}
			finally {
				this.lock.writeLock().unlock();
			}
		}
	}

	public void sendPacket(Packet packet, GenericFutureListener<? extends Future<? super Void>> listener) {
		if(this.isChannelOpen()) {
			this.flushOutboundQueue();
			this.dispatchPacket(packet, new GenericFutureListener[] {listener});
		}
		else {
			this.lock.writeLock().lock();

			try {
				this.queue.add(new NetConnection.InboundHandlerTuplePacketListener(packet, listener));
			}
			finally {
				this.lock.writeLock().unlock();
			}
		}
	}

	private void dispatchPacket(final Packet packet, final GenericFutureListener<? extends Future<? super Void>>[] listeners) {
		final PacketRegistry state = PacketRegistry.getType(packet);
		final PacketRegistry current = this.channel.attr(ATTR_STATE).get();

		if(current != state) {
			if(DEBUG)
				Log.NETWORK.info("Automatisches Lesen ausgeschaltet");
			this.channel.config().setAutoRead(false);
		}

		if(this.channel.eventLoop().inEventLoop()) {
			if(state != current) {
				this.setConnectionState(state);
			}

			if(DEBUG)
				Log.NETWORK.info("AUS: [" + this.channel.attr(NetConnection.ATTR_STATE).get() + "] " + packet.getClass().getName());
			ChannelFuture future = this.channel.writeAndFlush(packet);

			if(listeners != null) {
				future.addListeners(listeners);
			}

			future.addListener(ChannelFutureListener.FIRE_EXCEPTION_ON_FAILURE);
		}
		else {
			this.channel.eventLoop().execute(new Runnable() {
				public void run() {
					if(state != current) {
						NetConnection.this.setConnectionState(state);
					}

					if(DEBUG)
						Log.NETWORK.info("AUS: [" + NetConnection.this.channel.attr(NetConnection.ATTR_STATE).get() + "] " + packet.getClass().getName());
					ChannelFuture future = NetConnection.this.channel.writeAndFlush(packet);

					if(listeners != null) {
						future.addListeners(listeners);
					}

					future.addListener(ChannelFutureListener.FIRE_EXCEPTION_ON_FAILURE);
				}
			});
		}
	}

	private void flushOutboundQueue() {
		if(this.channel != null && this.channel.isOpen()) {
			this.lock.readLock().lock();

			try {
				while(!this.queue.isEmpty()) {
					NetConnection.InboundHandlerTuplePacketListener entry = this.queue.poll();
					if(entry != null) // NPE Fix
						this.dispatchPacket(entry.packet, entry.listeners);
				}
			}
			finally {
				this.lock.readLock().unlock();
			}
		}
	}

	public void processReceivedPackets() {
		this.flushOutboundQueue();
		this.handler.update();
		if(this.channel != null)
			this.channel.flush();
		if(DEBUG)
			Log.NETWORK.info("Pakete synchronisiert");
	}

	public String getCutAddress() {
		return this.address == null ? "?.?.*.*" : IP_REPLACER.matcher(this.address.toString()).replaceAll("$1.$2.*.*");
	}

	public void closeChannel(String message) {
		if(this.channel.isOpen()) {
			this.channel.close().awaitUninterruptibly();
			this.exitReason = message;
		}
	}

	public boolean isChannelOpen() {
		return this.channel != null && this.channel.isOpen();
	}

	public boolean hasNoChannel() {
		return this.channel == null;
	}

	public void disableAutoRead() {
		this.channel.config().setAutoRead(false);
	}

	public void setCompressionTreshold(int treshold) {
		if(treshold >= 0) {
			if(this.channel.pipeline().get("decompress") instanceof CompressionDecoder) {
				((CompressionDecoder)this.channel.pipeline().get("decompress")).setTreshold(treshold);
			}
			else {
				this.channel.pipeline().addBefore("decoder", "decompress", new CompressionDecoder(treshold));
			}

			if(this.channel.pipeline().get("compress") instanceof CompressionEncoder) {
				((CompressionEncoder)this.channel.pipeline().get("compress")).setTreshold(treshold);
			}
			else {
				this.channel.pipeline().addBefore("encoder", "compress", new CompressionEncoder(treshold));
			}
		}
		else {
			if(this.channel.pipeline().get("decompress") instanceof CompressionDecoder) {
				this.channel.pipeline().remove("decompress");
			}

			if(this.channel.pipeline().get("compress") instanceof CompressionEncoder) {
				this.channel.pipeline().remove("compress");
			}
		}
	}

	public void checkDisconnected() {
		if(this.channel != null && !this.channel.isOpen()) {
			if(!this.disconnected) {
				this.disconnected = true;
				if(this.handler != null)
					this.handler.onDisconnect(this.exitReason != null ? this.exitReason : "Verbindung getrennt");
			}
			else {
				Log.NETWORK.warn("handleDisconnection() zweifach aufgerufen");
			}
		}
	}

	public void startEncryption(SecretKey key) {
		this.channel.pipeline().addBefore("splitter", "decrypt", new EncryptionDecoder(EncryptUtil.createCipher(Cipher.DECRYPT_MODE, key)));
		this.channel.pipeline().addBefore("prepender", "encrypt", new EncryptionEncoder(EncryptUtil.createCipher(Cipher.ENCRYPT_MODE, key)));
	}

	private static class InboundHandlerTuplePacketListener {
		private final Packet packet;
		private final GenericFutureListener<? extends Future<? super Void>>[] listeners;

		public InboundHandlerTuplePacketListener(Packet packet, GenericFutureListener<? extends Future<? super Void>> listener) {
			this.packet = packet;
			this.listeners = listener == null ? null : new GenericFutureListener[] {listener};
		}
	}
}
