import username from './helpers/username';
import extract from './utils/extract';

/**
 * Translator with the same API as Symfony's.
 *
 * Derived from https://github.com/willdurand/BazingaJsTranslationBundle
 * which is available under the MIT License.
 * Copyright (c) William Durand <william.durand1@gmail.com>
 */
export default class Translator {
  constructor() {
    /**
     * A map of translation keys to their translated values.
     *
     * @type {Object}
     * @public
     */
    this.translations = {};

    this.locale = null;
  }

  addTranslations(translations) {
    Object.assign(this.translations, translations);
  }

  trans(id, parameters) {
    const translation = this.translations[id];

    if (translation) {
      return this.apply(translation, parameters || {});
    }

    return id;
  }

  transChoice(id, number, parameters) {
    let translation = this.translations[id];

    if (translation) {
      number = parseInt(number, 10);

      translation = this.pluralize(translation, number);

      return this.apply(translation, parameters || {});
    }

    return id;
  }

  apply(translation, input) {
    // If we've been given a user model as one of the input parameters, then
    // we'll extract the username and use that for the translation. In the
    // future there should be a hook here to inspect the user and change the
    // translation key. This will allow a gender property to determine which
    // translation key is used.
    if ('user' in input) {
      const user = extract(input, 'user');

      if (!input.username) input.username = username(user);
    }

    translation = translation.split(new RegExp('({[a-z0-9_]+}|</?[a-z0-9_]+>)', 'gi'));

    const hydrated = [];
    const open = [hydrated];

    translation.forEach((part) => {
      const match = part.match(new RegExp('{([a-z0-9_]+)}|<(/?)([a-z0-9_]+)>', 'i'));

      if (match) {
        // Either an opening or closing tag.
        if (match[1]) {
          open[0].push(input[match[1]]);
        } else if (match[3]) {
          if (match[2]) {
            // Closing tag. We start by removing all raw children (generally in the form of strings) from the temporary
            // holding array, then run them through m.fragment to convert them to vnodes. Usually this will just give us a
            // text vnode, but using m.fragment as opposed to an explicit conversion should be more flexible. This is necessary because
            // otherwise, our generated vnode will have raw strings as its children, and mithril expects vnodes.
            // Finally, we add the now-processed vnodes back onto the holding array (which is the same object in memory as the
            // children array of the vnode we are currently processing), and remove the reference to the holding array so that
            // further text will be added to the full set of returned elements.
            const rawChildren = open[0].splice(0, open[0].length);
            open[0].push(...m.fragment(rawChildren).children);
            open.shift();
          } else {
            // If a vnode with a matching tag was provided in the translator input, we use that. Otherwise, we create a new vnode
            // with this tag, and an empty children array (since we're expecting to insert children, as that's the point of having this in translator)
            let tag = input[match[3]] || { tag: match[3], children: [] };
            open[0].push(tag);
            // Insert the tag's children array as the first element of open, so that text in between the opening
            // and closing tags will be added to the tag's children, not to the full set of returned elements.
            open.unshift(tag.children || tag);
          }
        }
      } else {
        // Not an html tag, we add it to open[0], which is either the full set of returned elements (vnodes and text),
        // or if an html tag is currently being processed, the children attribute of that html tag's vnode.
        open[0].push(part);
      }
    });

    return hydrated.filter((part) => part);
  }

  pluralize(translation, number) {
    const sPluralRegex = new RegExp(/^\w+\: +(.+)$/),
      cPluralRegex = new RegExp(/^\s*((\{\s*(\-?\d+[\s*,\s*\-?\d+]*)\s*\})|([\[\]])\s*(-Inf|\-?\d+)\s*,\s*(\+?Inf|\-?\d+)\s*([\[\]]))\s?(.+?)$/),
      iPluralRegex = new RegExp(/^\s*(\{\s*(\-?\d+[\s*,\s*\-?\d+]*)\s*\})|([\[\]])\s*(-Inf|\-?\d+)\s*,\s*(\+?Inf|\-?\d+)\s*([\[\]])/),
      standardRules = [],
      explicitRules = [];

    translation.split('|').forEach((part) => {
      if (cPluralRegex.test(part)) {
        const matches = part.match(cPluralRegex);
        explicitRules[matches[0]] = matches[matches.length - 1];
      } else if (sPluralRegex.test(part)) {
        const matches = part.match(sPluralRegex);
        standardRules.push(matches[1]);
      } else {
        standardRules.push(part);
      }
    });

    explicitRules.forEach((rule, e) => {
      if (iPluralRegex.test(e)) {
        const matches = e.match(iPluralRegex);

        if (matches[1]) {
          const ns = matches[2].split(',');

          for (let n in ns) {
            if (number == ns[n]) {
              return explicitRules[e];
            }
          }
        } else {
          var leftNumber = this.convertNumber(matches[4]);
          var rightNumber = this.convertNumber(matches[5]);

          if (
            ('[' === matches[3] ? number >= leftNumber : number > leftNumber) &&
            (']' === matches[6] ? number <= rightNumber : number < rightNumber)
          ) {
            return explicitRules[e];
          }
        }
      }
    });

    return standardRules[this.pluralPosition(number, this.locale)] || standardRules[0] || undefined;
  }

  convertNumber(number) {
    if ('-Inf' === number) {
      return Number.NEGATIVE_INFINITY;
    } else if ('+Inf' === number || 'Inf' === number) {
      return Number.POSITIVE_INFINITY;
    }

    return parseInt(number, 10);
  }

  pluralPosition(number, locale) {
    if ('pt_BR' === locale) {
      locale = 'xbr';
    }

    if (locale.length > 3) {
      locale = locale.split('_')[0];
    }

    switch (locale) {
      case 'bo':
      case 'dz':
      case 'id':
      case 'ja':
      case 'jv':
      case 'ka':
      case 'km':
      case 'kn':
      case 'ko':
      case 'ms':
      case 'th':
      case 'vi':
      case 'zh':
        return 0;

      case 'af':
      case 'az':
      case 'bn':
      case 'bg':
      case 'ca':
      case 'da':
      case 'de':
      case 'el':
      case 'en':
      case 'eo':
      case 'es':
      case 'et':
      case 'eu':
      case 'fa':
      case 'fi':
      case 'fo':
      case 'fur':
      case 'fy':
      case 'gl':
      case 'gu':
      case 'ha':
      case 'he':
      case 'hu':
      case 'is':
      case 'it':
      case 'ku':
      case 'lb':
      case 'ml':
      case 'mn':
      case 'mr':
      case 'nah':
      case 'nb':
      case 'ne':
      case 'nl':
      case 'nn':
      case 'no':
      case 'om':
      case 'or':
      case 'pa':
      case 'pap':
      case 'ps':
      case 'pt':
      case 'so':
      case 'sq':
      case 'sv':
      case 'sw':
      case 'ta':
      case 'te':
      case 'tk':
      case 'tr':
      case 'ur':
      case 'zu':
        return number == 1 ? 0 : 1;

      case 'am':
      case 'bh':
      case 'fil':
      case 'fr':
      case 'gun':
      case 'hi':
      case 'ln':
      case 'mg':
      case 'nso':
      case 'xbr':
      case 'ti':
      case 'wa':
        return number === 0 || number == 1 ? 0 : 1;

      case 'be':
      case 'bs':
      case 'hr':
      case 'ru':
      case 'sr':
      case 'uk':
        return number % 10 == 1 && number % 100 != 11 ? 0 : number % 10 >= 2 && number % 10 <= 4 && (number % 100 < 10 || number % 100 >= 20) ? 1 : 2;

      case 'cs':
      case 'sk':
        return number == 1 ? 0 : number >= 2 && number <= 4 ? 1 : 2;

      case 'ga':
        return number == 1 ? 0 : number == 2 ? 1 : 2;

      case 'lt':
        return number % 10 == 1 && number % 100 != 11 ? 0 : number % 10 >= 2 && (number % 100 < 10 || number % 100 >= 20) ? 1 : 2;

      case 'sl':
        return number % 100 == 1 ? 0 : number % 100 == 2 ? 1 : number % 100 == 3 || number % 100 == 4 ? 2 : 3;

      case 'mk':
        return number % 10 == 1 ? 0 : 1;

      case 'mt':
        return number == 1 ? 0 : number === 0 || (number % 100 > 1 && number % 100 < 11) ? 1 : number % 100 > 10 && number % 100 < 20 ? 2 : 3;

      case 'lv':
        return number === 0 ? 0 : number % 10 == 1 && number % 100 != 11 ? 1 : 2;

      case 'pl':
        return number == 1 ? 0 : number % 10 >= 2 && number % 10 <= 4 && (number % 100 < 12 || number % 100 > 14) ? 1 : 2;

      case 'cy':
        return number == 1 ? 0 : number == 2 ? 1 : number == 8 || number == 11 ? 2 : 3;

      case 'ro':
        return number == 1 ? 0 : number === 0 || (number % 100 > 0 && number % 100 < 20) ? 1 : 2;

      case 'ar':
        return number === 0 ? 0 : number == 1 ? 1 : number == 2 ? 2 : number >= 3 && number <= 10 ? 3 : number >= 11 && number <= 99 ? 4 : 5;

      default:
        return 0;
    }
  }
}
