import { Grammar } from "./grammar.js";

const DEFAULT_FIND_OPTIONS = {
  mapper: it => it.name,
  preFilter: it => true,
  description: 'valeur',
  onMessage: m => ui.notifications.info(m)
}

/**
 * This class is intended as a placeholder for utility methods unrelated
 * to actual classes of the game system or of FoundryVTT
 */
export class Misc {
  static isFunction(v) {
    return v && {}.toString.call(v) === '[object Function]';
  }

  static upperFirst(text) {
    return text.charAt(0).toUpperCase() + text.slice(1);
  }

  static lowerFirst(text) {
    return text.charAt(0).toLowerCase() + text.slice(1);
  }

  static stripHtml(html) {
    const doc = new DOMParser().parseFromString(html, 'text/html');
    return doc.body.textContent || ''
  }

  static toSignedString(number) {
    const value = parseInt(number)
    const isPositiveNumber = value != NaN && value > 0;
    return isPositiveNumber ? "+" + number : number
  }

  static modulo(n, m) {
    return ((n % m) + m) % m;
  }

  static inRange(value, min, max) {
    if (min > max) {
      return Misc.inRange(value, max, min)
    }
    return Math.max(min, Math.min(value, max))
  }

  static sum() {
    return (a, b) => Number(a) + Number(b);
  }

  static and(predicates) {
    return value => predicates.map(predicate => predicate(value))
      .reduce((v1, v2) => v1 && v2, true);
  }


  static ascending(orderFunction = x => x) {
    return (a, b) => Misc.sortingBy(orderFunction(a), orderFunction(b));
  }

  static descending(orderFunction = x => x) {
    return (a, b) => Misc.sortingBy(orderFunction(b), orderFunction(a));
  }

  static sortingBy(a, b) {
    if (a > b) return 1;
    if (a < b) return -1;
    return 0;
  }

  static fakeId(base) {
    return (base + foundry.utils.randomID(16)).substring(0, 16)
  }

  static typeName(type, subType) {
    return subType ? game.i18n.localize(`TYPES.${type}.${subType}`)
      : '';
  }

  static arrayOrEmpty(items) {
    return items?.length ? items : [];
  }

  static findOrFirst(list, predicate) {
    return list.find(predicate) ?? list[0]
  }

  /**
   * Converts the value to an integer, or to 0 if undefined/null/not representing integer
   * @param {*} value value to convert to an integer using parseInt
   */
  static safeJsonParse(str, fallback = null) {
    try { return JSON.parse(str) } catch { return fallback }
  }

  static toInt(value) {
    const parsed = parseInt(value);
    return isNaN(parsed) ? 0 : parsed;
  }

  static keepDecimals(num, decimals) {
    if (decimals <= 0 || decimals > 6) return num;
    const power10n = Math.pow(10, parseInt(decimals));
    return Math.round(num * power10n) / power10n;
  }


  static getFractionOneN(divider) {
    if (!divider || divider == 1) {
      return 1
    }
    switch (divider) {
      case 2: return '&frac12;'
      case 4: return '&frac14;'
      case 8: return '&frac18;'
    }
    return `1/${divider}`
  }

  static indexLowercase(list) {
    const obj = {};
    const addToObj = (map, val) => {
      const key = Grammar.toLowerCaseNoAccent(val);
      if (key && !map[key]) map[key] = val
    }
    list.forEach(it => addToObj(obj, it))
    return obj;
  }
  static indexed(list, index = 'index') {
    let i = 0;
    return list.map(it => { it[index] = i++; return it })
  }

  static concat(lists) {
    return lists.reduce((a, b) => a.concat(b), []);
  }

  static classify(items, classifier = it => it.type) {
    let itemsBy = {}
    Misc.classifyInto(itemsBy, items, classifier)
    return itemsBy
  }

  static classifyFirst(items, classifier, classified = it => it) {
    let itemsBy = {};
    for (const item of items) {
      const classification = classifier(item);
      if (!itemsBy[classification]) {
        itemsBy[classification] = classified(item);
      }
    }
    return itemsBy;
  }

  static classifyInto(itemsBy, items, classifier = it => it.type) {
    for (const item of items) {
      const classification = classifier(item)
      let list = itemsBy[classification];
      if (!list) {
        list = []
        itemsBy[classification] = list
      }
      list.push(item)
    }
  }

  /**
   * @returns an array of incremental integers (including from / excluding to).
   *  if max<min, the array is decrementing integers 
   */
  static intArray(from, to) {
    if (from > to) {
      return Array.from(Array(from - to).keys()).map(i => from - i)
    }
    return Array.from(Array(to - from).keys()).map(i => from + i)
  }

  static distinct(array) {
    return [...new Set(array)];
  }

  static join(params, separator = '') {
    return (!params || params.length == 0) ? '' : params.reduce(Misc.joining(separator))
  }

  static joining(separator = '') {
    return (a, b) => a + separator + b;
  }

  static connectedUserOrGM(userId = undefined) {
    if (userId && game.user.id == userId) {
      return userId
    }
    return Misc.firstConnectedGM()?.id
  }

  static connectedGMOrUser(userId = undefined) {
    return Misc.connectedUserOrGM(userId) ?? game.user.id
  }

  static isRollModeHiddenToPlayer() {
    const mode = game.settings.get("core", "messageMode");
    return mode === "blind" || mode === "gm" || mode === "self";
  }

  static getActiveUser(id) {
    return game.users.find(u => u.id == id && u.active);
  }

  static firstConnectedGM() {
    return game.users.activeGM
  }

  static connectedGMs() {
    return game.users.filter(u => u.isGM && u.active);
  }

  /**
   * This helper method allows to get the docuument, for a single user (either first connected GM, or the owner
   * if there is no connected GMs), or else return undefined.
   * 
   * This allows for example update hooks that should apply modifications to actors to be called only for one
   * user (preventing the "User ... lacks permission to update Item" that was occuring on hooks when Item updates
   * were triggering other changes)
   * 
   * @param {*} document the Document with is potentially an Actor
   * @returns the actor if either the game.user is the first connected GM, or if the game.user is the owner
   *  and there is no connected GM
   */
  static documentIfResponsible(document) {
    if (Misc.isFirstConnectedGM() || (Misc.connectedGMs().length == 0 && Misc.isFirstOwnerPlayer(document))) {
      if (document?.isOwner) {
        return document
      }
    }
    return undefined
  }

  static async doIfOwner(document, action, orElse = async it => { ui.notifications.warn(`Vous n'avez pas le droit d'agir pour ${it.name}`) }) {
    if (Misc.isOwnerPlayer(document)) {
      return await action(document)
    } else {
      return await orElse(document ?? { name: '<aucune sélection>' })
    }
  }

  static isOwnerPlayer(document) {
    return document?.isOwner
  }

  static isFirstOwnerPlayer(document) {
    if (!document?.isOwner) {
      return false
    }
    return game.users.find(u => document.testUserPermission(u, CONST.DOCUMENT_OWNERSHIP_LEVELS.OWNER)) == game.user
  }

  /**
   * @returns true pour un seul utilisateur: le premier GM connecté par ordre d'id
   */
  static isFirstConnectedGM() {
    return game.users.activeGM != undefined && game.user == game.users.activeGM
  }

  static hasConnectedGM() {
    return game.users.activeGM
  }

  static firstConnectedGMId() {
    return game.users.activeGM?.id;
  }

  /* -------------------------------------------- */
  static findPlayer(name) {
    return Misc.findFirstLike(name, game.users, { description: 'joueur' });
  }

  /* -------------------------------------------- */
  static findActor(name, actors = game.actors) {
    return Misc.findFirstLike(name, actors, { description: 'acteur' });
  }

  /* -------------------------------------------- */
  static findFirstLike(value, elements, options = {}) {
    options = foundry.utils.mergeObject(DEFAULT_FIND_OPTIONS, options, { overwrite: true, inplace: false });
    const subset = this.findAllLike(value, elements, options)
    if (subset.length == 0) {
      console.log(`Aucune ${options.description} pour ${value}`);
      return undefined
    }
    if (subset.length == 1) {
      return subset[0]
    }
    let single = subset.find(it => Grammar.toLowerCaseNoAccent(options.mapper(it)) == Grammar.toLowerCaseNoAccent(value));
    if (!single) {
      single = subset[0];
      const choices = Misc.join(subset.map(it => options.mapper(it)), '<br>');
      options.onMessage(`Plusieurs choix de ${options.description}s possibles:<br>${choices}<br>Le premier sera choisi: ${options.mapper(single)}`);
    }
    return single;
  }

  static findAllLike(value, elements, options = {}) {
    options = foundry.utils.mergeObject(DEFAULT_FIND_OPTIONS, options, { overwrite: true, inplace: false });
    if (!value) {
      options.onMessage(`Pas de ${options.description} correspondant à une valeur vide`);
      return [];
    }
    value = Grammar.toLowerCaseNoAccent(value);
    const subset = elements.filter(options.preFilter)
      .filter(it => Grammar.toLowerCaseNoAccent(options.mapper(it))?.includes(value))
      .sort(Misc.ascending(it => options.mapper(it)))
    if (subset.length == 0) {
      options.onMessage(`Pas de ${options.description} correspondant à ${value}`);
    }
    return subset;
  }

  static cssRotation(angle) {
    const rotation = `rotate(${angle}deg)`;
    return {
      'transform': rotation,
      '-ms-transform': rotation,
      '-moz-transform': rotation,
      '-webkit-transform': rotation,
      '-o-transform': rotation
    };
  }

  static async random(count) {
    const roll = new Roll(`1d${count}`)
    await roll.evaluate()
    return roll.total
  }

  static shuffled(source) {
    const result = source.slice(0)
    for (let i = result.length - 1; i > 0; i--) {
        const j = Math.floor(Math.random() * (i + 1));
        [result[i], result[j]] = [result[j], result[i]];
    }
    return result
}
}
