package ms.net;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.SocketChannel;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

import ms.ServerConstants;
import ms.net.producer.HSEventProducer;
import ms.world.GameServer;

/**
 * Represents a connected I/O session.
 * @author Emperor
 * 
 */
public class IoSession {
	
	/**
	 * The handshake event producer.
	 */
	private static final EventProducer HANDSHAKE_PRODUCER = new HSEventProducer();

	/**
	 * The selection key.
	 */
	private final SelectionKey key;
	
	/**
	 * The executor service.
	 */
	private final ExecutorService service;
	
	/**
	 * The event producer.
	 */
	private EventProducer producer = HANDSHAKE_PRODUCER;
	
	/**
	 * The currently queued writing data.
	 */
	private List<ByteBuffer> writingQueue = new ArrayList<>(20);
	
	/**
	 * The currently queued reading data.
	 */
	private ByteBuffer readingQueue;
	
	/**
	 * The writing lock.
	 */
	private final Lock writingLock = new ReentrantLock();
	
	/**
	 * The name hash.
	 */
	private int nameHash;
	
	/**
	 * The server key.
	 */
	private long serverKey;
	
	/**
	 * The JS-5 encryption value.
	 */
	private int js5Encryption;
	
	/**
	 * If the session is active.
	 */
	private boolean active = true;
	
	/**
	 * The last ping time stamp.
	 */
	private long lastPing;
	
	/**
	 * The address.
	 */
	private final String address;
	
	/**
	 * The game server object for this session.
	 */
	private GameServer gameServer;
	
	/**
	 * Constructs a new {@code IoSession}.
	 * @param key The selection key.
	 * @param service The executor service.
	 */
	public IoSession(SelectionKey key, ExecutorService service) {
		this.key = key;
		this.service = service;
		String address = getRemoteAddress().replaceAll("/", "").split(":")[0];
		if (address.equals("127.0.0.1")) {
			address = ServerConstants.HOST_ADDRESS;
		}
		this.address = address;
	}

	/**
	 * Fires a write event created using the current event producer.
	 * @param context The event context.
	 */
	public void write(Object context) {
		write(context, false);
	}
	
	/**
	 * Fires a write event created using the current event producer.
	 * @param context The event context.
	 * @param instant If the event should be instantly executed on this thread.
	 */
	public void write(Object context, boolean instant) {
		if (context == null) {
			throw new IllegalStateException("Invalid writing context!");
		}
		if (instant) {
			producer.produceWriter(this, context).run();
			return;
		}
		service.execute(producer.produceWriter(this, context));
	}
	
	/**
	 * Sends the packet data (without write event encoding).
	 * @param buffer The buffer.
	 */
	public void queue(ByteBuffer buffer) {
		try {
			writingLock.tryLock(1000L, TimeUnit.MILLISECONDS);
		} catch (Exception e){
			e.printStackTrace();
			writingLock.unlock();
			return;
		}
		writingQueue.add(buffer);
		writingLock.unlock();
		write();
	}
	
	/**
	 * Handles the writing of all buffers in the queue.
	 */
	public void write() {
		if (!key.isValid()) {
			disconnect();
			return;
		}
		writingLock.lock();
		SocketChannel channel = (SocketChannel) key.channel();
		try {
			while (!writingQueue.isEmpty()) {
				ByteBuffer buffer = writingQueue.get(0);
				channel.write(buffer);
				if (buffer.hasRemaining()) {
					key.interestOps(key.interestOps() | SelectionKey.OP_WRITE);
					break;
				}
				writingQueue.remove(0);
			}
		} catch (IOException e) {
			disconnect();
		}
		writingLock.unlock();
	}

	/**
	 * Disconnects the session.
	 */
	public void disconnect() {
		try {
			if (!active) {
				return;
			}
			active = false;
			key.cancel();
			SocketChannel channel = (SocketChannel) key.channel();
			channel.socket().close();
		} catch (IOException e) {
			e.printStackTrace();
		}
	}

	/**
	 * Gets the IP-address (without the port).
	 * @return The address.
	 */
	public String getAddress() {
		return address;
	}
	
	/**
	 * Gets the remote address of this session.
	 * @return The remote address, as a String.
	 */
	public String getRemoteAddress() {
		try {
			return ((SocketChannel) key.channel()).getRemoteAddress().toString();
		} catch (IOException e) {
			throw new IllegalStateException(e);
		}
	}
	
	/**
	 * Gets the current event producer.
	 * @return The producer.
	 */
	public EventProducer getProducer() {
		return producer;
	}

	/**
	 * Sets the event producer.
	 * @param producer The producer to set.
	 */
	public void setProducer(EventProducer producer) {
		this.producer = producer;
	}

	/**
	 * Gets the queued reading data.
	 * @return The readingQueue.
	 */
	public ByteBuffer getReadingQueue() {
		synchronized (this) {
			return readingQueue;
		}
	}

	/**
	 * Queues reading data.
	 * @param readingQueue The readingQueue to set.
	 */
	public void setReadingQueue(ByteBuffer readingQueue) {
		synchronized (this) {
			this.readingQueue = readingQueue;
		}
	}
	
	/**
	 * Gets the writing lock.
	 * @return The writing lock.
	 */
	public Lock getWritingLock() {
		return writingLock;
	}

	/**
	 * Gets the selection key.
	 * @return The selection key.
	 */
	public SelectionKey getKey() {
		return key;
	}

	/**
	 * @return The active.
	 */
	public boolean isActive() {
		return active;
	}

	/**
	 * @return The js5Encryption.
	 */
	public int getJs5Encryption() {
		return js5Encryption;
	}

	/**
	 * @param js5Encryption The js5Encryption to set.
	 */
	public void setJs5Encryption(int js5Encryption) {
		this.js5Encryption = js5Encryption;
	}
	
	/**
	 * Gets the lastPing.
	 * @return The lastPing.
	 */
	public long getLastPing() {
		return lastPing;
	}

	/**
	 * Sets the lastPing.
	 * @param lastPing The lastPing to set.
	 */
	public void setLastPing(long lastPing) {
		this.lastPing = lastPing;
	}

	/**
	 * Gets the nameHash.
	 * @return The nameHash.
	 */
	public int getNameHash() {
		return nameHash;
	}

	/**
	 * Sets the nameHash.
	 * @param nameHash The nameHash to set.
	 */
	public void setNameHash(int nameHash) {
		this.nameHash = nameHash;
	}

	/**
	 * Gets the serverKey.
	 * @return The serverKey.
	 */
	public long getServerKey() {
		return serverKey;
	}

	/**
	 * Sets the serverKey.
	 * @param serverKey The serverKey to set.
	 */
	public void setServerKey(long serverKey) {
		this.serverKey = serverKey;
	}

	/**
	 * Gets the gameServer value.
	 * @return The gameServer.
	 */
	public GameServer getGameServer() {
		return gameServer;
	}

	/**
	 * Sets the gameServer value.
	 * @param gameServer The gameServer to set.
	 */
	public void setGameServer(GameServer gameServer) {
		this.gameServer = gameServer;
	}

}