
package net.firefang.ip2c;

import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.RandomAccessFile;
import java.util.Comparator;
import java.util.Iterator;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.TreeMap;

//import net.firefang.ip2c.input.MemoryMappedRandoeAccessFile;
import net.firefang.ip2c.input.RandomAccessBuffer;
import net.firefang.ip2c.input.RandomAccessFile2;
import net.firefang.ip2c.input.RandomAccessInput;

public class IP2Country
{
	public static final int NO_CACHE = 0;
	public static final int MEMORY_MAPPED = 1;
	public static final int MEMORY_CACHE = 2;
	private RandomAccessInput m_input;
	private int m_firstTableOffset;
	private int m_secondTableOffset;
	private int m_countriesOffset;
	private int m_numRangesFirstTable;
	private int m_numRangesSecondTable;

	private int m_numCountries;

	public IP2Country() throws IOException
	{
		this(NO_CACHE);
	}

	public IP2Country(int cacheMode) throws IOException
	{
		this("ip-to-country.bin", cacheMode);
	}

	public IP2Country(String file, int cacheMode) throws IOException
	{
		switch (cacheMode)
		{
			case NO_CACHE :
				m_input = new RandomAccessFile2(file, "r");
			break;
			/* cabos
			case MEMORY_MAPPED :
				m_input = new MemoryMappedRandoeAccessFile(file, "r");
			break;
			*/
			case MEMORY_CACHE :
				m_input = new RandomAccessBuffer(file);
			break;
		}

		byte sig[] = new byte[4];
		readFully(sig);
		if(sig[0] != 'i' || sig[1] != 'p' || sig[2] != '2' || sig[3] != 'c')
			throw new IOException("Invalid file signature");
		if(readInt() != 2)
			throw new IOException("Invalid format version");

		m_firstTableOffset = readInt();
		m_numRangesFirstTable = readInt();
		m_secondTableOffset = readInt();
		m_numRangesSecondTable = readInt();
		m_countriesOffset = readInt();
		m_numCountries = readInt();
	}

	public Country getCountry(String ipStr) throws IOException
	{
		StringTokenizer tok = new StringTokenizer(ipStr, ".");
		short b1 = Short.parseShort(tok.nextToken());
		short b2 = Short.parseShort(tok.nextToken());
		short b3 = Short.parseShort(tok.nextToken());
		short b4 = Short.parseShort(tok.nextToken());

		int ip = ((b1 << 24) & 0xff000000) | ((b2 << 16) & 0x00ff0000)
				| ((b3 << 8) & 0x0000ff00) | (b4 & 0x000000ff);
		return getCountry(ip);
	}

	public Country getCountry(int ip) throws IOException
	{
		short key;
		if(ip >= 0)
		{
			key = findCountyCode(ip, 0, m_numRangesFirstTable, true);
		}
		else
		{
			int nip = ip - Integer.MAX_VALUE;
			key = findCountyCode(nip, 0, m_numRangesSecondTable, false);
		}

		// System.out.println(Country.get2cStr(key));
		if(key == 0)
		{
			return null;
		}
		else
		{
			return findCountry(key, 0, m_numCountries);
		}
	}

	private short findCountyCode(long ip, int startIndex, int endIndex,
			boolean firstTable) throws IOException
	{
		int middle = (startIndex + endIndex) / 2;
		Pair mp = getPair(middle, firstTable);
		long mip = mp.m_ip;
		if(ip < mip)
		{
			if(startIndex + 1 == endIndex)
				return 0; // not found
			return findCountyCode(ip, startIndex, middle, firstTable);
		}
		else if(ip > mip)
		{
			Pair np = getPair(middle + 1, firstTable);
			if(ip < np.m_ip)
			{
				return mp.m_key;
			}
			else
			{
				if(startIndex + 1 == endIndex)
					return 0; // not found
				return findCountyCode(ip, middle, endIndex, firstTable);
			}
		}
		else
		// ip == mip
		{
			return mp.m_key;
		}
	}

	public Country findCountry(short code, int startIndex, int endIndex)
			throws IOException
	{
		int middle = (startIndex + endIndex) / 2;
		short mc = getCountryCode(middle);
		// System.out.println(Country.get2cStr(mc));
		if(mc == code)
		{
			// found.
			return loadCountry(middle);
		}
		else if(code > mc)
		{
			return findCountry(code, middle, endIndex);
		}
		else
		{
			return findCountry(code, startIndex, middle);
		}
	}

	private Country loadCountry(int index) throws IOException
	{
		int offset = m_countriesOffset + index * 10;
		seek(offset);
		short id2c = readShort();
		int id3c = readInt();
		int nameOffset = readInt();
		seek(nameOffset);
		short len = readShort();
		byte nameBuf[] = new byte[len];
		readFully(nameBuf);
		return new Country(id2c, id3c, new String(nameBuf));
	}

	private short getCountryCode(int index) throws IOException
	{
		int offset = m_countriesOffset + index * 10;
		seek(offset);
		return readShort();
	}

	private Pair getPair(int index, boolean firstTable) throws IOException
	{
		int offset;
		if(firstTable)
		{
			if(index > m_numRangesFirstTable)
			{
				return new Pair(); // zero
			}
			offset = m_firstTableOffset + index * 6;
		}
		else
		{
			if(index > m_numRangesSecondTable)
			{
				return new Pair(); // zero
			}
			offset = m_secondTableOffset + index * 6;

		}

		seek(offset);
		Pair p = new Pair();
		byte a[] = new byte[4];
		readFully(a);
		p.m_ip = toInt(a);
		p.m_key = readShort();
		return p;

	}

	static class Pair
	{
		int m_ip; // silly java have no unsigened int32.
		short m_key;
	}

	public static void compileCSV(String csvFile, String binFile)
			throws IOException
	{
		Map h = new TreeMap(new Comparator()
		{

			public int compare(Object a, Object b)
			{
				return ((String) a).compareTo((String) b);
			}
		});
		RandomAccessFile dout = new RandomAccessFile(binFile, "rw");
		dout.setLength(0);
		BufferedReader bin = new BufferedReader(new InputStreamReader(
				new FileInputStream(csvFile)));
		// write signature
		dout.write("ip2c".getBytes());

		// write version 1.
		dout.writeInt(2);

		// placeholder for the country names offset

		int offsets = (int) dout.getFilePointer();
		dout.writeInt(0xcccccccc); // place holder for first ranges table
									// offset
		dout.writeInt(0xbbbbbbbb); // place holder for first table ranges count
		dout.writeInt(0xcccccccc); // place holder for second ranges table
									// offset
		dout.writeInt(0xbbbbbbbb); // place holder for second table ranges
									// count
		dout.writeInt(0xcccccccc); // place holder for countries offset
		dout.writeInt(0xbbbbbbbb); // place holder for num countries

		int cur;
		cur = (int) dout.getFilePointer();
		dout.seek(offsets);
		dout.writeInt(cur);
		dout.seek(cur);

		String line;
		boolean firstHalf = true;
		int numRangesInTable = 0;
		long lastEndLong = -1;
		long lastEndInt = -1;
		// write first element in the ranges array. ip 0.0.0.0
		while ((line = bin.readLine()) != null)
		{
			if(line.length() == 0)
				continue;
			StringTokenizer tok = new StringTokenizer(line, "\",");
			try
			{
				long startLong = Long.parseLong(tok.nextToken());
				long endLong = Long.parseLong(tok.nextToken());
				if(startLong > endLong)
					throw new IllegalArgumentException("start > end");
				if(startLong < lastEndLong)
					throw new IllegalArgumentException("File not sorted");

				String id2c = tok.nextToken();
				/* cabos */
				Locale[] locales = Locale.getAvailableLocales();
				boolean found = false;
				for (int i = 0; i < locales.length; i++) {
					if (id2c.equals(locales[i].getCountry())) {
						id2c = locales[i].getLanguage();
						found = true;
						break;
					}
				}
				if (!found) continue;

				String id3c = "";
				String name = "";
				if(tok.hasMoreElements())
				{
					id3c = tok.nextToken();
					if(tok.hasMoreElements())
					{
						// name can contain a comma, which messes things up.
						int i = line.lastIndexOf('\"', line.length() - 2);
						name = line.substring(i + 1, line.length() - 1);
					}
				}

				if(id3c.length() == 0)
					id3c = "   ";
				Country country = new Country(id2c, id3c, name);
				if(!h.containsKey(id2c))
				{
					h.put(id2c, country);
				}

				if(lastEndLong + 1 < startLong)
				{
					dout.write(toBytes(lastEndInt + 1));
					dout.writeShort(0); // dummy
					numRangesInTable++;
				}

				if(firstHalf)
				{
					if(startLong > Integer.MAX_VALUE)
					{
						// end of first table. close range.
						firstHalf = false;
						dout.write(toBytes(Integer.MAX_VALUE)); // split at
																// Integer.MAX_VALUE
						dout.writeShort(country.get2c());
						numRangesInTable++;
						lastEndLong = -1;
						lastEndInt = -1;

						long tableHalfOffset = dout.getFilePointer();
						if(tableHalfOffset > Integer.MAX_VALUE)
							throw new RuntimeException("Invalid offset");
						cur = (int) dout.getFilePointer();
						dout.seek(offsets + 4);
						dout.writeInt(numRangesInTable);
						dout.writeInt(cur);
						dout.seek(cur);

						numRangesInTable = 0;

					}
				}

				int startInt;
				int endInt;
				if(firstHalf)
				{
					startInt = toInt(toBytes(startLong));
					endInt = toInt(toBytes(endLong));
				}
				else
				{
					long nip = startLong - Integer.MAX_VALUE;
					startInt = toInt(toBytes(nip));
					if(endLong > (long) Integer.MAX_VALUE * 2)
					{
						endLong = (long) Integer.MAX_VALUE * 2;
					}
					endInt = toInt(toBytes(endLong - Integer.MAX_VALUE));
				}

				lastEndLong = endLong;
				lastEndInt = endInt;
				dout.write(toBytes(startInt));
				dout.writeShort(country.get2c());
				numRangesInTable++;

			}
			catch (RuntimeException e)
			{
				System.err.println("Error parsing : " + line);
				throw e;
			}
		}

		// finish up the last element
		dout.write(toBytes(lastEndInt));
		dout.writeShort(0); // dummy

		cur = (int) dout.getFilePointer();
		dout.seek(offsets + 12);
		dout.writeInt(numRangesInTable); // num ranges in second half
		dout.writeInt(cur); // countries offset
		dout.writeInt(h.size()); // num countries
		dout.seek(cur);

		int countryCodesOffset = cur;

		Set keys = h.keySet();
		for (Iterator iter = keys.iterator(); iter.hasNext();)
		{
			String key = (String) iter.next();
			Country country = (Country) h.get(key);
			short id2c = country.get2c();
			int id3c = country.get3c();
			dout.writeShort(id2c);
			dout.writeInt(id3c);
			dout.writeInt(0xcccccccc); // name offset place holder
		}

		int n = 0;
		for (Iterator iter = keys.iterator(); iter.hasNext();)
		{
			String key = (String) iter.next();
			Country country = (Country) h.get(key);
			String name = country.getName();
			long nameOffset = dout.getFilePointer();
			if(nameOffset > Integer.MAX_VALUE)
				throw new RuntimeException("Invalid offset");
			dout.seek(countryCodesOffset + (n * 10) + 6);
			dout.writeInt((int) nameOffset);
			dout.seek(nameOffset);
			dout.writeShort(name.length());
			dout.writeBytes(name);
			n++;
		}

		dout.close();
		System.out.println("Converted " + csvFile + " to " + binFile);

	}

	static int toInt(byte a[])
	{
		return ((a[0] << 24) & 0xff000000) | ((a[1] << 16) & 0x00ff0000)
				| ((a[2] << 8) & 0x0000ff00) | (a[3] & 0x000000ff);
	}

	static byte[] toBytes(long i)
	{
		// break down to bytes
		byte a[] = new byte[4];
		a[0] = (byte) ((i & 0xff000000l) >> 24);
		a[1] = (byte) ((i & 0x00ff0000l) >> 16);
		a[2] = (byte) ((i & 0x0000ff00l) >> 8);
		a[3] = (byte) ((i & 0x000000ffl) >> 0);

		int ii = toInt(a);
		if(ii != i)
		{
			throw new RuntimeException("Error 69");
		}

		// System.err.println("ll " + l);
		// System.err.println("nl " + nl);
		return a;
	}

	public static void main(String[] args) throws IOException
	{
		if(args.length < 1)
		{
			System.err.println("Usage : ");
			System.err.println("To resolve an IP address:");
			System.err.println("java -jar ip2c.jar ip-address");
			System.err.println("Output format:");
			System.err.println("if not found:");
			System.err.println("UNKNOWN");
			System.err.println("");
			System.err.println("if found:");
			System.err.println("2C 3C NAME");
			System.err.println("");
			System.err.println("Example:");
			System.err.println("java -jar ip2c.jar 85.64.225.159");
			System.err.println("Outputs:");
			System.err.println("IL ISR ISRAEL");
			System.err.println("");
			System.err.println("To build binary file from CSV:");
			System.err.println("java -jar ip2c csv_file bin_file");
			System.exit(1);
		}

		if(args.length == 1)
		{
			String ip = args[0];
			IP2Country ip2c = new IP2Country();
			Country c = ip2c.getCountry(ip);
			if(c == null)
			{
				System.out.println("UNKNOWN");
			}
			else
			{
				System.out.println(c.get2cStr() + "," + c.get3cStr() + ","
						+ c.getName());
			}
		}
		else
		{
			compileCSV(args[0], args[1]);
		}
	}

	private int readInt() throws IOException
	{
		return m_input.readInt();
	}

	private short readShort() throws IOException
	{
		return m_input.readShort();
	}

	private void readFully(byte[] a) throws IOException
	{
		m_input.readFully(a);
	}

	private void seek(int n) throws IOException
	{
		m_input.seek(n);
	}

    /* cabos */
	public static IP2Country IP2C;
	static {
		IP2Country ip2c = null;
		try {
			ip2c = new IP2Country("ip2language.bin", MEMORY_CACHE);
		} catch (IOException e) {
		}
		IP2C = ip2c;
	}
	
	public static String getIP2Language(String ip, String defaultLocale) {
		Country c = null;
		try {
			c = IP2C.getCountry(ip);
		} catch (Exception e) {
		}
		return (c != null) ? c.get2cStr() : defaultLocale;
    }
}
