/**
 * @module JsHtml
 * @local JsHtml
 * @local ____JsHtml
 * @local AttribData
 * @local Attribs
 * @local EachCallback
 * @local ActionCallback
 * @tutorial js-html
 * @tutorial mel-html
 */

import { BnumEvent } from '../../mel_events.js';

export { JsHtml, ____JsHtml };

/**
 * [(string | function | number)]Une chaîne de charactère, une fonction ou un nombre. (ex : 'test', test.bind(this), 5)
 * @typedef {(string | function | number)} AttribData
 */

/**
 * [(string | Object<string, AttribData>)]Une chaîne de charactère ou un objet (ex : 'class:test', {class:'test'})
 * @typedef {(string | Object<string, AttribData>)} Attribs
 */

/**
 * Callback utiliser pour la fonction "each" du JsHtml.
 * @callback EachCallback
 * @param {____JsHtml} jsHtml JsHtml en cours
 * @param {*} item Objet de la boucle
 * @return {____JsHtml} Chaîne de JsHtml
 */

/**
 * Callback utiliser pour la fonction "action" du JsHtml.
 * @callback ActionCallback
 * @param {____JsHtml} jsHtml JsHtml en cours
 * @param {...*} args Arguments
 * @return {____JsHtml} Chaîne de JsHtml
 */

/**
 * @class
 * @classdesc Permet de générer du html en javascript et décrire du javascript sous forme html.
 * @package
 * @tutorial js-html
 */
class ____JsHtml {
  /**
   *
   * @param {string} balise
   * @param {____JsHtml} parent
   * @param {Attribs} attribs
   */
  constructor(balise, parent, attribs = {}) {
    this.balise = balise;
    this.attribs = attribs;
    this.childs = [];
    this._parent = parent;
  }

  /**
   * Ajoute une classe à la balise
   * @param {string} class_to_add Classe à ajouter
   * @returns {____JsHtml}
   */
  addClass(class_to_add) {
    let navigator = this._updated_balise();
    if (!navigator.hasClass(class_to_add))
      navigator.attribs.class.push(class_to_add);
    return this;
  }

  /**
   * Si la balise à une classe ou non
   * @param {string} class_to_verify Classe à vérifier
   * @returns {____JsHtml}
   */
  hasClass(class_to_verify) {
    return this._updated_balise()
      ._update_class()
      .attribs.class.includes(class_to_verify);
  }

  /**
   * Supprime une classe à la balise
   * @param {string} class_to_remove Classe à supprimer
   * @returns {____JsHtml}
   */
  removeClass(class_to_remove) {
    let navigator = this._updated_balise();
    if (navigator.hasClass(class_to_remove))
      navigator.attribs.class = navigator.attribs.class.filter(
        (x) => x !== class_to_remove,
      );

    return this;
  }

  /**
   * Désactive la balise.
   *
   * Ajoute la classe `disabled` et l'attribut `disabled`
   * @returns {____JsHtml}
   */
  disable() {
    return this.addClass('disabed').attr('disabled', 'disabled');
  }

  /**
   * Ajoute un attribut css à la balise
   * @param {(string | Object<string, string>)} key_or_attrib Clé ou attributs
   * @param {!string} value Valeur de la propriété css si il ne s'agit pas d'un attribut.
   * @returns {____JsHtml}
   */
  css(key_or_attrib, value = '') {
    if (typeof key_or_attrib === 'string') {
      let navigator = this._update_attribs()._updated_balise()._update_css();
      navigator.attribs.style[key_or_attrib] = value;
    } else {
      for (const key in key_or_attrib) {
        if (Object.hasOwnProperty.call(key_or_attrib, key)) {
          const element = key_or_attrib[key];
          this.css(key, element);
        }
      }
    }

    return this;
  }

  /**
   * Récupère la bonne balise.
   * @private
   * @returns {____JsHtml}
   */
  _updated_balise() {
    if (this.childs.length > 0) {
      if (
        ['img', 'input', 'br', 'hr'].includes(
          this.childs[this.childs.length - 1].balise,
        )
      )
        return this.childs[this.childs.length - 1];
      else if (
        this.childs[this.childs.length - 1]._update_attribs().attribs
          ?.one_line === true
      )
        return this.childs[this.childs.length - 1];
    }

    return this;
  }

  /**
   * Si un attribut existe ou non
   * @param {string} name Nom de l'attribut
   * @returns {boolean}
   */
  hasAttr(name) {
    return !!this._updated_balise().attribs[name];
  }

  /**
   * Attribut à rajouter à la balise
   * @param {string} name Nom de la balise
   * @param {string} value valeur de l'attribut
   * @returns {____JsHtml}
   */
  attr(name, value) {
    let navigator = this._updated_balise();

    if (navigator._update_attribs()._isOn(name)) {
      navigator.attribs[name] = navigator._getOn(name);
      navigator.attribs[name].push(value);
    } else navigator.attribs[name] = value;
    return this;
  }

  /**
   * Attributs à rajouter à la balise
   * @param {Object<string, string>} attributes Attributs à ajouter
   * @returns {____JsHtml}
   */
  attrs(attributes) {
    for (const key in attributes) {
      if (Object.hasOwnProperty.call(attributes, key)) {
        const element = attributes[key];
        this.attr(key, element);
      }
    }

    return this;
  }

  /**
   * Met à jours les attributs. Si il s'agissait d'une chaîne de caractère à la base, elle est transformée en objet.
   * @private
   * @returns {____JsHtml}
   */
  _update_attribs() {
    if (typeof this.attribs === 'string') {
      const regex = /\s(\w+?)="(.+?)"/g;
      const str = this.attribs;
      this.attribs = {};

      for (const iterator of str.matchAll(regex)) {
        this.attribs[iterator[1]] = iterator[2];
      }
    }

    return this;
  }

  /**
   * Vérifie si un attribut est un évènement.
   * @param {string} name Nom de l'attribut.
   * @private
   * @returns {boolean}
   */
  _isOn(name) {
    return name.length > 2 && name[0] === 'o' && name[1] === 'n';
  }

  /**
   * Récupère un atrribut d'évènement.
   * @param {string} name Nom de l'attribut.
   * @returns {Function}
   * @private
   */
  _getOn(name) {
    if (!this.attribs[name]) this.attribs[name] = new BnumEvent();
    else if (
      !!this.attribs[name] &&
      !(this.attribs[name] instanceof BnumEvent)
    ) {
      const old = this.attribs[name];
      this.attribs[name] = new BnumEvent();

      if (typeof old === 'string')
        this.attribs[name].push((callback) => eval(callback), old);
      else this.attribs[name].push(old);
    }

    return this.attribs[name];
  }

  /**
   * Supprime un attribut
   * @param {string} name Nom de l'attribut
   * @returns {____JsHtml}
   */
  removeAttr(name) {
    this._updated_balise().attribs[name] = undefined;
    return this;
  }

  /**
   * Récupère la balise parente.
   * @returns {____JsHtml}
   */
  parent() {
    return this._parent;
  }

  /**
   * Récpère le premier enfant
   * @returns {____JsHtml}
   */
  first() {
    return this.childs[0];
  }

  /**
   * Ajoute une balise enfant à la balise actuelle et la retourne.
   *
   * Terminez par {@link end} pour fermer la balise.
   * @param {string} balise Nom de la balise
   * @param {Attribs} attribs Attributs de la balise
   * @returns {____JsHtml} Balise créée
   */
  tag(balise, attribs = {}) {
    return this._create(balise, this, attribs, false);
  }

  /**
   * Ajoute une balise enfant à la balise actuelle et la retourne.
   *
   * Il s'agit d'une balise qui ne possède pas de balise de fermeture comme `input` ou `br`
   * @param {string} balise Nom de la balise
   * @param {Attribs} attribs Attributs de la balise
   * @returns {____JsHtml} Balise créée
   */
  tag_one_line(balise, attribs = {}) {
    return this._create_oneline(balise, this, attribs);
  }

  /**
   * Ajoute une balise `a` à la balise actuelle et la retourne.
   * @param {Attribs} attribs Attributs de la balise
   * @returns {____JsHtml} Balise `a` créée
   */
  a(attribs = {}) {
    return this.tag('a', attribs);
  }

  /**
   * Ajoute une balise `dd` à la balise actuelle et la retourne.
   * @param {Attribs} attribs Attributs de la balise
   * @returns {____JsHtml} Balise `dd` créée
   */
  dd(attribs = {}) {
    return this.tag('dd', attribs);
  }

  /**
   * Ajoute une balise `dt` à la balise actuelle et la retourne.
   * @param {Attribs} attribs Attributs de la balise
   * @returns {____JsHtml} Balise `dt` créée
   */
  dt(attribs = {}) {
    return this.tag('dt', attribs);
  }

  /**
   * Ajoute une balise `dl` à la balise actuelle et la retourne.
   * @param {Attribs} attribs Attributs de la balise
   * @returns {____JsHtml} Balise `dl` créée
   */
  dl(attribs = {}) {
    return this.tag('dl', attribs);
  }

  /**
   * Ajoute une balise `div` à la balise actuelle et la retourne.
   * @param {Attribs} attribs Attributs de la balise
   * @returns {____JsHtml} Balise `div` créée
   */
  div(attribs = {}) {
    return this.tag('div', attribs);
  }

  /**
   * Ajoute une balise `blockquote` à la balise actuelle et la retourne.
   * @param {Attribs} attribs Attributs de la balise
   * @returns {____JsHtml} Balise `blockquote` créée
   */
  blockquote(attribs = {}) {
    return this.tag('blockquote', attribs);
  }

  /**
   * Ajoute une balise `ul` à la balise actuelle et la retourne.
   * @param {Attribs} attribs Attributs de la balise
   * @returns {____JsHtml} Balise `ul` créée
   */
  ul(attribs = {}) {
    let ul = this.tag('ul', attribs);

    if (attribs.unstyled) {
      ul.removeAttr('unstyled').addClass('list-unstyled');
    }

    return ul;
  }

  /**
   * Ajoute une balise `ol` à la balise actuelle et la retourne.
   * @param {Attribs} attribs Attributs de la balise
   * @returns {____JsHtml} Balise `ol` créée
   */
  ol(attribs = {}) {
    return this.tag('ol', attribs);
  }

  /**
   * Ajoute une balise `li` à la balise actuelle et la retourne.
   * @param {Attribs} attribs Attributs de la balise
   * @returns {____JsHtml} Balise `li` créée
   */
  li(attribs = {}) {
    return this.tag('li', attribs);
  }

  /**
   * Ajoute une balise `span` à la balise actuelle et la retourne.
   * @param {Attribs} attribs Attributs de la balise
   * @returns {____JsHtml} Balise `span` créée
   */
  span(attribs = {}) {
    return this.tag('span', attribs);
  }

  /**
   * Ajoute une balise `p` à la balise actuelle et la retourne.
   * @param {Attribs} attribs Attributs de la balise
   * @returns {____JsHtml} Balise `p` créée
   */
  p(attribs = {}) {
    return this.tag('p', attribs);
  }

  /**
   * Ajoute une balise `img` à la balise actuelle et la retourne.
   * @param {Attribs} attribs Attributs de la balise
   * @returns {____JsHtml} Balise `img` créée
   */
  img(attribs = {}) {
    return this.tag_one_line('img', attribs);
  }

  /**
   * Ajoute une balise `style` à la balise actuelle et la retourne.
   * @returns {____JsHtml} Balise `style` créée
   */
  style() {
    return this.tag('style');
  }

  /**
   * Ajoute une propriété css. Idéalement après une balise `style`.
   * @param {string} key Clé de la propriété css
   * @param {string} value Valeur de la propriété css
   * @returns {____JsHtml} Propriété css créée
   * @see {@link ____JsHtml.style}
   */
  style_css_prop(key, value) {
    return this.text(`${key}:${value};`);
  }

  /**
   * Essaye d'ajouter un label.
   * @param {Attribs} attribs
   * @returns {____JsHtml}
   * @private
   */
  _try_add_label(attribs = {}) {
    let html_input = this;
    if (!!attribs?.id && attribs?.label) {
      html_input = html_input
        .label({ for: attribs.id })
        .text(attribs.label)
        .end();
    }

    return html_input;
  }

  /**
   * Ajoute une balise `input` à la balise actuelle et la retourne.
   * @param {Attribs} attribs Attributs de la balise
   * @returns {____JsHtml} Balise `input` créée
   */
  input(attribs = {}) {
    return this._try_add_label(attribs).tag_one_line('input', attribs);
  }

  /**
   * Ajoute une balise `select` à la balise actuelle et la retourne.
   *
   * Si l'attribut `label` éxiste, ajoute un `label` avant les `select`.
   * @param {Attribs} attribs Attributs de la balise
   * @returns {____JsHtml} Balise `select` créée
   */
  select(attribs = {}) {
    return this._try_add_label(attribs).tag('select', attribs);
  }

  /**
   * Ajoute une balise `option` à la balise actuelle et la retourne.
   * @param {Attribs} attribs Attributs de la balise
   * @returns {____JsHtml} Balise `option` créée
   */
  option(attribs = {}) {
    return this.tag('option', attribs);
  }

  /**
   * Ajoute une balise `option` à la balise actuelle et la retourne.
   *
   * Pas besoin de mettre une balise `end` pour fermer l'option.
   * @param {string} value Valeur de l'option
   * @param {string} text Texte de l'option
   * @param {Attribs} attribs Attributs de la balise
   * @returns {____JsHtml} Balise `img` créée
   */
  option_one_line(value, text, attribs = {}) {
    attribs.value = value;
    return this.option(attribs).text(text).end();
  }

  /**
   * Ajoute une balise `textarea` à la balise actuelle et la retourne.
   * @param {Attribs} attribs Attributs de la balise
   * @returns {____JsHtml} Balise `textarea` créée
   */
  textarea(attribs = {}) {
    return this._try_add_label(attribs).tag('textarea', attribs);
  }

  /**
   * Ajoute une balise `form` à la balise actuelle et la retourne.
   * @param {Attribs} attribs Attributs de la balise
   * @returns {____JsHtml} Balise `form` créée
   */
  form(attribs = {}) {
    return this.tag('form', attribs);
  }

  /**
   * Ajoute une balise `button` à la balise actuelle et la retourne.
   * @param {Attribs} attribs Attributs de la balise
   * @returns {____JsHtml} Balise `button` créée
   */
  button(attribs = {}) {
    return this.tag('button', attribs);
  }

  /**
   * Ajoute une balise `fieldset` à la balise actuelle et la retourne.
   * @param {Attribs} attribs Attributs de la balise
   * @returns {____JsHtml} Balise `fieldset` créée
   */
  fieldset(attribs = {}) {
    return this.tag('fieldset', attribs);
  }

  /**
   * Ajoute une balise `label` à la balise actuelle et la retourne.
   * @param {Attribs} attribs Attributs de la balise
   * @returns {____JsHtml} Balise `label` créée
   */
  label(attribs = {}) {
    return this.tag('label', attribs);
  }

  /**
   * Ajoute une balise `legend` à la balise actuelle et la retourne.
   * @param {Attribs} attribs Attributs de la balise
   * @returns {____JsHtml} Balise `legend` créée
   */
  legend(attribs = {}) {
    return this.tag('legend', attribs);
  }

  /**
   * Ajoute une balise `meter` à la balise actuelle et la retourne.
   * @param {Attribs} attribs Attributs de la balise
   * @returns {____JsHtml} Balise `meter` créée
   */
  meter(attribs = {}) {
    return this.tag('meter', attribs);
  }

  /**
   * Ajoute une balise `optgroup` à la balise actuelle et la retourne.
   * @param {Attribs} attribs Attributs de la balise
   * @returns {____JsHtml} Balise `optgroup` créée
   */
  optgroup(attribs = {}) {
    return this.tag('optgroup', attribs);
  }

  /**
   * Ajoute une balise `output` à la balise actuelle et la retourne.
   * @param {Attribs} attribs Attributs de la balise
   * @returns {____JsHtml} Balise `output` créée
   */
  output(attribs = {}) {
    return this.tag('output', attribs);
  }

  /**
   * Ajoute une balise `progress` à la balise actuelle et la retourne.
   * @param {Attribs} attribs Attributs de la balise
   * @returns {____JsHtml} Balise `progress` créée
   */
  progress(attribs = {}) {
    return this.tag('progress', attribs);
  }

  /**
   * Ajoute une balise `br` à la balise actuelle et la retourne.
   * @param {Attribs} attribs Attributs de la balise
   * @returns {____JsHtml} Balise `br` créée
   */
  br() {
    return this.tag_one_line('br');
  }

  /**
   * Ajoute une balise `hr` à la balise actuelle et la retourne.
   * @param {Attribs} attribs Attributs de la balise
   * @returns {____JsHtml} Balise `hr` créée
   */
  hr() {
    return this.tag_one_line('hr');
  }

  /**
   * Ajoute une balise `address` à la balise actuelle et la retourne.
   * @param {Attribs} attribs Attributs de la balise
   * @returns {____JsHtml} Balise `address` créée
   */
  address(attribs = {}) {
    return this.tag('address', attribs);
  }

  /**
   * Ajoute une balise `article` à la balise actuelle et la retourne.
   * @param {Attribs} attribs Attributs de la balise
   * @returns {____JsHtml} Balise `article` créée
   */
  article(attribs = {}) {
    return this.tag('article', attribs);
  }

  /**
   * Ajoute une balise `aside` à la balise actuelle et la retourne.
   * @param {Attribs} attribs Attributs de la balise
   * @returns {____JsHtml} Balise `aside` créée
   */
  aside(attribs = {}) {
    return this.tag('aside', attribs);
  }

  /**
   * Ajoute une balise `footer` à la balise actuelle et la retourne.
   * @param {Attribs} attribs Attributs de la balise
   * @returns {____JsHtml} Balise `footer` créée
   */
  footer(attribs = {}) {
    return this.tag('footer', attribs);
  }

  /**
   * Ajoute une balise `header` à la balise actuelle et la retourne.
   * @param {Attribs} attribs Attributs de la balise
   * @returns {____JsHtml} Balise `header` créée
   */
  header(attribs = {}) {
    return this.tag('header', attribs);
  }

  /**
   * Ajoute une balise `h` à la balise actuelle et la retourne.
   * @param {Attribs} attribs Attributs de la balise
   * @returns {____JsHtml} Balise `h` créée
   */
  h(num, attribs = {}) {
    return this.tag(`h${num}`, attribs);
  }

  /**
   * Ajoute une balise `h1` à la balise actuelle et la retourne.
   * @param {Attribs} attribs Attributs de la balise
   * @returns {____JsHtml} Balise `h1` créée
   */
  h1(attribs = {}) {
    return this.h(1, attribs);
  }

  /**
   * Ajoute une balise `h2` à la balise actuelle et la retourne.
   * @param {Attribs} attribs Attributs de la balise
   * @returns {____JsHtml} Balise `h2` créée
   */
  h2(attribs = {}) {
    return this.h(2, attribs);
  }

  /**
   * Ajoute une balise `h3` à la balise actuelle et la retourne.
   * @param {Attribs} attribs Attributs de la balise
   * @returns {____JsHtml} Balise `h3` créée
   */
  h3(attribs = {}) {
    return this.h(3, attribs);
  }

  /**
   * Ajoute une balise `h4` à la balise actuelle et la retourne.
   * @param {Attribs} attribs Attributs de la balise
   * @returns {____JsHtml} Balise `h4` créée
   */
  h4(attribs = {}) {
    return this.h(4, attribs);
  }

  /**
   * Ajoute une balise `h5` à la balise actuelle et la retourne.
   * @param {Attribs} attribs Attributs de la balise
   * @returns {____JsHtml} Balise `h5` créée
   */
  h5(attribs = {}) {
    return this.h(5, attribs);
  }

  /**
   * Ajoute une balise `h6` à la balise actuelle et la retourne.
   * @param {Attribs} attribs Attributs de la balise
   * @returns {____JsHtml} Balise `h6` créée
   */
  h6(attribs = {}) {
    return this.h(6, attribs);
  }

  /**
   * Ajoute une balise `hgroup` à la balise actuelle et la retourne.
   * @param {Attribs} attribs Attributs de la balise
   * @returns {____JsHtml} Balise `hgroup` créée
   */
  hgroup(attribs = {}) {
    return this.tag('hgroup', attribs);
  }

  /**
   * Ajoute une balise `main` à la balise actuelle et la retourne.
   * @param {Attribs} attribs Attributs de la balise
   * @returns {____JsHtml} Balise `main` créée
   */
  main(attribs = {}) {
    return this.tag('main', attribs);
  }

  /**
   * Ajoute une balise `nav` à la balise actuelle et la retourne.
   * @param {Attribs} attribs Attributs de la balise
   * @returns {____JsHtml} Balise `nav` créée
   */
  nav(attribs = {}) {
    return this.tag('nav', attribs);
  }

  /**
   * Ajoute une balise `section` à la balise actuelle et la retourne.
   * @param {Attribs} attribs Attributs de la balise
   * @returns {____JsHtml} Balise `section` créée
   */
  section(attribs = {}) {
    return this.tag('section', attribs);
  }

  /**
   * Ajoute une balise `menu` à la balise actuelle et la retourne.
   * @param {Attribs} attribs Attributs de la balise
   * @returns {____JsHtml} Balise `menu` créée
   */
  menu(attribs = {}) {
    return this.tag('menu', attribs);
  }

  /**
   * Ajoute une balise `iframe` à la balise actuelle et la retourne.
   * @param {Attribs} attribs Attributs de la balise
   * @returns {____JsHtml} Balise `iframe` créée
   */
  iframe(attribs = {}, close = true) {
    let tmp = this.tag('iframe', attribs);

    if (close) return tmp.end();
    else return tmp;
  }

  /**
   * Ajoute une balise `canvas` à la balise actuelle et la retourne.
   * @param {Attribs} attribs Attributs de la balise
   * @returns {____JsHtml} Balise `canvas` créée
   */
  canvas(attribs = {}) {
    return this.tag('canvas', attribs);
  }

  /**
   * Ajoute une balise `script` à la balise actuelle et la retourne.
   * @param {Attribs} attribs Attributs de la balise
   * @returns {____JsHtml} Balise `script` créée
   */
  script(attribs = {}) {
    return this.tag('script', attribs);
  }

  /**
   * Ajoute une balise `table` à la balise actuelle et la retourne.
   * @param {Attribs} attribs Attributs de la balise
   * @returns {____JsHtml} Balise `table` créée
   */
  table(attribs = {}) {
    return this.tag('table', attribs);
  }

  /**
   * Ajoute une balise `caption` à la balise actuelle et la retourne.
   * @param {Attribs} attribs Attributs de la balise
   * @returns {____JsHtml} Balise `caption` créée
   */
  caption(attribs = {}) {
    return this.tag('caption', attribs);
  }

  /**
   * Ajoute une balise `thead` à la balise actuelle et la retourne.
   * @param {Attribs} attribs Attributs de la balise
   * @returns {____JsHtml} Balise `thead` créée
   */
  thead(attribs = {}) {
    return this.tag('thead', attribs);
  }

  /**
   * Ajoute une balise `col` à la balise actuelle et la retourne.
   * @param {Attribs} attribs Attributs de la balise
   * @returns {____JsHtml} Balise `col` créée
   */
  col() {
    return this.tag_one_line('col');
  }

  /**
   * Ajoute une balise `colgroup` à la balise actuelle et la retourne.
   * @param {Attribs} attribs Attributs de la balise
   * @returns {____JsHtml} Balise `colgroup` créée
   */
  colgroup(attribs = {}) {
    return this.tag('colgroup', attribs);
  }

  /**
   * Ajoute une balise `tbody` à la balise actuelle et la retourne.
   * @param {Attribs} attribs Attributs de la balise
   * @returns {____JsHtml} Balise `tbody` créée
   */
  tbody(attribs = {}) {
    return this.tag('tbody', attribs);
  }

  /**
   * Ajoute une balise `td` à la balise actuelle et la retourne.
   * @param {Attribs} attribs Attributs de la balise
   * @returns {____JsHtml} Balise `td` créée
   */
  td(attribs = {}) {
    return this.tag('td', attribs);
  }

  /**
   * Ajoute une balise `th` à la balise actuelle et la retourne.
   * @param {Attribs} attribs Attributs de la balise
   * @returns {____JsHtml} Balise `th` créée
   */
  th(attribs = {}) {
    return this.tag('th', attribs);
  }

  /**
   * Ajoute une balise `tr` à la balise actuelle et la retourne.
   * @param {Attribs} attribs Attributs de la balise
   * @returns {____JsHtml} Balise `tr` créée
   */
  tr(attribs = {}) {
    return this.tag('tr', attribs);
  }

  /**
   * Ajoute une balise `tfoot` à la balise actuelle et la retourne.
   * @param {Attribs} attribs Attributs de la balise
   * @returns {____JsHtml} Balise `tfoot` créée
   */
  tfoot(attribs = {}) {
    return this.tag('tfoot', attribs);
  }

  /**
   * Ajoute une balise `details` à la balise actuelle et la retourne.
   * @param {Attribs} attribs Attributs de la balise
   * @returns {____JsHtml} Balise `details` créée
   */
  details(attribs = {}) {
    return this.tag('details', attribs);
  }

  /**
   * Ajoute une balise `summary` à la balise actuelle et la retourne.
   * @param {Attribs} attribs Attributs de la balise
   * @returns {____JsHtml} Balise `summary` créée
   */
  summary(attribs = {}) {
    return this.tag('summary', attribs);
  }

  dialog(attribs = {}) {
    return this.tag('dialog', attribs);
  }

  /**
   * Affichera un commentaire html
   * @param {string} text Commentaire à afficher
   * @returns {____JsHtml}
   */
  comment(text) {
    return this.text(`<!-- ${text} -->`);
  }

  /**
   * Commentaire qui ne sera pas affiché
   * @param {string} commentary Commentaire
   * @returns {____JsHtml}
   */
  // eslint-disable-next-line no-unused-vars
  _(commentary) {
    return this;
  }

  /**
   * Affiche un texte brute
   * @param {string} text Texte à afficher
   * @returns {____JsHtml}
   */
  text(text) {
    return this._create(text, this, { is_raw: true }, true);
  }

  /**
   * Termine une balise
   * @param {?string} debug Commentaire à afficher pour pouvoir s'y retrouver plus facilement
   * @returns {____JsHtml}
   * @example JsHtml.start.div().end()
   */
  end(debug = null) {
    let end = this._parent._create(`/${this.balise}`, this._parent, null, true);

    if (debug) end.text(`<!-- ${debug} -->`);

    return end;
  }

  /**
   * Permet d'ajouter des éléments en jshtml qui nécéssitent d'être bouclés.
   * @param {EachCallback} callback Action
   * @param  {...*} items Objets
   * @returns {____JsHtml} JsHtml en cours
   */
  each(callback, ...items) {
    let html = this;
    for (const iterator of items) {
      html = callback(this, iterator);
    }

    return html;
  }

  /**
   * Actions à éxécuter dans le JsHtml.
   * @param {ActionCallback} callback
   * @param  {...any} args Arguments qui seront transmits au callback
   * @returns {____JsHtml} Chaîne de JsHtml
   */
  action(callback, ...args) {
    return callback(this, ...args);
  }

  /**
   * Génère en jQuery
   * @returns {external:jQuery}
   */
  generate({ context = window } = {}) {
    return this._generate({ mode: 1, context });
  }

  generate_dom() {
    return this._generate({ mode: 1 })[0];
  }

  /**
   * Génère le code en html brut. Les fonctions des évènements ne fonctionneront pas.
   * @param {Object} param0
   * @param {boolean} param0.joli_html Si on doit mettre en forme le html (retour à la ligne et tabulations)
   * @returns {string}
   */
  generate_html({ joli_html = false }) {
    return this._generate({ joli_html });
  }

  /**
   * Ajoute un JsHtml enfant
   * @param {____JsHtml} jshtml JsHtml à ajouter
   * @returns {____JsHtml}
   */
  add_child(jshtml) {
    if (jshtml.balise === 'start') {
      this.childs.push(
        ...jshtml.childs.map((x) => {
          x.parent = this;
          return x;
        }),
      );
    } else {
      jshtml.parent = this;
      this.childs.push(jshtml);
    }

    return this;
  }

  /**
   * Ajoute une balise html enfant.
   * @param {string} balise Balise à ajouter en enfant
   * @param {____JsHtml} parent Parent de la balise
   * @param {Attribs} attribs Attributs de la balise
   * @param {boolean} isend Si la balise est une balise de fermeture
   * @returns {____JsHtml}
   */
  _create(balise, parent, attribs, isend) {
    this.childs.push(new this.constructor(balise, parent, attribs));

    return isend ? parent : this.childs[this.childs.length - 1];
  }

  /**
   * Ajoute une balise enfant qui ne possède pas de balises de fermeture.
   * @param {string} balise Balise à ajouter en enfant
   * @param {____JsHtml} parent Parent de la balise
   * @param {Attribs} attribs Attributs de la balise
   * @returns {____JsHtml}
   */
  _create_oneline(balise, parent, attribs) {
    return this._create(balise, parent, attribs, true);
  }

  /**
   * Si la classe est sous forme de string, le transforme en objet.
   * @returns {____JsHtml}
   * @private
   */
  _update_class() {
    if (!this._update_attribs().attribs) this.attribs = { class: [] };
    else if (!this.attribs.class) this.attribs.class = [];
    else if (!!this.attribs.class && typeof this.attribs.class === 'string')
      this.attribs.class = this.attribs.class.split(' ');
    else if (!!this.attribs.class && (!this.attribs.class) instanceof Array)
      this.attribs.class = [this.attribs.class];

    return this;
  }

  /**
   * Si le css est sous forme de string, le transforme en objet.
   * @returns {____JsHtml}
   * @private
   */
  _update_css() {
    if (!this.attribs) this.attribs = { style: {} };
    else if (!this.attribs.style) this.attribs.style = {};
    else if (!!this.attribs.style && typeof this.attribs.style === 'string') {
      const [key, value] = this.attribs.style.split(':');
      this.attribs.style = { [key]: value };
    }

    return this;
  }

  /**
   * Génère le html.
   * @param {*} param0
   * @returns {(string | external:jQuery)}
   * @private
   */
  _generate({ i = -1, mode = 0, joli_html = false, context = window }) {
    let html = [];

    if (this.balise !== 'start')
      html.push(
        `${this.balise !== '/textarea' && joli_html ? this._create_blanks(i) : ''}${this._get_balise()}`,
      );

    for (const iterator of this.childs) {
      html.push(iterator._generate({ i: i + 1, joli_html, context }));
    }

    html = html.join(joli_html ? '\r\n' : '');

    switch (mode) {
      case 0:
        break;
      case 1:
        html = context.$(html);

        // eslint-disable-next-line no-case-declarations
        let id;
        // eslint-disable-next-line no-case-declarations
        let $item;
        // eslint-disable-next-line no-case-declarations
        let $node;
        // eslint-disable-next-line no-case-declarations, quotes
        const $nodes = [html, ...html.find("[data-on-id!=''][data-on-id]")];
        for (const iterator of $nodes) {
          $item = context.$(iterator);

          for (const node of $item) {
            $node = context.$(node);
            id = $node.attr('data-on-id');

            if (id) {
              for (const key in ____JsHtml.actions[id]) {
                if (Object.hasOwnProperty.call(____JsHtml.actions[id], key)) {
                  const element = ____JsHtml.actions[id][key];
                  $node.on(
                    key.replace('on', ''),
                    element instanceof BnumEvent
                      ? element.call.bind(element)
                      : element,
                  );
                }
              }
              ____JsHtml.remove_id(id);
              id = null;
            }

            $node = null;
          }

          $item = null;
        }

        break;
      default:
        throw new Error('mode not exist');
    }

    return html;
  }

  /**
   * Récupère une balise générée
   * @returns {string}
   * @private
   */
  _get_balise() {
    var current_class;
    const memory_tag =
      typeof this.balise === 'function' ? this.balise(this) : this.balise;
    let balise;

    if (this.attribs?.is_raw === true) balise = memory_tag;
    else {
      balise = [`<${memory_tag}`];

      if (typeof this.attribs === 'string') {
        if (this.attribs !== '') balise.push(this.attribs);
      } else if (!!this.attribs && Object.keys(this.attribs).length > 0) {
        for (const key in this.attribs) {
          if (Object.hasOwnProperty.call(this.attribs, key)) {
            const element = this.attribs[key];

            if (element === undefined || element === null) continue;

            if (
              !this._isOn(key) ||
              (this._isOn(key) &&
                typeof element !== 'function' &&
                !(element instanceof BnumEvent))
            ) {
              switch (key) {
                case 'raw-content':
                  break;

                case 'data-custom-tag':
                  continue;

                case 'class':
                  if (element instanceof Array) {
                    current_class = [];

                    for (const iterator of element) {
                      if (typeof iterator === 'function')
                        current_class.push(iterator(this));
                      else current_class.push(iterator);
                    }

                    balise.push(`${key}="${current_class.join(' ')}"`);
                    current_class.length = 0;
                    current_class = null;
                    break;
                  }

                case 'style':
                  if (typeof element === 'object') {
                    current_class = [];

                    // eslint-disable-next-line no-shadow
                    for (const key in element) {
                      if (Object.hasOwnProperty.call(element, key)) {
                        if (typeof element[key] === 'function')
                          current_class.push(`${key}:${element[key](this)}`);
                        else current_class.push(`${key}:${element[key]}`);
                      }
                    }

                    balise.push(`${key}="${current_class.join(';')}"`);
                    current_class.length = 0;
                    current_class = null;
                    break;
                  }

                default:
                  balise.push(
                    `${key}="${typeof element === 'function' ? element(this) : element}"`,
                  );
                  break;
              }
            } else if (this._isOn(key)) {
              // eslint-disable-next-line vars-on-top
              var id = id || ____JsHtml.generate_ids();
              balise.push(`data-on-id="${id}"`);
              ____JsHtml.add_action(id, key, this._getOn(key));
            }
          }
        }
      }

      id = null;

      if (this.attribs?.['data-custom-tag'] && this.attribs?.one_line) {
        balise.push('>');
        balise.push(`</${this.balise}>`);
      } else balise.push(`${this.attribs?.one_line === true ? '/' : ''}>`);

      let join;
      if (balise.length === 2) join = '';
      else join = ' ';

      if (!!this.attribs && !!this.attribs['raw-content'])
        balise.push(
          typeof this.attribs['raw-content'] === 'function'
            ? this.attribs['raw-content'](this)
            : this.attribs['raw-content'],
        );

      balise = balise.join(join);
    }

    return balise;
  }

  /**
   * Créer un espace
   * @return {string}
   * @private
   */
  _create_blanks(i) {
    if (i === 0) return '';

    const tab = 4;
    let blanks = [];

    for (let index = 0, len = i * tab; index < len; ++index) {
      blanks.push(' ');
    }

    return blanks.join('');
  }

  /**
   * Même effet que {@link ____JsHtml.generate_html}
   * @returns {string}
   */
  toString() {
    return this.generate_html({ joli_html: true });
  }

  /**
   * Commence une session de JsHtml.
   * @returns {____JsHtml}
   * @static
   */
  static start() {
    return new ____JsHtml('start', null);
  }

  /**
   * Créer un alias.
   *
   * Permet de créer une nouvelle balise html en js html.
   * @param {string} alias Nom de la fonction.
   * @param {Object} param1
   * @param {boolean} param1.online Si la balise doit être sur une ligne.
   * @param {function} param1.before_callback Fonction qui sera appelé avant la création de la balise.
   * @param {function} param1.generate_callback Fonction qui sera appelé pour la création de la balise.
   * @param {function} param1.after_callback Fonction qui sera appelé après la création de la balise.
   * @param {string} param1.tag Nom de la balise.
   * @returns {typeof ____JsHtml}
   */
  static create_alias(
    alias,
    {
      online = false,
      before_callback = null,
      generate_callback = null,
      after_callback = null,
      tag = 'div',
    },
  ) {
    ____JsHtml.prototype[alias] = function (attribs = {}, ...args) {
      if (before_callback) {
        const before = before_callback(attribs, ...args);

        if (before.attribs) attribs = before.attribs;
      }

      if (online && typeof attribs === 'object') attribs.one_line = true;

      let html = generate_callback
        ? generate_callback(this, attribs, ...args)
        : this._create(
            tag,
            this,
            typeof attribs === 'object' ? attribs : null,
            online,
          );

      if (after_callback) {
        const call = after_callback(html, attribs, ...args);

        if (!!call && call instanceof ____JsHtml) html = call;
      }

      return html;
    };

    return this;
  }
}

/**
 * Sauvegarde les fonctions des balises pour les appliqués après lors de la génération.
 * @type {Object.<string, function>}
 * @static
 */
____JsHtml.actions = {};
/**
 * Génère un id.
 * @param {!number} length Taille de l'id
 * @returns {string}
 * @static
 */
____JsHtml.generate_ids = function generate_ids(length = 5) {
  let result = '';
  const characters =
    'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
  const charactersLength = characters.length;
  let counter = 0;
  while (counter < length) {
    result += characters.charAt(Math.floor(Math.random() * charactersLength));
    counter += 1;
  }

  if (Object.keys(this.actions).includes(result))
    result = ____JsHtml.generate_ids(~~(Math.random() * 100));

  return result;
};
/**
 * Supprime une action.
 * @param {string} id Id de l'action à supprimée
 * @static
 */
____JsHtml.remove_id = function (id) {
  ____JsHtml.actions[id] = null;
};

/**
 * Ajoute une action pour l'utiliser plus tard.
 * @param {string} id Id de l'action
 * @param {string} action Nom de l'action
 * @param {function} callback Callback de l'évènement
 * @static
 */
____JsHtml.add_action = function (id, action, callback) {
  if (!____JsHtml.actions[id]) ____JsHtml.actions[id] = {};

  if (!____JsHtml.actions[id][action])
    ____JsHtml.actions[id][action] = new BnumEvent();

  ____JsHtml.actions[id][action] = callback;
};

/**
 * @class
 * @classdesc Englobe les fonctions de la classe ____JsHtml
 * @package
 * @tutorial js-html
 */
class ____js_html___ {
  constructor() {
    /**
     * Commence un texte html en javascript
     * @type {____JsHtml}
     * @readonly
     */
    this.start = null;
    Object.defineProperties(this, {
      start: {
        get() {
          return ____JsHtml.start();
        },
        configurable: false,
        enumerable: false,
      },
    });
  }

  /**
   * Permet d'ajouter des nouvelles balises à l'écriture js html
   * @param {string} alias Nom de la fonction
   * @param {Object} param1
   * @param {boolean} param1.online Si la balise doit être sur une ligne
   * @param {function} param1.before_callback Fonction qui sera appelé avant la création de la balise
   * @param {function} param1.generate_callback Fonction qui sera appelé pour la création de la balise
   * @param {function} param1.after_callback Fonction qui sera appelé après la création de la balise
   * @param {string} param1.tag Nom de la balise
   * @returns
   */
  create_alias(
    alias,
    {
      online = false,
      before_callback = null,
      generate_callback = null,
      after_callback = null,
      tag = 'div',
    },
  ) {
    return ____JsHtml.create_alias(alias, {
      online,
      before_callback,
      generate_callback,
      after_callback,
      tag,
    });
  }

  /**
   * Permet d'ajouter des nouvelles fonctions à l'écriture js html
   * @param {string} name Nom de la fonction
   * @param {function} callback Fonction qui sera appelé
   */
  extend(name, callback) {
    ____JsHtml.prototype[name] = callback;
  }

  /**
   * Permet de maj une fonction de l'écriture js html
   * @param {string} name Nom de la fonction à override
   * @param {function} callback Nouvelle fonction arg1 => this, arg2 => ancienne fonction, arg3 => arguments de la fonction
   */
  update(name, callback) {
    const old = ____JsHtml.prototype[name];
    ____JsHtml.prototype[name] = function (...args) {
      return callback(this, old, ...args);
    };
  }

  /**
   * Ecrit une page en js html
   * @param {function} callback Function qui contient le js html.
   * @param  {...any} args Arguments de la fonction `callback`
   * @returns {____JsHtml}
   */
  write(callback, ...args) {
    return callback(this.start, ...args);
  }

  /**
   * Charge une page js html en fonction de la skin
   * @async
   * @param {string} name Nom du fichier
   * @param {string} plugin Nom du plugin qui contient le fichier
   * @param {string} skin Nom de la skin
   * @returns {Promise<null | ____JsHtml>}
   */
  async load_page(
    name,
    plugin = 'mel_metapage',
    skin = window?.rcmail?.env?.skin ?? '',
  ) {
    const load =
      top?.loadJsModule ?? parent?.loadJsModule ?? window?.loadJsModule;
    const returned = await load(plugin, name, `/skins/${skin}/js_templates/`);
    const keys = Object.keys(returned);
    const len = keys.length;

    if (len > 0) {
      for (let index = 0; index < len; ++index) {
        return returned[keys[index]];
      }
    }

    return null;
  }
}

/**
 * @memberof module:JsHtml
 * @type {____js_html___}
 * @description Permet de générer du html en javascript et d'écrire du javascript sous forme html.
 * @tutorial js-html
 */
const JsHtml = new ____js_html___();