# src/impl/locale.js

    import { Util } from './util';
    import { English } from './english';
    import { DateTime } from '../datetime';

    const localeCache = new Map();

    function intlConfigString(locale, numberingSystem, outputCalendar) {
      let loc = locale || new Intl.DateTimeFormat().resolvedOptions().locale;
      loc = Array.isArray(locale) ? locale : [locale];

      if (outputCalendar || numberingSystem) {
        loc = loc.map(l => {
          l += '-u';

          if (outputCalendar) {
            l += '-ca-' + outputCalendar;
          }

          if (numberingSystem) {
            l += '-nu-' + numberingSystem;
          }
          return l;
        });
      }
      return loc;
    }

    function mapMonths(f) {
      const ms = [];
      for (let i = 1; i <= 12; i++) {
        const dt = DateTime.utc(2016, i, 1);
        ms.push(f(dt));
      }
      return ms;
    }

    function mapWeekdays(f) {
      const ms = [];
      for (let i = 1; i <= 7; i++) {
        const dt = DateTime.utc(2016, 11, 13 + i);
        ms.push(f(dt));
      }
      return ms;
    }

    /**
     * @private
     */

    export class Locale {
      static fromOpts(opts) {
        return Locale.create(opts.locale, opts.numberingSystem, opts.outputCalendar);
      }

      static create(locale, numberingSystem, outputCalendar) {
        const localeR = locale || 'en-US',
          numberingSystemR = numberingSystem || null,
          outputCalendarR = outputCalendar || null,
          cacheKey = `${localeR}|${numberingSystemR}|${outputCalendarR}`,
          cached = localeCache.get(cacheKey);

        if (cached) {
          return cached;
        } else {
          const fresh = new Locale(localeR, numberingSystemR, outputCalendarR);
          localeCache.set(cacheKey, fresh);
          return fresh;
        }
      }

      static fromObject({ locale, numberingSystem, outputCalendar } = {}) {
        return Locale.create(locale, numberingSystem, outputCalendar);
      }

      constructor(locale, numbering, outputCalendar) {
        Object.defineProperty(this, 'locale', { value: locale, enumerable: true });
        Object.defineProperty(this, 'numberingSystem', {
          value: numbering || null,
          enumerable: true
        });
        Object.defineProperty(this, 'outputCalendar', {
          value: outputCalendar || null,
          enumerable: true
        });
        Object.defineProperty(this, 'intl', {
          value: intlConfigString(this.locale, this.numberingSystem, this.outputCalendar),
          enumerable: false
        });

        // cached usefulness
        Object.defineProperty(this, 'weekdaysCache', {
          value: { format: {}, standalone: {} },
          enumerable: false
        });
        Object.defineProperty(this, 'monthsCache', {
          value: { format: {}, standalone: {} },
          enumerable: false
        });
        Object.defineProperty(this, 'meridiemCache', {
          value: null,
          enumerable: false,
          writable: true
        });
        Object.defineProperty(this, 'eraCache', {
          value: {},
          enumerable: false,
          writable: true
        });
      }

      knownEnglish() {
        return (
          (this.locale === 'en' ||
            this.locale.toLowerCase() === 'en-us' ||
            Intl.DateTimeFormat(this.intl)
              .resolvedOptions()
              .locale.startsWith('en-US')) &&
          (this.numberingSystem === null || this.numberingSystem === 'latn') &&
          (this.outputCalendar === null || this.outputCalendar === 'gregory')
        );
      }

      clone(alts) {
        if (!alts || Object.getOwnPropertyNames(alts).length === 0) {
          return this;
        } else {
          return Locale.create(
            alts.locale || this.locale,
            alts.numberingSystem || this.numberingSystem,
            alts.outputCalendar || this.outputCalendar
          );
        }
      }

      months(length, format = false) {
        if (this.knownEnglish()) {
          const english = English.months(length);
          if (english) {
            return english;
          }
        }

        const intl = format ? { month: length, day: 'numeric' } : { month: length },
          formatStr = format ? 'format' : 'standalone';
        if (!this.monthsCache[formatStr][length]) {
          this.monthsCache[formatStr][length] = mapMonths(dt => this.extract(dt, intl, 'month'));
        }
        return this.monthsCache[formatStr][length];
      }

      weekdays(length, format = false) {
        if (this.knownEnglish()) {
          const english = English.weekdays(length);
          if (english) {
            return english;
          }
        }

        const intl = format
            ? { weekday: length, year: 'numeric', month: 'long', day: 'numeric' }
            : { weekday: length },
          formatStr = format ? 'format' : 'standalone';
        if (!this.weekdaysCache[formatStr][length]) {
          this.weekdaysCache[formatStr][length] = mapWeekdays(dt => this.extract(dt, intl, 'weekday'));
        }
        return this.weekdaysCache[formatStr][length];
      }

      meridiems() {
        if (this.knownEnglish()) {
          return English.meridiems;
        }

        // In theory there could be aribitrary day periods. We're gonna assume there are exactly two
        // for AM and PM. This is probably wrong, but it's makes parsing way easier.
        if (!this.meridiemCache) {
          const intl = { hour: 'numeric', hour12: true };
          this.meridiemCache = [DateTime.utc(2016, 11, 13, 9), DateTime.utc(2016, 11, 13, 19)].map(dt =>
            this.extract(dt, intl, 'dayperiod')
          );
        }

        return this.meridiemCache;
      }

      eras(length) {
        if (this.knownEnglish()) {
          return English.eras(length);
        }

        const intl = { era: length };

        // This is utter bullshit. Different calendars are going to define eras totally differently. What I need is the minimum set of dates
        // to definitely enumerate them.
        if (!this.eraCache[length]) {
          this.eraCache[length] = [DateTime.utc(-40, 1, 1), DateTime.utc(2017, 1, 1)].map(dt =>
            this.extract(dt, intl, 'era')
          );
        }

        return this.eraCache[length];
      }

      extract(dt, intlOpts, field) {
        const [df, d] = this.dtFormatter(dt, intlOpts),
          results = df.formatToParts(d),
          matching = results.find(m => m.type.toLowerCase() === field);

        return matching ? matching.value : null;
      }

      numberFormatter(opts = {}, intlOpts = {}) {
        const realIntlOpts = Object.assign({ useGrouping: false }, intlOpts);

        if (opts.padTo > 0) {
          realIntlOpts.minimumIntegerDigits = opts.padTo;
        }

        if (opts.round) {
          realIntlOpts.maximumFractionDigits = 0;
        }

        return new Intl.NumberFormat(this.intl, realIntlOpts);
      }

      dtFormatter(dt, intlOpts = {}) {
        let d, z;

        if (dt.zone.universal) {
          // if we have a fixed-offset zone that isn't actually UTC,
          // (like UTC+8), we need to make do with just displaying
          // the time in UTC; the formatter how to handle UTC+8
          d = Util.asIfUTC(dt);
          z = 'UTC';
        } else if (dt.zone.type === 'local') {
          d = dt.toJSDate();
        } else {
          d = dt.toJSDate();
          z = dt.zone.name;
        }

        const realIntlOpts = Object.assign({}, intlOpts);
        if (z) {
          realIntlOpts.timeZone = z;
        }

        return [new Intl.DateTimeFormat(this.intl, realIntlOpts), d];
      }

      equals(other) {
        return (
          this.locale === other.locale &&
          this.numberingSystem === other.numberingSystem &&
          this.outputCalendar === other.outputCalendar
        );
      }
    }