'use strict';

/* eslint-disable no-restricted-syntax */
/**
 * Configuration options for warframe-items
 * @typedef {Object} Options
 *
 * @property {string[]} category List of allowed categories to be pulled in.
 *                        Default ['All'].
 *                        Allows any of:
 *                         - All
 *                         - Arcanes
 *                         - Archwing
 *                         - Arch-Gun
 *                         - Arch-Melee
 *                         - Corpus
 *                         - Enemy
 *                         - Fish
 *                         - Gear
 *                         - Glyphs
 *                         - Melee
 *                         - Misc
 *                         - Mods
 *                         - Pets
 *                         - Primary
 *                         - Quests
 *                         - Relics
 *                         - Resources
 *                         - Secondary
 *                         - Sentinels
 *                         - Skins
 *                         - Warframes
 * @property {boolean} ignoreEnemies If true, don't load any enemy categories
 * @property {boolean|Array<string>} i18n Whether or not to include i18n, or a list of allowed locales
 * @property {boolean} i18nOnObject Whether or not to include i18n on the object itself and not on the "array"
 */

const path = require('path');
const fs = require('fs');

const canAccess = (filePath) => {
  try {
    fs.accessSync(filePath, fs.constants.R_OK);
    return true;
  } catch (e) {
    return false;
  }
};

const cache = {};
const readJson = (filePath) => {
  if (cache[filePath]) return cache[filePath];

  const resolved = path.resolve(__dirname, filePath);
  if (canAccess(resolved)) {
    const parsed = JSON.parse(fs.readFileSync(resolved, 'utf-8'));
    cache[filePath] = parsed;
    return parsed;
  }
  return [];
};

const versions = readJson('./data/cache/.export.json');

let i18n = {};
try {
  i18n = readJson('./data/json/i18n.json');
} catch (ignored) {
  // can only happen in really weird stuff, and we're already defaulting, so it's ok
}

const ignored = ['All', 'i18n'];
const defaultCategories = fs
  .readdirSync(path.join(__dirname, './data/json/'))
  .filter((f) => f.includes('.json'))
  .map((f) => f.replace('.json', ''))
  .filter((f) => !ignored.includes(f));

const defaultOptions = { category: defaultCategories, i18n: false, i18nOnObject: false };

module.exports = class Items extends Array {
  constructor(options, ...existingItems) {
    super(...existingItems);

    // Merge provided options with defaults
    this.options = {
      ...defaultOptions,
      ...options,
    };

    const containedAll = this.options.category.includes('All');
    if (containedAll) {
      this.options.category = Array.from(
        new Set(this.options.category.filter((c) => c !== 'All').concat(defaultCategories))
      );
    }

    this.i18n = {};

    // Add items from options to array. Type equals the file name.
    for (const category of this.options.category) {
      // Ignores the enemy category.
      if (this.options.ignoreEnemies && category === 'Enemy') continue;
      const items = readJson(`./data/json/${category}.json`); // eslint-disable-line import/no-dynamic-require
      for (const item of items) {
        if (this.options.i18n) {
          // only insert i18n for the objects we're inserting, so we don't bloat memory
          if (Array.isArray(this.options.i18n)) {
            const raw = i18n[item.uniqueName];
            // only process if passed language is a supported i18n value
            if (raw) {
              Object.keys(raw).forEach((locale) => {
                if (!this.options.i18n.includes(locale)) {
                  delete raw[locale];
                }
              });
            }
            this.i18n[item.uniqueName] = raw;
          } else {
            this.i18n[item.uniqueName] = i18n[item.uniqueName];
          }
        }
        if (this.options.i18n && this.options.i18nOnObject) {
          item.i18n = this.i18n[item.uniqueName];
          // keep data just on the object so no bloat in extra this.i18n
          delete this.i18n[item.uniqueName];
        }
        this.push(item);
      }
    }
    if (!this.options.i18n || (this.options.i18n && this.options.i18nOnObject)) {
      this.i18n = undefined;
    }

    // Output won't be sorted if separate categories are chosen
    this.sort((a, b) => {
      const res = a.name.localeCompare(b.name);
      if (res === 0) {
        return a.uniqueName.localeCompare(b.uniqueName);
      }
      return res;
    });

    this.versions = versions;
  }

  /**
   * @Override Array.prototype.filter
   *
   * This roughly implements Mozilla's builtin for `Array.prototype.filter`[1].
   * V8 passes the prototype of the original Array into `ArraySpeciesCreate`[2][3],
   * which is the Array that gets returned from `filter()`. However, they don't
   * pass the arguments passed to the constructor of the original Array (this Class),
   * which means that it will always return a new Array with ALL items, even when
   * different categories are specified.[4]
   *
   * [1] https://hg.mozilla.org/mozilla-central/file/tip/js/src/builtin/Array.js#l320
   * [2] https://github.com/v8/v8/blob/master/src/builtins/array-filter.tq#L193
   * [3] https://www.ecma-international.org/ecma-262/7.0/#sec-arrayspeciescreate
   * [4] https://runkit.com/kaptard/5c9daf33090ab900120465fe
   */
  filter(fn) {
    const A = [];

    for (const el of this) {
      if (fn(el)) A.push(el);
    }
    return A;
  }

  /**
   * @Override Array.prototype.map
   *
   * See filter override
   */
  map(fn) {
    const a = [];
    for (const el of this) a.push(fn(el));

    return a;
  }
};