/*
 * Copyright 2006 Robert Sterling Moore II
 * 
 * This computer 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 computer 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 computer program; if not, write to the Free Software Foundation, Inc.,
 * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
 */

import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Vector;

public class Bencoder
{
	// Used to determine the next bencoded input type in the file.
	private final int NULL_TYPE = 0;

	private final int STRING = 1;

	private final int INTEGER = 2;

	private final int LIST = 3;

	private final int DICTIONARY = 4;

	private final int STRUCTURE_END = 5;

	private class Index
	{
		public int index;

		public Index()
		{
			this.index = 0;
		}
	}

	private Index index;

	public Bencoder()
	{
		super();
		index = new Index();
	}

	public byte[] bencodeString(String input)
	{
		
		int length_of_string = input.length();
		int digits_of_string_length = 0;

		while (length_of_string > 0)
		{
			length_of_string /= 10;
			digits_of_string_length++;
		}

		// We need to store the length of the string, a ':', and the string
		// itself
		byte[] return_bytes = new byte[input.length() + digits_of_string_length
				+ 1];

		length_of_string = input.length();

		for (int i = digits_of_string_length + 1; i < return_bytes.length; i++)
		{
			return_bytes[i] = (byte) input.charAt(i - digits_of_string_length
					- 1);
		}

		return_bytes[digits_of_string_length] = (byte) ':';

		while (digits_of_string_length > 0)
		{
			return_bytes[digits_of_string_length - 1] = (byte) ((length_of_string % 10) + 48);
			length_of_string /= 10;
			digits_of_string_length--;
		}

		return return_bytes;
	}

	public String unbencodeString(byte[] input)
	{
		index.index = 0;
		return unbencodeString(input, index);
	}

	/**
	 * Parses a bencoded String located <code>input[index</code> and returns
	 * it as a String object.
	 * 
	 * @param input
	 *            Contains bencoded data.
	 * @param index
	 *            A valid index into <code>data</code> that points to the
	 *            beginning of a bencoded String.
	 * @return A String representing the bencoded String at
	 *         <code>data[index]</code>.
	 */
	public String unbencodeString(byte[] input, Index index)
	{
		String return_string = null;
		int temp_index = index.index;
		int power_of_ten = 1;
		int length_of_string = 0;
		boolean first_digit = false;
		StringBuffer temp_string = new StringBuffer();

		// Determine the length of the integer representing the String's length.
		while (input[temp_index] != (byte) ':')
		{
			if (first_digit)
			{
				power_of_ten *= 10;
			}
			first_digit = true;
			temp_index++;
		}

		// Determine the length of the string.
		while (input[index.index] != (byte) ':')
		{
			length_of_string += ((input[index.index] - 48) * power_of_ten);
			power_of_ten /= 10;
			index.index++;
			
		}

		// Skip the ':'
		index.index++;

		// Extract the string.
		while ((length_of_string > 0) && (index.index <= input.length))
		{
			temp_string.append((char) input[index.index]);

			length_of_string--;
			index.index++;
		}

		return_string = temp_string.toString();

		return return_string;
	}

	public byte[] bencodeInteger(Integer input)
	{
		byte[] return_bytes;

		if (input.intValue() == 0)
		{
			return_bytes = new byte[3];
			return_bytes[0] = (byte) 'i';
			return_bytes[1] = (byte) 48;
			return_bytes[2] = (byte) 'e';
		}

		else
		{
			int number_of_digits = 0;
			int input_value = input.intValue();
			boolean is_negative = false;
			if (input_value < 0)
			{
				is_negative = true;
				input_value = -input_value;
			}

			while (input_value > 0)
			{
				number_of_digits++;
				input_value /= 10;
			}

			input_value = input.intValue();

			if (is_negative)
			{
				input_value = -input_value;
			}

			if (is_negative)
			{
				return_bytes = new byte[number_of_digits + 3];
				return_bytes[1] = (byte) '-';
			}
			else
			{
				return_bytes = new byte[number_of_digits + 2];
			}
			return_bytes[0] = (byte) 'i';

			return_bytes[return_bytes.length - 1] = (byte) 'e';

			int stop_value = 0;
			if (is_negative)
			{
				stop_value = 1;
			}
			for (int i = return_bytes.length - 2; i > stop_value; i--)
			{
				return_bytes[i] = (byte) ((input_value % 10) + 48);
				input_value /= 10;
			}
		}
		return return_bytes;
	}

	public Integer unbencodeInteger(byte[] input)
	{
		index.index = 0;
		return unbencodeInteger(input, index);
	}

	/**
	 * Parses a bencoded Integer stored in <code>input</code> and returns it
	 * as an Integer object.
	 * 
	 * @param input
	 *            Contains bencoded data.
	 * @param index
	 *            A valid index into <code>input</code> that points to the
	 *            beginning of a bencoded Integer.
	 * @return An Integer representing the bencoded Integer in
	 *         <code>input</code>.
	 */
	public Integer unbencodeInteger(byte[] input, Index index)
	{
		Integer return_integer;
		int temp_value = 0;
		int power_of_ten = 1;
		boolean first_digit = false;
		boolean is_negative = false;

		// Skip the 'i'
		index.index++;

		if (input[index.index] == (byte) '-')
		{
			is_negative = true;
			index.index++;
		}

		int temp_index = index.index;

		// Determine the length of the integer representing the String's length.
		while (input[temp_index] != (byte) 'e')
		{
			if (first_digit)
			{
				power_of_ten *= 10;
			}
			first_digit = true;
			temp_index++;
		}

		// Determine the length of the string.
		while (input[index.index] != (byte) 'e')
		{
			temp_value += ((input[index.index] - 48) * power_of_ten);
			power_of_ten /= 10;
			index.index++;
		}

		// Skip the 'e'
		index.index++;

		if (is_negative)
		{
			return_integer = new Integer(-temp_value);
		}
		else
		{
			return_integer = new Integer(temp_value);
		}

		return return_integer;
	}

	/**
	 * Bencodes a HashMap into a bencoded dictionary.The HashMap must contain
	 * only bencoded strings representing bencoded integers, strings, lists, and
	 * dictionaries.
	 * 
	 * @param input
	 *            A HashMap containing bencoded objects representing the entries
	 *            in a dictionary.
	 * @return
	 */
	public byte[] bencodeDictionary(HashMap input)
	{
		byte[] return_bytes = null;
		int length = 0;
		Vector dictionary_entries = new Vector();

		Iterator input_iterator = input.entrySet().iterator();

		while (input_iterator.hasNext())
		{
			Map.Entry me = (Map.Entry) input_iterator.next();
			length += ((byte[]) me.getKey()).length;
			length += ((byte[]) me.getValue()).length;
			dictionary_entries.add((byte[]) me.getKey());
			dictionary_entries.add((byte[]) me.getValue());
		}
		return_bytes = new byte[length + 2];
		return_bytes[0] = (byte) 'd';
		return_bytes[return_bytes.length - 1] = (byte) 'e';

		int index = 1;

		for (int i = 0; i < dictionary_entries.size(); i++)
		{

			for (int j = 0; j < ((byte[]) dictionary_entries.get(i)).length; j++)
			{
				return_bytes[index] = ((byte[]) dictionary_entries.get(i))[j];
				index++;
			}
		}

		return return_bytes;
	}

	/**
	 * Parses a bencoded Dictionary represented by <code>input</code> and
	 * returns it as a Map object.
	 * 
	 * @param input
	 *            Contains bencoded input.
	 * @param index
	 *            A valid index into <code>input</code> that points to the
	 *            beginning of a bencoded Dictionary.
	 * @return A Map representing the bencoded Dictionary at <code>input</code>.
	 */
	public HashMap unbencodeDictionary(byte[] input)
	{
		index.index = 0;
		return unbencodeDictionary(input, index);
	}

	/**
	 * Parses a bencoded Dictionary located <code>input[index]</code> and
	 * returns it as a Map object.
	 * 
	 * @param input
	 *            Contains bencoded input.
	 * @param index
	 *            A valid index into <code>input</code> that points to the
	 *            beginning of a bencoded Dictionary.
	 * @return A Map representing the bencoded Dictionary at
	 *         <code>input[index]</code>.
	 */
	public HashMap unbencodeDictionary(byte[] input, Index index)
	{
		HashMap returned_map = new HashMap();
		String key;
		Object value;

		// Skip the 'd'
		index.index++;

		int next_data_type = getEncodedType(input, index.index);

		// As long as there isn't an error or the end of our dictionary, keep
		// parsing the entries.
		while ((next_data_type != NULL_TYPE)
				&& (next_data_type != STRUCTURE_END)
				&& (index.index < input.length))
		{
			// The key is ALWAYS a string.
			if (next_data_type != STRING)
			{
				System.err
						.println("Error: The bencoded object beginning at index.index "
								+ index.index
								+ " is not a String, but must be according to the BitTorrent definition.");
			}

			key = unbencodeString(input, index);

			// Now get the input type of the value
			next_data_type = getEncodedType(input, index.index);

			switch (next_data_type)
			{
				case INTEGER:
					value = unbencodeInteger(input, index);
					break;
				case STRING:
					value = unbencodeString(input, index);
					break;
				case LIST:
					value = unbencodeList(input, index);
					break;
				case DICTIONARY:
					value = unbencodeDictionary(input, index);
					break;
				default:
					System.err.println("Error: The value of the key \"" + key
							+ "\" is not a valid bencoded data type.");
					return null;
			}

			returned_map.put(key, value);
			// System.out.println("[" + key + "/" + value.toString() + "]");

			next_data_type = getEncodedType(input, index.index);
		}

		// Skip the 'e'
		index.index++;

		return returned_map;
	}

	/**
	 * Takes a vector of bencoded objects and returns a bencoded list containing
	 * the same data.
	 * 
	 * @param input
	 *            A Vector containing bencoded data.
	 * @return
	 */
	public byte[] bencodeList(Vector input)
	{
		
		byte[] return_bytes = null;
		int length = 0;

		for (int i = 0; i < input.size(); i++)
		{
			length += ((byte[])input.get(i)).length;
		}
		
		return_bytes = new byte[length + 2];
		return_bytes[0] = (byte)'l';
		return_bytes[return_bytes.length - 1] = (byte)'e';
		
		int index = 1;
		
		for(int i = 0; i < input.size(); i++)
		{
			for(int j = 0; j < ((byte[])input.get(i)).length; j++)
			{
				return_bytes[index] = ((byte[])input.get(i))[j];
				index++;
			}
		}

		return return_bytes;
	}

	public Vector unbencodeList(byte[] input)
	{
		index.index = 0;
		return unbencodeList(input, index);
	}

	/**
	 * Parses a bencoded List stored in <code>input</code> and returns it as a
	 * List object.
	 * 
	 * @param input
	 *            Contains a bencoded List.
	 * @param index
	 *            A valid index into <code>input</code> that points to the
	 *            beginning of a bencoded List.
	 * @return A List representing the bencoded List in <code>input</code>.
	 */
	public Vector unbencodeList(byte[] input, Index index)
	{
		Vector return_list = new Vector();

		// Skip the 'l'
		index.index++;

		int next_data_type = getEncodedType(input, index.index);

		while ((next_data_type != STRUCTURE_END)
				&& (next_data_type != NULL_TYPE)
				&& (index.index < input.length))
		{
			switch (next_data_type)
			{
				case INTEGER:
					return_list.add(unbencodeInteger(input, index));
					break;
				case STRING:
					return_list.add(unbencodeString(input, index));
					break;
				case LIST:
					return_list.add(unbencodeList(input, index));
					break;
				case DICTIONARY:
					return_list.add(unbencodeDictionary(input, index));
					break;
				default:
					System.err.println("Error: The object at position "
							+ index.index
							+ " is not a valid bencoded data type.");
					return null;
			}
			next_data_type = getEncodedType(input, index.index);
		}

		// Skip the 'e'
		index.index++;

		return return_list;
	}

	/**
	 * Reads the byte at <code>input[index]</code> and returns an integer
	 * based on the value.
	 * 
	 * @param input
	 *            Contains bencoded input.
	 * @param index
	 *            A valid index into input that points to the beginning of a
	 *            bencoded String, Integer, List or Dictionary.
	 * @return An <code>int</code> based on the value of the byte at
	 *         <code>input[index]</code>.
	 */
	public int getEncodedType(byte[] input, int index)
	{
		// The value to be returned
		int return_value = NULL_TYPE;

		// Set return_value according to the byte at input[index]
		switch ((char) input[index])
		{
			case '0':
			case '1':
			case '2':
			case '3':
			case '4':
			case '5':
			case '6':
			case '7':
			case '8':
			case '9':
				return_value = STRING;
				break;
			case 'i':
				return_value = INTEGER;
				break;
			case 'l':
				return_value = LIST;
				break;
			case 'd':
				return_value = DICTIONARY;
				break;
			case 'e':
				return_value = STRUCTURE_END;
				break;
			default:
				System.err
						.println("Error: The byte at position "
								+ index
								+ " in the .torrent file is not the beginning of a bencoded data type.");
				break;
		}

		return return_value;
	}
}