'use strict';

function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } }

var string_prototype_startswith = require('string.prototype.startswith');
var string_prototype_endswith = require('string.prototype.endswith');
var fs = require('fs');

function L10nError(message, id, lang) {
  this.name = 'L10nError';
  this.message = message;
  this.id = id;
  this.lang = lang;
}
L10nError.prototype = Object.create(Error.prototype);
L10nError.prototype.constructor = L10nError;

function load(url) {
  return new Promise(function (resolve, reject) {
    fs.readFile(url, function (err, data) {
      if (err) {
        reject(new L10nError(err.message));
      } else {
        resolve(data.toString());
      }
    });
  });
}

function fetchResource$1(res, _ref) {
  var code = _ref.code;

  var url = res.replace('{locale}', code);
  return res.endsWith('.json') ? load(url).then(JSON.parse) : load(url);
}

var KNOWN_MACROS = ['plural'];
var MAX_PLACEABLE_LENGTH = 2500;

var FSI = '⁨';
var PDI = '⁩';

var resolutionChain = new WeakSet();

function format(ctx, lang, args, entity) {
  if (typeof entity === 'string') {
    return [{}, entity];
  }

  if (resolutionChain.has(entity)) {
    throw new L10nError('Cyclic reference detected');
  }

  resolutionChain.add(entity);

  var rv = undefined;

  try {
    rv = resolveValue({}, ctx, lang, args, entity.value, entity.index);
  } finally {
    resolutionChain.delete(entity);
  }
  return rv;
}

function resolveIdentifier(ctx, lang, args, id) {
  if (KNOWN_MACROS.indexOf(id) > -1) {
    return [{}, ctx._getMacro(lang, id)];
  }

  if (args && args.hasOwnProperty(id)) {
    if (typeof args[id] === 'string' || typeof args[id] === 'number' && !isNaN(args[id])) {
      return [{}, args[id]];
    } else {
      throw new L10nError('Arg must be a string or a number: ' + id);
    }
  }

  if (id === '__proto__') {
    throw new L10nError('Illegal id: ' + id);
  }

  var entity = ctx._getEntity(lang, id);

  if (entity) {
    return format(ctx, lang, args, entity);
  }

  throw new L10nError('Unknown reference: ' + id);
}

function subPlaceable(locals, ctx, lang, args, id) {
  var newLocals = undefined,
      value = undefined;

  try {
    var _resolveIdentifier = resolveIdentifier(ctx, lang, args, id);

    newLocals = _resolveIdentifier[0];
    value = _resolveIdentifier[1];
  } catch (err) {
    return [{ error: err }, FSI + '{{ ' + id + ' }}' + PDI];
  }

  if (typeof value === 'number') {
    var formatter = ctx._getNumberFormatter(lang);
    return [newLocals, formatter.format(value)];
  }

  if (typeof value === 'string') {
    if (value.length >= MAX_PLACEABLE_LENGTH) {
      throw new L10nError('Too many characters in placeable (' + value.length + ', max allowed is ' + MAX_PLACEABLE_LENGTH + ')');
    }
    return [newLocals, FSI + value + PDI];
  }

  return [{}, FSI + '{{ ' + id + ' }}' + PDI];
}

function interpolate(locals, ctx, lang, args, arr) {
  return arr.reduce(function (_ref2, cur) {
    var localsSeq = _ref2[0];
    var valueSeq = _ref2[1];

    if (typeof cur === 'string') {
      return [localsSeq, valueSeq + cur];
    } else {
      var _subPlaceable = subPlaceable(locals, ctx, lang, args, cur.name);

      var value = _subPlaceable[1];

      return [localsSeq, valueSeq + value];
    }
  }, [locals, '']);
}

function resolveSelector(ctx, lang, args, expr, index) {
  var selectorName = undefined;
  if (index[0].type === 'call' && index[0].expr.type === 'prop' && index[0].expr.expr.name === 'cldr') {
    selectorName = 'plural';
  } else {
    selectorName = index[0].name;
  }
  var selector = resolveIdentifier(ctx, lang, args, selectorName)[1];

  if (typeof selector !== 'function') {
    return selector;
  }

  var argValue = index[0].args ? resolveIdentifier(ctx, lang, args, index[0].args[0].name)[1] : undefined;

  if (selectorName === 'plural') {
    if (argValue === 0 && 'zero' in expr) {
      return 'zero';
    }
    if (argValue === 1 && 'one' in expr) {
      return 'one';
    }
    if (argValue === 2 && 'two' in expr) {
      return 'two';
    }
  }

  return selector(argValue);
}

function resolveValue(locals, ctx, lang, args, expr, index) {
  if (!expr) {
    return [locals, expr];
  }

  if (typeof expr === 'string' || typeof expr === 'boolean' || typeof expr === 'number') {
    return [locals, expr];
  }

  if (Array.isArray(expr)) {
    return interpolate(locals, ctx, lang, args, expr);
  }

  if (index) {
    var selector = resolveSelector(ctx, lang, args, expr, index);
    if (selector in expr) {
      return resolveValue(locals, ctx, lang, args, expr[selector]);
    }
  }

  var defaultKey = expr.__default || 'other';
  if (defaultKey in expr) {
    return resolveValue(locals, ctx, lang, args, expr[defaultKey]);
  }

  throw new L10nError('Unresolvable value');
}

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
};

function isIn(n, list) {
  return list.indexOf(n) !== -1;
}
function isBetween(n, start, end) {
  return typeof n === typeof start && start <= n && n <= end;
}

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';
  }
};

function getPluralRule(code) {
  var index = locales2rules[code.replace(/-.*$/, '')];
  if (!(index in pluralRules)) {
    return function () {
      return 'other';
    };
  }
  return pluralRules[index];
}

var L20nIntl = typeof Intl !== 'undefined' ? Intl : {
  NumberFormat: function () {
    return {
      format: function (v) {
        return v;
      }
    };
  }
};

var Context = (function () {
  function Context(env, langs, resIds) {
    var _this = this;

    _classCallCheck(this, Context);

    this.langs = langs;
    this.resIds = resIds;
    this.env = env;
    this.emit = function (type, evt) {
      return env.emit(type, evt, _this);
    };
  }

  Context.prototype._formatTuple = function _formatTuple(lang, args, entity, id, key) {
    try {
      return format(this, lang, args, entity);
    } catch (err) {
      err.id = key ? id + '::' + key : id;
      err.lang = lang;
      this.emit('resolveerror', err);
      return [{ error: err }, err.id];
    }
  };

  Context.prototype._formatEntity = function _formatEntity(lang, args, entity, id) {
    var _formatTuple2 = this._formatTuple(lang, args, entity, id);

    var value = _formatTuple2[1];

    var formatted = {
      value: value,
      attrs: null
    };

    if (entity.attrs) {
      formatted.attrs = Object.create(null);
      for (var key in entity.attrs) {
        var _formatTuple3 = this._formatTuple(lang, args, entity.attrs[key], id, key);

        var attrValue = _formatTuple3[1];

        formatted.attrs[key] = attrValue;
      }
    }

    return formatted;
  };

  Context.prototype._formatValue = function _formatValue(lang, args, entity, id) {
    return this._formatTuple(lang, args, entity, id)[1];
  };

  Context.prototype.fetch = function fetch() {
    var _this2 = this;

    var langs = arguments.length <= 0 || arguments[0] === undefined ? this.langs : arguments[0];

    if (langs.length === 0) {
      return Promise.resolve(langs);
    }

    return Promise.all(this.resIds.map(function (resId) {
      return _this2.env._getResource(langs[0], resId);
    })).then(function () {
      return langs;
    });
  };

  Context.prototype._resolve = function _resolve(langs, keys, formatter, prevResolved) {
    var _this3 = this;

    var lang = langs[0];

    if (!lang) {
      return reportMissing.call(this, keys, formatter, prevResolved);
    }

    var hasUnresolved = false;

    var resolved = keys.map(function (key, i) {
      if (prevResolved && prevResolved[i] !== undefined) {
        return prevResolved[i];
      }

      var _ref3 = Array.isArray(key) ? key : [key, undefined];

      var id = _ref3[0];
      var args = _ref3[1];

      var entity = _this3._getEntity(lang, id);

      if (entity) {
        return formatter.call(_this3, lang, args, entity, id);
      }

      _this3.emit('notfounderror', new L10nError('"' + id + '"' + ' not found in ' + lang.code, id, lang));
      hasUnresolved = true;
    });

    if (!hasUnresolved) {
      return resolved;
    }

    return this.fetch(langs.slice(1)).then(function (nextLangs) {
      return _this3._resolve(nextLangs, keys, formatter, resolved);
    });
  };

  Context.prototype.formatEntities = function formatEntities() {
    var _this4 = this;

    for (var _len = arguments.length, keys = Array(_len), _key = 0; _key < _len; _key++) {
      keys[_key] = arguments[_key];
    }

    return this.fetch().then(function (langs) {
      return _this4._resolve(langs, keys, _this4._formatEntity);
    });
  };

  Context.prototype.formatValues = function formatValues() {
    var _this5 = this;

    for (var _len2 = arguments.length, keys = Array(_len2), _key2 = 0; _key2 < _len2; _key2++) {
      keys[_key2] = arguments[_key2];
    }

    return this.fetch().then(function (langs) {
      return _this5._resolve(langs, keys, _this5._formatValue);
    });
  };

  Context.prototype._getEntity = function _getEntity(lang, id) {
    var cache = this.env.resCache;

    for (var i = 0, resId = undefined; resId = this.resIds[i]; i++) {
      var resource = cache.get(resId + lang.code + lang.src);
      if (resource instanceof L10nError) {
        continue;
      }
      if (id in resource) {
        return resource[id];
      }
    }
    return undefined;
  };

  Context.prototype._getNumberFormatter = function _getNumberFormatter(lang) {
    if (!this.env.numberFormatters) {
      this.env.numberFormatters = new Map();
    }
    if (!this.env.numberFormatters.has(lang)) {
      var formatter = L20nIntl.NumberFormat(lang);
      this.env.numberFormatters.set(lang, formatter);
      return formatter;
    }
    return this.env.numberFormatters.get(lang);
  };

  Context.prototype._getMacro = function _getMacro(lang, id) {
    switch (id) {
      case 'plural':
        return getPluralRule(lang.code);
      default:
        return undefined;
    }
  };

  return Context;
})();

function reportMissing(keys, formatter, resolved) {
  var _this6 = this;

  var missingIds = new Set();

  keys.forEach(function (key, i) {
    if (resolved && resolved[i] !== undefined) {
      return;
    }
    var id = Array.isArray(key) ? key[0] : key;
    missingIds.add(id);
    resolved[i] = formatter === _this6._formatValue ? id : { value: id, attrs: null };
  });

  this.emit('notfounderror', new L10nError('"' + Array.from(missingIds).join(', ') + '"' + ' not found in any language', missingIds));

  return resolved;
}

var MAX_PLACEABLES = 100;

var PropertiesParser = {
  patterns: null,
  entryIds: null,
  emit: null,

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

  parse: function (emit, source) {
    if (!this.patterns) {
      this.init();
    }
    this.emit = emit;

    var entries = {};

    var lines = source.match(this.patterns.entries);
    if (!lines) {
      return entries;
    }
    for (var i = 0; i < lines.length; i++) {
      var line = lines[i];

      if (this.patterns.comment.test(line)) {
        continue;
      }

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

      var entityMatch = line.match(this.patterns.entity);
      if (entityMatch) {
        try {
          this.parseEntity(entityMatch[1], entityMatch[2], entries);
        } catch (e) {
          if (!this.emit) {
            throw e;
          }
        }
      }
    }
    return entries;
  },

  parseEntity: function (id, value, entries) {
    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('.');

    if (nameElements.length > 2) {
      throw this.error('Error in ID: "' + name + '".' + ' Nested attributes are not supported.');
    }

    var attr;
    if (nameElements.length > 1) {
      name = nameElements[0];
      attr = nameElements[1];

      if (attr[0] === '$') {
        throw this.error('Attribute can\'t start with "$"');
      }
    } else {
      attr = null;
    }

    this.setEntityValue(name, attr, key, this.unescapeString(value), entries);
  },

  setEntityValue: function (id, attr, key, rawValue, entries) {
    var value = rawValue.indexOf('{{') > -1 ? this.parseString(rawValue) : rawValue;

    var isSimpleValue = typeof value === 'string';
    var root = entries;

    var isSimpleNode = typeof entries[id] === 'string';

    if (!entries[id] && (attr || key || !isSimpleValue)) {
      entries[id] = Object.create(null);
      isSimpleNode = false;
    }

    if (attr) {
      if (isSimpleNode) {
        var val = entries[id];
        entries[id] = Object.create(null);
        entries[id].value = val;
      }
      if (!entries[id].attrs) {
        entries[id].attrs = Object.create(null);
      }
      if (!entries[id].attrs && !isSimpleValue) {
        entries[id].attrs[attr] = Object.create(null);
      }
      root = entries[id].attrs;
      id = attr;
    }

    if (key) {
      isSimpleNode = false;
      if (typeof root[id] === 'string') {
        var val = root[id];
        root[id] = Object.create(null);
        root[id].index = this.parseIndex(val);
        root[id].value = Object.create(null);
      }
      root = root[id].value;
      id = key;
      isSimpleValue = true;
    }

    if (isSimpleValue) {
      if (id in root) {
        throw this.error('Duplicated id: ' + id);
      }
      root[id] = value;
    } else {
      if (!root[id]) {
        root[id] = Object.create(null);
      }
      root[id].value = value;
    }
  },

  parseString: function (str) {
    var chunks = str.split(this.patterns.placeables);
    var complexStr = [];

    var len = chunks.length;
    var placeablesCount = (len - 1) / 2;

    if (placeablesCount >= MAX_PLACEABLES) {
      throw this.error('Too many placeables (' + placeablesCount + ', max allowed is ' + MAX_PLACEABLES + ')');
    }

    for (var i = 0; i < chunks.length; i++) {
      if (chunks[i].length === 0) {
        continue;
      }
      if (i % 2 === 1) {
        complexStr.push({ type: 'idOrVar', name: chunks[i] });
      } else {
        complexStr.push(chunks[i]);
      }
    }
    return complexStr;
  },

  unescapeString: function (str) {
    if (str.lastIndexOf('\\') !== -1) {
      str = str.replace(this.patterns.controlChars, '$1');
    }
    return str.replace(this.patterns.unicode, function (match, token) {
      return String.fromCodePoint(parseInt(token, 16));
    });
  },

  parseIndex: function (str) {
    var match = str.match(this.patterns.index);
    if (!match) {
      throw new L10nError('Malformed index');
    }
    if (match[2]) {
      return [{
        type: 'call',
        expr: {
          type: 'prop',
          expr: {
            type: 'glob',
            name: 'cldr'
          },
          prop: 'plural',
          cmpt: false
        }, args: [{
          type: 'idOrVar',
          name: match[2]
        }]
      }];
    } else {
      return [{ type: 'idOrVar', name: match[1] }];
    }
  },

  error: function (msg) {
    var type = arguments.length <= 1 || arguments[1] === undefined ? 'parsererror' : arguments[1];

    var err = new L10nError(msg);
    if (this.emit) {
      this.emit(type, err);
    }
    return err;
  }
};

var MAX_PLACEABLES$1 = 100;

var L20nParser = {
  parse: function (emit, string) {
    this._source = string;
    this._index = 0;
    this._length = string.length;
    this.entries = Object.create(null);
    this.emit = emit;

    return this.getResource();
  },

  getResource: function () {
    this.getWS();
    while (this._index < this._length) {
      try {
        this.getEntry();
      } catch (e) {
        if (e instanceof L10nError) {
          this.getJunkEntry();
          if (!this.emit) {
            throw e;
          }
        } else {
          throw e;
        }
      }

      if (this._index < this._length) {
        this.getWS();
      }
    }

    return this.entries;
  },

  getEntry: function () {
    if (this._source[this._index] === '<') {
      ++this._index;
      var id = this.getIdentifier();
      if (this._source[this._index] === '[') {
        ++this._index;
        return this.getEntity(id, this.getItemList(this.getExpression, ']'));
      }
      return this.getEntity(id);
    }

    if (this._source.startsWith('/*', this._index)) {
      return this.getComment();
    }

    throw this.error('Invalid entry');
  },

  getEntity: function (id, index) {
    if (!this.getRequiredWS()) {
      throw this.error('Expected white space');
    }

    var ch = this._source[this._index];
    var hasIndex = index !== undefined;
    var value = this.getValue(ch, hasIndex, hasIndex);
    var attrs = undefined;

    if (value === undefined) {
      if (ch === '>') {
        throw this.error('Expected ">"');
      }
      attrs = this.getAttributes();
    } else {
      var ws1 = this.getRequiredWS();
      if (this._source[this._index] !== '>') {
        if (!ws1) {
          throw this.error('Expected ">"');
        }
        attrs = this.getAttributes();
      }
    }

    ++this._index;

    if (id in this.entries) {
      throw this.error('Duplicate entry ID "' + id, 'duplicateerror');
    }
    if (!attrs && !index && typeof value === 'string') {
      this.entries[id] = value;
    } else {
      this.entries[id] = {
        value: value,
        attrs: attrs,
        index: index
      };
    }
  },

  getValue: function () {
    var ch = arguments.length <= 0 || arguments[0] === undefined ? this._source[this._index] : arguments[0];
    var index = arguments.length <= 1 || arguments[1] === undefined ? false : arguments[1];
    var required = arguments.length <= 2 || arguments[2] === undefined ? true : arguments[2];

    switch (ch) {
      case '\'':
      case '"':
        return this.getString(ch, 1);
      case '{':
        return this.getHash(index);
    }

    if (required) {
      throw this.error('Unknown value type');
    }

    return;
  },

  getWS: function () {
    var cc = this._source.charCodeAt(this._index);

    while (cc === 32 || cc === 10 || cc === 9 || cc === 13) {
      cc = this._source.charCodeAt(++this._index);
    }
  },

  getRequiredWS: function () {
    var pos = this._index;
    var cc = this._source.charCodeAt(pos);

    while (cc === 32 || cc === 10 || cc === 9 || cc === 13) {
      cc = this._source.charCodeAt(++this._index);
    }
    return this._index !== pos;
  },

  getIdentifier: function () {
    var start = this._index;
    var cc = this._source.charCodeAt(this._index);

    if (cc >= 97 && cc <= 122 || cc >= 65 && cc <= 90 || cc === 95) {
      cc = this._source.charCodeAt(++this._index);
    } else {
      throw this.error('Identifier has to start with [a-zA-Z_]');
    }

    while (cc >= 97 && cc <= 122 || cc >= 65 && cc <= 90 || cc >= 48 && cc <= 57 || cc === 95) {
      cc = this._source.charCodeAt(++this._index);
    }

    return this._source.slice(start, this._index);
  },

  getUnicodeChar: function () {
    for (var i = 0; i < 4; i++) {
      var cc = this._source.charCodeAt(++this._index);
      if (cc > 96 && cc < 103 || cc > 64 && cc < 71 || cc > 47 && cc < 58) {
        continue;
      }
      throw this.error('Illegal unicode escape sequence');
    }
    this._index++;
    return String.fromCharCode(parseInt(this._source.slice(this._index - 4, this._index), 16));
  },

  stringRe: /"|'|{{|\\/g,
  getString: function (opchar, opcharLen) {
    var body = [];
    var placeables = 0;

    this._index += opcharLen;
    var start = this._index;

    var bufStart = start;
    var buf = '';

    while (true) {
      this.stringRe.lastIndex = this._index;
      var match = this.stringRe.exec(this._source);

      if (!match) {
        throw this.error('Unclosed string literal');
      }

      if (match[0] === '"' || match[0] === '\'') {
        if (match[0] !== opchar) {
          this._index += opcharLen;
          continue;
        }
        this._index = match.index + opcharLen;
        break;
      }

      if (match[0] === '{{') {
        if (placeables > MAX_PLACEABLES$1 - 1) {
          throw this.error('Too many placeables, maximum allowed is ' + MAX_PLACEABLES$1);
        }
        placeables++;
        if (match.index > bufStart || buf.length > 0) {
          body.push(buf + this._source.slice(bufStart, match.index));
          buf = '';
        }
        this._index = match.index + 2;
        this.getWS();
        body.push(this.getExpression());
        this.getWS();
        this._index += 2;
        bufStart = this._index;
        continue;
      }

      if (match[0] === '\\') {
        this._index = match.index + 1;
        var ch2 = this._source[this._index];
        if (ch2 === 'u') {
          buf += this._source.slice(bufStart, match.index) + this.getUnicodeChar();
        } else if (ch2 === opchar || ch2 === '\\') {
          buf += this._source.slice(bufStart, match.index) + ch2;
          this._index++;
        } else if (this._source.startsWith('{{', this._index)) {
          buf += this._source.slice(bufStart, match.index) + '{{';
          this._index += 2;
        } else {
          throw this.error('Illegal escape sequence');
        }
        bufStart = this._index;
      }
    }

    if (body.length === 0) {
      return buf + this._source.slice(bufStart, this._index - opcharLen);
    }

    if (this._index - opcharLen > bufStart || buf.length > 0) {
      body.push(buf + this._source.slice(bufStart, this._index - opcharLen));
    }

    return body;
  },

  getAttributes: function () {
    var attrs = Object.create(null);

    while (true) {
      this.getAttribute(attrs);
      var ws1 = this.getRequiredWS();
      var ch = this._source.charAt(this._index);
      if (ch === '>') {
        break;
      } else if (!ws1) {
        throw this.error('Expected ">"');
      }
    }
    return attrs;
  },

  getAttribute: function (attrs) {
    var key = this.getIdentifier();
    var index = undefined;

    if (this._source[this._index] === '[') {
      ++this._index;
      this.getWS();
      index = this.getItemList(this.getExpression, ']');
    }
    this.getWS();
    if (this._source[this._index] !== ':') {
      throw this.error('Expected ":"');
    }
    ++this._index;
    this.getWS();
    var hasIndex = index !== undefined;
    var value = this.getValue(undefined, hasIndex);

    if (key in attrs) {
      throw this.error('Duplicate attribute "' + key, 'duplicateerror');
    }

    if (!index && typeof value === 'string') {
      attrs[key] = value;
    } else {
      attrs[key] = {
        value: value,
        index: index
      };
    }
  },

  getHash: function (index) {
    var items = Object.create(null);

    ++this._index;
    this.getWS();

    var defKey = undefined;

    while (true) {
      var _getHashItem = this.getHashItem();

      var key = _getHashItem[0];
      var value = _getHashItem[1];
      var def = _getHashItem[2];

      items[key] = value;

      if (def) {
        if (defKey) {
          throw this.error('Default item redefinition forbidden');
        }
        defKey = key;
      }
      this.getWS();

      var comma = this._source[this._index] === ',';
      if (comma) {
        ++this._index;
        this.getWS();
      }
      if (this._source[this._index] === '}') {
        ++this._index;
        break;
      }
      if (!comma) {
        throw this.error('Expected "}"');
      }
    }

    if (defKey) {
      items.__default = defKey;
    } else if (!index) {
      throw this.error('Unresolvable Hash Value');
    }

    return items;
  },

  getHashItem: function () {
    var defItem = false;
    if (this._source[this._index] === '*') {
      ++this._index;
      defItem = true;
    }

    var key = this.getIdentifier();
    this.getWS();
    if (this._source[this._index] !== ':') {
      throw this.error('Expected ":"');
    }
    ++this._index;
    this.getWS();

    return [key, this.getValue(), defItem];
  },

  getComment: function () {
    this._index += 2;
    var start = this._index;
    var end = this._source.indexOf('*/', start);

    if (end === -1) {
      throw this.error('Comment without a closing tag');
    }

    this._index = end + 2;
  },

  getExpression: function () {
    var exp = this.getPrimaryExpression();

    while (true) {
      var ch = this._source[this._index];
      if (ch === '.' || ch === '[') {
        ++this._index;
        exp = this.getPropertyExpression(exp, ch === '[');
      } else if (ch === '(') {
        ++this._index;
        exp = this.getCallExpression(exp);
      } else {
        break;
      }
    }

    return exp;
  },

  getPropertyExpression: function (idref, computed) {
    var exp = undefined;

    if (computed) {
      this.getWS();
      exp = this.getExpression();
      this.getWS();
      if (this._source[this._index] !== ']') {
        throw this.error('Expected "]"');
      }
      ++this._index;
    } else {
      exp = this.getIdentifier();
    }

    return {
      type: 'prop',
      expr: idref,
      prop: exp,
      cmpt: computed
    };
  },

  getCallExpression: function (callee) {
    this.getWS();

    return {
      type: 'call',
      expr: callee,
      args: this.getItemList(this.getExpression, ')')
    };
  },

  getPrimaryExpression: function () {
    var ch = this._source[this._index];

    switch (ch) {
      case '$':
        ++this._index;
        return {
          type: 'var',
          name: this.getIdentifier()
        };
      case '@':
        ++this._index;
        return {
          type: 'glob',
          name: this.getIdentifier()
        };
      default:
        return {
          type: 'id',
          name: this.getIdentifier()
        };
    }
  },

  getItemList: function (callback, closeChar) {
    var items = [];
    var closed = false;

    this.getWS();

    if (this._source[this._index] === closeChar) {
      ++this._index;
      closed = true;
    }

    while (!closed) {
      items.push(callback.call(this));
      this.getWS();
      var ch = this._source.charAt(this._index);
      switch (ch) {
        case ',':
          ++this._index;
          this.getWS();
          break;
        case closeChar:
          ++this._index;
          closed = true;
          break;
        default:
          throw this.error('Expected "," or "' + closeChar + '"');
      }
    }

    return items;
  },

  getJunkEntry: function () {
    var pos = this._index;
    var nextEntity = this._source.indexOf('<', pos);
    var nextComment = this._source.indexOf('/*', pos);

    if (nextEntity === -1) {
      nextEntity = this._length;
    }
    if (nextComment === -1) {
      nextComment = this._length;
    }

    var nextEntry = Math.min(nextEntity, nextComment);

    this._index = nextEntry;
  },

  error: function (message) {
    var type = arguments.length <= 1 || arguments[1] === undefined ? 'parsererror' : arguments[1];

    var pos = this._index;

    var start = this._source.lastIndexOf('<', pos - 1);
    var lastClose = this._source.lastIndexOf('>', pos - 1);
    start = lastClose > start ? lastClose + 1 : start;
    var context = this._source.slice(start, pos + 10);

    var msg = message + ' at pos ' + pos + ': `' + context + '`';
    var err = new L10nError(msg);
    if (this.emit) {
      this.emit(type, err);
    }
    return err;
  }
};

function walkEntry(entry, fn) {
  if (typeof entry === 'string') {
    return fn(entry);
  }

  var newEntry = Object.create(null);

  if (entry.value) {
    newEntry.value = walkValue(entry.value, fn);
  }

  if (entry.index) {
    newEntry.index = entry.index;
  }

  if (entry.attrs) {
    newEntry.attrs = Object.create(null);
    for (var key in entry.attrs) {
      newEntry.attrs[key] = walkEntry(entry.attrs[key], fn);
    }
  }

  return newEntry;
}

function walkValue(value, fn) {
  if (typeof value === 'string') {
    return fn(value);
  }

  if (value.type) {
    return value;
  }

  var newValue = Array.isArray(value) ? [] : Object.create(null);
  var keys = Object.keys(value);

  for (var i = 0, key = undefined; key = keys[i]; i++) {
    newValue[key] = walkValue(value[key], fn);
  }

  return newValue;
}

function createGetter(id, name) {
  var _pseudo = null;

  return function getPseudo() {
    if (_pseudo) {
      return _pseudo;
    }

    var reAlphas = /[a-zA-Z]/g;
    var reVowels = /[aeiouAEIOU]/g;
    var reWords = /[^\W0-9_]+/g;

    var reExcluded = /(%[EO]?\w|\{\s*.+?\s*\}|&[#\w]+;|<\s*.+?\s*>)/;

    var charMaps = {
      'fr-x-psaccent': 'ȦƁƇḒḖƑƓĦĪĴĶĿḾȠǾƤɊŘŞŦŬṼẆẊẎẐ[\\]^_`ȧƀƈḓḗƒɠħīĵķŀḿƞǿƥɋřşŧŭṽẇẋẏẑ',
      'ar-x-psbidi': '∀ԐↃpƎɟפHIſӼ˥WNOԀÒᴚS⊥∩ɅＭXʎZ[\\]ᵥ_,ɐqɔpǝɟƃɥıɾʞʅɯuodbɹsʇnʌʍxʎz'
    };

    var mods = {
      'fr-x-psaccent': function (val) {
        return val.replace(reVowels, function (match) {
          return match + match.toLowerCase();
        });
      },

      'ar-x-psbidi': function (val) {
        return val.replace(reWords, function (match) {
          return '‮' + match + '‬';
        });
      }
    };

    var replaceChars = function (map, val) {
      return val.replace(reAlphas, function (match) {
        return map.charAt(match.charCodeAt(0) - 65);
      });
    };

    var transform = function (val) {
      return replaceChars(charMaps[id], mods[id](val));
    };

    var apply = function (fn, val) {
      if (!val) {
        return val;
      }

      var parts = val.split(reExcluded);
      var modified = parts.map(function (part) {
        if (reExcluded.test(part)) {
          return part;
        }
        return fn(part);
      });
      return modified.join('');
    };

    return _pseudo = {
      name: transform(name),
      process: function (str) {
        return apply(transform, str);
      }
    };
  };
}

var pseudo = Object.defineProperties(Object.create(null), {
  'fr-x-psaccent': {
    enumerable: true,
    get: createGetter('fr-x-psaccent', 'Runtime Accented')
  },
  'ar-x-psbidi': {
    enumerable: true,
    get: createGetter('ar-x-psbidi', 'Runtime Bidi')
  }
});

function emit(listeners) {
  var _this7 = this;

  for (var _len3 = arguments.length, args = Array(_len3 > 1 ? _len3 - 1 : 0), _key3 = 1; _key3 < _len3; _key3++) {
    args[_key3 - 1] = arguments[_key3];
  }

  var type = args.shift();

  if (listeners['*']) {
    listeners['*'].slice().forEach(function (listener) {
      return listener.apply(_this7, args);
    });
  }

  if (listeners[type]) {
    listeners[type].slice().forEach(function (listener) {
      return listener.apply(_this7, args);
    });
  }
}

function addEventListener(listeners, type, listener) {
  if (!(type in listeners)) {
    listeners[type] = [];
  }
  listeners[type].push(listener);
}

function removeEventListener(listeners, type, listener) {
  var typeListeners = listeners[type];
  var pos = typeListeners.indexOf(listener);
  if (pos === -1) {
    return;
  }

  typeListeners.splice(pos, 1);
}

var Env$1 = (function () {
  function Env$1(fetchResource) {
    _classCallCheck(this, Env$1);

    this.fetchResource = fetchResource;

    this.resCache = new Map();
    this.resRefs = new Map();
    this.numberFormatters = null;
    this.parsers = {
      properties: PropertiesParser,
      l20n: L20nParser
    };

    var listeners = {};
    this.emit = emit.bind(this, listeners);
    this.addEventListener = addEventListener.bind(this, listeners);
    this.removeEventListener = removeEventListener.bind(this, listeners);
  }

  Env$1.prototype.createContext = function createContext(langs, resIds) {
    var _this8 = this;

    var ctx = new Context(this, langs, resIds);
    resIds.forEach(function (resId) {
      var usedBy = _this8.resRefs.get(resId) || 0;
      _this8.resRefs.set(resId, usedBy + 1);
    });

    return ctx;
  };

  Env$1.prototype.destroyContext = function destroyContext(ctx) {
    var _this9 = this;

    ctx.resIds.forEach(function (resId) {
      var usedBy = _this9.resRefs.get(resId) || 0;

      if (usedBy > 1) {
        return _this9.resRefs.set(resId, usedBy - 1);
      }

      _this9.resRefs.delete(resId);
      _this9.resCache.forEach(function (val, key) {
        return key.startsWith(resId) ? _this9.resCache.delete(key) : null;
      });
    });
  };

  Env$1.prototype._parse = function _parse(syntax, lang, data) {
    var _this10 = this;

    var parser = this.parsers[syntax];
    if (!parser) {
      return data;
    }

    var emit = function (type, err) {
      return _this10.emit(type, amendError(lang, err));
    };
    return parser.parse.call(parser, emit, data);
  };

  Env$1.prototype._create = function _create(lang, entries) {
    if (lang.src !== 'pseudo') {
      return entries;
    }

    var pseudoentries = Object.create(null);
    for (var key in entries) {
      pseudoentries[key] = walkEntry(entries[key], pseudo[lang.code].process);
    }
    return pseudoentries;
  };

  Env$1.prototype._getResource = function _getResource(lang, res) {
    var _this11 = this;

    var cache = this.resCache;
    var id = res + lang.code + lang.src;

    if (cache.has(id)) {
      return cache.get(id);
    }

    var syntax = res.substr(res.lastIndexOf('.') + 1);

    var saveEntries = function (data) {
      var entries = _this11._parse(syntax, lang, data);
      cache.set(id, _this11._create(lang, entries));
    };

    var recover = function (err) {
      err.lang = lang;
      _this11.emit('fetcherror', err);
      cache.set(id, err);
    };

    var langToFetch = lang.src === 'pseudo' ? { code: 'en-US', src: 'app', ver: lang.ver } : lang;

    var resource = this.fetchResource(res, langToFetch).then(saveEntries, recover);

    cache.set(id, resource);

    return resource;
  };

  return Env$1;
})();

function amendError(lang, err) {
  err.lang = lang;
  return err;
}

exports.fetchResource = fetchResource$1;
exports.Env = Env$1;