/**
 * @module EventView/Parts/Guests
 */

import { MelEnumerable } from '../../../classes/enum.js';
import { EMPTY_STRING } from '../../../constants/constants.js';
import { REG_BETWEEN_PARENTHESIS } from '../../../constants/regexp.js';
import { BnumException } from '../../../exceptions/bnum_base_exceptions.js';
import { MelHtml } from '../../../html/JsHtml/MelHtml.js';
import { BnumEvent } from '../../../mel_events.js';
import { Mel_Promise } from '../../../mel_promise.js';
import { FreeBusyGuests } from './guestspart.free_busy.js';
import {
  CLASS_MANUALLY_CHANGED,
  GUEST_SEPARATOR,
  REPLACE_CHAR,
  ROLE_ATTENDEE_OPTIONNAL,
  SELECTOR_CHECKBOX_NOTIFY,
} from './parts.constants.js';
import { FakePart, Parts } from './parts.js';
import { TimePartManager } from './timepart.js';

/**
 * @typedef User
 * @property {string} name
 * @property {string} email
 */

/**
 * @typedef Attendee
 * @property {Role} role
 * @property {string} name
 * @property {string} email
 */

/**
 * Contient les mêmes informations que Attendee mais avec l'information de la partie parente.
 * @typedef AttendeeEx
 * @property {Role} role
 * @property {string} name
 * @property {string} email
 * @property {GuestsPart} parent
 * @see {@link Attendee}
 */

/**
 * @class
 * @classdesc Exception envoyé lorsque le champ lié aux participants ne fonctionne pas correctement.
 * @extends BnumException
 * @package
 * @frommodule Exceptions
 */
class GuestPartFieldException extends BnumException {
  constructor(part, $field) {
    super(
      part,
      `Le champ ${$field.attr('id') || 'sans identifiant'} ne possède pas de parent !`,
    );

    /**
     * Champ qui pose problème
     * @type {external:jQuery}
     * @readonly
     * @member
     */
    this.$field = null;
    Object.defineProperty(this, '$field', {
      get() {
        return $field;
      },
    });
  }
}

/**
 * @class
 * @classdesc Bouton qui affiche ou non les différents champs liés aux participants.
 * @extends Parts
 * @frommodule EventView/Parts
 * @package
 */
class GuestButton extends Parts {
  constructor($button) {
    super($button, Parts.MODE.click);

    /**
     * Sera appelé lorsque l'on cliquera sur le bouton
     * @event
     * @member
     * @type {BnumEvent}
     */
    this.clicked = new BnumEvent();
    /**
     * Etat du bouton
     * @private
     * @member
     * @type {boolean}
     */
    this._state = false;
  }

  /**
   * Met à jour la visibilité des champs et change l'état interne du bouton.
   * @override
   */
  onUpdate() {
    this._state = !this._state;

    if (this._state) this._$field.find('span').html('expand_less');
    else this._$field.find('span').html('expand_more');
  }

  /**
   * Action éxecuter lors du clique sur le bouton.
   * @override
   * @overload
   */
  onClick() {
    this.clicked.call(!this._state);
    this.onUpdate();
  }

  /**
   * Simule un clique sur le bouton
   */
  click() {
    this.onClick();
  }
}

/**
 * @class
 * @classdesc Représente un participant
 * @package
 */
class Guest {
  constructor(name, email) {
    this._init()._setup(name, email);
  }

  /**
   * Initialise les membres de la classe
   * @private
   * @return {Guest}
   */
  _init() {
    /**
     * Nom du participant
     * @type {string}
     * @member
     * @readonly
     */
    this.name = EMPTY_STRING;
    /**
     * Email du participant
     * @type {string}
     * @member
     * @readonly
     */
    this.email = EMPTY_STRING;

    return this;
  }

  /**
   * Assigne les membres de la classe
   * @private
   * @param {string} name
   * @param {string} email
   */
  _setup(name, email) {
    Object.defineProperties(this, {
      name: {
        get() {
          return name.replaceAll('"', EMPTY_STRING);
        },
      },
      email: {
        get() {
          return email;
        },
      },
    });
  }

  /**
   * Récupère la disponibilité du participant
   * @async
   * @param {Date | external:moment} start Date de début de l'évènement
   * @param {Date | external:moment} end Date de fin de l'évènement
   * @param {!boolean} allDay Si l'évènement dure la journée entière ou non. `false` par défaut.
   * @return {Promise<string>}
   */
  async get_dispo(start, end, allDay = false) {
    let dispo = EMPTY_STRING;
    const date2servertime = cal.date2ISO8601;
    const clone_date = function (date, adjust) {
      var d = 'toDate' in date ? date.toDate() : new Date(date.getTime());

      // set time to 00:00
      if (adjust === 1) {
        d.setHours(0);
        d.setMinutes(0);
      }
      // set time to 23:59
      else if (adjust === 2) {
        d.setHours(23);
        d.setMinutes(59);
      }

      return d;
    };

    await $.ajax({
      type: 'GET',
      dataType: 'html',
      //Pamela - correction de l'url
      url: rcmail.get_task_url(
        'calendar&_action=freebusy-status',
        window.location.origin + window.location.pathname,
      ), //rcmail.url('freebusy-status'),
      data: {
        email: this.email,
        start: date2servertime(clone_date(start, allDay ? 1 : 0)),
        end: date2servertime(clone_date(end, allDay ? 2 : 0)),
        _remote: 1,
      },
      success: function (status) {
        var avail = String(status).toLowerCase();
        //console.log('avail', avail);
        dispo = avail;
        //icon.removeClass('loading').addClass(avail).attr('title', rcmail.gettext('avail' + avail, 'calendar'));
      },
      error: function () {
        dispo = 'unknown';
        //icon.removeClass('loading').addClass('unknown').attr('title', rcmail.gettext('availunknown', 'calendar'));
      },
    });

    return dispo;
  }

  /**
   * Supprime le participant de la liste des participants de sauvegarde.
   * @param {string} email email du participant à supprimer
   */
  _remove_from_table(email) {
    $(`#edit-attendees-table .attendee-name [title="${email}"]`)
      .closest('tr')
      .remove();
  }

  /**
   * Appeler lorsque que l'on clique sur le bouton "supprimer" lié au participant.
   * @package
   * @param {Event} e
   */
  _close_button_click(e) {
    const email = $(e.currentTarget).attr('data-parent');
    this._remove_from_table(email);
    let $querry = $(`div.mel-attendee[data-email="${email}"]`);
    const add = true;
    const attendee = this.toAttendee(
      Guest._GuestRole($querry.attr('data-parent'), GuestsPart.INSTANCE),
    );
    const attendee_list = cal.edit_update_current_event_attendee(
      attendee,
      !add,
    );
    $querry.remove();
    $querry = null;
    GuestsPart.SetAttendeesListOrganizer(attendee_list, attendee);
    GuestsPart.UpdateFreeBusy(GuestsPart.INSTANCE._datePart);
  }

  /**
   * Renvoie le participant sous forme de texte.
   * @private
   * @return {string}
   */
  _formated() {
    if (this.name || false) return `${this.name} <${this.email}>`;
    else return this.email;
  }

  /**
   * Appeler lorsque l'on démarre un drag and drop sur le participant.
   * @package
   * @param {DragEvent} ev
   */
  _dragStart(ev) {
    ev = ev.dataTransfer ? ev : ev.originalEvent;
    ev.dataTransfer.dropEffect = 'move';
    ev.dataTransfer.setData(
      'text/plain',
      JSON.stringify({
        name: this.name,
        email: this.email,
        string: this.toString(),
      }),
    );

    $('.mel-show-attendee-container, [data-linked="attendee-input"]').addClass(
      'mel-guest-drag-started',
    );
  }

  /**
   * Appeler lorsque l'on termine le drag and drop sur le participant.
   * @package
   * @param {DragEvent} ev
   */
  _dragEnd(ev) {
    $(
      '.mel-show-attendee-container, [data-linked="attendee-input"]',
    ).removeClass('mel-guest-drag-started');

    return ev;
  }

  /**
   * Convertit le participant sous une représentation html avec son comportement.
   * @param {*} event Evènement du plugin `Calendar`
   * @return {____JsHtml}
   */
  toHtml(event) {
    let html = MelHtml.start
      .div({
        class: 'mel-attendee',
        title: this._formated(),
        'data-email': this.email,
        'data-name': this.name,
        draggable: true,
        ondragstart: this._dragStart.bind(this),
        ondragend: this._dragEnd.bind(this),
      })
      .input({
        type: 'hidden',
        class: 'edit-attendee-reply',
        value: this.email,
        checked: 'checked',
      })
      .span({ class: 'availability' })
      .css({ 'margin-left': '5px', 'margin-right': '5px' })
      .span({ class: 'availabilityicon loading' })
      .end()
      .end()
      .span({ class: 'attendee-name' })
      .text(this.name || this.email)
      .end()
      .button({
        type: 'button',
        class: 'close-button btn btn-secondary for-attendee',
        'data-parent': this.email,
        onclick: this._close_button_click.bind(this),
      })
      .removeClass('mel-button')
      .removeClass('no-button-margin')
      .removeClass('no-margin-button')
      .icon('close')
      .end()
      .end()
      .end()
      .generate();

    this.get_dispo(event.start, event.end, event.AllDay).then((dispo) => {
      html
        .find('.availabilityicon')
        .removeClass('loading')
        .addClass(dispo)
        .attr('title', rcmail.gettext('avail' + dispo, 'calendar'));
    });

    return html;
  }

  checkValid($html, parent) {
    if (
      $html.data('hiddenrole') &&
      $html.data('hiddenrole') !==
        Guest._GuestRole($html.attr('data-parent'), parent)
    ) {
      $html
        .attr('draggable', false)
        .css('cursor', 'not-allowed')
        .find('.close-button')
        .remove();
      $html[0].ondragstart = null;
    }

    return $html;
  }

  /**
   * Affiche le participant sous forme de texte.
   * @return {string}
   */
  toString() {
    return this._formated();
  }

  /**
   * Change le participant en un objet qui sera lisible pour d'autre fonctions.
   * @param {!Role} role Roe du participant. "Requis" par défaut.
   * @return {!Attendee}
   */
  toAttendee(role = GuestsPart.ROLES.required) {
    return {
      role,
      name: this.name,
      email: this.email,
    };
  }

  /**
   * Récupère l'email, le nom et le rôle d'un participant à partir de son html.
   * @static
   * @param {external:jQuery} $attendee
   * @param {GuestsPart} parent
   * @return {AttendeeEx}
   */
  static From($attendee, parent) {
    return {
      name: $attendee.attr('data-name'),
      email: $attendee.attr('data-email'),
      role: Guest._GuestRole($attendee.attr('data-parent'), parent),
    };
  }

  /**
   * Récupère la dispnobilitée d'un participant à partir de son html.
   * @param {external:jQuery} $attendee
   * @param {Date | external:moment} start Date de l'évènement
   * @param {Date | external:moment} end Date de fin de l'évènement
   * @param {!boolean} allDay Si l'évènement dure toute la journée ou non. `false` par défaut.
   * @return {Promise<string>}
   * @async
   * @static
   */
  static async GetDispo($attendee, start, end, allDay = false) {
    const email = $attendee.attr('data-email');

    return await new Guest(EMPTY_STRING, email).get_dispo(start, end, allDay);
  }

  /**
   * Met à jour la disponibilitée d'un participant à partir de son html.
   * @param {external:jQuery} $attendee
   * @param {Date | external:moment} start Date de l'évènement
   * @param {Date | external:moment} end Date de fin de l'évènement
   * @param {!boolean} allDay Si l'évènement dure toute la journée ou non. `false` par défaut.
   * @return {Promise<string>}
   * @async
   * @static
   */
  static async UpdateDispo($attendee, start, end, allDay = false) {
    let $avail = $attendee.find('.availabilityicon');
    let tmp = MelEnumerable.from($avail[0].classList).toArray();
    for (const iterator of tmp) {
      if (!['availabilityicon', 'loading'].includes(iterator))
        $avail.removeClass(iterator);
    }

    $avail.attr('title', EMPTY_STRING).addClass('loading');

    const dispo = await this.GetDispo($attendee, start, end, allDay);

    $avail
      .addClass(dispo)
      .removeClass('loading')
      .attr('title', rcmail.gettext('avail' + dispo, 'calendar'));

    return dispo;
  }

  /**
   * @private
   * @param {string} id
   * @param {string} parent
   * @return {Role}
   */
  static _GuestRole(id, parent) {
    let role = EMPTY_STRING;

    switch (id) {
      case null:
      case parent._$fakeField.attr('id'):
        role = GuestsPart.ROLES.required;
        break;

      default:
        role = GuestsPart.ROLES.optional;
        break;

      case parent._$animators.attr('id'):
        role = GuestsPart.ROLES.chair;
        break;
    }

    return role;
  }
}

/**
 * @class
 * @classdesc Représente la partie des participants.
 * @extends FakePart
 * @frommodule EventView/Parts
 */
export class GuestsPart extends FakePart {
  /**
   *
   * @param {external:jQuery} $addInput
   * @param {external:jQuery} $neededInput
   * @param {external:jQuery} $optionalInput
   * @param {external:jQuery} $animatorsInput
   * @param {external:jQuery} $switchButton
   * @param {TimePartManager} datePart
   */
  constructor(
    $addInput,
    $neededInput,
    $optionalInput,
    $animatorsInput,
    $switchButton,
    datePart,
  ) {
    super($addInput, $neededInput, Parts.MODE.change);
    GuestsPart.stop = true;
    /**
     * Champ lié aux participants optionnels
     * @type {external:jquery}
     * @package
     */
    this._$optionnals = $optionalInput;
    /**
     * Champ lié aux participants qui dirige la réunion
     * @type {external:jquery}
     * @package
     */
    this._$animators = $animatorsInput;
    /**
     * Bouton qui permet d'afficher tout les champs ou non.
     * @type {GuestButton}
     * @package
     */
    this._switchButton = new GuestButton($switchButton);
    /**
     * Partie lié aux dates
     * @type {TimePartManager}
     * @package
     */
    this._datePart = datePart;
    /**
     * Si on peut modifier les participants ou non.
     * @type {boolean}
     */
    this.cant_modify = false;

    /**
     * Instance de l'évènement en cours
     * @static
     * @type {GuestsPart}
     */
    GuestsPart.INSTANCE = this;

    const ids = {
      start_date: this._datePart._$start_date.attr('id'),
      end_date: this._datePart._$end_date.attr('id'),
      start_time: this._datePart.start._$fakeField.attr('id'),
      end_time: this._datePart.end._$fakeField.attr('id'),
    };

    //Action à faire lorsque l'on quitte la dialogue "freebusy"
    if (!rcmail.has_guest_dialog_attendees_save) {
      const fnc = function (date) {
        const { start, end } = date;
        GuestsPart.can = false;
        $(`#${this.start_date}`).val(start.date).change();
        TimePartManager.UpdateOption(this.start_time, start.time);
        $(`#${this.start_time}`).val(start.time).change();
        $(`#${this.end_date}`).val(end.date).change();
        TimePartManager.UpdateOption(this.end_time, end.time);
        $(`#${this.end_time}`).val(end.time).change();
        GuestsPart.can = true;

        GuestsPart.INSTANCE.update_free_busy();
      };

      rcmail.addEventListener('dialog-attendees-save', fnc.bind(ids));

      rcmail.has_guest_dialog_attendees_save = true;
    }

    //Ajout des actions sur certains boutons.
    $(SELECTOR_CHECKBOX_NOTIFY)
      .removeClass(CLASS_MANUALLY_CHANGED)
      .off('change')
      .on('change', this.onNotificationChanged.bind(this));

    if (rcmail.env['event_prop.mails'])
      $('#event-mail-add-member')
        .off('click')
        .on('click', this.onButtonMailClicked.bind(this))
        .parent()
        .css('display', '')
        .addClass('d-flex');
    else
      $('#event-mail-add-member')
        .off('click')
        .parent()
        .css('display', 'none')
        .removeClass('d-flex');

    $('#emel-free-busy').removeClass('full-power');
  }

  /**
   * Initialise la partie
   * @param {*} event Evènement du plugin `Calendar`
   * @override
   */
  init(event) {
    let it = 0;
    const fields = [this._$fakeField, this._$optionnals, this._$animators];
    const main_field_index = 0;
    const cant_modify =
      (event?.attendees?.length || 0) > 0 && !cal.is_internal_organizer(event);

    $('.mel-attendee').remove();
    $('#emel-free-busy').html('');

    for (const $field of fields) {
      if ($(`[data-linked="${$field.attr('id')}"]`).length === 0)
        throw new GuestPartFieldException(this, $field);

      this._add_focus_if_not_exist($field);
      rcmail.init_address_input_events($field);

      if (it++ !== main_field_index)
        this._p_try_add_event(
          $field,
          'change',
          this.onChange.bind(this, $field),
        );

      this._p_try_add_event($field, 'input', this.onInput.bind(this, $field));
      this._p_try_add_event($field, 'keyup', this.onKeyUp.bind(this, $field));

      if (cant_modify) {
        $field.attr('disabled', 'disabled').addClass('disabled');
      } else {
        $field.removeAttr('disabled').removeClass('disabled');
      }
    }

    if (this._switchButton.clicked.count() === 0) {
      this._switchButton.clicked.push(this._on_switch.bind(this));
    }

    if ((event?.attendees?.length ?? 0) > 0) {
      let $field;
      for (const iterator of event.attendees) {
        switch (iterator.role) {
          case GuestsPart.ROLES.required:
            $field = this._$fakeField;
            break;

          case 'NON-PARTICIPANT':
          case GuestsPart.ROLES.optional:
            $field = this._$optionnals;
            break;

          case GuestsPart.ROLES.chair:
            $field = this._$animators;
            break;

          default:
            continue;
        }

        $field.before(
          new Guest(iterator.name, iterator.email)
            .toHtml(event)
            .attr('data-parent', $field.attr('id')),
        );
      }

      $field = null;
    }

    this._switchButton._state = true;
    this._switchButton.click();
    $(SELECTOR_CHECKBOX_NOTIFY).prop('checked', false);

    if (cant_modify) {
      $('#showcontacts_attendee-input')
        .attr('disabled', 'disabled')
        .addClass('disabled');
    } else {
      $('#showcontacts_attendee-input')
        .removeAttr('disabled')
        .removeClass('disabled');

      if ((event?.attendees?.length ?? 0) > 0) {
        $(SELECTOR_CHECKBOX_NOTIFY).prop('checked', true);
        this.update_free_busy();
      }
    }

    if (!!event.attendees && !!event.attendees.length) {
      if (
        MelEnumerable.from(event.attendees)
          .where((x) => x.email === GuestsPart.GetMe().email)
          .firstOrDefault()?.role !== 'ORGANIZER' ||
        cant_modify
      ) {
        $('#edit-attendees-donotify').parent().hide();
        $('#emel-free-busy').hide();
      } else {
        $('#edit-attendees-donotify').parent().show();
        $('#emel-free-busy').show();
      }
    }

    this.cant_modify = cant_modify;
  }

  /**
   * Récupère l'email et le nom d'un participant mis dans un champ
   * @private
   * @param {string} val Valeur récupérer dans le champ
   * @return {User}
   */
  _slice_user_datas(val) {
    if (val.includes(REPLACE_CHAR))
      val = val.replaceAll(REPLACE_CHAR, GUEST_SEPARATOR);

    let data = {
      name: EMPTY_STRING,
      email: EMPTY_STRING,
    };

    if (val.includes('<')) {
      val = val.split('<');
      data.name = val[0].trim();
      data.email = val[1].replace('>', EMPTY_STRING).trim();
    } else if (val.includes('@')) data.email = val.trim();

    return data;
  }

  /**
   * Met en forme les arguments pour les fonctions de la classe.
   * @private
   * @param {*[]} args
   * @return {Array}
   */
  _get_args(args) {
    let [event, $field] = args;

    if (event.val) {
      return [$field, event];
    }

    return args;
  }

  /**
   * Ajoute les focus et focusout sur un champ si ils n'existent pas.
   * @private
   * @param {external:jQuery} $field
   */
  _add_focus_if_not_exist($field) {
    if (!$._data($field[0], 'events')?.focus)
      $field.on('focus', (e) => {
        $(`[data-linked="${$(e.currentTarget).attr('id')}"]`).addClass(
          'forced-focus',
        );
      });

    if (!$._data($field[0], 'events')?.focusout)
      $field.on('focusout', (e) => {
        $(`[data-linked="${$(e.currentTarget).attr('id')}"]`).removeClass(
          'forced-focus',
        );
      });
    return this;
  }

  /**
   * Action qui sera éffectuer lorsque l'on cliquera sur le bouton qui déplie ou non les champs
   * @param {boolean} state
   * @package
   */
  _on_switch(state) {
    if (state) {
      //Affiche "Participants obligatoires"
      this._$fakeField.attr(
        'placeholder',
        rcmail.gettext('req_guest', 'mel_metapage'),
      );
      this._$optionnals.parent().css('display', EMPTY_STRING);
      this._$animators.parent().css('display', EMPTY_STRING);

      for (const iterator of this._$fakeField.parent().find('.mel-attendee')) {
        $(`#${$(iterator).attr('data-parent')}`).before($(iterator));
      }
    } else {
      //Affiche "Ajouter des participants"
      this._$fakeField.attr(
        'placeholder',
        rcmail.gettext('add_guests', 'mel_metapage'),
      );
      this._$optionnals.parent().css('display', 'none');
      this._$animators.parent().css('display', 'none');

      const fields = [this._$optionnals, this._$animators];

      for (const $field of fields) {
        for (const iterator of $field.parent().find('.mel-attendee')) {
          this._$fakeField.before($(iterator));
        }
      }
    }
  }

  /**
   * Met à jour les disponibilitées des participants
   * @async
   * @return {Promise<void>}
   */
  async update_free_busy() {
    await GuestsPart.UpdateFreeBusy(this._datePart);
  }

  includes(email) {
    return MelEnumerable.from($('div.mel-attendee')).any(
      (x) => $(x).attr('data-email') === email,
    );
  }

  /**
   * Met à jours les données liés aux participants lorsqu'un champ a été modifié
   * @param {string} val
   * @param {external:jQuery} $field
   * @package
   * @returns {boolean}
   */
  _onUpdate(val, $field) {
    var $attendee_field;
    var guest;
    var role;
    var attendees;

    console.log('update', val);

    {
      //On vérifie si il y a des virgules entre parenthèses puis on les gères.
      const regex = REG_BETWEEN_PARENTHESIS;
      const match = val.match(regex);

      if (!!match && match.length > 0) {
        for (const iterator of match) {
          val = val.replaceAll(
            iterator,
            iterator.replace(GUEST_SEPARATOR, REPLACE_CHAR),
          );
        }
      }
    }

    if (val.includes(GUEST_SEPARATOR)) {
      const event = cal.selected_event;
      val = val.split(GUEST_SEPARATOR);
      let has_invalid_email = false;
      let invalids = [];

      //Itère sur chaque participant, ils sont sous la forme nom<email>
      for (const iterator of MelEnumerable.from(val)
        .where((x) => x.trim() !== EMPTY_STRING)
        .select(this._slice_user_datas.bind(this))) {
        if (iterator.email === EMPTY_STRING) {
          if (iterator.name === EMPTY_STRING) continue;

          has_invalid_email = true;
          invalids.push(iterator);
          continue;
        }

        if (iterator.email === GuestsPart.GetMe().email) continue;

        if ($(`div.mel-attendee[data-email="${iterator.email}"`).length === 0) {
          //On créer un participant, on lui met son rôle
          $attendee_field =
            $attendee_field ||
            $(`.mel-show-attendee[data-linked="${$field.attr('id')}"]`);
          guest = new Guest(iterator.name, iterator.email);

          if (
            (guest.name || false) &&
            guest.name.includes('=') &&
            guest.name.includes(':')
          ) {
            if (guest.name.includes('role=')) {
              role = guest.name.split('role=')[1].split(':')[0];
              guest = new Guest(guest.name.split(':')[1], guest.email);
              guest.aer = true;
            }
          }

          if (!guest.aer) role = Guest._GuestRole($field.attr('id'), this);
          //Met à jours les participants dans le plugin "Calendar" pour qu'ils soient sauvegardés
          attendees = cal.edit_update_current_event_attendee(
            guest.toAttendee(role),
          );
          //Gère si il y a un organisateur
          GuestsPart.SetAttendeesListOrganizer(
            attendees,
            guest.toAttendee(role),
          );
          //Ajoute au champs
          $field.before(
            guest
              .toHtml(event)
              .attr('data-parent', $field.attr('id'))
              .attr('data-hiddenrole', role),
          );
          //On vide les variables
          $attendee_field = null;
          guest = null;
          role = null;
          attendees = null;
        } else {
          rcmail.display_message(
            rcmail
              .gettext('existing_guest', 'mel_metapage')
              .replaceAll('%0', iterator.name || iterator.email),
            'error',
          );
        }
      }

      if (has_invalid_email) {
        rcmail.display_message(
          rcmail.gettext('invalid_guests', 'mel_metapage'),
          'warning',
        );
        console.warn('/!\\[onUpdate]', invalids);
      }

      if (!$(SELECTOR_CHECKBOX_NOTIFY).hasClass(CLASS_MANUALLY_CHANGED))
        $(SELECTOR_CHECKBOX_NOTIFY).prop('checked', true);

      $field.val(EMPTY_STRING);
      $field.focus();

      this.update_free_busy();

      return true;
    }

    return false;
  }

  /**
   * Met à jour les données lorsque l'on appuyer sur "entrer"
   * @param  {...any} args
   */
  onKeyUp(...args) {
    const [e, $field] = this._get_args(args);

    if (e.keyCode === 13) {
      this.onUpdate($(e.currentTarget).val(), $field);
    }
  }

  /**
   * Met à jours les données liés aux participants lorsqu'un champ a été modifié
   * @param {string} val
   * @param {external:jQuery} $field
   * @override
   */
  onUpdate(val, $field) {
    if (!this._onUpdate(val ?? '', $field))
      this._onUpdate(`${val}${GUEST_SEPARATOR}`, $field);
  }

  /**
   * Met à jours les données liés aux participants lorsqu'un champ a été modifié
   * @param  {...any} args
   * @override
   */
  onInput(...args) {
    const [e, $field] = this._get_args(args);
    this._onUpdate(
      $(e.currentTarget).val(),
      ($field?.click ? $field : null) ?? this._$fakeField,
    );
  }

  /**
   * Action qui sera appelé lorsque l'un des champs changera de valeur
   * @param  {...any} args
   * @override
   */
  onChange(...args) {
    const [e, $field] = this._get_args(args);
    this.onUpdate(
      $(e.currentTarget).val(),
      ($field?.click ? $field : null) ?? this._$fakeField,
    );
  }

  /**
   * Appeler lorsque l'on change l'état de la checkbox de notification des participants
   * @param {Event} e
   */
  onNotificationChanged(e) {
    e = $(e.currentTarget);
    let $querry = $(SELECTOR_CHECKBOX_NOTIFY);
    //Lorsque cette checkbox à "manually-changed", le changement de cette checkbox ne pourra plus se faire automatiquement
    if (!$querry.hasClass(CLASS_MANUALLY_CHANGED)) {
      $querry.addClass(CLASS_MANUALLY_CHANGED);
    }

    //Désactiver les autres checkboxes
    $('#edit-attendees-invite').prop('checked', e.prop('checked'));
    $('input.edit-attendee-reply').prop('checked', e.prop('checked'));

    $querry = null;
    e = null;
  }

  /**
   * Appeler Lorsque l'on clique sur le bouton qui permet d'ajouter les déstinataires d'un mail dans les participants
   * @param {Event} e
   */
  onButtonMailClicked(e) {
    const data = rcmail.env['event_prop.mails'];

    if (data.cc) this._$fakeField.val(`${data.cc},`).change();

    if (data.from) this._$fakeField.val(`${data.from},`).change();

    if (data.to) this._$fakeField.val(`${data.to},`).change();

    return e;
  }

  /**
   * Gère les participants en supprimant ou en ajoutant l'organisateur.
   *
   * Cela permet d'avoir l'organisateur dans la dialogue de disponibilitée.
   * @param {Attendee[]} list
   * @param {Attendee} current_guest
   * @static
   */
  static SetAttendeesListOrganizer(list, current_guest) {
    const add = true;
    if (list.length === 1) {
      if (!MelEnumerable.from(list).any((x) => x.role === 'ORGANIZER')) {
        cal.edit_update_current_event_attendee(current_guest, !add);
        cal.edit_update_current_event_attendee(
          this.GetMe().toAttendee('ORGANIZER'),
          add,
        );
        cal.edit_update_current_event_attendee(current_guest, add);
      } else cal.edit_clear_attendees();
    }
  }

  /**
   * Ajout ou met à jours les créneaux de disponibilités des participants
   * @param {TimePartManager} timePart Pour récupérer la date et l'heure de début et de fin de l'évènement
   * @return {Promise<void>}
   * @static
   * @async
   */
  static async UpdateFreeBusy(timePart) {
    if (GuestsPart.can === false) return;

    if (GuestsPart.stop) GuestsPart.stop = false;

    const CLASS_FULL_WIDTH = 'full-power';
    const start = timePart.date_start;
    const end = timePart.date_end;
    const attendees = MelEnumerable.from($('.mel-attendee'))
      .select((x) => Guest.From($(x), GuestsPart.INSTANCE))
      .aggregate([this.GetMe().toAttendee()])
      .toArray();

    if (attendees.length > 1 && !GuestsPart.INSTANCE.cant_modify) {
      let $main_div = $('#emel-free-busy')
        .addClass(CLASS_FULL_WIDTH)
        .html(
          $(
            '<div class="absolute-center"><span class="spinner-border"><span class="sr-only">Loading...</span></span></div>',
          ),
        );
      let promise = new Mel_Promise(
        async () =>
          await new FreeBusyGuests().load_freebusy_data({
            start,
            end,
            attendees,
          }),
      );

      while (promise.isPending()) {
        await Mel_Promise.Sleep(10);

        if (GuestsPart.stop) {
          $('#emel-free-busy').removeClass(CLASS_FULL_WIDTH).html('');
          GuestsPart.stop = false;
          console.info('Stopped !');
          return;
        }
      }

      const slots = await promise;
      promise = null;
      let $find_next = MelHtml.start
        .button({
          class: 'slot',
          type: 'button',
          onclick: () => $('#edit-attendee-schedule').click(),
        })
        .css('height', '100%')
        // eslint-disable-next-line quotes
        .text("Trouver d'autres disponibilités")
        .end()
        .generate();

      if (slots.length > 0) {
        $('#emel-free-busy').find('.spinner-border').addClass('text-success');
        let $div = $('<div>').addClass('row');

        for (const slot of slots) {
          slot
            .generate(timePart)
            .generate()
            .appendTo(
              $('<div>')
                .addClass('col-6 col-md-3')
                .css('padding', '5px')
                .appendTo($div),
            );
        }

        $find_next.appendTo(
          $('<div>')
            .addClass('col-6 col-md-3')
            .css('padding', '5px')
            .appendTo($div),
        );

        $main_div.html($div);
      } else
        $main_div.html(
          $find_next.appendTo(
            $('<div>').addClass('col-6 col-md-3').css('padding', '5px'),
          ),
        );

      $('<div>')
        .addClass('dispo-freebusy-text')
        .text('Premières disponibilité commune')
        .css('margin-bottom', '5px')
        .prependTo($main_div);
    } else
      (GuestsPart.stop = true),
        $('#emel-free-busy').removeClass(CLASS_FULL_WIDTH).html(EMPTY_STRING);
  }

  /**
   * Récupère les informations de l'utilisateur courant, parmis ses calendriers.
   * @return {Guest}
   * @static
   */
  static GetMe() {
    const cal = rcmail.env.calendars[$('#edit-calendar').val()];

    return new Guest(cal?.owner_name || EMPTY_STRING, cal.owner_email);
  }

  /**
   * Met à jours les disponibilités des participants
   * @param {external:moment} start Date de début de l'évènement
   * @param {external:moment} end Date de fin de l'évènement
   * @param {boolean} allDay Si l'évènement dure la journée entière ou non
   * @return {Promise<void>}
   */
  static async UpdateDispos(start, end, allDay = false) {
    //Si une mise à jours est déjà en cours, on attend qu'elle se termine
    if (this.UpdateDispos.promises) {
      //Si une mise à jours est différente de la précédente, on attend l'ancienne puis on fait la nouvelle
      if (
        this.UpdateDispos.promises.start.format() !== start.format() ||
        this.UpdateDispos.promises.end.format() !== end.format() ||
        this.UpdateDispos.promises.allDay !== allDay
      )
        await this.UpdateDispos.promises.promises;
      else return;
    }

    let promises = [];
    for (const iterator of $('.mel-attendee')) {
      promises.push(Guest.UpdateDispo($(iterator), start, end, allDay));
    }

    promises = Promise.allSettled(promises);

    this.UpdateDispos.promises = { promises, start, end, allDay };
    await this.UpdateDispos.promises.promises;

    this.UpdateDispos.promises = null;
  }
}

/**
 * @typedef DispoPromise
 * @property {external:moment} start
 * @property {external:moment} end
 * @property {Promise} promises
 */

/**
 * Contient les promesses de la modification de date en cours.
 *
 * Cela permet de ne pas faire de requêtes inutiles.
 * @type {?DispoPromise}
 * @static
 * @package
 */
GuestsPart.UpdateDispos.promises = undefined;

/**
 * Si l'on peut mettre à jour les créneaux ou non.
 * @see {@link GuestsPart.UpdateFreeBusy}
 * @type {boolean}
 * @static
 */
GuestsPart.can = true;
/**
 * Si l'on doit arrêter la mise à jours des créneaux ou non.
 * @see {@link GuestsPart.UpdateFreeBusy}
 * @type {boolean}
 * @static
 */
GuestsPart.stop = false;
/**
 * Instance de l'évènement en cours
 * @static
 * @readonly
 */
GuestsPart.event = null;
Object.defineProperty(GuestsPart, 'event', {
  get() {
    return cal.selected_event;
  },
});

/**
 * @typedef {string} Role
 */

/**
 * @enum {Role}
 * @static
 * @readonly
 */
GuestsPart.ROLES = {
  required: 'REQ-PARTICIPANT',
  optional: ROLE_ATTENDEE_OPTIONNAL,
  chair: 'CHAIR',
  resource: 'RESOURCE',
};