/**
 * Contient toute la logique et la gestion des frames.<br/><br />
 *
 * Changer de tâche : FramesManager.Instance.switch_frame(task); <br/><br/>
 *
 * Obtenir la tâche en cours : FramesManager.Instance.currentTask <br/>
 *
 * @module Frames
 * @local OnFrameCreatedCallback
 * @local OnLoadCallback
 * @local FrameData
 * @local HistoryManager
 * @local FrameDataEssential
 * @local Window
 * @local FrameManager
 * @local FrameManagerWrapperHelper
 * @local FrameManagerWrapper
 * @local FramesManager
 */

import { EMPTY_STRING } from '../constants/constants.js';
import { MelWindow } from '../html/JsHtml/CustomAttributes/frames_web_elements.js';
import { HTMLButtonGroup } from '../html/JsHtml/CustomAttributes/HTMLButtonGroup.js';
import { MelHtml } from '../html/JsHtml/MelHtml.js';
import { isNullOrUndefined } from '../mel.js';
import { BnumEvent } from '../mel_events.js';
import { MelObject } from '../mel_object.js';
import { Mel_Promise } from '../mel_promise.js';
import { BaseStorage } from './base_storage.js';
import { BnumLog } from './bnum_log.js';
import { BnumMessage, eMessageType } from './bnum_message.js';
import { MelEnumerable } from './enum.js';
import { MainNav } from './main_nav.js';

export {
  FramesManager,
  FrameManager,
  MODULE as FrameManger_ModuleName,
  MODULE_CUSTOM_FRAMES as FrameManger_ModuleName_custom,
  MULTI_FRAME_FROM_NAV_BAR,
};

/**
 * Z index pour la barre des autres applications
 * @type {number}
 * @default 1000
 * @constant
 */
const OTHERAPPS_Z_INDEX = 1000;

/**
 * Callback utilisé lors de la création des frames en jshtml.
 *
 * Permet de modifier la frame en jshtml à différents endroits de la création.
 * @callback OnFrameCreatedCallback
 * @param {____JsHtml} jFrame Frame créée en jshtml
 * @param {Object} options
 * @param {boolean} [options.changepage=true] Si la frame doit être chargée en arrière plan ou non.
 * @param {?Object<string, string>}  [options.args=null] Les autres arguments pour le changement d'url.
 * @param {FrameData} frame Référence vers la frame créatrice
 * @return {____JsHtml}
 * @frommoduleparam JsHtml jFrame
 * @frommodulereturn {JsHtml}
 */

/**
 * Ajoute des actions à faire lorsque la frame est chargée.
 * @callback OnLoadCallback
 * @param {Object} options
 * @param {boolean} [options.changepage=true] Si la frame doit être chargée en arrière plan ou non.
 * @param {?Object<string, string>}  [options.args=null] Les autres arguments pour le changement d'url.
 * @return {null}
 */

/**
 * Nom du module
 * @constant
 * @type {string}
 * @default 'FrameManager'
 */
const MODULE = 'FrameManager';
/**
 * Nom du module lié aux actions custom
 * @constant
 * @type {string}
 * @default 'FrameManager_custom_actions'
 * @deprecated
 */
const MODULE_CUSTOM_FRAMES = `${MODULE}_custom_actions`;
/**
 * Nombre de fenêtres maximal que l'on peut avoir.
 * @constant
 * @type {number}
 */
const MAX_FRAME = rcmail.env['frames.max_multi_frame'];
/**
 * Si le multi-fenêtre manuel est activé.
 *
 * Si c'est le cas, au clic droit sur un bouton de la barre de navigation, le choix d'ouvrir une nouvelle fenêtre ou non sera proposé.
 * @constant
 * @type {boolean}
 */
const MULTI_FRAME_FROM_NAV_BAR = rcmail.env['frames.multi_frame_enabled'];

/**
 * @class
 * @classdesc Contient les donénes d'une frame, un lien vers ça version html et quelque actions qui peuvent l'affecter.
 * @package
 */
class FrameData {
  /**
   * Initialise les variables
   * @param {string} task Tâche lié à la frame
   * @param {Window} parent Fenêtre parente
   */
  constructor(task, parent) {
    /**
     * Tache de la frame
     * @type {string}
     * @readonly
     */
    this.task = EMPTY_STRING;

    /**
     * Nom de la frame
     * @type {string}
     * @readonly
     * @deprecated
     */
    this.name = EMPTY_STRING;

    /**
     * Frame de la tâche
     * @type {external:jQuery}
     * @readonly
     */
    this.$frame = null;

    /**
     * Id de la frame
     * @type {number}
     * @readonly
     */
    this.id = 0;

    /**
     * Fenêtre parente
     * @type {Window}
     */
    this.parent = parent;

    /**
     * Actions à faire lorsque la frame est créée
     * @type {BnumEvent<OnFrameCreatedCallback>}
     * @frommodule Frames {@linkto OnFrameCreatedCallback}
     * @event
     */
    this.onframecreated = new BnumEvent();

    /**
     * Actions à faire après que la frame est créée
     * @type {BnumEvent<OnFrameCreatedCallback>}
     * @frommodule Frames {@linkto OnFrameCreatedCallback}
     * @event
     */
    this.onframecreatedafter = new BnumEvent();

    /**
     * Actions à faire lorsque la page est chargée
     * @type {BnumEvent<OnLoadCallback>}
     * @frommodule Frames {@linkto OnLoadCallback}
     * @event
     */
    this.onload = new BnumEvent();
    Object.defineProperties(this, {
      task: {
        value: task,
        enumerable: true,
        configurable: false,
        writable: false,
      },
      name: {},
      $frame: {
        get: () => {
          return top.$(
            `#${this.parent.get_window_id()} .mm-frame.${this.task}-frame`,
          );
        },
      },
      id: {
        value: this.generate_id(),
        enumerable: true,
        configurable: false,
        writable: false,
      },
    });
  }

  /**
   * Créer une frame en JsHtml
   * @param {Object} param0
   * @param {boolean} [param0.changepage=true] Si la frame doit être chargée en arrière plan ou non.
   * @param {?Object<string, string>}  [param0.args=null] Les autres arguments pour le changement d'url.
   * @param {Array<any>} [param0.actions=[]] Mots clé à utiliser qui pourront être utiliser lors de différents callbacks
   * @returns {____JsHtml}
   * @frommodulereturn {JsHtml}
   */
  create({ changepage = true, args = null, actions = [] }) {
    /**
     * Si on ferme les balises iframes ou non
     * @type {boolean}
     * @default false
     * @package
     * @readonly
     */
    const close = false;
    /**
     * Dictionnaires des arguments en url pour dire si la page est en mode iframe ou non
     * @type {Readonly<{complete:string, key:string, value:string}>}
     * @package
     * @readonly
     */
    const frameArg = Object.freeze({
      complete: '_is_from=iframe',
      key: '_is_from',
      value: 'iframe',
    });

    //Passer en mode iframe
    args[frameArg.key] = frameArg.value;

    let jFrame = MelHtml.start
      .iframe(
        {
          id: `frame-${this.id}`,
          allow: 'clipboard-read; clipboard-write',
          title: `Page : ${this.name}`,
          class: `mm-frame ${this.task}-frame`,
          src: FrameManager.Helper.url(this.task, {
            params: args,
          }),
          onload: this._onload.bind(this, { changepage, actions }),
          'data-frame-task': this.task,
        },
        close,
      )
      .css({
        width: '100%',
        height: '100%',
        border: 'none',
        'padding-left': 0,
      });

    //Si on charge en arrière plan, on affiche pas la frame
    if (!changepage) jFrame.css('display', 'none');

    jFrame =
      this.onframecreated.call(jFrame, { changepage, args, actions }, this) ||
      jFrame;

    jFrame = jFrame.end();

    return (
      this.onframecreatedafter.call(
        jFrame,
        { changepage, args, actions },
        this,
      ) || jFrame
    );
  }

  /**
   * Est appelé au chargement de la frame.
   *
   * Appele `this.onload`.
   * @param {Object} [options={}]
   * @param {boolean} [options.changepage=true] Si la frame doit être chargée en arrière plan ou non.
   * @param {Array<string>} [options.actions=[]] Mots clé à utiliser qui pourront être utiliser lors de différents callbacks
   * @package
   */
  _onload({ changepage = true, actions = [] } = {}) {
    this.onload.call({ changepage, actions });
  }

  /**
   * Génère un id unique.
   * @returns {number}
   */
  generate_id() {
    if ($('.mm-frame').length)
      return (
        MelEnumerable.from($('.mm-frame'))
          .select((x) => +$(x).attr('id').split('-')[1])
          .max() + 1
      );
    else return 0;
  }

  /**
   * Met à jours la source de la frame
   * @param {Object<string, string>} args Arguments. Ne pas mettre la tâche.
   * @returns {FrameData} Chaîne
   */
  update_src(args) {
    let url = FrameManager.Helper.url(this.task, { params: args });

    if (!url.includes('_is_from=iframe')) url += '&_is_from=iframe';

    this.$frame.attr('src', url);
    url = null;
    return this;
  }

  /**
   * Affiche la frame
   * @returns {FrameData} Chaînage
   */
  show() {
    this.$frame.parent().show();
    return this;
  }

  /**
   * Cache la frame
   * @returns {FrameData} Chaînage
   */
  hide() {
    this.$frame.parent().hide();
    return this;
  }
}

/**
 * @class
 * @classdesc Gestion de l'historique de la fenêtre
 * @package
 */
class HistoryManager {
  /**
   * Si rcmail.env.menu_last_frame_enabled ne vaut pas vrai, alors l'historique ne sera pas activé.
   */
  constructor() {
    /**
     * Anciennes frames
     * @type {Array<string>}
     * @package
     */
    this._history = [];
    /**
     * Bouton de retour
     * @type {external:jQuery}
     */
    this.$back = null;
    Object.defineProperties(this, {
      $back: {
        get() {
          return rcmail.env.menu_last_frame_enabled
            ? $('.menu-last-frame')
            : null;
        },
      },
    });
  }

  /**
   * Ajoute une tâche à l'historique
   * @param {string} task Tâche à ajouter
   * @returns {HistoryManager} Chaînage
   */
  add(task) {
    this.update_button_back(task)._history.push(task);

    if (this._history.length > 10) this._history = this._history.slice(1);

    return this;
  }

  /**
   * Met à jours le bouton "Revenir a", change son texte et son icône.
   * @param {string} last_task Tâche à afficher
   * @returns {HistoryManager} Chaînage
   */
  update_button_back(last_task) {
    if (this.back_enabled()) {
      let $back = this.$back;

      //Gestion du premier changement de frame
      if ($back.hasClass('disabled'))
        $back.removeClass('disabled').removeAttr('disabled');

      if (!$back.find('.menu-last-frame-item').length)
        $back.append($('<span>').addClass('menu-last-frame-item'));

      //Changement du css
      const css_key = 'm_mp_CreateOrUpdateIcon';

      let font;
      let content;

      try {
        content = window
          .getComputedStyle(
            document.querySelector(`#taskmenu [data-task="${last_task}"]`),
            ':before',
          )
          .getPropertyValue('content')
          .replace(/"/g, '')
          .charCodeAt(0)
          .toString(16);

        font = window
          .getComputedStyle(
            document.querySelector(`#taskmenu [data-task="${last_task}"]`),
            ':before',
          )
          .getPropertyValue('font-family');
      } catch (error) {
        content = EMPTY_STRING;
        font = 'DWP';
      }

      if (last_task === 'settings') {
        content = 'e926';
        font = 'DWP';
      }

      FrameManager.Helper.get_custom_rules().remove(css_key);
      FrameManager.Helper.get_custom_rules().addAdvanced(
        css_key,
        '.menu-last-frame-item:before',
        `content:"\\${content}"`,
        `font-family:${font}`,
      );

      //Changement du text
      const text = rcmail.gettext('last_frame_opened', 'mel_metapage');
      const down = $(`#layout-menu a[data-task="${last_task}"]`)
        .find('.inner')
        .html();

      $back = $back.find('.inner');

      if (!$back.find('.menu-last-frame-inner-up').length) {
        $back.html(
          MelHtml.start
            .span({ class: 'menu-last-frame-inner-up' })
            .text(text)
            .end()
            .span({ class: 'menu-last-frame-inner-down' })
            .text(down)
            .end()
            .generate(),
        );
      } else {
        $back.find('.menu-last-frame-inner-down').html(down);
      }
    }

    return this;
  }

  /**
   * Affiche la page d'historique.
   *
   * Affiche un volet à gauche avec les anciennes frames.
   * @returns {void}
   */
  show_history() {
    /**
     * Quitte l'historique
     * @type {Function}
     * @package
     * @readonly
     */
    const quit = () => {
      $('#frame-mel-history').remove();
      $('#black-frame-mel-history').remove();
      $('#layout-menu').css('left', '0');
    };

    //Si un historique est déjà ouvert pour une raison X ou Y, on le supprime pour éviter les conflits
    if ($('#frame-mel-history').length > 0) {
      $('#frame-mel-history').remove();
      $('#black-frame-mel-history').remove();
    }

    //Création de la page
    //TODO: Template ou jshtml
    let $history = $(
      '<div id="frame-mel-history"><div style="margin-top:5px"><center><h3>Historique</h3></center></div></div>',
    );
    let $buttons = $('<div id="frame-buttons-history"></div>').appendTo(
      $history,
    );

    for (let index = this._history.length - 1; index >= 0; --index) {
      const element = $(`#layout-menu a[data-task="${this._history[index]}"]`)
        .find('.inner')
        .text();
      $buttons.append(
        $(
          `<button class="history-button mel-button btn btn-secondary no-button-margin color-for-dark bckg true"><span class="history-text">${element}</span></button>`,
        ).click(
          function (task) {
            FramesManager.Helper.current.switch_frame(task, {});
            quit();
          }.bind(this, this._history[index]),
        ),
      );
    }

    $('#layout')
      .append($history)
      .append(
        $('<div id="black-frame-mel-history"></div>').click(() => {
          quit();
        }),
      );

    $('#layout-menu').css('left', '120px');
  }

  /**
   * Si l'historique est activé ou non
   * @returns {boolean}
   */
  back_enabled() {
    return !!this.$back;
  }

  /**
   * Revien en arrière dans l'historique
   * @param {Object} [options={}]
   * @param {?string} [options.defaultFrame=null] fallback, si l'historique est vide, sur quel frame on revient ?
   * @returns {Promise<*>}
   * @async
   */
  back({ defaultFrame = null } = {}) {
    return FramesManager.Helper.current.switch_frame(
      this._history?.[this._history.length - 1] ?? defaultFrame,
      {},
    );
  }
}

/**
 * Contient seulement les données d'une frame data, sans les fonctions
 * @typedef FrameDataEssential
 * @property {string | number} id
 * @property {string} task
 * @property {Window} parent
 *
 */

/**
 * @class
 * @classdesc Fenêtre qui gère plusieurs frames
 * @package
 */
class Window {
  /**
   * Initialise les variables
   * @param {string | number} uid Id de la fenêtre
   */
  constructor(uid) {
    this._init()._setup(uid);
  }

  /**
   * Données de la frame en cours
   * @type {Readonly<FrameDataEssential>}
   * @readonly
   * @frommodule Frames {@linkto FrameDataEssential}
   */
  get currentFrameData() {
    return Object.freeze({
      id: this._current_frame.id,
      task: this.currentTask,
      parent: this._current_frame.parent,
    });
  }

  /**
   * Page en cours
   * @type {string}
   * @readonly
   */
  get currentTask() {
    return this._current_frame.task;
  }

  /**
   * Initialise les variables
   * @private
   * @returns {Window} Chaîne
   */
  _init() {
    /**
     * Historique des frames
     * @type {HistoryManager}
     * @package
     */
    this._history = new HistoryManager();
    /**
     * Liste des frames ouvertes
     * @type {BaseStorage<FrameData>}
     * @frommodule BaseStorage
     * @frommodule2 Frames {@linkto FrameData}
     * @package
     */
    this._frames = new BaseStorage();
    /**
     * Frame en cours
     * @type {FrameData}
     * @package
     */
    this._current_frame = null;
    /**
     * Id de la fenêtre
     * @type {number}
     * @package
     */
    this._id = 0;
    /**
     * Si la fenêtre peut-être séléctionnée ou non
     * @type {boolean}
     * @package
     */
    this._can_be_selected = true;
    return this;
  }

  /**
   * Associe les variables
   * @param {number} uid
   * @returns {Window} Chaîne
   * @private
   */
  _setup(uid) {
    this._id = uid;
    return this;
  }

  /**
   * Créer une frame
   * @param {string} task Tâche à afficher
   * @param {Object} [options={}]
   * @param {boolean} [options.changepage=true] Si on charge la frame en arrière plan ou non
   * @param {?Object<string, string>} [options.args=null] Arguments - sauf la tâche - de la l'url à ajouter à la source de la frame
   * @param {string[]} [options.actions=[]] Mots clés qui pourront être utiliser dans différents callbacks
   * @returns {Mel_Promise<Window>}
   * @package
   * @async
   * @frommodulereturn Frames {@linkto Window}
   */
  _create_frame(task, { changepage = true, args = null, actions = [] } = {}) {
    return new Mel_Promise((promise) => {
      promise.start_resolving();

      if (!args) args = {};

      //Action externes à faire avant la création
      this._trigger_event('frame.create.before', {
        task,
        changepage,
        args,
        actions,
      });

      if (changepage) {
        this._trigger_event('frame.create.changepage.before', {
          task,
          changepage,
          args,
          actions,
        });
      }

      //Création de la frame qui contient les layouts si elle n'existe pas
      if (!$('#layout-frames').length)
        $('#layout').append(this._generate_layout_frames());

      //Création de la fenêtre si elle n'éxiste pas
      if (!this.get_window().length)
        $('#layout-frames').append(this._generate_window());

      /**
       * Id du loader
       * @type {string}
       * @constant
       * @default `frameid${this._id}`
       */
      const frame_id = `frameid${this._id}`;

      //Si on change de page, on affiche un loader
      if (changepage)
        MelObject.Empty()
          .generate_loader(frame_id, true)
          .generate()
          .appendTo(this.get_window());

      //Ajoute la frame à l'historique et cache l'ancienne frame
      if (this._current_frame && changepage) {
        this._history.add(this._current_frame.task);
        this._current_frame.hide();
      }

      //On créer une variable locale pour éviter les conflits suite à l'asynchrone
      let current_frame = new FrameData(task, this);

      // Voir _first_load
      current_frame.onload.add(
        'resolve',
        this._first_load.bind(
          this,
          promise,
          frame_id,
          task,
          changepage,
          args,
          actions,
        ),
      );

      /*
        Au chargement de la frame, vire les éléments indésirable, et 
        si le multi-fenêtre est activé, gérer la séléction de la fenêtre
      */
      current_frame.onload.push(() => {
        let querry_content = this.get_frame()?.[0]?.contentWindow;
        const _$ = querry_content?.$;

        if (_$) {
          _$('#layout-menu').remove();
          _$('.barup').remove();
          _$('html').addClass('framed');
        }

        //Pas besoin d'aller plus loin si le multi-frame est désactivé
        if (!MULTI_FRAME_FROM_NAV_BAR) return;

        //Si on a Jquery
        if (this.get_frame()[0].contentWindow.$) {
          //pour tout les iframes de l'iframe
          for (let iterator of this.get_frame()[0].contentWindow.$('iframe')) {
            //on ajoute un listener sur chaque frames chargés
            iterator.contentWindow.document.addEventListener('click', () => {
              if (!this.is_selected()) {
                FramesManager.Helper.current.unselect_all();
                FramesManager.Helper.current.select_window(this._id);
              }
            });

            iterator.onload = (e) => {
              e = e.srcElement;
              e.contentWindow.document.addEventListener('click', () => {
                if (
                  FramesManager.Instance.manual_multi_frame_enabled() &&
                  !this.is_selected()
                ) {
                  FramesManager.Helper.current.unselect_all();
                  FramesManager.Helper.current.select_window(this._id);
                }
              });
            };
          }
        }

        this.get_frame()[0].contentWindow.document.addEventListener(
          'click',
          () => {
            if (
              FramesManager.Instance.manual_multi_frame_enabled() &&
              !this.is_selected()
            ) {
              FramesManager.Helper.current.unselect_all();
              FramesManager.Helper.current.select_window(this._id);
            }
          },
        );
      });

      //Création de la frame
      let tmp_frame = current_frame.create({ changepage, args, actions });

      //if (!changepage) tmp_frame.first().css('display', 'none');

      //La fenêtre est un webcomponent, on utilise les fonctions de celui-ci pour créer la frame
      //sinon, ça risque de ne pas ce comporter correctement
      this.get_window()[0].add_frame(tmp_frame);

      this._frames.add(task, current_frame);

      if (changepage) {
        this._current_frame = current_frame;
        this.select();
      } else current_frame.hide();

      //Gestion si il y a d'autres fenêtres
      if (this.has_other_window()) $('.mel-windows').addClass('multiple');
      else $('.mel-windows').removeClass('multiple');

      current_frame = null;
      tmp_frame = null;
    });
  }

  /**
   * Ouvre une frame déjà ouverte.
   * @param {string} task Frame à ouvrir
   * @param {Object}  [options={}]
   * @param {?Object<string, string>} [options.new_args=null] Nouveau arguments à ajouter à la frame. Si ils éxistent, force le rechargement de la frame.
   * @returns {Promise<Window>}
   * @async
   * @frommodulereturn Frames {@linkto Window}
   */
  async _open_frame(task, { new_args = null } = {}) {
    //Ajoute à l'historique et cache l'ancienne frame
    this._history.add(this._current_frame.task);
    this._current_frame.hide();
    this._current_frame = this._frames.get(task);

    //Met à jour la source de la frame et attend qu'elle soit chargée
    if (new_args && Object.keys(new_args).length > 0) {
      await new Mel_Promise((promise) => {
        promise.start_resolving();
        this._current_frame.onload.add('src_updated', () => {
          this._current_frame.onload.remove('src_updated');
          promise.resolve(true);
        });
        this._current_frame.update_src(new_args);
      });
    }

    //Affiche la frame
    this._current_frame.show();

    this._trigger_event('frame.opened', {
      task,
      changepage: true,
      args: new_args,
      actions: [],
      manager: this,
      first: false,
    });

    return this;
  }

  /**
   * Premier chargement d'une frame. Est supprimé après la fin de l'éxécution de la fonction
   * @param {Mel_Promise<Window>} promise Promesse en cour d'éxécution
   * @param {string} frame_id Id de la frame en cours
   * @param {string} task Tâche en cours
   * @param {boolean} changepage Si on charge la frame seulement ou si on l'affiche aussi
   * @param {?Object<string, string>} args Arguments à ajouter en plus à l'url
   * @param {Array[string]} actions Mot clé pouvent être utiliser dans les différents callbacks
   * @package
   * @frommoduleparam Frames promise {@linkto Window}
   */
  _first_load(promise, frame_id, task, changepage, args, actions) {
    let current = this._frames.get(task);

    //On supprime le premier chargement de la liste des évènements, il ne servira plus
    current.onload.remove('resolve');

    if (changepage) $('#layout-frames').css('display', EMPTY_STRING);

    this._trigger_event('frame.loaded', {
      task,
      changepage,
      args,
      actions,
      manager: FramesManager.Helper.current,
      window: this,
    });
    this._trigger_event('frame.opened', {
      task,
      changepage,
      args,
      actions,
      manager: FramesManager.Helper.current,
      first: true,
      window: this,
    });

    if (changepage) {
      this._current_frame.show();
      this.get_window().find(`#${frame_id}`).remove();
    }

    promise.resolve(this);
  }

  /**
   * Génère la fenêtre en jshtml
   * @returns {MelWindow}
   * @package
   */
  _generate_window() {
    return MelWindow.CreateNode(this._id); //MelHtml.start.mel_window(this._id).end();
  }

  /**
   * Génère la div qui contiendra les fenêtre
   * @returns {HTMLDivElement}
   * @package
   */
  _generate_layout_frames() {
    let frames = document.createElement('div');
    frames.setAttribute('id', 'layout-frames');

    if ($('#layout-menu').css('position') !== 'fixed')
      $('#layout-menu').css('position', 'fixed');

    return frames;
  }

  /**
   * Trigger un évènement rcmail et MelObject
   * @private
   * @param {string} key Clé du listener
   * @param {Array} args Arguments
   * @returns {Window} Chaînage
   */
  _trigger_event(key, args) {
    FrameManager.Helper.trigger_event(key, args);
    rcmail.triggerEvent(key, args);
    return this;
  }

  /**
   * Change de frame.
   *
   * Si la frame n'existe pas la créer, sinon l'ouvre.
   * @param {string} task Tâche à ouvrir
   * @param {Object} [options={}]
   * @param {boolean} [options.changepage=true] Si on charge la frame en arrière plan ou non
   * @param {?Object<string, string>} [options.args=null] Arguments - sauf la tâche - de la l'url à ajouter à la source de la frame
   * @param {Array<string>} [options.actions=[]] Mot clés pouvant être utiliser lors des différents callbacks
   * @return {Promise}
   * @async
   */
  async switch_frame(task, { changepage = true, args = null, actions = [] }) {
    //Si la fenêtre est cachée et qu'on change de page, on l'affiche
    if (changepage && this.is_hidden()) this.show();

    //On Séléctionne la page en cours
    if (changepage) MainNav.select(task);

    //Ne prend pas en compte le changement de titre si l'attache "before_url" renvoie "break"
    const break_next = rcmail.triggerEvent('frames.attach.url.before'); //FramesManager.Instance.call_attach('before_url') === 'break';

    if (!break_next && changepage) {
      Window.UpdateNavUrl(Window.UrlFromTask(task));
      Window.UpdateDocumentTitle(Window.GetTaskTitle(task));
    }

    //Appelle les ataches lié à l'url
    //FramesManager.Instance.call_attach('url');
    rcmail.triggerEvent('frames.attach.url');

    if (this._frames.has(task)) {
      if (changepage) await this._open_frame(task, { new_args: args });
    } else await this._create_frame(task, { changepage, args, actions });

    if (!break_next) {
      this.get_window()
        .find('.mel-window-header .mel-window-title')
        .text(Window.GetTaskTitle(task));
    }

    //Focus le titre de la frame, le cas échéant, le titre de la page
    if (changepage) {
      this._current_frame.$frame.focus();

      if (
        this._current_frame.$frame?.[0]?.contentWindow?.$?.(
          '#sr-document-title-focusable',
        )?.length
      ) {
        this._current_frame.$frame[0].contentWindow
          .$('#sr-document-title-focusable')
          .focus();
      } else {
        await Mel_Promise.wait(
          () => this._current_frame.$frame?.[0]?.contentWindow?.$,
        );

        if (this._current_frame.$frame?.[0]?.contentWindow?.$) {
          this._current_frame.$frame[0].contentWindow
            .$('body')
            .prepend(
              $('<div>')
                .attr('id', 'sr-document-title-focusable')
                .addClass('sr-only')
                .attr('tabindex', '-1')
                .text(document.title),
            )
            .find('.sr-document-title-focusable')
            .focus();
        } else {
          this._current_frame.$frame.focus();
        }
      }

      //Gestion du multi-fenêtre
      if (this.has_other_window()) $('.mel-windows').addClass('multiple');
      else $('.mel-windows').removeClass('multiple');
    }
  }

  /**
   * Sélectionne la fenêtre
   * @returns {Window} Chaîne
   */
  select() {
    if (this._can_be_selected) {
      this.get_window()
        .addClass('selected')
        .find('.mel-window-header .mel-window-title')
        .text(Window.GetTaskTitle(this._current_frame.task));
    }
    return this;
  }

  /**
   * Déselectionne la fenêtre
   * @returns {Window} Chaîne
   */
  unselect() {
    this.get_window().removeClass('selected');
    return this;
  }

  /**
   * Vérifie si la fenêtre est séléctionnée ou non
   * @returns {Boolean}
   */
  is_selected() {
    return this.get_window().hasClass('selected');
  }

  /**
   * Active le tag "remove_on_change".
   *
   * Lorsque le tag est activé, si il y a un change de frame, on supprime les autres fenêtre ayant ce tag d'activé.
   * @returns {Window} Chaînage
   */
  set_remove_on_change() {
    this._remove_on_change = true;
    return this;
  }

  /**
   * Désactive le tag "remove_on_change".
   *
   * Lorsque le tag est activé, si il y a un change de frame, on supprime les autres fenêtre ayant ce tag d'activé.
   * @returns {Window} Chaînage
   */
  unset_remove_on_change() {
    this._remove_on_change = false;
    return this;
  }

  /**
   * Vérifie l'état du tag "remove_on_change".
   *
   * Lorsque le tag est activé, si il y a un change de frame, on supprime les autres fenêtre ayant ce tag d'activé.
   * @returns {boolean}
   */
  is_remove_on_change() {
    return this._remove_on_change;
  }

  /**
   * Active le fait que la feneêtre ne peut pas être séléctionnée
   * @returns {Window} Chaînage
   */
  set_cannot_be_select() {
    this._can_be_selected = false;
    return this;
  }

  /**
   * Active le fait que la fenêtre peut être séléctionnée
   * @returns {Window} Chaînage
   */
  set_can_be_select() {
    this._can_be_selected = true;
    return this;
  }

  /**
   * Vérifie si la fenêtre peut être séléctionnée
   * @returns {Boolean}
   */
  can_be_selected() {
    return this._can_be_selected ?? true;
  }

  /**
   * Vérifie si il y a d'autres fenêtres
   * @returns {boolean}
   */
  has_other_window() {
    return $('#layout-frames .mel-windows').length > 1;
  }

  /**
   * Récupère l'id de la fenêtre
   * @returns {string}
   */
  get_window_id() {
    return `mel-window-${this._id}`;
  }

  /**
   * Récupère la fenêtre
   * @returns {external:jQuery}
   */
  get_window() {
    return $(`#${this.get_window_id()}`);
  }

  /**
   * Récupère la frame
   * @param {string} task Une frame spécifique ou celle en cours.
   * @returns {external:jQuery}
   */
  get_frame(task = null) {
    const frame = !task ? this._current_frame : this._frames.get(task);
    return frame.$frame;
  }

  /**
   * Supprime une frame
   * @param {string} task Tâche à supprimer
   * @returns  {Window} Chaînage
   */
  remove_frame(task) {
    let frame = this._frames.get(task);
    this._frames.remove(task);

    frame.$frame.parent().remove();
    return this;
  }

  /**
   * Simule un "f5" sur la frame en cours <br/>
   *
   * Evènements trigger : <br/>
   *
   * - frame.refresh.manual {stop:string, caller:Window} <br/>
   * @returns {Promise<Window>} Chaînage
   * @frommodulereturn Frames {@linkto Window}
   */
  async refresh() {
    const plugin = rcmail.triggerEvent('frame.refresh.manual', {
      stop: false,
      caller: this,
    }) ?? { stop: false };

    if (plugin.stop) return this;

    const url = this.get_frame()[0].contentWindow.location.href;
    if (!url) this.get_frame()[0].contentWindow.location.reload();
    else {
      const task = this._current_frame.task;
      let args = {};

      for (const element of url.split('?_task=')[1].split('&')) {
        const [key, value] = element.split('=');

        if (!isNullOrUndefined(value) && !!key && !key.includes('_task'))
          args[key] = value;
      }

      this.remove_frame(task);

      await this.switch_frame(task, {
        args,
      });
    }

    return this;
  }

  /**
   * Supprime la fenêtre
   * @returns {Window} Chaîne
   */
  delete() {
    this.get_window().remove();
    return this;
  }

  /**
   * Change l'id de la feneêtre
   * @param {number | string} new_id Nouvelle id
   * @returns {Window} Chaînage
   */
  update_id(new_id) {
    this.get_window().attr('id', `mel-window-${new_id}`);
    this._id = new_id;
    return this;
  }

  /**
   * Vérifie si une frame lié à une tâche éxiste
   * @param {string} task  Tâche
   * @returns {boolean}
   */
  has_frame(task) {
    return this._frames.has(task);
  }

  /**
   * Cache la fenêtre
   * @returns {Window} Chaîne
   */
  hide() {
    this.get_window().css('display', 'none');

    return this;
  }

  /**
   * Affiche la fenêtre
   * @returns {Window} Chaîne
   */
  show() {
    this.get_window().css('display', EMPTY_STRING);

    return this;
  }

  /**
   * Vérifie si la fenêtre est affiché ou non
   * @returns {boolean} Chaîne
   */
  is_hidden() {
    return this.get_window().css('display') === 'none';
  }

  /**
   * Ajoute un tag à la fenêtre.
   *
   * Les tags sont des attributs `data` ajouté à la balise, il ont la forme :
   *
   * `data-ftag-${tag_name}=true`
   * @param {string} tag_name Nom du tag
   * @returns {Window}
   */
  add_tag(tag_name) {
    this.get_window().attr(`data-ftag-${tag_name}`, true);

    return this;
  }

  /**
   * Supprime un tag à la fenêtre
   * @param {string} tag_name Nom du tag
   * @returns {Window} Chaîne
   */
  remove_tag(tag_name) {
    this.get_window().removeAttr(`data-ftag-${tag_name}`);

    return this;
  }

  /**
   * Vérifie si un tag existe
   * @param {string} tag_name Nom du tag
   * @returns {boolean}
   */
  has_tag(tag_name) {
    return this.get_window().attr(`data-ftag-${tag_name}`) ?? false;
  }

  /**
   *
   * @returns Promise<void>
   */
  async back({ defaultFrame = null } = {}) {
    return await this._history.back({ defaultFrame });
  }

  *[Symbol.iterator]() {
    for (const element of this._frames) {
      yield element.value;
    }
  }

  /**
   * Met à jours l'url
   * @param {string} url Nouvelle url
   * @param {boolean} [top_context=false] Si on change l'url du document en cours ou du document en top.
   * @static
   */
  static UpdateNavUrl(url, top_context = false) {
    (top_context ? top : window).history.replaceState({}, document.title, url);
  }

  /**
   * Met à jours le titre du document ou de l'onglet.
   *
   * Met aussi à jours le titre focusable.
   * @param {string} new_title Nouveau titre
   * @param {boolean} [top_context=false] Si on change l'url du document en cours ou du document en top.
   * @static
   */
  static UpdateDocumentTitle(new_title, top_context = false) {
    (top_context ? top : window).document.title = new_title;
    $('.sr-document-title-focusable').text(document.title);
  }

  /**
   * Créer une url depuis une tâche
   * @param {string} task Tâche
   * @returns {string} Nouvelle url
   * @static
   */
  static UrlFromTask(task) {
    return rcmail.get_task_url(
      task,
      window.location.origin + window.location.pathname,
    );
  }

  /**
   * Récupère le titre depuis une tâche
   * @param {string} task
   * @returns {external:jQuery}
   */
  static GetTaskTitle(task) {
    return $(
      `#layout-menu a[data-task="${task}"], #otherapps a[data-task="${task}"]`,
    )
      .find('.inner, .button-inner')
      .text();
  }
}

/**
 * @class
 * @classdesc Classe principale qui gère la gestion des frames. Contient une liste de fenêtres.
 * @package
 */
class FrameManager {
  constructor() {
    this._init()._main();
  }

  /**
   * @private
   * @returns {FrameManager}
   */
  _init() {
    /**
     * Liste des fenêtres
     * @package
     * @type {BaseStorage<Window>}
     * @frommodule BaseStorage
     * @frommodule2 Frames {@linkto Window}
     */
    this._windows = new BaseStorage();
    /**
     * Fenêtre en cours
     * @package
     * @type {Window}
     */
    this._selected_window = null;

    /**
     * Si le mutliframe gérer par l'utilisateur est actif ou non
     * @package
     * @type {string}
     */
    this._manual_multi_frame_enabled = true;

    /**
     * Liste des modes de fenêtrage
     * @type {Object<string, Function>}
     * @package
     */
    this._modes = {};

    /**
     * Liste des attaches.
     *
     * Les attaches sont des actions supplémentaires qui peuvent "parasiter" des actions du changement de frame.
     * @type {Object<string, Function>}
     * @package
     */
    this._attaches = {};

    /**
     * Raccourcis vers layout-frames
     * @readonly
     * @type {external:jQuery}
     */
    this.$layout_frames = null;

    Object.defineProperties(this, {
      $layout_frames: {
        get: () => $('#layout-frames'),
      },
    });

    return this;
  }

  /**
   * Cache le layout-content.
   * @private
   */
  _main() {
    if (rcmail.env.task === 'bnum')
      $('#layout-content').addClass('hidden').css('display', 'none');
  }

  /**
   * Génère le menu multifenêtre
   * @package
   * @param {external:jQuery} $element
   * @returns {any}
   */
  _generate_menu($element) {
    const task = $element.data('task');
    const max_frame_goal = this._windows.length >= MAX_FRAME;
    const button_disabled = !this._manual_multi_frame_enabled || max_frame_goal;
    let buttons = ['open', 'new'];

    if (MULTI_FRAME_FROM_NAV_BAR) {
      buttons.push('column');
    }

    let node = HTMLButtonGroup.CreateNode(buttons, {
      texts: [
        'Ouvrir',
        'Ouvrir dans un nouvel onglet',
        'Ouvrir dans une nouvelle colonne',
      ],
      voice: 'Actions supplémentaires',
    });

    if (MULTI_FRAME_FROM_NAV_BAR && button_disabled) {
      node
        .getButton(2)
        .addClass('disabled')
        .setAttribute('disabled', 'disabled');
    }

    node.addEventListener(
      'api:button.clicked',
      function (args, event) {
        $('#popoverback').remove();

        switch (event.id) {
          case 'open':
            $(`[data-task="${args.task}"]`).click();
            break;

          case 'new':
            open(FrameManager.Helper.url(args.task, {}));
            break;

          case 'column':
            this.open_another_window(args.task, {});
            break;

          default:
            break;
        }
      }.bind(this, { task, max_frame_goal, button_disabled }),
    );

    return node;
  }

  /**
   * Ajout un tag aux layout-frames.
   *
   * Les tags ont le format `data-tag-${tag}`.
   * @param {string} tag
   */
  add_tag(tag) {
    let $layout = this.$layout_frames;
    if ($layout.length) {
      $layout.attr(`data-tag-${tag}`, 1);
    }
  }

  /**
   * Supprime un tag aux layout-frames
   * @param {string} tag
   */
  remove_tag(tag) {
    let $layout = this.$layout_frames;
    if ($layout.length) {
      $layout.removeAttr(`data-tag-${tag}`);
    }
  }

  /**
   * Change de frame.
   *
   * Soit une fenêtre précise soit celle en cours.
   *
   * Liste des attaches :
   *
   * - switch_frame (task:string, changepage:boolean, args:?Object<string, string>, actions:string[], wind:?number) => ?string
   *
   * Liste des triggers :
   *
   * - switch_frame ({task:string, changepage:boolean, args:?Object<string, string>, actions:string[], wind:?number}) => ?boolean
   *
   * - switch_frame.after ({task:string, changepage:boolean, args:?Object<string, string>, actions:string[], wind:?number}) => void
   * @param {string} task Tâche à afficher
   * @param {Object} [param1={}]
   * @param {boolean} [param1.changepage=true] Si on charge la frame en arrière plan ou non
   * @param {?Object<string, string>} [param1.args=null] Arguments - sauf la tâche - de la l'url à ajouter à la source de la frame
   * @param {string[]} [param1.actions=[]] Mot clé pouvant être utiliser dans les différents callbacks
   * @param {?number} [param1.wind=null] Id de la fenêtre à changer. Si null, celle en cours.
   * @returns {Promise}
   */
  async switch_frame(
    task,
    { changepage = true, args = null, actions = [], wind = null } = {},
  ) {
    //Action à faire avant le changement de frame
    let quit =
      this.call_attach(
        'switch_frame',
        task,
        changepage,
        args,
        actions,
        wind,
      ) === 'break';

    if (quit) return;

    quit = rcmail.triggerEvent('switch_frame', {
      task,
      changepage,
      args,
      actions,
      wind,
    });

    if (quit) return;
    else quit = null;

    if (wind !== null) {
      if (rcmail.busy) return;
      else if (changepage) BnumMessage.SetBusyLoading();

      if (changepage) this.close_windows_to_remove_on_change();

      if (!this._windows.has(wind)) {
        this._windows.add(wind, new Window(wind));
      }

      if (this._selected_window?._id !== wind) {
        this._selected_window?.unselect?.();
        this._selected_window = this._windows.get(wind);
      }

      await this._selected_window.switch_frame(task, {
        changepage,
        args,
        actions,
      });
      this._selected_window.select();

      if (changepage) BnumMessage.StopBusyLoading();

      rcmail.triggerEvent('switch_frame.after', {
        task,
        changepage,
        args,
        actions,
        wind,
      });

      return this._selected_window;
    } else
      return await this.switch_frame(task, {
        changepage,
        args,
        actions,
        wind: this._selected_window?._id ?? 0,
      });
  }

  /**
   * Vérifie si il y a plusieurs fenêtres
   * @returns {boolean}
   */
  has_multiples_windows() {
    return this._selected_window.has_other_window();
  }

  /**
   * Ajoute les actions sur les boutons de navigation principale
   */
  add_buttons_actions() {
    let $it;

    $('#otherapps').css('z-index', OTHERAPPS_Z_INDEX);

    for (const iterator of $('#taskmenu a, #otherapps a')) {
      $it = $(iterator);

      $it
        .attr('data-task', $it.attr('href').split('=')[1])
        .attr('href', '#')
        .attr('onclick', EMPTY_STRING)
        .parent()
        .addClass('not-busy-only');

      if ($it.hasClass('menu-last-frame'))
        $it
          .click(() => {
            this._selected_window._history.back();
          })
          .on('mousedown', (e) => {
            if (e.button === 1) {
              e.preventDefault();
              return this._selected_window._history.show_history();
            }
          })
          .on('contextmenu', () => {
            this._selected_window._history.show_history();
          });
      else if ($it.hasClass('more-options')) {
        $it.click(() => {
          rcmail.command('more_options');
        });
      } else if ($it.attr('data-command')) {
        $it.click((e) => {
          rcmail.command($(e.currentTarget).attr('data-command'));
        });
      } else if ($it.attr('id') === 'menu-small') continue;
      else {
        $it.click(this.button_action.bind(this));
      }

      // $it.click(() => {
      //   $('#popoverback').remove();
      // })

      if (!$it.hasClass('menu-last-frame') && !$it.attr('data-command')) {
        $it.on(
          'auxclick',
          function (args, e) {
            if (e.originalEvent.button === 1) {
              e.preventDefault();

              open(FrameManager.Helper.url(args.task, {}));
            }
          }.bind(this, { task: $it.attr('data-task') }),
        );
        // $it
        // .popover({
        //   trigger: 'manual',
        //   content: this._generate_menu.bind(this, $it),
        //   html: true,
        // })
        // // .on('hide.bs.popover', () => {
        // //   $('#popoverback').remove();
        // // })
        // .on('contextmenu', (e) => {
        //   e.preventDefault();
        //   $('#layout-menu [aria-describedby], #otherapps [aria-describedby]').each((i, e) => {
        //     $(e).popover('hide');
        //   });
        //    $('#popoverback').remove();
        //   $(e.currentTarget).popover('show');
        //   //console.log('$(e.currentTarget)', $(e.currentTarget));
        //   $('body').prepend($('<div>').click(() => $('#popoverback').remove()).attr('id', 'popoverback').css({width:'100%', height:'100%', position:'absolute', top:0, left:0, 'z-index':1, 'background-color':'var(--ui-widget-overlay)'}));
        // });
      }
    }

    // if (!window.fmbodyoptionsa) {
    //   $('.barup, #layout-menu').click(() => {
    //     $('#popoverback').remove();
    //   });

    //   window.fmbodyoptionsa = true;
    // }
  }

  /**
   * Désélectionne toute les fenêtres
   * @returns {FrameManager} Chaînage
   */
  unselect_all() {
    for (const iterator of this._windows) {
      iterator.value.unselect();
    }

    return this;
  }

  /**
   * Sélectionne une fenêtre
   * @param {number} id Id de la fenêtre
   * @returns {FrameManager} Chaînage
   */
  select_window(id) {
    {
      const tmp_window = this._windows.get(id).select();

      if (!tmp_window.can_be_selected()) return this;

      this._selected_window = tmp_window;
    }

    const task = this.currentTask;

    MainNav.select(task);
    Window.UpdateNavUrl(Window.UrlFromTask(task));
    Window.UpdateDocumentTitle(Window.GetTaskTitle(task));

    return this;
  }

  /**
   * Supprime une fenâtre
   * @param {number} id id de la fenêtre à supprimer
   * @returns {FrameManager} Chaînage
   */
  delete_window(id) {
    this._windows.get(id).delete();
    this._windows.remove(id);

    let array = MelEnumerable.from(this._windows)
      .where((x) => !!x?.value)
      .select((x) => x.value)
      .toArray();

    this._windows.clear();

    for (let index = 0, len = array.length; index < len; ++index) {
      array[index].update_id(index);
      this._windows.add(index, array[index]);
    }

    this.select_window(array.length - 1);

    array.length = 0;
    array = null;

    if (!this.has_multiples_windows())
      $('.mel-windows').removeClass('multiple');

    return this;
  }

  /**
   * Ajoute un mode.
   * @param {string} name Nom du mode
   * @param {function} callback Callback du mode
   * @returns {FrameManager} Chaînage
   */
  add_mode(name, callback) {
    this._modes[name] = callback;
    return this;
  }

  /**
   * Démarre un mode
   * @param {string} name Nom du mode
   * @param  {...any} args
   * @returns {any}
   */
  start_mode(name, ...args) {
    const func = this._modes[name];

    if (func) return func(...args);
  }

  /**
   * Créer une fenêtre
   * @param {string | number} uid Id de la fenêtre
   * @param {?external:jQuery} [$parent=null] Element parent
   * @returns {{win:external:jQuery, $parent:external:jQuery}}
   */
  create_window(uid, $parent = null) {
    let win = new Window(uid);

    if ($parent === null) {
      $parent = $('<div>').class('fixed-window').appendTo($('#layout'));
    }

    $parent.append(win._generate_window());

    return {
      win,
      $parent,
    };
  }

  /**
   * Ouvre une nouvelle fenêtre
   * @param {string} task
   * @param {?Object<string, string>} [args=null]
   * @returns {Promise<void>}
   */
  async open_another_window(task, args = null) {
    if (this._windows.length >= MAX_FRAME) {
      BnumMessage.DisplayMessage(
        `Vous ne pouvez pas avoir au dessus de ${MAX_FRAME} pages dans le Bnum.`,
        eMessageType.Error,
      );
    } else {
      return await this.switch_frame(task, {
        args,
        wind:
          MelEnumerable.from(this._windows.keys)
            .select((x) => +x)
            .max() + 1,
      });
    }
  }

  /**
   * Attache une action
   * @param {string} action Nom de l'action
   * @param {Function} callback
   * @returns {Window} Chaînage
   */
  attach(action, callback) {
    this._attaches[action] = callback;

    return this;
  }

  /**
   * Supprime une action
   * @param {string} action Action à supprimer
   * @returns {Window} Chaînage
   */
  detach(action) {
    return this.attach(action, null);
  }

  /**
   * Appèle une action
   * @param {string} action Action à appeller
   * @param  {...any} args
   * @returns {*}
   */
  call_attach(action, ...args) {
    return this._attaches[action]?.(...args);
  }

  /**
   * Vérifie si une frame de la fenêtre séléctionnée éxiste
   * @param {string} task
   * @returns {boolean}
   */
  has_frame(task) {
    return this._selected_window.has_frame(task);
  }

  /**
   * Désactive le fait qu'un utilisateur puisse lancer plusieurs fenêtres
   * @returns {Window} Chaînage
   */
  disable_manual_multiframe() {
    this._manual_multi_frame_enabled = false;
    return this;
  }

  /**
   * Active le fait qu'un utilisateur puisse lancer plusieurs fenêtres
   * @returns {Window} Chaînage
   */
  enable_manual_multiframe() {
    this._manual_multi_frame_enabled = true;
    return this;
  }

  /**
   * Vérifie le fait qu'un utilisateur puisse lancer plusieurs fenêtres
   * @returns {boolean}
   */
  manual_multi_frame_enabled() {
    return this._manual_multi_frame_enabled;
  }

  /**
   * Démarre un multi-fenêtre custom. CAD que la barre au dessus de la fenêtre ne va pas s'afficher.
   * @returns {Window} Chaînage
   */
  start_custom_multi_frame() {
    $('body').addClass('multiframe-header-disabled');
    return this;
  }

  /**
   * Arrête un multi-fenêtre custom.
   * @returns {Window} Chaînage
   */
  stop_custom_multi_frame() {
    $('body').removeClass('multiframe-header-disabled');
    return this;
  }

  /**
   * Cache toute les fenêtres sauf celle qui est séléctionnée
   * @returns {Window} Chaînage
   */
  hide_except_selected() {
    for (const { key, value } of this._windows) {
      if (this._selected_window._id !== value._id) value.hide();

      BnumLog.debug('hide_except_selected', key, value);
    }

    return this;
  }

  /**
   * Supprime toute les fenêtres sauf celle qui est séléctionnée
   * @returns {Window} Chaînage
   */
  close_except_selected() {
    let uid;
    while (this._windows.length > 1) {
      uid = +this._windows.keys[0];

      if (uid === this._selected_window._id) uid += 1;

      this.delete_window(uid);
    }

    return this;
  }

  /**
   * Supprime toute les fenêtres aux changement de frame
   * @returns {Window} Chaînage
   */
  close_windows_to_remove_on_change() {
    while (
      MelEnumerable.from(this._windows)
        .where((x) => x.value.is_remove_on_change())
        .any()
    ) {
      this.delete_window(
        MelEnumerable.from(this._windows)
          .where((x) => x.value.is_remove_on_change())
          .first().value._id,
      );
    }

    return this;
  }

  /**
   * Récupère la fenêtre courante
   * @returns {Window}
   */
  get_window({ uid = null } = {}) {
    return this._windows.get(uid, this._selected_window);
  }

  /**
   * Récupère la frame courante
   * @param {?string} task
   * @returns {external:jQuery}
   */
  get_frame(task = null, { jquery = true } = {}) {
    const frame = this._selected_window.get_frame(task);

    if (frame) return jquery ? frame : frame[0];
    else return frame;
  }

  /**
   * Récupère la frame courante
   * @returns {FrameData}
   */
  current_frame() {
    return this.get_window()._current_frame;
  }

  /**
   * Tâche en cours
   * @type {string}
   * @readonly
   */
  get currentTask() {
    return this.get_window()?.currentTask;
  }

  /**
   *
   * @param {Event} e
   */
  button_action(e) {
    event.preventDefault();
    this.switch_frame($(e.currentTarget).data('task'), {});
  }

  /**
   * @yield {Window}
   */
  *[Symbol.iterator]() {
    for (const element of this._windows) {
      yield element.value;
    }
  }
}

FrameManager._helper = null;
/**
 * @static
 * @readonly
 * @type {FrameManagerWrapperHelper}
 */
FrameManager.Helper = null;
Object.defineProperty(FrameManager, 'Helper', {
  get() {
    if (!FrameManager._helper) FrameManager._helper = MelObject.Empty();

    return FrameManager._helper;
  },
});

/**
 * @typedef FrameManagerWrapperHelper
 * @property {FrameManager} current Récupère le frame manager de la frame courante
 * @property {typeof Window} window_object
 */

/**
 * @class
 * @classdesc Wrapper d'instance
 */
class FrameManagerWrapper {
  constructor() {
    /**
     * Récupère l'instance du FrameManager le plus haut
     * @readonly
     * @type {FrameManager}
     */
    this.Instance = null;
    let _instance = null;

    /**
     * Contient divers fonction d'aide
     * @type {FrameManagerWrapperHelper}
     * @readonly
     */
    this.Helper = null;
    let _helper = {};

    Object.defineProperties(_helper, {
      current: {
        get() {
          if (!_instance) _instance = new FrameManager();

          return _instance;
        },
      },
      window_object: {
        get: () => Window,
      },
    });

    Object.defineProperties(this, {
      Instance: {
        get() {
          if (window !== parent && !!parent)
            return parent.mel_modules[MODULE].Instance;
          else if (!_instance) _instance = new FrameManager();

          return _instance;
        },
      },
      Helper: {
        get() {
          return _helper;
        },
      },
    });
  }
}

/**
 * Gère les différentes frames du Bnum. <br/><br/>
 *
 * Changer de tâche : <br/>
 *
 * FramesManager.Instance.switch_frame(task); <br/><br/>
 *
 * Obtenir la tâche en cours : <br/>
 *
 * FramesManager.Instance.currentTask <br/>
 * @type {FrameManagerWrapper}
 */
const FramesManager =
  window?.mel_modules?.[MODULE] || new FrameManagerWrapper();

window.mm_st_OpenOrCreateFrame = function (
  eClass,
  changepage = true,
  args = null,
  actions = [],
) {
  return FramesManager.Instance.switch_frame(eClass, {
    changepage,
    args,
    actions,
  });
};

window.mm_st_CreateOrOpenModal = function (eclass, changepage = true) {
  return window.mm_st_OpenOrCreateFrame(eclass, changepage);
};

if (!window.mel_modules) window.mel_modules = {};
if (!window.mel_modules[MODULE]) window.mel_modules[MODULE] = FramesManager;
if (!window.mel_modules[MODULE_CUSTOM_FRAMES])
  window.mel_modules[MODULE_CUSTOM_FRAMES] = {};

// FramesManager.Instance.add_mode('visio', async function (...args) {
//   const [page, params] = args;
//   if (!page) {
//     if (params) params._page = 'index';

//     await FramesManager.Instance.switch_frame('webconf', {
//       args: params ?? { _page: 'index' },
//     });
//   } else if (page !== 'index') {
//     params._page = page || 'init';
//     FramesManager.Instance.close_except_selected()
//       .disable_manual_multiframe()
//       .start_custom_multi_frame()
//       .get_window()
//       .hide();
//     window.current_visio = await FramesManager.Instance.open_another_window(
//       'webconf',
//       params,
//     );
//     FramesManager.Instance.get_window()
//       .set_cannot_be_select()
//       //.set_remove_on_change()
//       .add_tag('dispo-visio');
//     FramesManager.Instance.select_window(0);
//   }
// });

// FramesManager.Instance.add_mode('stop_visio', function () {
//   FramesManager.Instance.enable_manual_multiframe().stop_custom_multi_frame();
// });

// FramesManager.Instance.add_mode('reinit_visio', function () {
//   FramesManager.Instance.close_except_selected().get_window().show();
//   FramesManager.Instance.start_mode('stop_visio');
//   FramesManager.Instance.start_mode('visio');
// });