// Various reverse-engineered versions of the allocation algorithms
// used by different countries to allocate 24-bit ICAO addresses based
// on the aircraft registration.
//
// These were worked out by looking at the allocation patterns and
// working backwards to an algorithm that generates that pattern,
// spot-checking aircraft to see if it worked.
// YMMV.

registration_from_hexid = (function () {
        // hide the guts in a closure

        var limited_alphabet = "ABCDEFGHJKLMNPQRSTUVWXYZ"; // 24 chars; no I, O
        var full_alphabet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";  // 26 chars

        // handles 3-letter suffixes assigned with a regular pattern
        //
        // start: first hexid of range
        // s1: major stride (interval between different first letters)
        // s2: minor stride (interval between different second letters)
        // prefix: the registration prefix
        //
        // optionally:
        //   alphabet: the alphabet to use (defaults full_alphabet)
        //   first: the suffix to use at the start of the range (default: AAA)
        //   last: the last valid suffix in the range (default: ZZZ)

        var stride_mappings = [
                { start: 0x008011, s1: 26*26, s2: 26, prefix: "ZS-" },

                { start: 0x390000, s1: 1024, s2:  32, prefix: "F-G" },
                { start: 0x398000, s1: 1024, s2:  32, prefix: "F-H" },

                { start: 0x3C4421, s1: 1024,  s2: 32, prefix: "D-A", first: 'AAA', last: 'OZZ' },
                { start: 0x3C0001, s1: 26*26, s2: 26, prefix: "D-A", first: 'PAA', last: 'ZZZ' },
                { start: 0x3C8421, s1: 1024,  s2: 32, prefix: "D-B", first: 'AAA', last: 'OZZ' },
                { start: 0x3C2001, s1: 26*26, s2: 26, prefix: "D-B", first: 'PAA', last: 'ZZZ' },
                { start: 0x3CC000, s1: 26*26, s2: 26, prefix: "D-C" },
                { start: 0x3D04A8, s1: 26*26, s2: 26, prefix: "D-E" },
                { start: 0x3D4950, s1: 26*26, s2: 26, prefix: "D-F" },
                { start: 0x3D8DF8, s1: 26*26, s2: 26, prefix: "D-G" },
                { start: 0x3DD2A0, s1: 26*26, s2: 26, prefix: "D-H" },
                { start: 0x3E1748, s1: 26*26, s2: 26, prefix: "D-I" },

                { start: 0x448421, s1: 1024,  s2: 32, prefix: "OO-" },
                { start: 0x458421, s1: 1024,  s2: 32, prefix: "OY-" },
                { start: 0x460000, s1: 26*26, s2: 26, prefix: "OH-" },
                { start: 0x468421, s1: 1024,  s2: 32, prefix: "SX-" },
                { start: 0x490421, s1: 1024,  s2: 32, prefix: "CS-" },
                { start: 0x4A0421, s1: 1024,  s2: 32, prefix: "YR-" },
                { start: 0x4B8421, s1: 1024,  s2: 32, prefix: "TC-" },
                { start: 0x740421, s1: 1024,  s2: 32, prefix: "JY-" },
                { start: 0x760421, s1: 1024,  s2: 32, prefix: "AP-" },
                { start: 0x768421, s1: 1024,  s2: 32, prefix: "9V-" },
                { start: 0x778421, s1: 1024,  s2: 32, prefix: "YK-" },
                { start: 0xC00001, s1: 26*26, s2: 26, prefix: "C-F" },
                { start: 0xC044A9, s1: 26*26, s2: 26, prefix: "C-G" },
                { start: 0xE01041, s1: 4096,  s2: 64, prefix: "LV-" }
        ];

        // numeric registrations
        //  start: start hexid in range
        //  first: first numeric registration
        //  count: number of numeric registrations
        //  template: registration template, trailing characters are replaced with the numeric registration
        var numeric_mappings = [
                { start: 0x140000, first: 0,    count: 100000, template: "RA-00000" },
                { start: 0x0B03E8, first: 1000, count: 1000,   template: "CU-T0000" }
        ];

        // fill in some derived data
        for (var i = 0; i < stride_mappings.length; ++i) {
                var mapping = stride_mappings[i];

                if (!mapping.alphabet) {
                        mapping.alphabet = full_alphabet;
                }

                if (mapping.first) {
                        var c1 = mapping.alphabet.indexOf(mapping.first.charAt(0));
                        var c2 = mapping.alphabet.indexOf(mapping.first.charAt(1));
                        var c3 = mapping.alphabet.indexOf(mapping.first.charAt(2));
                        mapping.offset = c1 * mapping.s1 + c2 * mapping.s2 + c3;
                } else {
                        mapping.offset = 0;
                }

                if (mapping.last) {
                        var c1 = mapping.alphabet.indexOf(mapping.last.charAt(0));
                        var c2 = mapping.alphabet.indexOf(mapping.last.charAt(1));
                        var c3 = mapping.alphabet.indexOf(mapping.last.charAt(2));
                        mapping.end = mapping.start - mapping.offset +
                                c1 * mapping.s1 +
                                c2 * mapping.s2 +
                                c3 - mapping.offset;
                } else {
                        mapping.end = mapping.start - mapping.offset +
                                (mapping.alphabet.length - 1) * mapping.s1 +
                                (mapping.alphabet.length - 1) * mapping.s2 +
                                (mapping.alphabet.length - 1);
                }
        }

        for (var i = 0; i < numeric_mappings.length; ++i) {
                numeric_mappings[i].end = numeric_mappings[i].start + numeric_mappings[i].count - 1;
        }

        function lookup(hexid) {
                var hexid = +("0x" + hexid);

                reg = n_reg(hexid);
                if (reg)
                        return reg;

                reg = ja_reg(hexid);
                if (reg)
                        return reg;

                reg = hl_reg(hexid);
                if (reg)
                        return reg;

                reg = numeric_reg(hexid);
                if (reg)
                        return reg;

                reg = stride_reg(hexid);
                if (reg)
                        return reg;

                return null;
        }

        function stride_reg(hexid) {
                // try the mappings in stride_mappings
                var i;
                for (i = 0; i < stride_mappings.length; ++i) {
                        var mapping = stride_mappings[i];
                        if (hexid < mapping.start || hexid > mapping.end)
                                continue;

                        var offset = hexid - mapping.start + mapping.offset;

                        var i1 = Math.floor(offset / mapping.s1);
                        offset = offset % mapping.s1;
                        var i2 = Math.floor(offset / mapping.s2);
                        offset = offset % mapping.s2;
                        var i3 = offset;

                        if (i1 < 0 || i1 >= mapping.alphabet.length ||
                            i2 < 0 || i2 >= mapping.alphabet.length ||
                            i3 < 0 || i3 >= mapping.alphabet.length)
                                continue;

                        return mapping.prefix + mapping.alphabet.charAt(i1) + mapping.alphabet.charAt(i2) + mapping.alphabet.charAt(i3);
                }

                // nothing
                return null;
        }

        function numeric_reg(hexid) {
                // try the mappings in numeric_mappings
                var i;
                for (i = 0; i < numeric_mappings.length; ++i) {
                        var mapping = numeric_mappings[i];
                        if (hexid < mapping.start || hexid > mapping.end)
                                continue;

                        var reg = (hexid - mapping.start + mapping.first) + "";
                        return mapping.template.substring(0, mapping.template.length - reg.length) + reg;
                }
        }

        //
        // US N-numbers
        //

        function n_letters(rem) {
                if (rem == 0)
                        return "";

                --rem;
                return limited_alphabet.charAt(Math.floor(rem / 25)) + n_letter(rem % 25);
        }

        function n_letter(rem) {
                if (rem == 0)
                        return "";

                --rem;
                return limited_alphabet.charAt(rem);
        }

        function n_reg(hexid) {
                var offset = hexid - 0xA00001;
                if (offset < 0 || offset >= 915399) {
                        return null;
                }

                var digit1 = Math.floor(offset / 101711) + 1;
                var reg = "N" + digit1;
                offset = offset % 101711;
                if (offset <= 600) {
                        // Na, NaA .. NaZ, NaAA .. NaZZ
                        return reg + n_letters(offset);
                }

                // Na0* .. Na9*
                offset -= 601;

                var digit2 = Math.floor(offset / 10111);
                reg += digit2;
                offset = offset % 10111;

                if (offset <= 600) {
                        // Nab, NabA..NabZ, NabAA..NabZZ
                        return reg + n_letters(offset);
                }

                // Nab0* .. Nab9*
                offset -= 601;

                var digit3 = Math.floor(offset / 951);
                reg += digit3;
                offset = offset % 951;

                if (offset <= 600) {
                        // Nabc, NabcA .. NabcZ, NabcAA .. NabcZZ
                        return reg + n_letters(offset);
                }

                // Nabc0* .. Nabc9*
                offset -= 601;

                var digit4 = Math.floor(offset / 35);
                reg += digit4.toFixed(0);
                offset = offset % 35;

                if (offset <= 24) {
                        // Nabcd, NabcdA .. NabcdZ
                        return reg + n_letter(offset);
                }

                // Nabcd0 .. Nabcd9
                offset -= 25;
                return reg + offset.toFixed(0);
        }

        // South Korea
        function hl_reg(hexid) {
                if (hexid >= 0x71BA00 && hexid <= 0x71bf99) {
                        return "HL" + (hexid - 0x71BA00 + 0x7200).toString(16);
                }

                if (hexid >= 0x71C000 && hexid <= 0x71C099) {
                        return "HL" + (hexid - 0x71C000 + 0x8000).toString(16);
                }

                if (hexid >= 0x71C200 && hexid <= 0x71C299) {
                        return "HL" + (hexid - 0x71C200 + 0x8200).toString(16);
                }

                return null;
        }

        // Japan
        function ja_reg(hexid) {
                var offset = hexid - 0x840000;
                if (offset < 0 || offset >= 229840)
                        return null;

                var reg = "JA";

                var digit1 = Math.floor(offset / 22984);
                if (digit1 < 0 || digit1 > 9)
                        return null;
                reg += digit1;
                offset = offset % 22984;

                var digit2 = Math.floor(offset / 916);
                if (digit2 < 0 || digit2 > 9)
                        return null;
                reg += digit2;
                offset = offset % 916;

                if (offset < 340) {
                        // 3rd is a digit, 4th is a digit or letter
                        var digit3 = Math.floor(offset / 34);
                        reg += digit3;
                        offset = offset % 34;

                        if (offset < 10) {
                                // 4th is a digit
                                return reg + offset;
                        }

                        // 4th is a letter
                        offset -= 10;
                        return reg + limited_alphabet.charAt(offset);
                }

                // 3rd and 4th are letters
                offset -= 340;
                var letter3 = Math.floor(offset / 24);
                return reg + limited_alphabet.charAt(letter3) + limited_alphabet.charAt(offset % 24);
        }

        return lookup;
})();

// make nodejs happy:
if (typeof module !== 'undefined') {
        module.exports = registration_from_hexid;
}
