/* This is a JavaScript module (JSM) to be imported via Components.utils.import() and acts as a singleton.
   Only the following listed symbols will exposed on import, and only when and where imported. */
var EXPORTED_SYMBOLS = ["ipdb"];

/*
    The IP location database is in the format:
        [4 byte start IP 1][4 byte start IP 2]... [2 byte country code 1][2 byte country code 2]...
        (all IPs from 0x00000000 to 0xFFFFFFFF must exist, with gaps listed as code "??")
*/

var stream = null;
var binary = null;

var entryCount = 0;

var ipdb =
{
    countrynames : null,  // Localized country name strings

    lastmodifiedtime : 0,  // The age of the current db.dat file

    load : function(file)
    {
        if (stream != null)
            throw "Tried to load IPDB twice!";

        // Load the localized country names
        ipdb.countrynames = Components.classes["@mozilla.org/intl/stringbundle;1"]
                                      .getService(Components.interfaces.nsIStringBundleService)
                                      .createBundle("chrome://flagfox/locale/countrynames.properties");
        if (!ipdb.countrynames || !ipdb.countrynames.getSimpleEnumeration().hasMoreElements())
            throw "Could not load localized country names!";

        if (!file.exists())
            throw "IPDB file not found (" + file.path + ")";

        ipdb.lastmodifiedtime = file.lastModifiedTime;
        if (!ipdb.lastmodifiedtime)
            throw "IPDB file modified time error (" + file.path + ")";

        // Open an input stream for file
        stream = Components.classes["@mozilla.org/network/file-input-stream;1"]
                           .createInstance(Components.interfaces.nsIFileInputStream)
                           .QueryInterface(Components.interfaces.nsISeekableStream);
        stream.init(file, 0x01, 0444, 0);  // read-only, read by owner/group/others, normal behavior
        if (!stream.available())
            throw "IPDB file failed to load (" + file.path + ")";

        // Reading binary needs a helper interface
        binary = Components.classes["@mozilla.org/binaryinputstream;1"]
                           .createInstance(Components.interfaces.nsIBinaryInputStream);
        binary.setInputStream(stream);

        entryCount = stream.available() / 6;   // 4 bytes for IP; 2 bytes for country code
        if (entryCount < 80000 || Math.floor(entryCount) != entryCount)
            throw "IPDB file is corrupt (loaded bytes: " + stream.available() + ")";
    },

    close : function()
    {
        if (stream != null)
        {
            stream.close();
            stream = null;
            binary = null;
            entryCount = 0;
        }
    },

    generateQuickHash : function(pointCount)  // Generates a quick 6 character alphanumeric hash ID from a given number of points in the file
    {
        if (pointCount < 2 || pointCount > 100)
            throw "Invalid point count; must be between 2 and 100";
        var int32hash = 0xFFFFFFFF;
        const hop_length = Math.floor(6*entryCount/pointCount)-32;
        for (var i=0; i<pointCount; i++)
        {
            stream.seek(0, i*hop_length);
            int32hash ^= binary.read32();  // Read 'pointCount' number of blocks from various parts of the stream and XOR them together
        }
        return Math.abs(int32hash).toString(36);  // 6 char base-36 number string (possible characters are all alphanumeric: 0-9 + a-z)
    },

    lookupIP : function(ipString)
    {
        function getEntry(index)  // Reading past the end will throw an exception
        {
            stream.seek(0,index*4);
            return new Array( binary.read32(), binary.read32() );  // [start IP, end IP]
        }

        function readCountry()  // Finds country code based on current position via last getEntry() call
        {
            const IPblock_length = entryCount*4;
            stream.seek(0,IPblock_length + (stream.tell()-8)/2);
            return String.fromCharCode( binary.read8(), binary.read8() );  // Country code (2 char string)
        }

        function ipStringToInteger(dottedquad)
        {
            const bytes = dottedquad.split("\.");
            return (16777216 * parseInt(bytes[0])) + (65536 * parseInt(bytes[1])) + (256 * parseInt(bytes[2])) + parseInt(bytes[3]);
        }

        function isIPv6()
        {
            return ipString.indexOf(':') != -1;  // a:b:c:d:e:f:g:h for IPv6 vs. a.b.c.d for IPv4
        }

        function binarySearch(x, low, high)
        {
            if (low > high)
                throw null;

            var middle = Math.floor((low + high) / 2);
            var entry = getEntry(middle);

            if (entry[0] <= x && x < entry[1])  // entry[1] is start of next range in list; not part of this range
                return entry;
            else if (x < entry[0])
                return binarySearch(x, low, middle-1);
            else  // x >= entry[1]
                return binarySearch(x, middle+1, high);
        }

        if (ipString == "")
            return null;

        if (isIPv6())  // IPDB is IPv4 only
        {
            if (ipString == "::1")  // Localhost is about all we can easily know here
                return ['-L', ipdb.countrynames.GetStringFromName('-L')];
            Components.utils.reportError("Flagfox warning: IPv6 not supported");
            return null;
        }

        try
        {
            binarySearch(ipStringToInteger(ipString), 0, entryCount);
            var countrycode = readCountry();
            if (countrycode != "??")  // countrycode "??" indicates a gap in the IPDB
                return new Array( countrycode, ipdb.countrynames.GetStringFromName(countrycode) );  // [country code, country name]
        } catch (e) {}
        return null;
    }
};
