import java.io.*;
import java.net.*;
import java.util.Hashtable;
import java.util.Vector;
import javax.swing.*;

/**
 * This class implements mlDonkey Core/GUI protocol.
 * This is a part of <a href="http://royale.zerezo.com/jmoule/" target="_blank">jMoule</a>.
 * This project is licenced under GPL.
 * @author Antoine Jacquet
 */
public class DonkeyCore extends Thread
{
	byte[] buffer=new byte[1024*100];
	int pos;
	int nbsearches=0;
	Socket connection;
	String password;
	
	/**
	  * This contains the informations about the files being downloaded.
	  * File ID are the hash keys.
	  * The values of the hash are vectors containing file informations : filename, size, downloaded, %, rate, ETA and running state.
	  */
	public Hashtable fileInfos=new Hashtable();
	
	/**
	  * This contains the informations about the results of searches.
	  * Files ID are the hash keys.
	  * The values of the hash are vectors containing result informations : filename, size, tags and download state.
	  */
	public Hashtable resultInfos=new Hashtable();
	
	/**
	 * This contains the informations about the donkey servers.
	 * Servers ID are the hash keys.
	 * The values of the hash are vectors containing server informations : host, users, files, connected state.
	 */
	public Hashtable serverInfos=new Hashtable();
	
	/**
	 * This contains all the searches results.
	 * Each entry is a vector containing a specific search result.
	 * Each specific search result contains vectors with same format as resultInfos.
	 */
	public Vector searchResults=new Vector();
	
	/**
	 * This contains statistics informations.
	 * The keys are the variables and the values are numbers.
	 */
	public Hashtable statInfos=new Hashtable();
	
	/**
	 * This is a text panel containing console messages...
	 */
	public JTextArea console=new JTextArea();
	//String console=new String();
	
	/**
	 * Constructs an unconnected DonkeyCore.
	 */
	public DonkeyCore()
	{
		/*
		Vector fileInfo;
		fileInfo=new Vector();
		fileInfo.add("a");
		fileInfo.add("b");
		fileInfo.add("m");
		fileInfo.add("x");
		fileInfo.add("x");
		fileInfo.add(new Boolean(true));
		fileInfos.put(new Long(0),fileInfo);
		fileInfo=new Vector();
		fileInfo.add("b");
		fileInfo.add("a");
		fileInfo.add("d");
		fileInfo.add("d");
		fileInfo.add("m");
		fileInfo.add(new Boolean(true));
		fileInfos.put(new Long(1),fileInfo);
		fileInfo=new Vector();
		fileInfo.add("g");
		fileInfo.add("a");
		fileInfo.add("k");
		fileInfo.add("k");
		fileInfo.add("i");
		fileInfo.add(new Boolean(true));
		fileInfos.put(new Long(2),fileInfo);
		Vector serverInfo;
		serverInfo=new Vector();
		serverInfo.add("a");
		serverInfo.add("a");
		serverInfo.add("a");
		serverInfo.add(Boolean.FALSE);
		serverInfos.put(new Long(0),serverInfo);
		serverInfo=new Vector();
		serverInfo.add("b");
		serverInfo.add("d");
		serverInfo.add("e");
		serverInfo.add(new Boolean(true));
		serverInfos.put(new Long(1),serverInfo);
		serverInfo=new Vector();
		serverInfo.add("e");
		serverInfo.add("x");
		serverInfo.add("q");
		serverInfo.add(new Boolean(false));
		serverInfos.put(new Long(2),serverInfo);
		serverInfo=new Vector();
		serverInfo.add("a");
		serverInfo.add("b");
		serverInfo.add("n");
		serverInfo.add(new Boolean(true));
		serverInfos.put(new Long(3),serverInfo);
		*/
	}
	
	/**
	 * Constructs a connected DonkeyCore.
	 * Same parameters as "connect".
	 */
	public DonkeyCore(String host,int port,String password)
	{
		this();
		connect(host,port,password);
	}
	
	/**
	 * Connects an unconnected DonkeyCore.
	 * @param host mlDonkey host you wish to connect to.
	 * @param port mlDonkey core port.
	 * @param password Password if needed.
	 */
	public boolean connect(String host,int port,String password)
	{
		this.password=password;
		//System.out.println(password);
		try
		{
			connection=new Socket(host,port);
			start();
			return true;
		}
		catch (Exception e)
		{
			e.printStackTrace();
			return false;
		}
	}
	
	/**
	 * Disconnects this DonkeyCore.
	 */
	public void disconnect()
	{
		try
		{
			connection.close();
			connection=null;
		}
		catch (Exception e)
		{
			e.printStackTrace();
		}
	}
	
	/**
	  * Tests if this DonkeyCore is already connected.
	  * @return true if connected.
	  */
	public boolean isConnected()
	{
		return connection!=null;
	}

	void debug(String message)
	{
		//System.out.println(message);
	}

	boolean readMessage()
	{
		try
		{
			InputStream i=connection.getInputStream();
			// read the length of the message
			long l=i.read()+256*(i.read()+256*(i.read()+256*i.read()));
			//System.out.print("size: "+l+" ");
			pos=0;
			// read the buffer
			while (pos<l) pos+=i.read(buffer,pos,(int)l-pos);
			pos=0;
			// ok
			
			// dump
/*
			if (buffer[0]==3)
			{
				System.out.println("Dump: "+new String(buffer,0,(int)l));
				for (pos=0;pos<l;pos++)
				{
					System.out.print(buffer[pos]+",");
				}
			}
			pos=0;
*/

			return true;
		}
		catch (Exception e)
		{
			e.printStackTrace();
			return false;
		}
	}
	
	int readByte()
	{
		byte temp=buffer[pos++];
		return(temp<0?temp+256:temp);
	}
	
	int readInt()
	{
		return(readByte()+(readByte()<<8));
	}
	
	long readLong()
	{
		return(readInt()+((long)readInt()<<16));
	}
	
	String readString()
	{
		int l=readInt();
		if (l>0)
		{
			String temp=new String(buffer,pos,l);
			pos+=l;
			return(temp);
		}
		else return("");
	}

	synchronized boolean sendMessage(byte[] buffer,int l)
	{
		try
		{
			connection.getOutputStream().write(l%256);
			connection.getOutputStream().write(l/256);
			connection.getOutputStream().write(0);
			connection.getOutputStream().write(0);
			connection.getOutputStream().write(buffer,0,l);
			return true;
		}
		catch (Exception e)
		{
			e.printStackTrace();
			return false;
		}
	}
	
	/**
	 * Asks the mlDonkey core to connect more servers.
	 */
	public void connectMore()
	{
  	byte[] buffer=new byte[2];
    buffer[0]=1; buffer[1]=0; // function
    sendMessage(buffer,2);
	}

	/**
	 * Asks the mlDonkey core to clean old servers.
	 */
	public void cleanOld()
	{
  	byte[] buffer=new byte[2];
    buffer[0]=2; buffer[1]=0; // function
    sendMessage(buffer,2);
	}

	/**
	 * Asks the mlDonkey core to kill (terminate) itself.
	 */
	public void kill()
	{
  	byte[] buffer=new byte[2];
    buffer[0]=3; buffer[1]=0; // function
    sendMessage(buffer,2);
	}

	/**
	 * Extends (redo) the last search.
	 */ 
	public void extend()
	{
  	byte[] buffer=new byte[2];
    buffer[0]=4; buffer[1]=0; // function
    sendMessage(buffer,2);
	}
	
	/**
	 * Start a new download.
	 * @param fileID ID of the file to download (found in resultInfos key).
	 */
	public void download(Long fileID)
	{
		byte[] buffer=new byte[9];
		int i=0;
    buffer[i++]=50; buffer[i++]=0; // function
    buffer[i++]=0; buffer[i++]=0; // result_names (empty list)
    long id=fileID.longValue();
    for (int j=0;j<4;j++)
    {
    	buffer[i++]=(byte)(id%256);
    	id/=256;
    }
    buffer[i++]=0; // force
  	sendMessage(buffer,i);
	}
	
	/**
	 * Pause/Resume a download.
	 * @param fileID ID of the file to switch (found in resultInfos key).
	 * @param bool true = resume, false = pause.
	 */
	public void switchDownload(Long fileID,boolean bool)
	{
		byte[] buffer=new byte[9];
		int i=0;
    buffer[i++]=23; buffer[i++]=0; // function
    long id=fileID.longValue();
    for (int j=0;j<4;j++)
    {
    	buffer[i++]=(byte)(id%256);
    	id/=256;
    }
    if (bool)
    	buffer[i++]=1;
    else
    	buffer[i++]=0;
  	sendMessage(buffer,i);
	}
	
	/**
	 * Asks mlDonkey to connect to a new server.
	 * @param serverID ID of the server to connect to (found in serverInfos key).
	 */
	public void connectServer(Long serverID)
	{
		byte[] buffer=new byte[6];
		int i=0;
    buffer[i++]=21; buffer[i++]=0; // function
    long id=serverID.longValue();
    for (int j=0;j<4;j++)
    {
    	buffer[i++]=(byte)(id%256);
    	id/=256;
    }
  	sendMessage(buffer,i);
	}
	
	/**
	 * Asks mlDonkey to disconnect from a server.
	 * @param serverID ID of the server to disconnect (found in serverInfos key).
	 */
	public void disconnectServer(Long serverID)
	{
		byte[] buffer=new byte[6];
		int i=0;
    buffer[i++]=22; buffer[i++]=0; // function
    long id=serverID.longValue();
    for (int j=0;j<4;j++)
    {
    	buffer[i++]=(byte)(id%256);
    	id/=256;
    }
  	sendMessage(buffer,i);
	}
	
	/**
	 * Start a new search.
	 * @param query A simple string to describe what you want to search.
	 * @return A vector of search results (entries of this vector have same structure as resultInfos entries).
	 */
	public Vector search(String query)
	{
		byte[] buffer=new byte[1024*100];
		int i=0,j;
		buffer[i++]=42; buffer[i++]=0; // function
		buffer[i++]=(byte)(nbsearches%256); buffer[i++]=(byte)(nbsearches/256); 
		buffer[i++]=0; buffer[i++]=0; // num
		buffer[i++]=4; // keywords
		buffer[i++]=0; buffer[i++]=0; // empty string
		buffer[i++]=(byte)(query.length()%256);
		buffer[i++]=(byte)(query.length()/256);
		for (j=0;j<query.length();j++)
			buffer[i++]=(byte)query.charAt(j);
		buffer[i++]=0; buffer[i++]=4; buffer[i++]=0; buffer[i++]=0; // max hits
		buffer[i++]=1; // type
		Vector searchResult=new Vector();
		searchResults.add(searchResult);
		sendMessage(buffer,i);
		nbsearches++;
		/*
		Vector resultInfo=new Vector();
		resultInfo.add(new Long(1));
		resultInfo.add("a");
		resultInfo.add("a");
		resultInfo.add("a");
		resultInfo.add(new Boolean(false));
		searchResult.add(resultInfo);
		resultInfo=new Vector();
		resultInfo.add(new Long(2));
		resultInfo.add("b");
		resultInfo.add("b");
		resultInfo.add("b");
		resultInfo.add(new Boolean(false));
		searchResult.add(resultInfo);
		resultInfo=new Vector();
		resultInfo.add(new Long(3));
		resultInfo.add("c");
		resultInfo.add("c");
		resultInfo.add("c");
		resultInfo.add(new Boolean(false));
		searchResult.add(resultInfo);
		*/
		return searchResult;
	}
	
	/**
	 * This class is a thread.
	 */
	public void run()
	{
		while(readMessage())
		{
			//String serverStates[]={"NotConnected -1","Connecting","Connected_initiating","Connected_downloading","Connected -1","Connected n","NewHost","RemovedHost","BlackListedHost","NotConnected n"};
			Boolean serverStates[]={
				Boolean.FALSE, // "NotConnected -1"
				Boolean.FALSE, // "Connecting"
				Boolean.TRUE,  // "Connected_initiating"
				Boolean.TRUE,  // "Connected_downloading"
				Boolean.TRUE,  // "Connected -1"
				Boolean.TRUE,  // "Connected n"
				Boolean.FALSE, // "NewHost"
				Boolean.FALSE, // "RemovedHost"
				Boolean.FALSE, // "BlackListedHost"
				Boolean.FALSE  // "NotConnected n"
			};
			Boolean fileStates[]={
				Boolean.TRUE,  // FileDownloading
				Boolean.FALSE, // FilePaused
  			Boolean.TRUE,  // FileDownloaded
  			Boolean.FALSE, // FileShared
  			Boolean.FALSE, // FileCancelled
  			Boolean.FALSE, // FileNew
  			Boolean.FALSE, // FileAborted s
				Boolean.FALSE  // FileQueued
			};
			int i,n,f;
			Long id;
			String tags;
			switch(f=readInt()) // fonction
			{
				
				// CoreProtocol
				case 0:
					debug("CoreProtocol: "+readLong());
					// (06:00:00:00:) 00:00:10:00:00:00 GuiProtocol
					for (i=0;i<6;i++) buffer[i]=0;
					buffer[2]=10;
					sendMessage(buffer,6);
					// (04:00:00:00:) 05:00:00:00 Password (empty)
					i=0;
					buffer[i++]=5; buffer[i++]=0; // function
					buffer[i++]=(byte)(password.length()%256);
					buffer[i++]=(byte)(password.length()/256);
					for (int j=0;j<password.length();j++)
						buffer[i++]=(byte)password.charAt(j);
					sendMessage(buffer,i);
					break;

				// Options_info
				case 1:
					debug("Options_info:");
					n=readInt();
					for (i=0;i<n;i++)
						debug(readString()+" = "+readString());
					break;
					
				// DefineSearches
				case 3:
					n=readInt();
					debug("DefineSearches: "+n);
					/*for (i=0;i<n;i++)
					{
						System.out.println(readString()); // name
						System.out.println(readByte());
					}*/
					break;
					
				// Result_info
				case 4:
					Vector resultInfo=new Vector();
					debug("Result_info: ");
					id=new Long(readLong());
					resultInfo.add(id);
					readLong(); // network
					n=readInt();
					resultInfo.add(readString());
					for (i=0;i<n-1;i++) readString(); // names
					for (i=0;i<16;i++) readByte(); // hash
					resultInfo.add(new Long(readLong())); // Size
					readString(); // Format
					readString(); // Type
					tags=new String();
					n=readInt();
					for (i=0;i<n;i++)
					{
						readString(); // TagName
						switch (readByte()) // TagValue
						{
							case 0:
							case 1:
								tags=tags.concat(""+readLong()+" ");
								break;
							case 2:
								tags=tags.concat(readString()+" ");
								break;
							case 3:
								tags=tags.concat(""+readByte()+"."+readByte()+"."+readByte()+"."+readByte()+" ");
								break;
						}
					}
					resultInfo.add(tags);
					readString(); // Comment
					readByte(); // Done (boolean)
					resultInfo.add(new Boolean(false)); // download ?
					resultInfos.put(id,resultInfo);
					break;

				// Search_result					
				case 5:
					int search=(int)readLong(); // Search
					Long info=new Long(readLong()); // Result_Info
					((Vector)searchResults.get(search)).add(resultInfos.get(info));
					break;
				
				// File_info
				case 7:
				case 40:
				case 43: 
					Vector fileInfo=new Vector();
					debug("FileInformation: ");
					id=new Long(readLong()); // id
					readLong(); // network
					n=readInt();
					if (n==0)
					{
						// no name, this should not happen...
						fileInfo.add("N/A");
					}
					else if (n==1)
					{
						// no name but the ID string
						fileInfo.add(readString());
					}
					else
					{
						// skip the ID
						readString();
						// and get the first name
						fileInfo.add(readString());
						for (i=0;i<n-2;i++)	debug(readString()); // names
						//for (i=0;i<n;i++)	debug(readString()); // names
					}
					for (i=0;i<16;i++) readByte(); // hash
					//readLong(); // size
					//readLong(); // downloaded
					long filesize=readLong();
					long filedl=readLong();
					fileInfo.add(new Long(filesize));
					fileInfo.add(new Long(filedl));
					fileInfo.add(new Float(Math.round(10000*filedl/filesize)/100.0));
					//debug(readLong()+"/"+readLong());
					readLong(); // nlocations
					readLong(); // nclients
					int state=readByte(); // file_state
					readString(); // chunks
					readString(); // availability
					//fileInfo.add(new Float(readString())); // rate
					double rate=Math.round(Float.parseFloat(readString()))/1000.0;
					fileInfo.add(new Float(rate));
					long time=Math.round((filesize-filedl)/(1024*rate));
					if (rate>0)
						fileInfo.add(new Float(time/3600.0));
					else
						fileInfo.add(new Float(0));
					n=readInt();
					for (i=0;i<n;i++) readString(); // chunks age
					readString(); // age
					switch (readByte()) // format
					{
						case 0:
							debug("Unknown_format");
							break;
						case 1: // FormatType
							//readString(); readString();
							debug("FormatType: "+readString()+" "+readString());
							break;
						case 2: // AVI
							debug("AVI: ");
							debug("codec: "+readString()); // codec
							debug(readLong()+"x"+readLong()+", "+readLong()+" fps "+readLong()+" rate")	;
							break;
						case 3: // MP3
							debug("MP3: ");
							debug("title: "+readString()); // title
							debug("artist: "+readString()); // artist
							debug("album: "+readString()); // album
							debug("year: "+readString()); // year
							debug("comment: "+readString()); // comment
							debug("track: "+readLong()); // track number
							debug("genre: "+readLong()); // genre
							break;
					}
					fileInfo.add(fileStates[state]);
					fileInfos.put(id,fileInfo);
					break;
				
				// File_source
				case 10:
					// file_num f, client_num c
					//debug("File_source: "+readInt()+" "+readInt());
					break;
				
				// Server_state
				case 13:
					//debug("Server_state: "+readLong()+" etat: "+readByte());
					id=new Long(readLong());
					((Vector)serverInfos.get(id)).setElementAt(serverStates[readByte()],3); // server_state
					//System.out.println(id+" : state changed");
					break;
					
				// Client_state
				case 16:
					debug("Client_state: "+readLong()+" etat: "+readByte());
					break;
				
				// Client_info
				case 15:
					/*
					debug("Client_info: ");
					debug("num: "+readLong());
					debug("network: "+readLong());
					switch(readByte())
					{
						case 0:
							// Known_location
							debug("type: Known_location");
							debug("ip: "+readByte()+"."+readByte()+"."+readByte()+"."+readByte());
							debug("port: "+readInt());
							break;
						case 1:
							// Indirect_location
							debug("type: Indirect_location");
							debug("name: "+readString());
							break;
					}
					*/
					break;
				
				// Console
				case 19: 
					debug("Console: ");
					//console=console.concat(readString());
					console.append(readString());
					break;
				
				// Network_info
				case 20:
					n=readInt();
					debug("Network_info: "+readString());
					break;
					
				// Server_info
				case 26:
					Vector serverInfo=new Vector();
					debug("Server_info: ");
					id=new Long(readLong()); // id
					readLong(); // network
					// only works if proto>=2
					String host=null;
					switch(readByte())
					{
						case 0:
							//debug("ip: "+readByte()+"."+readByte()+"."+readByte()+"."+readByte());
							host=""+readByte()+"."+readByte()+"."+readByte()+"."+readByte();
							break;
						case 1:
							//debug("name: "+readString());
							host=readString();
							break;
					}
					//debug("port: "+readInt());
					host=host+":"+readInt(); // port
					serverInfo.add(host);
					readLong(); // score
					tags=new String();
					n=readInt();
					for (i=0;i<n;i++)
					{
						readString(); // TagName
						switch (readByte()) // TagValue
						{
							case 0:
							case 1:
								tags=tags.concat(""+readLong()+" ");
								break;
							case 2:
								tags=tags.concat(readString()+" ");
								break;
							case 3:
								tags=tags.concat(""+readByte()+"."+readByte()+"."+readByte()+"."+readByte()+" ");
								break;
						}
					}
					serverInfo.add(new Long(readLong())); // nusers
					serverInfo.add(new Long(readLong())); // nfiles
					serverInfo.add(serverStates[readByte()]); // server_state
					serverInfos.put(id,serverInfo);
					break;
								
				// Add_section_option
				case 36:
					debug("Add_section_option: "+readString()+" "+readString()+" "+readString());
					break;
				
				// Add_plugin_option
				case 38:
					debug("Add_plugin_option: "+readString()+" "+readString()+" "+readString());
					break;
					
				// File_downloaded
				case 46:
					debug("File_downloaded: ");
					debug("n: "+readLong());
					debug("size: "+readLong());
					debug("rate: "+readString());
					debug("last_seen: "+readLong());
					break;
					
				// Shared_file_info
				case 48:
					debug("Shared_file_info: ");
					debug("num: "+readLong());
					debug("network: "+readLong());
					debug("name: "+readString());
					debug("size: "+readLong());
					// ...					
					break;
					
				// Client_stats
				case 49:
					debug("Client_stats: ");
					statInfos.put("upload_counter",new Long(readLong()+(readLong()<<32)));
					statInfos.put("download_counter",new Long(readLong()+(readLong()<<32)));
					statInfos.put("shared_counter",new Long(readLong()+(readLong()<<32)));
					statInfos.put("nshared_files",new Long(readLong()));
					statInfos.put("tcp_upload_rate",new Long(readLong()));
					statInfos.put("tcp_download_rate",new Long(readLong()));
					statInfos.put("udp_upload_rate",new Long(readLong()));
					statInfos.put("udp_download_rate",new Long(readLong()));
					statInfos.put("ndownloading_files",new Long(readLong()));
					statInfos.put("ndownloaded_files",new Long(readLong()));
					// ...
					break;

				default:
					debug("Fonction: "+f);
					break;
					
			}
			//System.out.println();
		}
	}
	
}