// This is the Gaia version of l20n: https://github.com/l20n/l20n.js
// l20n is Apache 2.0 licensed: https://github.com/l20n/l20n.js/blob/master/LICENSE
// You can find the latest build for Gaia here: https://github.com/mozilla-b2g/gaia/blob/master/shared/js/l10n.js
(function (window, undefined) {
    'use strict';

    /* jshint validthis:true */
    function L10nError(message, id, loc) {
        this.name = 'L10nError';
        this.message = message;
        this.id = id;
        this.loc = loc;
    }

    L10nError.prototype = Object.create(Error.prototype);
    L10nError.prototype.constructor = L10nError;


    /* jshint browser:true */

    var io = {
        load: function load(url, callback, sync) {
            var xhr = new XMLHttpRequest();

            if (xhr.overrideMimeType) {
                xhr.overrideMimeType('text/plain');
            }

            xhr.open('GET', url, !sync);

            xhr.addEventListener('load', function io_load(e) {
                if (e.target.status === 200 || e.target.status === 0) {
                    callback(null, e.target.responseText);
                } else {
                    callback(new L10nError('Not found: ' + url));
                }
            });
            xhr.addEventListener('error', callback);
            xhr.addEventListener('timeout', callback);

            // the app: protocol throws on 404, see https://bugzil.la/827243
            try {
                xhr.send(null);
            } catch (e) {
                callback(new L10nError('Not found: ' + url));
            }
        },

        loadJSON: function loadJSON(url, callback) {
            var xhr = new XMLHttpRequest();

            if (xhr.overrideMimeType) {
                xhr.overrideMimeType('application/json');
            }

            xhr.open('GET', url);

            xhr.responseType = 'json';
            xhr.addEventListener('load', function io_loadjson(e) {
                if (e.target.status === 200 || e.target.status === 0) {
                    callback(null, e.target.response);
                } else {
                    callback(new L10nError('Not found: ' + url));
                }
            });
            xhr.addEventListener('error', callback);
            xhr.addEventListener('timeout', callback);

            // the app: protocol throws on 404, see https://bugzil.la/827243
            try {
                xhr.send(null);
            } catch (e) {
                callback(new L10nError('Not found: ' + url));
            }
        }
    };

    function EventEmitter() {
    }

    EventEmitter.prototype.emit = function ee_emit() {
        if (!this._listeners) {
            return;
        }

        var args = Array.prototype.slice.call(arguments);
        var type = args.shift();
        if (!this._listeners[type]) {
            return;
        }

        var typeListeners = this._listeners[type].slice();
        for (var i = 0; i < typeListeners.length; i++) {
            typeListeners[i].apply(this, args);
        }
    };

    EventEmitter.prototype.addEventListener = function ee_add(type, listener) {
        if (!this._listeners) {
            this._listeners = {};
        }
        if (!(type in this._listeners)) {
            this._listeners[type] = [];
        }
        this._listeners[type].push(listener);
    };

    EventEmitter.prototype.removeEventListener = function ee_rm(type, listener) {
        if (!this._listeners) {
            return;
        }

        var typeListeners = this._listeners[type];
        var pos = typeListeners.indexOf(listener);
        if (pos === -1) {
            return;
        }

        typeListeners.splice(pos, 1);
    };


    function getPluralRule(lang) {
        var locales2rules = {
            'af': 3,
            'ak': 4,
            'am': 4,
            'ar': 1,
            'asa': 3,
            'az': 0,
            'be': 11,
            'bem': 3,
            'bez': 3,
            'bg': 3,
            'bh': 4,
            'bm': 0,
            'bn': 3,
            'bo': 0,
            'br': 20,
            'brx': 3,
            'bs': 11,
            'ca': 3,
            'cgg': 3,
            'chr': 3,
            'cs': 12,
            'cy': 17,
            'da': 3,
            'de': 3,
            'dv': 3,
            'dz': 0,
            'ee': 3,
            'el': 3,
            'en': 3,
            'eo': 3,
            'es': 3,
            'et': 3,
            'eu': 3,
            'fa': 0,
            'ff': 5,
            'fi': 3,
            'fil': 4,
            'fo': 3,
            'fr': 5,
            'fur': 3,
            'fy': 3,
            'ga': 8,
            'gd': 24,
            'gl': 3,
            'gsw': 3,
            'gu': 3,
            'guw': 4,
            'gv': 23,
            'ha': 3,
            'haw': 3,
            'he': 2,
            'hi': 4,
            'hr': 11,
            'hu': 0,
            'id': 0,
            'ig': 0,
            'ii': 0,
            'is': 3,
            'it': 3,
            'iu': 7,
            'ja': 0,
            'jmc': 3,
            'jv': 0,
            'ka': 0,
            'kab': 5,
            'kaj': 3,
            'kcg': 3,
            'kde': 0,
            'kea': 0,
            'kk': 3,
            'kl': 3,
            'km': 0,
            'kn': 0,
            'ko': 0,
            'ksb': 3,
            'ksh': 21,
            'ku': 3,
            'kw': 7,
            'lag': 18,
            'lb': 3,
            'lg': 3,
            'ln': 4,
            'lo': 0,
            'lt': 10,
            'lv': 6,
            'mas': 3,
            'mg': 4,
            'mk': 16,
            'ml': 3,
            'mn': 3,
            'mo': 9,
            'mr': 3,
            'ms': 0,
            'mt': 15,
            'my': 0,
            'nah': 3,
            'naq': 7,
            'nb': 3,
            'nd': 3,
            'ne': 3,
            'nl': 3,
            'nn': 3,
            'no': 3,
            'nr': 3,
            'nso': 4,
            'ny': 3,
            'nyn': 3,
            'om': 3,
            'or': 3,
            'pa': 3,
            'pap': 3,
            'pl': 13,
            'ps': 3,
            'pt': 3,
            'rm': 3,
            'ro': 9,
            'rof': 3,
            'ru': 11,
            'rwk': 3,
            'sah': 0,
            'saq': 3,
            'se': 7,
            'seh': 3,
            'ses': 0,
            'sg': 0,
            'sh': 11,
            'shi': 19,
            'sk': 12,
            'sl': 14,
            'sma': 7,
            'smi': 7,
            'smj': 7,
            'smn': 7,
            'sms': 7,
            'sn': 3,
            'so': 3,
            'sq': 3,
            'sr': 11,
            'ss': 3,
            'ssy': 3,
            'st': 3,
            'sv': 3,
            'sw': 3,
            'syr': 3,
            'ta': 3,
            'te': 3,
            'teo': 3,
            'th': 0,
            'ti': 4,
            'tig': 3,
            'tk': 3,
            'tl': 4,
            'tn': 3,
            'to': 0,
            'tr': 0,
            'ts': 3,
            'tzm': 22,
            'uk': 11,
            'ur': 3,
            've': 3,
            'vi': 0,
            'vun': 3,
            'wa': 4,
            'wae': 3,
            'wo': 0,
            'xh': 3,
            'xog': 3,
            'yo': 0,
            'zh': 0,
            'zu': 3
        };

        // utility functions for plural rules methods
        function isIn(n, list) {
            return list.indexOf(n) !== -1;
        }

        function isBetween(n, start, end) {
            return start <= n && n <= end;
        }

        // list of all plural rules methods:
        // map an integer to the plural form name to use
        var pluralRules = {
            '0': function () {
                return 'other';
            },
            '1': function (n) {
                if ((isBetween((n % 100), 3, 10))) {
                    return 'few';
                }
                if (n === 0) {
                    return 'zero';
                }
                if ((isBetween((n % 100), 11, 99))) {
                    return 'many';
                }
                if (n === 2) {
                    return 'two';
                }
                if (n === 1) {
                    return 'one';
                }
                return 'other';
            },
            '2': function (n) {
                if (n !== 0 && (n % 10) === 0) {
                    return 'many';
                }
                if (n === 2) {
                    return 'two';
                }
                if (n === 1) {
                    return 'one';
                }
                return 'other';
            },
            '3': function (n) {
                if (n === 1) {
                    return 'one';
                }
                return 'other';
            },
            '4': function (n) {
                if ((isBetween(n, 0, 1))) {
                    return 'one';
                }
                return 'other';
            },
            '5': function (n) {
                if ((isBetween(n, 0, 2)) && n !== 2) {
                    return 'one';
                }
                return 'other';
            },
            '6': function (n) {
                if (n === 0) {
                    return 'zero';
                }
                if ((n % 10) === 1 && (n % 100) !== 11) {
                    return 'one';
                }
                return 'other';
            },
            '7': function (n) {
                if (n === 2) {
                    return 'two';
                }
                if (n === 1) {
                    return 'one';
                }
                return 'other';
            },
            '8': function (n) {
                if ((isBetween(n, 3, 6))) {
                    return 'few';
                }
                if ((isBetween(n, 7, 10))) {
                    return 'many';
                }
                if (n === 2) {
                    return 'two';
                }
                if (n === 1) {
                    return 'one';
                }
                return 'other';
            },
            '9': function (n) {
                if (n === 0 || n !== 1 && (isBetween((n % 100), 1, 19))) {
                    return 'few';
                }
                if (n === 1) {
                    return 'one';
                }
                return 'other';
            },
            '10': function (n) {
                if ((isBetween((n % 10), 2, 9)) && !(isBetween((n % 100), 11, 19))) {
                    return 'few';
                }
                if ((n % 10) === 1 && !(isBetween((n % 100), 11, 19))) {
                    return 'one';
                }
                return 'other';
            },
            '11': function (n) {
                if ((isBetween((n % 10), 2, 4)) && !(isBetween((n % 100), 12, 14))) {
                    return 'few';
                }
                if ((n % 10) === 0 ||
                    (isBetween((n % 10), 5, 9)) ||
                    (isBetween((n % 100), 11, 14))) {
                    return 'many';
                }
                if ((n % 10) === 1 && (n % 100) !== 11) {
                    return 'one';
                }
                return 'other';
            },
            '12': function (n) {
                if ((isBetween(n, 2, 4))) {
                    return 'few';
                }
                if (n === 1) {
                    return 'one';
                }
                return 'other';
            },
            '13': function (n) {
                if ((isBetween((n % 10), 2, 4)) && !(isBetween((n % 100), 12, 14))) {
                    return 'few';
                }
                if (n !== 1 && (isBetween((n % 10), 0, 1)) ||
                    (isBetween((n % 10), 5, 9)) ||
                    (isBetween((n % 100), 12, 14))) {
                    return 'many';
                }
                if (n === 1) {
                    return 'one';
                }
                return 'other';
            },
            '14': function (n) {
                if ((isBetween((n % 100), 3, 4))) {
                    return 'few';
                }
                if ((n % 100) === 2) {
                    return 'two';
                }
                if ((n % 100) === 1) {
                    return 'one';
                }
                return 'other';
            },
            '15': function (n) {
                if (n === 0 || (isBetween((n % 100), 2, 10))) {
                    return 'few';
                }
                if ((isBetween((n % 100), 11, 19))) {
                    return 'many';
                }
                if (n === 1) {
                    return 'one';
                }
                return 'other';
            },
            '16': function (n) {
                if ((n % 10) === 1 && n !== 11) {
                    return 'one';
                }
                return 'other';
            },
            '17': function (n) {
                if (n === 3) {
                    return 'few';
                }
                if (n === 0) {
                    return 'zero';
                }
                if (n === 6) {
                    return 'many';
                }
                if (n === 2) {
                    return 'two';
                }
                if (n === 1) {
                    return 'one';
                }
                return 'other';
            },
            '18': function (n) {
                if (n === 0) {
                    return 'zero';
                }
                if ((isBetween(n, 0, 2)) && n !== 0 && n !== 2) {
                    return 'one';
                }
                return 'other';
            },
            '19': function (n) {
                if ((isBetween(n, 2, 10))) {
                    return 'few';
                }
                if ((isBetween(n, 0, 1))) {
                    return 'one';
                }
                return 'other';
            },
            '20': function (n) {
                if ((isBetween((n % 10), 3, 4) || ((n % 10) === 9)) && !(
                    isBetween((n % 100), 10, 19) ||
                    isBetween((n % 100), 70, 79) ||
                    isBetween((n % 100), 90, 99)
                    )) {
                    return 'few';
                }
                if ((n % 1000000) === 0 && n !== 0) {
                    return 'many';
                }
                if ((n % 10) === 2 && !isIn((n % 100), [12, 72, 92])) {
                    return 'two';
                }
                if ((n % 10) === 1 && !isIn((n % 100), [11, 71, 91])) {
                    return 'one';
                }
                return 'other';
            },
            '21': function (n) {
                if (n === 0) {
                    return 'zero';
                }
                if (n === 1) {
                    return 'one';
                }
                return 'other';
            },
            '22': function (n) {
                if ((isBetween(n, 0, 1)) || (isBetween(n, 11, 99))) {
                    return 'one';
                }
                return 'other';
            },
            '23': function (n) {
                if ((isBetween((n % 10), 1, 2)) || (n % 20) === 0) {
                    return 'one';
                }
                return 'other';
            },
            '24': function (n) {
                if ((isBetween(n, 3, 10) || isBetween(n, 13, 19))) {
                    return 'few';
                }
                if (isIn(n, [2, 12])) {
                    return 'two';
                }
                if (isIn(n, [1, 11])) {
                    return 'one';
                }
                return 'other';
            }
        };

        // return a function that gives the plural form name for a given integer
        var index = locales2rules[lang.replace(/-.*$/, '')];
        if (!(index in pluralRules)) {
            return function () {
                return 'other';
            };
        }
        return pluralRules[index];
    }


    var nestedProps = ['style', 'dataset'];

    var parsePatterns;

    function parse(ctx, source) {
        var ast = {};

        if (!parsePatterns) {
            parsePatterns = {
                comment: /^\s*#|^\s*$/,
                entity: /^([^=\s]+)\s*=\s*(.+)$/,
                multiline: /[^\\]\\$/,
                macro: /\{\[\s*(\w+)\(([^\)]*)\)\s*\]\}/i,
                unicode: /\\u([0-9a-fA-F]{1,4})/g,
                entries: /[\r\n]+/,
                controlChars: /\\([\\\n\r\t\b\f\{\}\"\'])/g
            };
        }

        var entries = source.split(parsePatterns.entries);
        for (var i = 0; i < entries.length; i++) {
            var line = entries[i];

            if (parsePatterns.comment.test(line)) {
                continue;
            }

            while (parsePatterns.multiline.test(line) && i < entries.length) {
                line = line.slice(0, -1) + entries[++i].trim();
            }

            var entityMatch = line.match(parsePatterns.entity);
            if (entityMatch) {
                try {
                    parseEntity(entityMatch[1], entityMatch[2], ast);
                } catch (e) {
                    if (ctx) {
                        ctx._emitter.emit('error', e);
                    } else {
                        throw e;
                    }
                }
            }
        }
        return ast;
    }

    function setEntityValue(id, attr, key, value, ast) {
        var obj = ast;
        var prop = id;

        if (attr) {
            if (!(id in obj)) {
                obj[id] = {};
            }
            if (typeof(obj[id]) === 'string') {
                obj[id] = {'_': obj[id]};
            }
            obj = obj[id];
            prop = attr;
        }

        if (!key) {
            obj[prop] = value;
            return;
        }

        if (!(prop in obj)) {
            obj[prop] = {'_': {}};
        } else if (typeof(obj[prop]) === 'string') {
            obj[prop] = {'_index': parseMacro(obj[prop]), '_': {}};
        }
        obj[prop]._[key] = value;
    }

    function parseEntity(id, value, ast) {
        var name, key;

        var pos = id.indexOf('[');
        if (pos !== -1) {
            name = id.substr(0, pos);
            key = id.substring(pos + 1, id.length - 1);
        } else {
            name = id;
            key = null;
        }

        var nameElements = name.split('.');

        var attr;
        if (nameElements.length > 1) {
            var attrElements = [];
            attrElements.push(nameElements.pop());
            if (nameElements.length > 1) {
                // Usually the last dot separates an attribute from an id
                //
                // In case when there are more than one dot in the id
                // and the second to last item is "style" or "dataset" then the last two
                // items are becoming the attribute.
                //
                // ex.
                // id.style.color = foo =>
                //
                // id:
                //   style.color: foo
                //
                // id.other.color = foo =>
                //
                // id.other:
                //   color: foo
                if (nestedProps.indexOf(nameElements[nameElements.length - 1]) !== -1) {
                    attrElements.push(nameElements.pop());
                }
            }
            name = nameElements.join('.');
            attr = attrElements.reverse().join('.');
        } else {
            attr = null;
        }

        setEntityValue(name, attr, key, unescapeString(value), ast);
    }

    function unescapeControlCharacters(str) {
        return str.replace(parsePatterns.controlChars, '$1');
    }

    function unescapeUnicode(str) {
        return str.replace(parsePatterns.unicode, function (match, token) {
            return unescape('%u' + '0000'.slice(token.length) + token);
        });
    }

    function unescapeString(str) {
        if (str.lastIndexOf('\\') !== -1) {
            str = unescapeControlCharacters(str);
        }
        return unescapeUnicode(str);
    }

    function parseMacro(str) {
        var match = str.match(parsePatterns.macro);
        if (!match) {
            throw new L10nError('Malformed macro');
        }
        return [match[1], match[2]];
    }


    var MAX_PLACEABLE_LENGTH = 2500;
    var MAX_PLACEABLES = 100;
    var rePlaceables = /\{\{\s*(.+?)\s*\}\}/g;

    function Entity(id, node, env) {
        this.id = id;
        this.env = env;
        // the dirty guard prevents cyclic or recursive references from other
        // Entities; see Entity.prototype.resolve
        this.dirty = false;
        if (typeof node === 'string') {
            this.value = node;
        } else {
            // it's either a hash or it has attrs, or both
            for (var key in node) {
                if (node.hasOwnProperty(key) && key[0] !== '_') {
                    if (!this.attributes) {
                        this.attributes = {};
                    }
                    this.attributes[key] = new Entity(this.id + '.' + key, node[key],
                        env);
                }
            }
            this.value = node._ || null;
            this.index = node._index;
        }
    }

    Entity.prototype.resolve = function E_resolve(ctxdata) {
        if (this.dirty) {
            return undefined;
        }

        this.dirty = true;
        var val;
        // if resolve fails, we want the exception to bubble up and stop the whole
        // resolving process;  however, we still need to clean up the dirty flag
        try {
            val = resolve(ctxdata, this.env, this.value, this.index);
        } finally {
            this.dirty = false;
        }
        return val;
    };

    Entity.prototype.toString = function E_toString(ctxdata) {
        try {
            return this.resolve(ctxdata);
        } catch (e) {
            return undefined;
        }
    };

    Entity.prototype.valueOf = function E_valueOf(ctxdata) {
        if (!this.attributes) {
            return this.toString(ctxdata);
        }

        var entity = {
            value: this.toString(ctxdata),
            attributes: {}
        };

        for (var key in this.attributes) {
            if (this.attributes.hasOwnProperty(key)) {
                entity.attributes[key] = this.attributes[key].toString(ctxdata);
            }
        }

        return entity;
    };

    function subPlaceable(ctxdata, env, match, id) {
        if (ctxdata && ctxdata.hasOwnProperty(id) &&
            (typeof ctxdata[id] === 'string' ||
            (typeof ctxdata[id] === 'number' && !isNaN(ctxdata[id])))) {
            return ctxdata[id];
        }

        if (env.hasOwnProperty(id)) {
            if (!(env[id] instanceof Entity)) {
                env[id] = new Entity(id, env[id], env);
            }
            var value = env[id].resolve(ctxdata);
            if (typeof value === 'string') {
                // prevent Billion Laughs attacks
                if (value.length >= MAX_PLACEABLE_LENGTH) {
                    throw new L10nError('Too many characters in placeable (' +
                    value.length + ', max allowed is ' +
                    MAX_PLACEABLE_LENGTH + ')');
                }
                return value;
            }
        }
        return match;
    }

    function interpolate(ctxdata, env, str) {
        var placeablesCount = 0;
        var value = str.replace(rePlaceables, function (match, id) {
            // prevent Quadratic Blowup attacks
            if (placeablesCount++ >= MAX_PLACEABLES) {
                throw new L10nError('Too many placeables (' + placeablesCount +
                ', max allowed is ' + MAX_PLACEABLES + ')');
            }
            return subPlaceable(ctxdata, env, match, id);
        });
        placeablesCount = 0;
        return value;
    }

    function resolve(ctxdata, env, expr, index) {
        if (typeof expr === 'string') {
            return interpolate(ctxdata, env, expr);
        }

        if (typeof expr === 'boolean' ||
            typeof expr === 'number' || !expr) {
            return expr;
        }

        // otherwise, it's a dict

        if (index && ctxdata && ctxdata.hasOwnProperty(index[1])) {
            var argValue = ctxdata[index[1]];

            // special cases for zero, one, two if they are defined on the hash
            if (argValue === 0 && 'zero' in expr) {
                return resolve(ctxdata, env, expr.zero);
            }
            if (argValue === 1 && 'one' in expr) {
                return resolve(ctxdata, env, expr.one);
            }
            if (argValue === 2 && 'two' in expr) {
                return resolve(ctxdata, env, expr.two);
            }

            var selector = env.__plural(argValue);
            if (expr.hasOwnProperty(selector)) {
                return resolve(ctxdata, env, expr[selector]);
            }
        }

        // if there was no index or no selector was found, try 'other'
        if ('other' in expr) {
            return resolve(ctxdata, env, expr.other);
        }

        return undefined;
    }

    function compile(env, ast) {
        env = env || {};
        for (var id in ast) {
            if (ast.hasOwnProperty(id)) {
                env[id] = new Entity(id, ast[id], env);
            }
        }
        return env;
    }


    function Locale(id, ctx) {
        this.id = id;
        this.ctx = ctx;
        this.isReady = false;
        this.entries = {
            __plural: getPluralRule(id)
        };
    }

    Locale.prototype.getEntry = function L_getEntry(id) {
        /* jshint -W093 */

        var entries = this.entries;

        if (!entries.hasOwnProperty(id)) {
            return undefined;
        }

        if (entries[id] instanceof Entity) {
            return entries[id];
        }

        return entries[id] = new Entity(id, entries[id], entries);
    };

    Locale.prototype.build = function L_build(callback) {
        var sync = !callback;
        var ctx = this.ctx;
        var self = this;

        var l10nLoads = ctx.resLinks.length;

        function onL10nLoaded(err) {
            if (err) {
                ctx._emitter.emit('error', err);
            }
            if (--l10nLoads <= 0) {
                self.isReady = true;
                if (callback) {
                    callback();
                }
            }
        }

        if (l10nLoads === 0) {
            onL10nLoaded();
            return;
        }

        function onJSONLoaded(err, json) {
            if (!err && json) {
                self.addAST(json);
            }
            onL10nLoaded(err);
        }

        function onPropLoaded(err, source) {
            if (!err && source) {
                var ast = parse(ctx, source);
                self.addAST(ast);
            }
            onL10nLoaded(err);
        }


        for (var i = 0; i < ctx.resLinks.length; i++) {
            var path = ctx.resLinks[i].replace('{{locale}}', this.id);
            var type = path.substr(path.lastIndexOf('.') + 1);

            switch (type) {
                case 'json':
                    io.loadJSON(path, onJSONLoaded, sync);
                    break;
                case 'properties':
                    io.load(path, onPropLoaded, sync);
                    break;
            }
        }
    };

    Locale.prototype.addAST = function (ast) {
        for (var id in ast) {
            if (ast.hasOwnProperty(id)) {
                this.entries[id] = ast[id];
            }
        }
    };

    Locale.prototype.getEntity = function (id, ctxdata) {
        var entry = this.getEntry(id);

        if (!entry) {
            return null;
        }
        return entry.valueOf(ctxdata);
    };


    function Context(id) {

        this.id = id;
        this.isReady = false;
        this.isLoading = false;

        this.supportedLocales = [];
        this.resLinks = [];
        this.locales = {};

        this._emitter = new EventEmitter();


        // Getting translations

        function getWithFallback(id) {
            /* jshint -W084 */

            if (!this.isReady) {
                throw new L10nError('Context not ready');
            }

            var cur = 0;
            var loc;
            var locale;
            while (loc = this.supportedLocales[cur]) {
                locale = this.getLocale(loc);
                if (!locale.isReady) {
                    // build without callback, synchronously
                    locale.build(null);
                }
                var entry = locale.getEntry(id);
                if (entry === undefined) {
                    cur++;
                    warning.call(this, new L10nError(id + ' not found in ' + loc, id,
                        loc));
                    continue;
                }
                return entry;
            }

            error.call(this, new L10nError(id + ' not found', id));
            return null;
        }

        this.get = function get(id, ctxdata) {
            var entry = getWithFallback.call(this, id);
            if (entry === null) {
                return '';
            }

            return entry.toString(ctxdata) || '';
        };

        this.getEntity = function getEntity(id, ctxdata) {
            var entry = getWithFallback.call(this, id);
            if (entry === null) {
                return null;
            }

            return entry.valueOf(ctxdata);
        };


        // Helpers

        this.getLocale = function getLocale(code) {
            /* jshint -W093 */

            var locales = this.locales;
            if (locales[code]) {
                return locales[code];
            }

            return locales[code] = new Locale(code, this);
        };


        // Getting ready

        function negotiate(available, requested, defaultLocale) {
            if (available.indexOf(requested[0]) === -1 ||
                requested[0] === defaultLocale) {
                return [defaultLocale];
            } else {
                return [requested[0], defaultLocale];
            }
        }

        function freeze(supported) {
            var locale = this.getLocale(supported[0]);
            if (locale.isReady) {
                setReady.call(this, supported);
            } else {
                locale.build(setReady.bind(this, supported));
            }
        }

        function setReady(supported) {
            this.supportedLocales = supported;
            this.isReady = true;
            this._emitter.emit('ready');
        }

        this.requestLocales = function requestLocales() {
            if (this.isLoading && !this.isReady) {
                throw new L10nError('Context not ready');
            }

            this.isLoading = true;
            var requested = Array.prototype.slice.call(arguments);

            var supported = negotiate(requested.concat('en-US'), requested, 'en-US');
            freeze.call(this, supported);
        };


        // Events

        this.addEventListener = function addEventListener(type, listener) {
            this._emitter.addEventListener(type, listener);
        };

        this.removeEventListener = function removeEventListener(type, listener) {
            this._emitter.removeEventListener(type, listener);
        };

        this.ready = function ready(callback) {
            if (this.isReady) {
                setTimeout(callback);
            }
            this.addEventListener('ready', callback);
        };

        this.once = function once(callback) {
            /* jshint -W068 */
            if (this.isReady) {
                setTimeout(callback);
                return;
            }

            var callAndRemove = (function () {
                this.removeEventListener('ready', callAndRemove);
                callback();
            }).bind(this);
            this.addEventListener('ready', callAndRemove);
        };


        // Errors

        function warning(e) {
            this._emitter.emit('warning', e);
            return e;
        }

        function error(e) {
            this._emitter.emit('error', e);
            return e;
        }
    }


    /* jshint -W104 */

    var DEBUG = false;
    var isPretranslated = false;
    var rtlList = ['ar', 'he', 'fa', 'ps', 'qps-plocm', 'ur'];

    // Public API

    navigator.mozL10n = {
        ctx: new Context(),
        get: function get(id, ctxdata) {
            return navigator.mozL10n.ctx.get(id, ctxdata);
        },
        localize: function localize(element, id, args) {
            return localizeElement.call(navigator.mozL10n, element, id, args);
        },
        translate: function translate(element) {
            return translateFragment.call(navigator.mozL10n, element);
        },
        ready: function ready(callback) {
            return navigator.mozL10n.ctx.ready(callback);
        },
        once: function once(callback) {
            return navigator.mozL10n.ctx.once(callback);
        },
        get readyState() {
            return navigator.mozL10n.ctx.isReady ? 'complete' : 'loading';
        },
        language: {
            set code(lang) {
                navigator.mozL10n.ctx.requestLocales(lang);
            },
            get code() {
                return navigator.mozL10n.ctx.supportedLocales[0];
            },
            get direction() {
                return getDirection(navigator.mozL10n.ctx.supportedLocales[0]);
            }
        },
        _getInternalAPI: function () {
            return {
                Error: L10nError,
                Context: Context,
                Locale: Locale,
                Entity: Entity,
                getPluralRule: getPluralRule,
                rePlaceables: rePlaceables,
                getTranslatableChildren: getTranslatableChildren,
                getL10nAttributes: getL10nAttributes,
                loadINI: loadINI,
                fireLocalizedEvent: fireLocalizedEvent,
                parse: parse,
                compile: compile
            };
        }
    };

    navigator.mozL10n.ctx.ready(onReady.bind(navigator.mozL10n));

    if (DEBUG) {
        navigator.mozL10n.ctx.addEventListener('error', console.error);
        navigator.mozL10n.ctx.addEventListener('warning', console.warn);
    }

    function getDirection(lang) {
        return (rtlList.indexOf(lang) >= 0) ? 'rtl' : 'ltr';
    }

    var readyStates = {
        'loading': 0,
        'interactive': 1,
        'complete': 2
    };

    function waitFor(state, callback) {
        state = readyStates[state];
        if (readyStates[document.readyState] >= state) {
            callback();
            return;
        }

        document.addEventListener('readystatechange', function l10n_onrsc() {
            if (readyStates[document.readyState] >= state) {
                document.removeEventListener('readystatechange', l10n_onrsc);
                callback();
            }
        });
    }

    if (window.document) {
        isPretranslated = (document.documentElement.lang === navigator.language);

        // this is a special case for netError bug; see https://bugzil.la/444165
        if (document.documentElement.dataset.noCompleteBug) {
            pretranslate.call(navigator.mozL10n);
            return;
        }


        if (isPretranslated) {
            waitFor('interactive', function () {
                window.setTimeout(initResources.bind(navigator.mozL10n));
            });
        } else {
            if (document.readyState === 'complete') {
                window.setTimeout(initResources.bind(navigator.mozL10n));
            } else {
                waitFor('interactive', pretranslate.bind(navigator.mozL10n));
            }
        }

    }

    function pretranslate() {
        /* jshint -W068 */
        if (inlineLocalization.call(this)) {
            waitFor('interactive', (function () {
                window.setTimeout(initResources.bind(this));
            }).bind(this));
        } else {
            initResources.call(this);
        }
    }

    function inlineLocalization() {
        var script = document.documentElement
            .querySelector('script[type="application/l10n"]' +
            '[lang="' + navigator.language + '"]');
        if (!script) {
            return false;
        }

        var locale = this.ctx.getLocale(navigator.language);
        // the inline localization is happenning very early, when the ctx is not
        // yet ready and when the resources haven't been downloaded yet;  add the
        // inlined JSON directly to the current locale
        locale.addAST(JSON.parse(script.innerHTML));
        // localize the visible DOM
        var l10n = {
            ctx: locale,
            language: {
                code: locale.id,
                direction: getDirection(locale.id)
            }
        };
        translateFragment.call(l10n);
        // the visible DOM is now pretranslated
        isPretranslated = true;
        return true;
    }

    function initResources() {
        var resLinks = document.head
            .querySelectorAll('link[type="application/l10n"]');
        var iniLinks = [];
        var i;

        for (i = 0; i < resLinks.length; i++) {
            var link = resLinks[i];
            var url = link.getAttribute('href');
            var type = url.substr(url.lastIndexOf('.') + 1);
            if (type === 'ini') {
                iniLinks.push(url);
            }
            this.ctx.resLinks.push(url);
        }

        var iniLoads = iniLinks.length;
        if (iniLoads === 0) {
            initLocale.call(this);
            return;
        }

        function onIniLoaded(err) {
            if (err) {
                this.ctx._emitter.emit('error', err);
            }
            if (--iniLoads === 0) {
                initLocale.call(this);
            }
        }

        for (i = 0; i < iniLinks.length; i++) {
            loadINI.call(this, iniLinks[i], onIniLoaded.bind(this));
        }
    }

    function initLocale() {
        this.ctx.requestLocales(navigator.language);
        window.addEventListener('languagechange', function l10n_langchange() {
            navigator.mozL10n.language.code = navigator.language;
        });
    }

    function onReady() {
        if (!isPretranslated) {
            this.translate();
        }
        isPretranslated = false;

        fireLocalizedEvent.call(this);
    }

    function fireLocalizedEvent() {
        var event = new CustomEvent('localized', {
            'bubbles': false,
            'cancelable': false,
            'detail': {
                'language': this.ctx.supportedLocales[0]
            }
        });
        window.dispatchEvent(event);
    }

    /* jshint -W104 */

    function loadINI(url, callback) {
        var ctx = this.ctx;
        io.load(url, function (err, source) {
            var pos = ctx.resLinks.indexOf(url);

            if (err) {
                // remove the ini link from resLinks
                ctx.resLinks.splice(pos, 1);
                return callback(err);
            }

            if (!source) {
                ctx.resLinks.splice(pos, 1);
                return callback(new Error('Empty file: ' + url));
            }

            var patterns = parseINI(source, url).resources.map(function (x) {
                return x.replace('en-US', '{{locale}}');
            });
            ctx.resLinks.splice.apply(ctx.resLinks, [pos, 1].concat(patterns));
            callback();
        });
    }

    function relativePath(baseUrl, url) {
        if (url[0] === '/') {
            return url;
        }

        var dirs = baseUrl.split('/')
            .slice(0, -1)
            .concat(url.split('/'))
            .filter(function (path) {
                return path !== '.';
            });

        return dirs.join('/');
    }

    var iniPatterns = {
        'section': /^\s*\[(.*)\]\s*$/,
        'import': /^\s*@import\s+url\((.*)\)\s*$/i,
        'entry': /[\r\n]+/
    };

    function parseINI(source, iniPath) {
        var entries = source.split(iniPatterns.entry);
        var locales = ['en-US'];
        var genericSection = true;
        var uris = [];
        var match;

        for (var i = 0; i < entries.length; i++) {
            var line = entries[i];
            // we only care about en-US resources
            if (genericSection && iniPatterns['import'].test(line)) {
                match = iniPatterns['import'].exec(line);
                var uri = relativePath(iniPath, match[1]);
                uris.push(uri);
                continue;
            }

            // but we need the list of all locales in the ini, too
            if (iniPatterns.section.test(line)) {
                genericSection = false;
                match = iniPatterns.section.exec(line);
                locales.push(match[1]);
            }
        }
        return {
            locales: locales,
            resources: uris
        };
    }

    /* jshint -W104 */

    function translateFragment(element) {
        if (!element) {
            element = document.documentElement;
            document.documentElement.lang = this.language.code;
            document.documentElement.dir = this.language.direction;
        }
        translateElement.call(this, element);

        var nodes = getTranslatableChildren(element);
        for (var i = 0; i < nodes.length; i++) {
            translateElement.call(this, nodes[i]);
        }
    }

    function getTranslatableChildren(element) {
        return element ? element.querySelectorAll('*[data-l10n-id]') : [];
    }

    function localizeElement(element, id, args) {
        if (!element) {
            return;
        }

        if (!id) {
            element.removeAttribute('data-l10n-id');
            element.removeAttribute('data-l10n-args');
            setTextContent(element, '');
            return;
        }

        element.setAttribute('data-l10n-id', id);
        if (args && typeof args === 'object') {
            element.setAttribute('data-l10n-args', JSON.stringify(args));
        } else {
            element.removeAttribute('data-l10n-args');
        }

        if (this.ctx.isReady) {
            translateElement.call(this, element);
        }
    }

    function getL10nAttributes(element) {
        if (!element) {
            return {};
        }

        var l10nId = element.getAttribute('data-l10n-id');
        var l10nArgs = element.getAttribute('data-l10n-args');

        var args = l10nArgs ? JSON.parse(l10nArgs) : null;

        return {id: l10nId, args: args};
    }


    function translateElement(element) {
        var l10n = getL10nAttributes(element);

        if (!l10n.id) {
            return;
        }

        var entity = this.ctx.getEntity(l10n.id, l10n.args);

        if (!entity) {
            return;
        }

        if (typeof entity === 'string') {
            setTextContent(element, entity);
            return true;
        }

        if (entity.value) {
            setTextContent(element, entity.value);
        }

        for (var key in entity.attributes) {
            if (entity.attributes.hasOwnProperty(key)) {
                var attr = entity.attributes[key];
                var pos = key.indexOf('.');
                if (pos !== -1) {
                    element[key.substr(0, pos)][key.substr(pos + 1)] = attr;
                } else if (key === 'ariaLabel') {
                    element.setAttribute('aria-label', attr);
                } else {
                    element[key] = attr;
                }
            }
        }

        return true;
    }

    function setTextContent(element, text) {
        // standard case: no element children
        if (!element.firstElementChild) {
            element.textContent = text;
            return;
        }

        // this element has element children: replace the content of the first
        // (non-blank) child textNode and clear other child textNodes
        var found = false;
        var reNotBlank = /\S/;
        for (var child = element.firstChild; child; child = child.nextSibling) {
            if (child.nodeType === Node.TEXT_NODE &&
                reNotBlank.test(child.nodeValue)) {
                if (found) {
                    child.nodeValue = '';
                } else {
                    child.nodeValue = text;
                    found = true;
                }
            }
        }
        // if no (non-empty) textNode is found, insert a textNode before the
        // element's first child.
        if (!found) {
            element.insertBefore(document.createTextNode(text), element.firstChild);
        }
    }

})(this);

