package cz.cuni.amis.pogamut.base.communication.connection;

import java.io.IOException;
import java.io.Reader;
import java.io.Writer;
import java.util.logging.Level;

import com.google.inject.Inject;

import cz.cuni.amis.pogamut.base.communication.exceptions.ConnectionException;
import cz.cuni.amis.pogamut.base.exceptions.PogamutIOException;
import cz.cuni.amis.pogamut.base.factory.guice.AgentScoped;
import cz.cuni.amis.pogamut.base.utils.logging.AgentLogger;
import cz.cuni.amis.pogamut.base.utils.logging.LogCategory;
import cz.cuni.amis.utils.ExceptionToString;
import cz.cuni.amis.utils.StringCutter;
import cz.cuni.amis.utils.flag.Flag;
import cz.cuni.amis.utils.flag.ImmutableFlag;

/**
 * Implementation of the basic connection to the world server. Note that it has some nice features :-)
 * <p><p>
 * This implementation is THREAD-SAFE!
 * <p><p>
 * Calling getReader().read(), getWriter().write() is synchronized as well!
 * <p><p>
 * Reader and writer can be got in advance (no need to call connect() before getReader() or getWriter()).
 * <p><p>
 * Calling reader.close() or writer.close() will close the connection as well.
 * <p><p>
 * Whenever an exception is thrown during read/write operation, the connection is immediately closed,
 * the flag will be changed correctly.
 * <p><p>
 * ... if you're waiting on the reader.read() and the socket is closed, be ready
 * to catch the SocketException ...
 * <p><p>
 * The instance of the class can be reused.
 * <p><p>
 * All you have to implement:
 * <ol>
 * <li>unsyncConnect(IConnectionAddress address) - no need to care of anything else then connection to 'address' + provide correct behavior for getConnectionReader(), getConnectionWriter()</li>
 * <li>unsyncClose() - just close the connection, no need to care of anything else</li>
 * <li>Reader getConnectionReader() - if connection is up, return raw reader for your connection (no extra assumptions), it will be wrapped with ConnectionReader ... if conection is down, return null</li>
 * <li>Writer getConnectionWriter() - if connection is down, return raw writer for your connection (no extra assumptions), it will be wrapped with ConnectionWriter ... if conection is down, return null</li>
 * </ol>
 * You might want to override method getMessageEnd() to return correct "message-end" string, so the messages are correctly split.
 * <p><p>
 * Use protected log methods to do the logging you need (it will chech whether there is a logger set for you).
 * 
 * @author Jimmy
 */
@AgentScoped
public abstract class AbstractConnection<ConnectionAddress extends IWorldConnectionAddress> implements IWorldConnection<ConnectionAddress> {
	
	/**
	 * Reader for the connection (wrapper for the getConnectionReader()),
	 * that takes care of sniffing messages (if required) + makes reader persistent
	 * over the connect() calls of the connection.  
	 * 
	 * @author Jimmy
	 */
	private class ConnectionReader extends Reader {
		
		/**
		 * Owner of the ConnectionReader (because of close() method).
		 */
		private AbstractConnection<ConnectionAddress> owner = null;
		
		/**
		 * Used when the observer is hooked to the connection.
		 */
		private StringCutter line = new StringCutter(getMessageEnd());

		/**
		 * Cached reader of the connection.
		 */
		private Reader reader = null;
		
		/**
		 * Connection token - we use it to distinguish between connect() calls to be able
		 * to correctly close the connection if the read fails (we have to close the
		 * connection iff it is the same of the cached reader)
		 */
		private int currentConnectionToken = -1;
		
		/**
		 * Mutex that handles access to logMessages field.
		 */
		private Object logMessagesMutex = new Object();
		
		/**
		 * Whether we have to sniff (log) messages from the reader.
		 */
		private boolean logMessages = false;

		public ConnectionReader(AbstractConnection<ConnectionAddress> owner) {
			this.owner = owner;
		}

		/**
		 * Sets whether we have to log messages from reader.
		 * @param state
		 */
		public void setLogMessages(boolean state) {
			synchronized(logMessagesMutex) {
				if (logMessages == state) return;
				logMessages = state;
				if (logMessages) line.clear();
			}
		}
				
		/**
		 * Inner method to get the reader of current connection. It always check whether the
		 * reader hasn't been changed - so it always returns a current one.
		 * @return
		 * @throws PogamutIOException
		 */
		private Reader getReader() throws PogamutIOException {
			synchronized(classMutex) {
				if (currentConnectionToken != connectionToken || this.reader == null) {
					currentConnectionToken = connectionToken;
					line.clear();
					this.reader = getConnectionReader();
				}
				return this.reader;
			}
		}
		
		@Override
		public void close() {
			this.owner.close();			
		}
		
		/**
		 * Close the connection iff connected and it's the same connection.
		 */
		private void checkedClose() {
			if (isConnected()) {
				// if we're still connected, we have to check the connection token,
				synchronized(classMutex) {
					// if we're in the same connection, close it!
					if (currentConnectionToken == connectionToken) {
						// we're in the middle of the same connection and error occurred, close the connection
						this.close();	
					}
				}
			}
		}
		
		public boolean ready() throws IOException {
			if (!isConnected()) return false;
			Reader currentReader;
			try {
				currentReader = this.getReader();
			} catch (PogamutIOException e1) {
				e1.logExceptionOnce(log);
				throw new IOException("can't get reader for the connection");
			}
			if (currentReader != null) return currentReader.ready();
			return false;
		}
		
		@Override
		public synchronized int read(char[] ac, int i, int j) throws IOException {
			if (!isConnected()) throw new IOException("not connected, can't read");
			Reader currentReader;
			try {
				currentReader = this.getReader();
			} catch (PogamutIOException e1) {
				e1.logExceptionOnce(log);
				throw new IOException("can't get reader for the connection");
			}
			if (currentReader == null) {
				checkedClose();
				IOException e = new IOException("inner reader of the connection is null, can't read");  
				log.severe(ExceptionToString.process(e));
				throw e;
			}
			
			int result;
			try {
				result = currentReader.read(ac, i, j);
			} catch (IOException e) {					
				checkedClose();
				log.severe("read failed: " + e.getMessage());
				throw e;
			}			
			synchronized(logMessagesMutex) {
				// should we log the messages?
				if (logMessages){
					String[] lines = line.add(new String(ac, i, result));
					for (int index = 0; index < lines.length; ++index) {
						log(Level.INFO, "Message read: " + lines[index]);
					}
					return result;
				} else 
					return result;
			}			
		}

	}
	
	/**
	 * Writer for the connection (wrapper for the getConnectionWriter()),
	 * that takes care of sniffing messages (if required) + makes writer persistent
	 * over the connect() calls of the connection.  
	 * 
	 * @author Jimmy
	 */
	private class ConnectionWriter extends Writer {
		
		/**
		 * Owner of the ConnectionWriter (because of close() method).
		 */
		private AbstractConnection<ConnectionAddress> owner = null;
		
		/**
		 * Used when the observer is hooked to the connection.
		 */
		private StringCutter line = new StringCutter(getMessageEnd());

		/**
		 * Cached writer of the connection.
		 */
		private Writer writer = null;
		
		/**
		 * Connection token - we use it to distinguish between connect() calls to be able
		 * to correctly close the connection if the write fails (we have to close the
		 * connection iff it is the same of the cached writer)
		 */
		private int currentConnectionToken = -1;
		
		/**
		 * Mutex that handles access to logMessages field.
		 */
		private Object logMessagesMutex = new Object();
		
		/**
		 * Whether we have to sniff (log) messages from the writer.
		 */
		private boolean logMessages = false;

		public ConnectionWriter(AbstractConnection<ConnectionAddress> owner) {
			this.owner = owner;
		}

		/**
		 * Sets whether we have to log messages from writer.
		 * @param state
		 */
		public void setLogMessages(boolean state) {
			synchronized(logMessagesMutex) {
				if (logMessages == state) return;
				logMessages = state;
				if (logMessages) line.clear();
			}
		}
				
		/**
		 * Inner method to get the writer of current connection. It always check whether the
		 * writer hasn't been changed - so it always returns a current one.
		 * @return
		 * @throws PogamutIOException
		 */
		private Writer getWriter() throws PogamutIOException {
			synchronized(classMutex) {
				if (currentConnectionToken != connectionToken || this.writer == null) {
					currentConnectionToken = connectionToken;
					line.clear();
					this.writer = getConnectionWriter();
				}
				return this.writer;
			}
		}
		
		@Override
		public void close() {
			this.owner.close();			
		}
		
		public void flush() throws IOException {
			Writer currentWriter;
			try {
				currentWriter = getWriter();
			} catch (PogamutIOException e1) {
				e1.logExceptionOnce(log);
				throw new IOException("can't get connection writer");
			}
			if (currentWriter != null) currentWriter.flush();
		}
		
		public boolean ready() throws PogamutIOException {
			if (!isConnected()) return false;
			Writer currentWriter = this.getWriter();
			return currentWriter != null;
		}
		
		/**
		 * Close the connection iff connected and it's the same connection.
		 */
		private void checkedClose() {
			if (isConnected()) {
				// if we're still connected, we have to check the connection token,
				synchronized(classMutex) {
					// if we're in the same connection, close it!
					if (currentConnectionToken == connectionToken) {
						// we're in the middle of the same connection and error occurred, close the connection
						this.close();	
					}
				}
			}
		}
				
		@Override
		public synchronized void write(char cbuf[], int off, int len) throws IOException {	
			if (!isConnected()) {
				log.severe("not connected, can't write");
				throw new IOException("not connected, can't write");
			}
			Writer currentWriter;
			try {
				currentWriter = this.getWriter();
			} catch (PogamutIOException e1) {
				e1.logExceptionOnce(log);
				throw new IOException("can't get writer for the connection");
			}
			if (currentWriter == null) {
				checkedClose();
				log.severe("inner reader of the connection is null, can't read");
				throw new IOException("inner reader of the connection is null, can't read");
			}
			
			try {
				currentWriter.write(cbuf, off, len);
			} catch (IOException e) {					
				checkedClose();	
				log.severe(ExceptionToString.process("write failed", e));
				throw e;
			}			
			synchronized(logMessagesMutex) {
				// should we log the messages?
				if (logMessages){
					String[] lines = line.add(new String(cbuf, off, len));
					for (int index = 0; index < lines.length; ++index) {
						log(Level.INFO, "Message written: " + lines[index]);
					}
				}
			}			
		}
	}	
	
	/**
	 * Now this is complicated ... what this is! :-)
	 * <BR><BR>
	 * We've got this problem: <BR>
	 * 1) we want this class to be thread-safe <BR>
	 * BUT <BR>
	 * 2) we're giving access to writer/reader to any thread ...
	 * <BR><BR>
	 * What should happen if read/write fails? <BR>
	 * 1) if it is the same connection -> close it <BR>
	 * 2) if it is different connection -> just raise the exception
	 * <BR><BR>
	 * Therefore we need some kind of marker in which connection we are. Study the code if you want to learn more.<BR>
	 * Note that one object may be used many times.
	 */
	private int connectionToken = 0;

	/**
	 * Used to synchronize the behavior of the object.
	 */
	private Object classMutex = new Object();
			
	/**
	 * The class is taking care of this flag, correctly sets whether we're connected.
	 * Note that if the connection is closed by the remote host, we will notice it
	 * upon first read/write operation that will result in SocketException.
	 */
	private Flag<Boolean> connected = new Flag<Boolean>(false);
	
	/**
	 * Current remote side address of the connection.
	 */
	protected ConnectionAddress address = null;
	
	/**
	 * Writer of the connection. Serves for sending messages through the connection.
	 */
	private ConnectionWriter writer = new ConnectionWriter(this);
	
	/**
	 * Reader of the connection. Serves for reading messages from the connection.
	 */
	private ConnectionReader reader = new ConnectionReader(this);
	
	/**
	 * Mutex synchronizing access to the logger field.
	 */
	private Object loggerAccessMutex = new Object();
	
	/**
	 * Logger of the connection - access it via protected log() methods rather then directly.
	 */
	private AgentLogger agentLogger = null;
	
	/**
	 * Special category for the connection.
	 */
	protected LogCategory log = null;
	
	/**
	 * Inner implementation of connect, unsynchronized, this is called from
	 * connect(IConnectionDescriptor). This is called only iff the connection is down
	 * and the address is a new address.
	 * 
	 * @throws ConnectionException
	 */
	protected abstract void unsyncConnect(ConnectionAddress address) throws ConnectionException;
	
	@Inject
	public AbstractConnection(AgentLogger logger) {
		agentLogger = logger;
		log = agentLogger.in();
	}

	@Override
	public void connect(ConnectionAddress address) throws ConnectionException {
		synchronized(classMutex) {
			if (address == null) throw new ConnectionException("address is null, can't do connect()", log, this);
			if (isConnected()) {
				if (address.equals(this.address)) {
					close();
				}
			}
			this.address = address;
			unsyncConnect(this.address);	
			connected.setFlag(true);
		}
	}
	
	/**
	 * Inner unsynchronized implementation of the close(), should close the connection
	 * to the remote side without throwing any exception. You may be sure that the connection
	 * is up (according to the flag) when this method is called.
	 */
	protected abstract void unsyncClose();
	
	/**
	 * This should return plain reader for the current connection. If connection is down,
	 * this should throw WorldConnectionException. We will wrap this reader with our own
	 * implementation that is capable of sniffing messages as they come (if required).
	 * 
	 * @return
	 */
	protected abstract Reader getConnectionReader() throws ConnectionException;
	
	/**
	 * This should return plain writer for the current connection. If connection is down,
	 * this should throw WorldConnectionException. We will wrap this writer with our own
	 * implementation that is capable of sniffing messages as they go (if required).
	 * 
	 * @return
	 */
	protected abstract Writer getConnectionWriter() throws ConnectionException;
	
	/**
	 * Returns a string that is used as a delimiter between messages. We need this to be able
	 * to correctly recognize end of messages so we can write them to log (if required).
	 * @return
	 */
	public String getMessageEnd() {
		return IWorldConnection.DEFAULT_LINE_END;
	}
	
	/**
	 * Closes the connection.
	 */
	public void close() {
		synchronized(classMutex) {
			if (!isConnected()) return;			
			reader.reader = null;
			writer.writer = null;
			unsyncClose();
			++connectionToken;
			connected.setFlag(false);
		}
	}
	
	public boolean isConnected() {
		return connected.getFlag();
	}

	public ImmutableFlag<Boolean> getConnected() {
		return connected.getImmutable();
	}

	@Override
	public Writer getWriter() throws ConnectionException {
		return this.writer;
	}

	public void finalize() {
		close();
	}

	@Override
	public Reader getReader() throws ConnectionException {
		return this.reader;
	}

	@Override
	public ConnectionAddress getAddress() {
		return address;
	}

	public String toString() {
		return this.getClass().getName()+"("+String.valueOf(address)+",connected:"+isConnected()+")";
	}
		
	protected void log(Level level, String message) {
		synchronized(loggerAccessMutex) {
			if (log != null) log.log(level, message);
		}
	}
	
	protected void log(Level level, String message, Object obj) {
		synchronized(loggerAccessMutex) {
			if (log != null) log.log(level, message, obj);
		}
	}
	
	@Override
	public void setLogMessages(boolean logMessages) {
		this.reader.setLogMessages(logMessages);
		this.writer.setLogMessages(logMessages);
	}

}