/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";

const parsePropertiesFile = require("devtools/shared/node-properties/node-properties");
const { sprintf } = require("devtools/shared/sprintfjs/sprintf");

const propertiesMap = {};

// We need some special treatment here for webpack.
//
// Webpack doesn't always handle dynamic requires in the best way.  In
// particular if it sees an unrestricted dynamic require, it will try
// to put all the files it can find into the generated pack.  (It can
// also try a bit to parse the expression passed to require, but in
// our case this doesn't work, because our call below doesn't provide
// enough information.)
//
// Webpack also provides a way around this: require.context.  The idea
// here is to tell webpack some constraints so that it can include
// fewer files in the pack.
//
// Here we introduce new require contexts for each possible locale
// directory.  Then we use the correct context to load the property
// file.  In the webpack case this results in just the locale property
// files being included in the pack; and in the devtools case this is
// a wordy no-op.
const reqShared = require.context("raw!devtools/shared/locales/",
                                  true, /^.*\.properties$/);
const reqClient = require.context("raw!devtools/client/locales/",
                                  true, /^.*\.properties$/);
const reqGlobal = require.context("raw!toolkit/locales/",
                                  true, /^.*\.properties$/);

/**
 * Memoized getter for properties files that ensures a given url is only required and
 * parsed once.
 *
 * @param {String} url
 *        The URL of the properties file to parse.
 * @return {Object} parsed properties mapped in an object.
 */
function getProperties(url) {
  if (!propertiesMap[url]) {
    // See the comment above about webpack and require contexts.  Here
    // we take an input like "devtools/shared/locales/debugger.properties"
    // and decide which context require function to use.  Despite the
    // string processing here, in the end a string identical to |url|
    // ends up being passed to "require".
    let index = url.lastIndexOf("/");
    // Turn "mumble/locales/resource.properties" => "./resource.properties".
    let baseName = "." + url.substr(index);
    let reqFn;
    if (/^toolkit/.test(url)) {
      reqFn = reqGlobal;
    } else if (/^devtools\/shared/.test(url)) {
      reqFn = reqShared;
    } else {
      reqFn = reqClient;
    }
    propertiesMap[url] = parsePropertiesFile(reqFn(baseName));
  }

  return propertiesMap[url];
}

/**
 * Localization convenience methods.
 *
 * @param string stringBundleName
 *        The desired string bundle's name.
 */
function LocalizationHelper(stringBundleName) {
  this.stringBundleName = stringBundleName;
}

LocalizationHelper.prototype = {
  /**
   * L10N shortcut function.
   *
   * @param string name
   * @return string
   */
  getStr: function (name) {
    let properties = getProperties(this.stringBundleName);
    if (name in properties) {
      return properties[name];
    }

    throw new Error("No localization found for [" + name + "]");
  },

  /**
   * L10N shortcut function.
   *
   * @param string name
   * @param array args
   * @return string
   */
  getFormatStr: function (name, ...args) {
    return sprintf(this.getStr(name), ...args);
  },

  /**
   * L10N shortcut function for numeric arguments that need to be formatted.
   * All numeric arguments will be fixed to 2 decimals and given a localized
   * decimal separator. Other arguments will be left alone.
   *
   * @param string name
   * @param array args
   * @return string
   */
  getFormatStrWithNumbers: function (name, ...args) {
    let newArgs = args.map(x => {
      return typeof x == "number" ? this.numberWithDecimals(x, 2) : x;
    });

    return this.getFormatStr(name, ...newArgs);
  },

  /**
   * Converts a number to a locale-aware string format and keeps a certain
   * number of decimals.
   *
   * @param number number
   *        The number to convert.
   * @param number decimals [optional]
   *        Total decimals to keep.
   * @return string
   *         The localized number as a string.
   */
  numberWithDecimals: function (number, decimals = 0) {
    // If this is an integer, don't do anything special.
    if (number === (number|0)) {
      return number;
    }
    // If this isn't a number (and yes, `isNaN(null)` is false), return zero.
    if (isNaN(number) || number === null) {
      return "0";
    }

    let localized = number.toLocaleString();

    // If no grouping or decimal separators are available, bail out, because
    // padding with zeros at the end of the string won't make sense anymore.
    if (!localized.match(/[^\d]/)) {
      return localized;
    }

    return number.toLocaleString(undefined, {
      maximumFractionDigits: decimals,
      minimumFractionDigits: decimals
    });
  }
};

function getPropertiesForNode(node) {
  let bundleEl = node.closest("[data-localization-bundle]");
  if (!bundleEl) {
    return null;
  }

  let propertiesUrl = bundleEl.getAttribute("data-localization-bundle");
  return getProperties(propertiesUrl);
}

/**
 * Translate existing markup annotated with data-localization attributes.
 *
 * How to use data-localization in markup:
 *
 *   <div data-localization="content=myContent;title=myTitle"/>
 *
 * The data-localization attribute identifies an element as being localizable.
 * The content of the attribute is semi-colon separated list of descriptors.
 * - "title=myTitle" means the "title" attribute should be replaced with the localized
 *   string corresponding to the key "myTitle".
 * - "content=myContent" means the text content of the node should be replaced by the
 *   string corresponding to "myContent"
 *
 * How to define the localization bundle in markup:
 *
 *   <div data-localization-bundle="url/to/my.properties">
 *     [...]
 *       <div data-localization="content=myContent;title=myTitle"/>
 *
 * Set the data-localization-bundle on an ancestor of the nodes that should be localized.
 *
 * @param {Element} root
 *        The root node to use for the localization
 */
function localizeMarkup(root) {
  let elements = root.querySelectorAll("[data-localization]");
  for (let element of elements) {
    let properties = getPropertiesForNode(element);
    if (!properties) {
      continue;
    }

    let attributes = element.getAttribute("data-localization").split(";");
    for (let attribute of attributes) {
      let [name, value] = attribute.trim().split("=");
      if (name === "content") {
        element.textContent = properties[value];
      } else {
        element.setAttribute(name, properties[value]);
      }
    }

    element.removeAttribute("data-localization");
  }
}

const sharedL10N = new LocalizationHelper("devtools/shared/locales/shared.properties");

/**
 * A helper for having the same interface as LocalizationHelper, but for more
 * than one file. Useful for abstracting l10n string locations.
 */
function MultiLocalizationHelper(...stringBundleNames) {
  let instances = stringBundleNames.map(bundle => {
    return new LocalizationHelper(bundle);
  });

  // Get all function members of the LocalizationHelper class, making sure we're
  // not executing any potential getters while doing so, and wrap all the
  // methods we've found to work on all given string bundles.
  Object.getOwnPropertyNames(LocalizationHelper.prototype)
    .map(name => ({
      name: name,
      descriptor: Object.getOwnPropertyDescriptor(LocalizationHelper.prototype,
                                                  name)
    }))
    .filter(({ descriptor }) => descriptor.value instanceof Function)
    .forEach(method => {
      this[method.name] = (...args) => {
        for (let l10n of instances) {
          try {
            return method.descriptor.value.apply(l10n, args);
          } catch (e) {
            // Do nothing
          }
        }
        return null;
      };
    });
}

exports.LocalizationHelper = LocalizationHelper;
exports.localizeMarkup = localizeMarkup;
exports.MultiLocalizationHelper = MultiLocalizationHelper;
Object.defineProperty(exports, "ELLIPSIS", { get: () => sharedL10N.getStr("ellipsis") });
