/*
 Eteria IRC Client, an RFC 1459 compliant client program written in Java.
 Copyright (C) 2000-2001  Javier Kohen <jkohen at tough.com>
 
 This program is free software; you can redistribute it and/or modify
 it under the terms of the GNU General Public License as published by
 the Free Software Foundation; either version 2 of the License, or
 (at your option) any later version.
 
 This program is distributed in the hope that it will be useful,
 but WITHOUT ANY WARRANTY; without even the implied warranty of
 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 GNU General Public License for more details.
 
 You should have received a copy of the GNU General Public License
 along with this program; if not, write to the Free Software
 Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */
 
 import java.io.*;
 import java.net.Socket;
 import java.net.SocketException;
 import java.util.Observable;
 import java.util.Observer;
 import java.text.ParseException;
 import ar.com.jkohen.irc.Message;
 import ar.com.jkohen.irc.MircMessage;
 import ar.com.jkohen.net.*;
 import ar.com.jkohen.util.ConfigurationProperties;
 
 public class ServerThread extends Thread implements ServerProcess, Observer
 {
 	private Socket sock;
 	private ClientProcess client;
 	private BufferedInputStream reader;
 	private BufferedOutputStream writer;
 	
 	private String decoding, encoding;
 
 	private boolean disconnected;
 
 	/* Configuration properties. */
 	private boolean filter_mirc_attribs = false;
 	
 	byte buffer[] = new byte[2048];
 	public long time;
 	public long delay;
 
 	public ServerThread(ClientProcess cl, Socket sk, String dec, String enc, int ping) throws IOException
 	{
 		client = cl;
 		sock = sk;
 		decoding = dec;
 		encoding = enc;
 		delay = ping * 1000;
 		
 		InputStream is = sock.getInputStream();
 		OutputStream os = sock.getOutputStream();
 		this.reader = new BufferedInputStream(is);
 		this.writer = new BufferedOutputStream(os);		
 
 		try
 		{
 			// FIXME: this doesn't seem to be working.
 			// Prevents the lock-up that occurs when the route to the peer is dropped.
 			sock.setSoLinger(true, 30);
 		}
 		catch (SocketException ex) { System.err.println(ex); }
 	}
 	
 	public void run()
 	{
 		while (!disconnected)
 		{
 			try
 			{
 				processNextLine();
 			}
 			catch (IOException e)
 			{
 				client.report(e);
 			}
 		}
 	}
 
 	private void processNextLine() throws IOException
 	{
 		String str = readLine();
 		time = System.currentTimeMillis();
 		  	
 		if (str != null)
 		{
 			try
 			{
 				Message m;
 				if (filter_mirc_attribs)
 					m = new MircMessage(str);
 				else
 					m = new Message(str);
 
 				client.processMessage(m);
 			}
 			catch (ParseException e)
 			{
 				System.err.println(e);
 			}
 		}
 	}
 
 	public String readLine()
 	{
 		boolean eol = false;
 		int i = 0, n = 0;
 		while (!eol)
 		{
 			try
 			{
 				i = reader.read();
 			}
 			catch (IOException ex)
 			{
 				i = -1;
 				if (!disconnected)
 					client.report(ex);
 			}
 			
 			if (i == -1 || ((i == 13 || i == 10) && n > 0) || n == 2048)
 			{
 				eol = true;
 			}
 			else if (i != 13 && i != 10)
 			{
 				buffer[n++] = (byte)i;
 			}
 		}
 		
 		if (i == -1)
 			disconnect();
 		
 		if (n > 0)
 			return(decode(n));		
 		else
 			return(null);
 	}
 	
 	private String decode(int n)
 	{
 		if (decoding.equalsIgnoreCase("IRC"))
 		{
 			try
 			{
 				return(fromU8(n));
 			}
 			catch (ParseException e) {}
 			
 			try
 			{
 				return(new String(buffer, 0, n, "ISO_8859-1"));
 			}
 			catch (UnsupportedEncodingException e) {}
 		}
 		else if (decoding.equalsIgnoreCase("UTF-8") || encoding.equalsIgnoreCase("UTF8"))
 		{
 			try
 			{
 				return(new String(buffer, 0, n, "ISO_8859-1"));
 			}
 			catch (UnsupportedEncodingException e) {}
 		}
 		else
 		{
 			try
 			{
 				return(new String(buffer, 0, n, decoding));
 			}
 			catch (UnsupportedEncodingException e) { System.out.println(e); }
 		}
 
 		return(new String(buffer));
 	}
 	
 	private byte[] encode(String str)
 	{
 		if (encoding.equalsIgnoreCase("IRC"))
 		{
 			try
 			{
 				if(isLatin(str))
 					return(str.getBytes("ISO_8859-1"));
 			}
 			catch (UnsupportedEncodingException e) {}
 			
 			try
 			{
 				return(str.getBytes("UTF8"));
 			}
 			catch (UnsupportedEncodingException e) {}
 		}
 		else if (encoding.equalsIgnoreCase("ISO_8859-1") || encoding.equalsIgnoreCase("ISO-8859-1"))
 		{
 			try
 			{
 				if(isLatin(str))
 					return(str.getBytes("ISO_8859-1"));
 			}
 			catch (UnsupportedEncodingException e) {}
 		}
 		else
 		{
 			try
 			{
 				return(str.getBytes(encoding));
 			}
 			catch (UnsupportedEncodingException e) { System.out.println(e); }
 		}
 		
 		return(str.getBytes());
 	}
 	
 	private boolean isLatin(String str)
 	{
 		char chars[] = str.toCharArray();
 		char c = 0;
 		for (int i = 0; i < chars.length; i++)
 		{
 			c = chars[i];
 			if ((c >= 0x7F && c <= 0x9F) || c > 0xFF)
 				return(false);
 		}
 
 		return(true);
 	}
 	
 	private String fromU8(int num) throws ParseException
 	{
 		StringBuffer decoded = new StringBuffer(num);
 		int pos = 0;
 		int c1 = 0, c2 = 0, c3 = 0, c4 = 0;
 		boolean valid = false;
 		while(pos < num)
 		{
 			valid = false;
 			
 			c1 = buffer[pos++] & 0xFF;
 			if (c1 < 0x80)
 			{
 				decoded.append((char)c1);
 				valid = true;
 			}
 			else if (c1 < 0xC2)
 			{
 				valid = false;
 			}
 			else if (c1 < 0xE0 && pos < num)
 			{
 				c2 = buffer[pos++] & 0xFF;
 				if ((c2 & 0xC0) == 0x80)
 				{
 					c1 = (c1 & 0x1F) << 6;
 					c2 &= 0x3F;
 					decoded.append((char)(c1 | c2));
 					valid = true;
 				}
 			}
 			else if (c1 < 0xF0 && (pos + 1 < num))
 			{
 				c2 = buffer[pos++] & 0xFF;
 				c3 = buffer[pos++] & 0xFF;
 				if ((c2 & c3 & 0xC0) == 0x80)
 				{
 					c1 = (c1 & 0x0F) << 12;
 					c2 = (c2 & 0x3F) << 6;
 					c3 &= 0x3F;
 					decoded.append((char)(c1 | c2 | c3));
 					valid = true;
 				}
 			}
 			else if (c1 < 0xF5 && (pos + 2 < num))
 			{
 				c2 = buffer[pos++] & 0xFF;
 				c3 = buffer[pos++] & 0xFF;
 				c4 = buffer[pos++] & 0xFF;
 				if ((c2 & c3 & c4 & 0xC0) == 0x80)
 				{
 					c1 = (c1 & 0x07) << 18;
 					c2 = (c2 & 0x3F) << 12;
 					c3 = (c3 & 0x3F) << 6;
 					c4 &= 0x3F;
 					decoded.append((char)(c1 | c2 | c3 | c4));
 					valid = true;
 				}
 			}
 
 			if (!valid)
 				throw new ParseException("UTF-8 decoding error", pos);
 		}
 		
 		return(decoded.toString());
 	}
 	
 	public void enqueueMessage(Message m) throws IOException
 	{
 		String [] slices = m.slices();
 		
 		for (int i = 0; i < slices.length; i++)
 		{
 			writer.write(encode(slices[i]));
 			writer.write(13);
 		}
 		writer.flush();
 	}
 
 	public boolean timedOut()
 	{
 		return((System.currentTimeMillis() - time) > delay);
 	}
 	
 	public void disconnect()
 	{
 		if (disconnected)
 			return;
 
 		disconnected = true;
 		
 		try
 		{
 			writer.close();
 		}
 		catch (IOException e) {}
 
 		try
 		{
 			reader.close();
 		}
 		catch (IOException e) {}
 		
 		try
 		{
 			sock.close();
 		}
 		catch (IOException e) {}
 	}
 	
 	public Socket getSocket()
 	{
 		return(sock);
 	}
 
 	public void update(Observable o, Object arg)
 	{
 		ConfigurationProperties props = (ConfigurationProperties) o;
 
 		if (arg == null || arg.equals("filter_mirc_attribs"))
 			this.filter_mirc_attribs = props.getBoolean("filter_mirc_attribs");
 	}
 }