/*
 * Copyright 2002-2005 Uwyn bvba/sprl <info[remove] at uwyn dot com>
 * Distributed under the terms of the GNU Lesser General Public
 * License, v2.1 or later
 *
 * $Id: Server.java 1702 2005-03-26 21:54:38Z gbevin $
 */
package com.uwyn.drone.core;

import com.uwyn.drone.core.exceptions.*;
import java.io.*;

import com.uwyn.drone.core.ServerListener;
import com.uwyn.drone.protocol.ServerMessage;
import com.uwyn.drone.protocol.commands.IrcCommand;
import com.uwyn.drone.protocol.commands.Ping;
import com.uwyn.rife.tools.ExceptionUtils;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketTimeoutException;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
import java.nio.charset.CharsetEncoder;
import java.nio.charset.CodingErrorAction;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.logging.Logger;

public class Server implements Runnable, TimedOutputStreamListener
{
	private String			mServerName = null;
	private ServerInfo		mServerInfo = null;
	private Thread			mServerThread = null;
	private Bot				mBot = null;
	
	private	Socket				mServerSocket = null;
	private BufferedReader		mInput = null;
	private BufferedWriter		mOutput = null;
	private TimedOutputStream	mTimedOutput = null;
	private HashMap				mChannels = null;
	private boolean				mConnected = false;
	private String				mConnectedHost = null;
	private int					mReadTimeOutCount = 0;
	private CharsetDecoder		mCharsetDecoder = null;
	private CharsetEncoder		mCharsetEncoder = null;

	private HashSet		mServerListeners = null;
	private HashSet		mCommandListeners = null;
	private HashSet		mResponseListeners = null;
	private Object		mServerListenersMonitor = new Object();
	private Object		mCommandListenersMonitor = new Object();
	private Object		mResponseListenersMonitor = new Object();
	
	Server(String serverName, ServerInfo serverInfo, Bot bot)
	{
		assert serverName != null;
		assert serverInfo != null;
		assert bot != null;
		
		mServerName = serverName.toLowerCase();
		mServerInfo = serverInfo;
		mBot = bot;
		
		mChannels = new HashMap();
		mServerListeners = new HashSet();
		mCommandListeners = new HashSet();
		mResponseListeners = new HashSet();

		cleanUp();
	}
	
	private void cleanUp()
	{
		mServerSocket = null;
		mInput = null;
		mOutput = null;
		mConnected = false;
		mConnectedHost = null;
	}
	
	public String getServerName()
	{
		return mServerName;
	}
	
	public ServerInfo getServerInfo()
	{
		return mServerInfo;
	}
	
	public Socket getServerSocket()
	{
		return mServerSocket;
	}
	
	public Channel getChannel(String name)
	{
		if (null == name)		throw new IllegalArgumentException("name can't be null.");
		if (0 == name.length())	throw new IllegalArgumentException("name can't be empty.");
		
		Channel channel = null;
		
		name = name.toLowerCase();
		
		synchronized (mChannels)
		{
			channel = (Channel)mChannels.get(name);
			
			if (null == channel)
			{
				channel = new Channel(name, this);
				mChannels.put(name, channel);
			}
		}
		
		assert channel != null;
		assert channel.getName().equals(name);
		assert channel.getServer() == this;
			
		return channel;
	}
	
	public synchronized void connect()
	throws CoreException
	{
		if (0 == mServerInfo.getAddresses().size())
		{
			throw new MissingAddressException(this);
		}
		
		InputStream			input_stream = null;
		Iterator			addresses_it = null;
		InetSocketAddress	address = null;
		
		boolean is_connected = false;
		
		while (!is_connected)
		{
			try
			{
				// try all addresses, one by one until a valid connection
				// could
				addresses_it = mServerInfo.getAddresses().iterator();
				while (addresses_it.hasNext())
				{
					address = (InetSocketAddress)addresses_it.next();
					try
					{
						mServerSocket = new Socket(address.getAddress(), address.getPort());
						break;
					}
					catch (IOException e)
					{
						mServerSocket = null;
					}
				}
				
				// no valid address was found, throw an error
				if (null == mServerSocket)
				{
					throw new NoValidAddressException(this);
				}
				
				// continue with the first valid address
				// and setup the socket timeout
				mServerSocket.setSoTimeout(mServerInfo.getTimeout());
				
				input_stream = mServerSocket.getInputStream();
				if (mTimedOutput != null)
				{
					mTimedOutput.close();
				}

				mTimedOutput = new TimedOutputStream(mServerSocket.getOutputStream(), mServerInfo.getMax(), mServerInfo.getAmount(), mServerInfo.getInterval());
				mTimedOutput.addTimedOutputStreamListener(this);
				is_connected = true;
			}
			catch (IOException e)
			{
				Logger.getLogger("com.uwyn.drone.core").severe("Error while connecting from the server '"+this+"', retrying : "+ExceptionUtils.getExceptionStackTrace(e));
			}
		}
		
		mCharsetDecoder = Charset.forName(mServerInfo.getCharset()).newDecoder();
		mCharsetDecoder.replaceWith("?");
		mCharsetDecoder.onMalformedInput(CodingErrorAction.REPLACE);
		mCharsetDecoder.onUnmappableCharacter(CodingErrorAction.REPLACE);
		mCharsetEncoder = Charset.forName(mServerInfo.getCharset()).newEncoder();

		mInput = new BufferedReader(new InputStreamReader(input_stream, mCharsetDecoder));
		mOutput = new BufferedWriter(new OutputStreamWriter(mTimedOutput, mCharsetEncoder));

		mConnected = true;
		mServerThread = new Thread(this);
		mServerThread.start();
		fireConnected();
		
		assert mServerSocket.isConnected();
		assert mInput != null;
		assert mOutput != null;
	}
	
	public synchronized void disconnect()
	throws CoreException
	{
		mConnected = false;
		mConnectedHost = null;
		
		try
		{
			mInput = null;
			
			if (mOutput != null)
			{
				mOutput.close();
			}

			if (mServerSocket != null)
			{
				mServerSocket.close();
			}
		}
		catch (IOException e)
		{
			Logger.getLogger("com.uwyn.drone.core").severe("Error while disconnecting from the server '"+this+"', reconnecting : "+ExceptionUtils.getExceptionStackTrace(e));
			cleanUp();
			try
			{
				connect();
			}
			catch (CoreException e2)
			{
				throw new ServerDisconnectionErrorException(this, e2);
			}
			
			return;
		}
		
		this.notifyAll();
		
		cleanUp();
		fireDisconnected();
	}
	
	private void handleTerminatedStream(Throwable e)
	{
		if (mConnected)
		{
			mConnected = false;
			mConnectedHost = null;
			while (!mConnected)
			{
				if (e != null)
				{
					Logger.getLogger("com.uwyn.drone.core").severe("Error during the server connection, reconnecting : "+ExceptionUtils.getExceptionStackTrace(e));
				}
				else
				{
					Logger.getLogger("com.uwyn.drone.core").severe("Stream to IRC server has terminated, reconnecting.");
				}
				
				try
				{
					reconnect();
				}
				catch (CoreException e2)
				{
					Logger.getLogger("com.uwyn.drone.core").severe("Unable to restore connection after a terminated server stream : "+ExceptionUtils.getExceptionStackTrace(e2));
					try
					{
						// sleep 30 seconds before trying again
						Thread.sleep(30000);
					}
					catch (InterruptedException e3)
					{
						return;
					}
				}
			}
		}
		
		return;
	}
	
    public void run()
    {
		while (mConnected)
		{
			try
			{
				if (mInput != null)
				{
					try
					{
						String message_string = mInput.readLine();
						mReadTimeOutCount = 0;
						
						// check if a message has been received, if this is not the case,
						// the connection has been terminated and the stream also
						// try to reconnect until this succeeds
						if (null == message_string)
						{
							handleTerminatedStream(null);
							return;
						}
						// handle the received message
						else
						{
							ServerMessage message = ServerMessage.parse(message_string);
							// detect the hostname of the irc server
							if (null == mConnectedHost &&
								message.getPrefix() != null)
							{
								mConnectedHost = message.getPrefix().getServerName();
							}
							fireServerMessageEvents(message);
						}
					}
					catch (SocketTimeoutException e)
					{
						// if there were two read timeouts in a row, the connection is closed
						// and let the exception bubble up
						if (mReadTimeOutCount > 1)
						{
							throw e;
						}
						
						mReadTimeOutCount++;
						try
						{
							send(new Ping(mConnectedHost));
						}
						catch (CoreException e2)
						{
							throw e;
						}
					}
				}
			}
			catch (IOException e)
			{
				handleTerminatedStream(e);
				return;
			}
			
			Thread.yield();
		}
		
		mServerThread = null;
	}
	
	private void fireServerMessageEvents(ServerMessage message)
	{
		if (message.isResponse())
		{
			fireReceivedResponse(message);
		}
		if (message.isCommand())
		{
			fireReceivedCommand(message);
		}
	}
	
	public boolean isConnected()
	{
		return mConnected;
	}
	
	synchronized boolean send(IrcCommand command)
	throws CoreException
	{
		if (null == mServerSocket ||
			!mServerSocket.isConnected() ||
			mServerSocket.isOutputShutdown() ||
		    !mServerSocket.isBound())
		{
			return false;
		}
		
		try
		{
			mOutput.write(command.getCommand());
			mOutput.newLine();
		}
		catch(IOException e)
		{
			throw new SendErrorException(this, command, e);
		}
		
		flush();
		
		fireServerMessageEvents(mBot.createServerMessage(command));
	
		return true;
	}
	
	public void reconnect()
	throws CoreException
	{
		synchronized (this)
		{
			disconnect();
			connect();
		}
	}
	
	public void exceptionThrow(IOException e)
	{
		try
		{
			reconnect();
		}
		catch (CoreException e2)
		{
			Logger.getLogger("com.uwyn.drone.core").severe("Unable to restore connection after socket error : "+ExceptionUtils.getExceptionStackTrace(e));
		}
	}
	
	synchronized private boolean flush()
	throws CoreException
	{
		if (null == mServerSocket ||
			!mServerSocket.isConnected() ||
			mServerSocket.isOutputShutdown())
		{
			return false;
		}
		
		try
		{
			mOutput.flush();
		}
		catch(IOException e)
		{
			throw new FlushErrorException(this, e);
		}
	
		return true;
	}

	private void fireConnected()
	throws CoreException
	{
		Iterator	listeners = mServerListeners.iterator();
		
		while (listeners.hasNext())
		{
			((ServerListener)listeners.next()).connected(this);
		}
	}

	private void fireDisconnected()
	throws CoreException
	{
		Iterator	listeners = mServerListeners.iterator();
		
		while (listeners.hasNext())
		{
			((ServerListener)listeners.next()).disconnected(this);
		}
	}

	private void fireReceivedCommand(ServerMessage command)
	{
		Iterator	listeners = mCommandListeners.iterator();
		
		while (listeners.hasNext())
		{
			try
			{
				((CommandListener)listeners.next()).receivedCommand(command);
			}
			catch (CoreException e)
			{
				Logger.getLogger("com.uwyn.drone.core").severe(ExceptionUtils.getExceptionStackTrace(e));
			}
		}
	}

	private void fireReceivedResponse(ServerMessage Response)
	{
		Iterator	listeners = mResponseListeners.iterator();
		
		while (listeners.hasNext())
		{
			try
			{
				((ResponseListener)listeners.next()).receivedResponse(Response);
			}
			catch (CoreException e)
			{
				Logger.getLogger("com.uwyn.drone.core").severe(ExceptionUtils.getExceptionStackTrace(e));
			}
		}
	}

	public boolean addServerListener(ServerListener listener)
	{
		if (null == listener)	throw new IllegalArgumentException("listener can't be null.");

		boolean result = false;
		
		synchronized (mServerListenersMonitor)
		{
			if (!mServerListeners.contains(listener))
			{
				HashSet clone = (HashSet)mServerListeners.clone();
				result = clone.add(listener);
				mServerListeners = clone;
			}
			else
			{
				result = true;
			}
		}
		
		assert true == mServerListeners.contains(listener);
		
		return result;
	}

	public boolean removeServerListener(ServerListener listener)
	{
		if (null == listener)	throw new IllegalArgumentException("listener can't be null.");

        boolean result = false;
		
		synchronized (mServerListenersMonitor)
		{
			HashSet clone = (HashSet)mServerListeners.clone();
			result = clone.remove(listener);
			mServerListeners = clone;
		}
		
		assert false == mServerListeners.contains(listener);
		
		return result;
	}

	public boolean addCommandListener(CommandListener listener)
	{
		if (null == listener)	throw new IllegalArgumentException("listener can't be null.");

		boolean result = false;
		
		synchronized (mCommandListenersMonitor)
		{
			if (!mCommandListeners.contains(listener))
			{
				HashSet clone = (HashSet)mCommandListeners.clone();
				result = clone.add(listener);
				mCommandListeners = clone;
			}
			else
			{
				result = true;
			}
		}
		
		assert true == mCommandListeners.contains(listener);
		
		return result;
	}

	public boolean removeCommandListener(CommandListener listener)
	{
		if (null == listener)	throw new IllegalArgumentException("listener can't be null.");

        boolean result = false;
		
		synchronized (mCommandListenersMonitor)
		{
			HashSet clone = (HashSet)mCommandListeners.clone();
			result = clone.remove(listener);
			mCommandListeners = clone;
		}
		
		assert false == mCommandListeners.contains(listener);
		
		return result;
	}

	public boolean addResponseListener(ResponseListener listener)
	{
		if (null == listener)	throw new IllegalArgumentException("listener can't be null.");

		boolean result = false;
		
		synchronized (mResponseListenersMonitor)
		{
			if (!mResponseListeners.contains(listener))
			{
				HashSet clone = (HashSet)mResponseListeners.clone();
				result = clone.add(listener);
				mResponseListeners = clone;
			}
			else
			{
				result = true;
			}
		}
		
		assert true == mResponseListeners.contains(listener);
		
		return result;
	}

	public boolean removeResponseListener(ResponseListener listener)
	{
		if (null == listener)	throw new IllegalArgumentException("listener can't be null.");

        boolean result = false;
		
		synchronized (mResponseListenersMonitor)
		{
			HashSet clone = (HashSet)mResponseListeners.clone();
			result = clone.remove(listener);
			mResponseListeners = clone;
		}
		
		assert false == mResponseListeners.contains(listener);
		
		return result;
	}
}
