/*
 * Created on Nov 12, 2003
 * Created by Alon Rohter
 * Copyright (C) 2003-2004 Alon Rohter, All Rights Reserved.
 * Copyright (C) Azureus Software, Inc, All Rights Reserved.
 *
 * 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.
 * 
 */
package com.aelitis.azureus.core.peermanager.utils;

import org.gudy.azureus2.core3.internat.MessageText;
import org.gudy.azureus2.core3.util.AEDiagnostics;
import org.gudy.azureus2.core3.util.AEDiagnosticsLogger;
import org.gudy.azureus2.core3.util.ByteFormatter;
import org.gudy.azureus2.core3.util.Constants;
import org.gudy.azureus2.core3.util.Debug;

import java.io.*;
import java.util.HashSet;


/**
 * Used for identifying clients by their peerID.
 */
public class BTPeerIDByteDecoder {

	final static boolean LOG_UNKNOWN;

	static {
		String	prop = System.getProperty("log.unknown.peerids");
		LOG_UNKNOWN = prop == null || prop.equals("1");
	}

	private static String logUnknownClient0(byte[] peer_id_bytes) throws IOException {
		String text = new String(peer_id_bytes, 0, 20, Constants.BYTE_ENCODING);
		text = text.replace((char)12, (char)32);
		text = text.replace((char)10, (char)32);

		return "[" + text + "] " + ByteFormatter.encodeString(peer_id_bytes) + " ";
	}
	
	private static String asUTF8ByteString(String text) {
		try {
			byte[] utf_bytes = text.getBytes(Constants.DEFAULT_ENCODING);
			return ByteFormatter.encodeString(utf_bytes);
		}
		catch (UnsupportedEncodingException uee) {return "";}
	}
	
	private static HashSet logged_discrepancies = new HashSet();
	public static void logClientDiscrepancy(String peer_id_name, String handshake_name, String discrepancy, String protocol, byte[] peer_id) {
		if (!client_logging_allowed) {return;}
		
		// Generate the string used that we will log.
		String line_to_log = discrepancy + " [" + protocol + "]: ";
		line_to_log += "\"" + peer_id_name + "\" / \"" + handshake_name + "\" ";
		
		// We'll encode the name in byte form to help us decode it.
		line_to_log += "[" + asUTF8ByteString(handshake_name) + "]";
		
		// Avoid logging the same combination of things again.
		boolean log_to_debug_out = Constants.isCVSVersion();
		if (log_to_debug_out || LOG_UNKNOWN) {
			// If this text has been recorded before, then avoid doing it again.
			if (!logged_discrepancies.add(line_to_log)) {return;}
		}
		
		// Add peer ID bytes.
		if (peer_id != null) {
			line_to_log += ", Peer ID: " + ByteFormatter.encodeString(peer_id);
		}
		
		// Enable this block for now - just until we get more feedback about
		// problematic clients.
		if (log_to_debug_out) {
			Debug.outNoStack("Conflicting peer identification: " + line_to_log);
		}
		
		if (!LOG_UNKNOWN) {return;}
		logClientDiscrepancyToFile(line_to_log);
	}
	
	private static AEDiagnosticsLogger logger = null;
	private synchronized static void logClientDiscrepancyToFile(String line_to_log) {
		if (logger == null) {logger = AEDiagnostics.getLogger("clientid");}
		try {logger.log(line_to_log);}
		catch (Throwable e) {Debug.printStackTrace(e);}
	}
	
	static boolean client_logging_allowed = true;

	// I don't expect this to grow too big, and it won't grow if there's no logging going on.
	private static HashSet logged_ids = new HashSet();
	static void logUnknownClient(byte[] peer_id_bytes) {logUnknownClient(peer_id_bytes, true);}
	static void logUnknownClient(byte[] peer_id_bytes, boolean to_debug_out) {
		
		if (!client_logging_allowed) {return;}
		
		// Avoid logging the same client ID multiple times.
		boolean log_to_debug_out = to_debug_out && Constants.isCVSVersion();
		if (log_to_debug_out || LOG_UNKNOWN) {
			// If the ID has been recorded before, then avoid doing it again.
			if (!logged_ids.add(makePeerIDReadableAndUsable(peer_id_bytes))) {return;}
		}
		
		// Enable this block for now - just until we get more feedback about
		// unknown clients.
		if (log_to_debug_out) {
			Debug.outNoStack("Unable to decode peer correctly - peer ID bytes: " + makePeerIDReadableAndUsable(peer_id_bytes));
		}
		
		if (!LOG_UNKNOWN) {return;}
		try {logClientDiscrepancyToFile(logUnknownClient0(peer_id_bytes));}
		catch (Throwable t) {Debug.printStackTrace(t);}
		
	}

	static void logUnknownClient(String peer_id) {
		try {logUnknownClient(peer_id.getBytes(Constants.BYTE_ENCODING));}
		catch (UnsupportedEncodingException uee) {}
	}

	public static String decode0(byte[] peer_id_bytes) {

		final String UNKNOWN = MessageText.getString("PeerSocket.unknown");
		final String FAKE = MessageText.getString("PeerSocket.fake_client");
		final String BAD_PEER_ID = MessageText.getString("PeerSocket.bad_peer_id");
		
		String peer_id = null;
		try {peer_id = new String(peer_id_bytes, Constants.BYTE_ENCODING);}
		catch (UnsupportedEncodingException uee) {return "";}

		// We store the result here.
		String client = null;

		/**
		 * If the client reuses parts of the peer ID of other peers, then try to determine this
		 * first (before we misidentify the client).
		 */
		if (BTPeerIDByteDecoderUtils.isPossibleSpoofClient(peer_id)) {
			client = decodeBitSpiritClient(peer_id, peer_id_bytes);
			if (client != null) {return client;}
			client = decodeBitCometClient(peer_id, peer_id_bytes);
			if (client != null) {return client;}
			return "BitSpirit? (" + BAD_PEER_ID + ")";
		}

		/**
		 * See if the client uses Az style identification.
		 */
		if (BTPeerIDByteDecoderUtils.isAzStyle(peer_id)) {
			client = BTPeerIDByteDecoderDefinitions.getAzStyleClientName(peer_id);
			if (client != null) {
				String client_with_version = BTPeerIDByteDecoderDefinitions.getAzStyleClientVersion(client, peer_id);

				/**
				 * Hack for fake ZipTorrent clients - there seems to be some clients
				 * which use the same identifier, but they aren't valid ZipTorrent clients.
				 */ 
				if (client.startsWith("ZipTorrent") && peer_id.startsWith("bLAde", 8)) {
					String client_name = (client_with_version == null) ? client : client_with_version; 
					return UNKNOWN + " [" + FAKE  + ": " + client_name + "]";
				}
				
				/**
				 * BitTorrent 6.0 Beta currently misidentifies itself.
				 */
				if ("\u00B5Torrent 6.0.0 Beta".equals(client_with_version)) {
					return "Mainline 6.0 Beta";
				}
				
				/**
				 * If it's the rakshasa libtorrent, then it's probably rTorrent.
				 */
				if (client.startsWith("libTorrent (Rakshasa)")) {
					String client_name = (client_with_version == null) ? client : client_with_version;
					return client_name + " / rTorrent*";
				}
				
				if (client_with_version != null) {return client_with_version;}
				
				return client;
			}
		
		}

		/**
		 * See if the client uses Shadow style identification.
		 */
		if (BTPeerIDByteDecoderUtils.isShadowStyle(peer_id)) {
			client = BTPeerIDByteDecoderDefinitions.getShadowStyleClientName(peer_id);
			if (client != null) {
				String client_ver = BTPeerIDByteDecoderUtils.getShadowStyleVersionNumber(peer_id);
				if (client_ver != null) {return client + " " + client_ver;}
				return client;
			}
		}

		/**
		 * See if the client uses Mainline style identification.
		 */
		client = BTPeerIDByteDecoderDefinitions.getMainlineStyleClientName(peer_id);
		if (client != null) {
			/**
			 * We haven't got a good way of detecting whether this is a Mainline style
			 * version of peer ID until we start decoding peer ID information. So for
			 * that reason, we wait until we get client version information here - if
			 * we don't manage to determine a version number, then we assume that it
			 * has been misidentified and carry on with it.
			 */
			String client_ver = BTPeerIDByteDecoderUtils.getMainlineStyleVersionNumber(peer_id);
			
			if (client_ver != null) {
				String result = client + " " + client_ver;
				return result;
			}
		}

		/**
		 * Check for BitSpirit / BitComet (non possible spoof client mode).
		 */
		client = decodeBitSpiritClient(peer_id, peer_id_bytes);
		if (client != null) {return client;}
		client = decodeBitCometClient(peer_id, peer_id_bytes);
		if (client != null) {return client;}


		/**
		 * See if the client identifies itself using a particular substring.
		 */
		BTPeerIDByteDecoderDefinitions.ClientData client_data = BTPeerIDByteDecoderDefinitions.getSubstringStyleClient(peer_id); 
		if (client_data != null) {
			client = client_data.client_name;
			String client_with_version = BTPeerIDByteDecoderDefinitions.getSubstringStyleClientVersion(client_data, peer_id, peer_id_bytes);
			if (client_with_version != null) {return client_with_version;}
			return client;
		}

		client = identifyAwkwardClient(peer_id_bytes);
		if (client != null) {return client;}
		return null;
	}

	/**
	 * Decodes the given peerID, returning an identification string.
	 */  
	public static String 
	decode(byte[] peer_id) 
	{
		if ( peer_id.length > 0 ){
			
			try {
				String client = decode0(peer_id);
	
				if (client != null ){
					
					return client;
				}		
			}catch (Throwable e) {
				
				Debug.out( "Failed to decode peer id " + ByteFormatter.encodeString(peer_id) + ": " + Debug.getNestedExceptionMessageAndStack( e ));
			}
	
			try {
				String peer_id_as_string = new String(peer_id, Constants.BYTE_ENCODING);
			
				boolean is_az_style = BTPeerIDByteDecoderUtils.isAzStyle(peer_id_as_string);
				
				boolean is_shadow_style = BTPeerIDByteDecoderUtils.isShadowStyle(peer_id_as_string);
				
				logUnknownClient(peer_id, !(is_az_style || is_shadow_style));
		
				if (is_az_style) {
					return BTPeerIDByteDecoderDefinitions.formatUnknownAzStyleClient(peer_id_as_string);
				}
				else if (is_shadow_style) {
					return BTPeerIDByteDecoderDefinitions.formatUnknownShadowStyleClient(peer_id_as_string);
				}
			}catch( Throwable e ){
				
				Debug.out( "Failed to decode peer id " + ByteFormatter.encodeString(peer_id) + ": " + Debug.getNestedExceptionMessageAndStack( e ));	
			}
		}
		
		String sPeerID = getPrintablePeerID(peer_id);
		return MessageText.getString("PeerSocket.unknown") + " [" + sPeerID + "]";
	}

	public static String identifyAwkwardClient(byte[] peer_id) { 

		int iFirstNonZeroPos = 0;

		iFirstNonZeroPos = 20;
		for( int i=0; i < 20; i++ ) {
			if( peer_id[i] != (byte)0 ) {
				iFirstNonZeroPos = i;
				break;
			}
		}      

		//Shareaza check
		if( iFirstNonZeroPos == 0 ) {
			boolean bShareaza = true;
			for( int i=0; i < 16; i++ ) {
				if( peer_id[i] == (byte)0 ) {
					bShareaza = false;
					break;
				}
			}
			if( bShareaza ) {
				for( int i=16; i < 20; i++ ) {
					if( peer_id[i] != ( peer_id[i % 16] ^ peer_id[15 - (i % 16)] ) ) {
						bShareaza = false;
						break;
					}
				}
				if( bShareaza )  return "Shareaza";
			}
		}

		byte three = (byte)3;
		if ((iFirstNonZeroPos == 9)
				&& (peer_id[9] == three)
				&& (peer_id[10] == three)
				&& (peer_id[11] == three)) {
			return "I2PSnark";
		}

		if ((iFirstNonZeroPos == 12) && (peer_id[12] == (byte)97) && (peer_id[13] == (byte)97)) {
			return "Experimental 3.2.1b2";
		}
		if ((iFirstNonZeroPos == 12) && (peer_id[12] == (byte)0) && (peer_id[13] == (byte)0)) {
			return "Experimental 3.1";
		}
		if (iFirstNonZeroPos == 12) return "Mainline";

		return null;

	}

	private static String decodeBitSpiritClient(String peer_id, byte[] peer_id_bytes) {
		if (!peer_id.substring(2, 4).equals("BS")) {return null;}
		String version = BTPeerIDByteDecoderUtils.decodeNumericValueOfByte(peer_id_bytes[1]);
		if ("0".equals(version)) {version = "1";}
		return "BitSpirit v" + version;
	}

	private static String decodeBitCometClient(String peer_id, byte[] peer_id_bytes) {
		String mod_name = null;
		if (peer_id.startsWith("exbc")) {mod_name = "";}
		else if (peer_id.startsWith("FUTB")) {mod_name  = "(Solidox Mod) ";}
		else if (peer_id.startsWith("xUTB")) {mod_name  = "(Mod 2) ";}
		else {return null;}

		boolean is_bitlord = (peer_id.substring(6, 10).equals("LORD"));

		/**
		 * Older versions of BitLord are of the form x.yy, whereas new versions (1 and onwards),
		 * are of the form x.y. BitComet is of the form x.yy.
		 */
		String client_name = (is_bitlord) ? "BitLord " : "BitComet ";
		String maj_version = BTPeerIDByteDecoderUtils.decodeNumericValueOfByte(peer_id_bytes[4]);
		int min_version_length = (is_bitlord && !maj_version.equals("0")) ? 1 : 2;

		return client_name + mod_name + maj_version + "." +
		BTPeerIDByteDecoderUtils.decodeNumericValueOfByte(peer_id_bytes[5], min_version_length);	  
	}


	protected static String getPrintablePeerID(byte[] peer_id) {
		return getPrintablePeerID(peer_id, '-');
	}
	
	protected static String getPrintablePeerID(byte[] peer_id, char fallback_char) {
		String	sPeerID = "";
		byte[] peerID = new byte[ peer_id.length ];
		System.arraycopy( peer_id, 0, peerID, 0, peer_id.length );

		try {
			for (int i = 0; i < peerID.length; i++) {
				int b = (0xFF & peerID[i]);
				if (b < 32 || b > 127)
					peerID[i] = (byte)fallback_char;
			}
			sPeerID = new String(peerID, Constants.BYTE_ENCODING);
		}
		catch (UnsupportedEncodingException ignore) {}
		catch (Throwable e) {}

		return( sPeerID );
	}
	
	private static String makePeerIDReadableAndUsable(byte[] peer_id) {
		boolean as_ascii = true;
		for (int i=0; i<peer_id.length; i++) {
			int b = 0xFF & peer_id[i];
			if (b < 32 || b > 127 || b == 10 || b == 9 || b==13) {
				as_ascii = false;
				break;
			}
		}
		if (as_ascii) {
			try {return new String(peer_id, Constants.BYTE_ENCODING);}
			catch (UnsupportedEncodingException uee) {return "";}
		}
		else {return ByteFormatter.encodeString(peer_id);}
	}
	
	static byte[] peerIDStringToBytes(String peer_id) throws Exception {
		if (peer_id.length() > 40) {
			peer_id = peer_id.replaceAll("[ ]", "");
		}

		byte[] byte_peer_id = null;
		if (peer_id.length() == 40) {
			byte_peer_id = ByteFormatter.decodeString(peer_id);
			String readable_peer_id = makePeerIDReadableAndUsable(byte_peer_id);
			if (!peer_id.equals(readable_peer_id)) {
				throw new RuntimeException("Use alternative format for peer ID - from " + peer_id + " to " + readable_peer_id);
			}
		}
		else if (peer_id.length() == 20) {
			byte_peer_id = peer_id.getBytes(Constants.BYTE_ENCODING);
		}
		else {
			throw new IllegalArgumentException(peer_id);
		}
		return byte_peer_id;
	}

	private static void assertDecode(String client_result, String peer_id) throws Exception {
		assertDecode(client_result, peerIDStringToBytes(peer_id));
	}
	
	private static void assertDecode(String client_result, byte[] peer_id) throws Exception {
		String peer_id_as_string = getPrintablePeerID(peer_id, '*');
		System.out.println("   Peer ID: " + peer_id_as_string + "   Client: " + client_result);
		
		// Do not log any clients.
		String decoded_result = decode(peer_id);
		if (client_result.equals(decoded_result)) {return;}
		throw new RuntimeException("assertion failure - expected \"" + client_result + "\", got \"" + decoded_result + "\": " + peer_id_as_string);
	}

	public static void main(String[] args) throws Exception {
		client_logging_allowed = false;
		
		final String FAKE = MessageText.getString("PeerSocket.fake_client");
		final String UNKNOWN = MessageText.getString("PeerSocket.unknown");
		final String BAD_PEER_ID = MessageText.getString("PeerSocket.bad_peer_id");
		
		System.out.println("Testing AZ style clients...");
		assertDecode("Ares 2.0.5.3", "-AG2053-Em6o1EmvwLtD");
		assertDecode("Ares 1.6.7.0", "-AR1670-3Ql6wM3hgtCc");
		assertDecode("Artemis 2.5.2.0", "-AT2520-vEEt0wO6v0cr");
		assertDecode("Vuze 2.2.0.0", "-AZ2200-6wfG2wk6wWLc");
		assertDecode("BT Next Evolution 1.0.9", "-NE1090002IKyMn4g7Ko");
		assertDecode("BitRocket 0.3(32)", "-BR0332-!XVceSn(*KIl");
		assertDecode("Mainline 6.0 Beta", "2D555436 3030422D A78DC290 C3F7BDE0 15EC3CC7");
		assertDecode("FlashGet 1.80", "2D464730 31383075 F8005782 1359D64B B3DFD265");
		assertDecode("GetRight 6.3", "-GR6300-13s3iFKmbArc");
		assertDecode("Halite 0.2.9", "-HL0290-xUO*9ugvENUE");
		assertDecode("KTorrent 1.1 RC1", "-KT11R1-693649213030");
		assertDecode("KTorrent 3.0", "2D4B543330302D006A7139727958377731756A4B");
		assertDecode("libTorrent (Rakshasa) 0.11.2 / rTorrent*", "2D6C74304232302D0D739B93E6BE21FEBB557B20");
		assertDecode("libtorrent (Rasterbar) 0.13.0", "-LT0D00-eZ0PwaDDr-~v"); // The latest version at time of writing is v0.12, but I'll assume this is valid.
		assertDecode("linkage 0.1.4", "-LK0140-ATIV~nbEQAMr");
		assertDecode("LimeWire", "2D4C57303030312D31E0B3A0B46F7D4E954F4103");
		assertDecode("Lphant 3.02", "2D4C5030 3330322D 00383336 35363935 37373030");
		assertDecode("Shareaza 2.1.3.2", "2D535A323133322D000000000000000000000000");
		assertDecode("SymTorrent 1.17", "-ST0117-01234567890!");
		assertDecode("Transmission 0.6", "-TR0006-01234567890!");
		assertDecode("Transmission 0.72 (Dev)", "-TR072Z-zihst5yvg22f");
		assertDecode("Transmission 0.72", "-TR0072-8vd6hrmp04an");
		assertDecode("TuoTu 2.1.0", "-TT210w-dq!nWf~Qcext");
		assertDecode("\u00B5Torrent 1.7.0 Beta", "2D555431 3730422D 92844644 1DB0A094 A01C01E5");
		assertDecode("\u54c7\u560E (Vagaa) 2.6.4.4", "2D5647323634342D4FD62CDA69E235717E3BB94B");
		//assertDecode("Wyzo 0.3.0.0", "-WY0300-6huHF5Pr7Vde");
		assertDecode("FireTorrent 0.3.0.0", "-WY0300-6huHF5Pr7Vde");
		assertDecode("CacheLogic 25.1-26", "-PC251Q-6huHF5Pr7Vde");
		assertDecode("KGet 2.4.5.0", "-KG2450-BDEw8OM14Hk6");
		
		
		System.out.println();

		// Shadow style clients.
		System.out.println("Testing Shadow style clients...");
		assertDecode("ABC", "A--------YMyoBPXYy2L"); // Seen this quite a bit - not sure that it is ABC, but I guess we should default to that...
		assertDecode("ABC 2.6.9", "413236392D2D2D2D345077199FAEC4A673BECA01");
		assertDecode("ABC 3.1", "A310--001v5Gysr4NxNK");
		assertDecode("BitTornado 0.3.12", "T03C-----6tYolxhVUFS");
		assertDecode("BitTornado 0.3.18", "T03I--008gY6iB6Aq27C");
		assertDecode("BitTornado 0.3.9", "T0390----5uL5NvjBe2z");
		assertDecode("Tribler 1", "R100--003hR6s07XWcov"); // Seen recently - is this really Tribler?
		assertDecode("Tribler 3.7", "R37---003uApHy851-Pq");
		System.out.println();

		// Simple substring style clients.
		System.out.println("Testing simple substring clients...");
		assertDecode("Azureus 1", "417A7572 65757300 00000000 000000A0 76F0AEF7");
		assertDecode("Azureus 2.0.3.2", "2D2D2D2D2D417A757265757354694E7A2A6454A7");
		assertDecode("G3 Torrent", "2D473341 6E6F6E79 6D6F7573 70E8D9CB 30250AD4");
		assertDecode("Hurricane Electric", "6172636C696768742E68652EA5860C157A5ADC35");
		assertDecode("Pando", "Pando-6B511B691CAC2E"); // Seen recently, have they changed peer ID format?
		assertDecode("\u00B5Torrent 1.7.0 RC", "2D55543137302D00AF8BC5ACCC4631481EB3EB60");
		System.out.println();
		
		// Version substring style clients.
		System.out.println("Testing versioned substring clients...");
		assertDecode("Bitlet 0.1", "4269744C657430319AEA4E02A09E318D70CCF47D");
		assertDecode("BitsOnWheels", "-BOWP05-EPICNZOGQPHP"); // Seen in the wild - no idea what version that's meant to be - a pre-release?
		assertDecode("Burst! 1.1.3", "Mbrst1-1-32e3c394b43");
		assertDecode("Opera (Build 7685)", "OP7685f2c1495b1680bf");
		assertDecode("Opera (Build 10063)", "O100634008270e29150a");
		assertDecode("Rufus 0.6.9", "00455253 416E6F6E 796D6F75 7382BE42 75024AE3");
		assertDecode("BitTorrent DNA 1.0", "444E413031303030DD01C9B2DA689E6E02803E91");
		assertDecode("BTuga Revolution 2.1", "BTM21abcdefghijklmno");
		assertDecode("AllPeers 0.70rc30", "4150302E3730726333302D3E3EB87B31F241DBFE"); // AP0.70rc30->>-{1-A--]"
		assertDecode("External Webseed", "45787420EC7CC30033D7801FEEB713FBB0557AC4");
		assertDecode("QVOD (Build 0054)", "QVOD00541234567890AB"); // Based on description on wiki.theory.org.
		assertDecode("Top-BT 1.0.0", "TB100----abcdefghijk");
		System.out.println();
		
		// BitComet/Lord/Spirit
		System.out.println("Testing BitComet/Lord/Spirit clients...");
		assertDecode("BitComet 0.56", "6578626300387A4463102D6E9AD6723B339F35A9");
		assertDecode("BitLord 0.56", "6578626300384C4F52443200048ECED57BD71028");
		assertDecode("BitSpirit? (" + BAD_PEER_ID + ")", "4D342D302D322D2D6898D9D0CAF25E4555445030");
		assertDecode("BitSpirit v2", "000242539B7ED3E058A8384AA748485454504254");
		assertDecode("BitSpirit v3", "00034253 07248896 44C59530 8A5FF2CA 55445030");
		System.out.println();

		// Mainline style clients.
		System.out.println("Testing new mainline style clients...");
		assertDecode("Mainline 5.0.7", "M5-0-7--9aa757efd5be");
		assertDecode("Amazon AWS S3", "S3-1-0-0--0123456789"); // Not currently decoded as mainline style...
		System.out.println();
		
		// Various specialised clients.
		System.out.println("Testing various specialised clients...");
		assertDecode("Mainline", "0000000000000000000000004C53441933104277");
		assertDecode(UNKNOWN + " [" + FAKE + ": ZipTorrent 1.6.0.0]", "-ZT1600-bLAdeY9rdjbe");
		
		assertDecode("Tixati 1.37", "TIX0137-i6i6f0i5d5b7");
		assertDecode("folx 0.9", "2D464C3039C6F22D5F436863327A6D792E283867");
		
		
		System.out.println();
		
		// Unknown clients - may be random bytes.
		System.out.println("Testing unknown (random byte?) clients...");
		assertDecode(UNKNOWN + " [--------1}-/---A---<]", "0000000000000000317DA32F831FF041A515FE3C");
		assertDecode(UNKNOWN + " [------- --  ------@(]", "000000DF05020020100020200008000000004028");
		assertDecode(UNKNOWN + " [-----------D-y-I--aO]", "0000000000000000F106CE44F179A2498FAC614F");
		assertDecode(UNKNOWN + " [--c--_-5-\\----t-#---]", "E7F163BB0E5FCD35005C09A11BC274C42385A1A0");
		System.out.println();
		
		// Unknown AZ style clients.
		System.out.println("Testing unknown AZ style clients...");
		String unknown_az;
		unknown_az = MessageText.getString("PeerSocket.unknown_az_style", new String[]{"BD", "0.3.0.0"});
		assertDecode(unknown_az, "-BD0300-1SGiRZ8uWpWH");
		unknown_az = MessageText.getString("PeerSocket.unknown_az_style", new String[]{"wF", "2.2.0.0"});
		assertDecode(unknown_az, "2D7746323230302D9DFF296B56AFC2DF751C609C");
		unknown_az = MessageText.getString("PeerSocket.unknown_az_style", new String[]{"X1", "0.0.6.4"});
		assertDecode(unknown_az, "2D5831303036342D12FB8A5B954153A114267F1F");
		unknown_az = MessageText.getString("PeerSocket.unknown_az_style", new String[]{"bF", "2q00"}); // I made this one up.
		assertDecode(unknown_az, "2D6246327130302D9DFF296B56AFC2DF751C609C");
		System.out.println();
		
		// Unknown Shadow style clients.
		System.out.println("Testing unknown Shadow style clients...");
		String unknown_shadow;
		unknown_shadow = MessageText.getString("PeerSocket.unknown_shadow_style", new String[]{"B", "1.2"});
		assertDecode(unknown_shadow, "B12------xgTofhetSVQ");
		System.out.println();

		// TODO
		//assertDecode("KTorrent 2.2", "-KT22B1-695754334315"); // We could use the B1 information...
		//assertDecode("KTorrent 2.1.4", "-KT2140-584815613993"); // Currently shows as 2.1.
		//assertDecode("", "C8F2D9CD3A90455354426578626300362D2D2D92"); // Looks like a BitLord client - ESTBexbc?
		//assertDecode("", "303030302D2D0000005433585859684B59584C72"); // Seen in the wild, appears to be a modified version of Azureus 2.5.0.0 (that's what was in the AZMP handshake)?
		//assertDecode("", "B5546F7272656E742F3330323520202020202020");

		assertDecode( "\u00B5Torrent Mac 1.5.11", "2D554D3135313130C964BE6F15CA71EF02AF2DD7" );
		
		assertDecode( "MediaGet",     "2D4D47314372302D3234705F6436000055673362" );
		
		assertDecode( "Invalid PeerID 0.0.0.0", "-#@0000-Em6o1EmvwLtD");
		
		assertDecode( "MediaGet2 2.1", "2D4D47323111302D3234705F6436706E55673362" );
		
		assertDecode( "Ares 2.1.7.1", "-AN2171-nr17R1h19O7n" );

		assertDecode( "\u00B5Torrent 3.4.0", "2D55543334302D000971FDE48C3688D2023506FC" );
		
		assertDecode( "BitTorrent 7.9.1", "2D42543739312D00A5792226709266A467EAD700" );
		
		assertDecode( "Tixati 1.1.0.7", "-TX1107-811513660630" );
		
		assertDecode( "Torch 6.2.9.2", "-TB6292-jhBrpKfnZ!6e" );	// I know this is wrong as the real version is apparently Torch 29.0.0.6292 according to LTEP but woreva
		
		System.out.println("Done.");
	}
}
