import { FreeBusyLoader } from '../../../mel_metapage/js/lib/calendar/free_busy_loader.js';
import { BnumMessage } from '../../../mel_metapage/js/lib/classes/bnum_message.js';
import { MelEnumerable } from '../../../mel_metapage/js/lib/classes/enum.js';
import { DATE_TIME_FORMAT } from '../../../mel_metapage/js/lib/constants/constants.dates.js';
import { EMPTY_STRING } from '../../../mel_metapage/js/lib/constants/constants.js';
import { MelHtml } from '../../../mel_metapage/js/lib/html/JsHtml/MelHtml.js';
import { FavoriteLoader } from './favorite_loader.js';
import { ResourcesBase } from './resource_base.js';

export { ResourceBaseFunctions };

/**
 * Ajoute une location à la liste de location d'un évènement
 * @module Resources/ResourceBaseFunctions/Functions
 * @local FunctionFrom
 * @local ResourceBaseFunctions
 */

/**
 * @class
 * @classdesc Change le "this" du callback associé
 * @package
 * @template {F}
 */
class FunctionFrom {
  /**
   * Constructeur de la classe
   * @param {function} callback Fonction qui nous intéresse
   * @param {F} thisArgs Nouveau "this" de la fonction
   */
  constructor(callback, thisArgs) {
    this._callback = callback;
    this._this = thisArgs;
  }

  /**
   * Appèle la fonction
   * @param  {...any} args Arguments de la fonction
   * @returns {*}
   */
  call(...args) {
    return this._callback.call(this._this, ...args);
  }

  /**
   * Récupère la fonction avec le nouveau "this"
   * @param  {...any} args
   * @returns
   */
  get(...args) {
    return this._callback.bind(this._this, ...args);
  }
}

/**
 * @class
 * @classdesc Contient les fonctions pour les différents appèles de la classe {@link ResourcesBase}
 */
class ResourceBaseFunctions {
  /**
   * Constructeur de la classe.
   *
   * Demande un objet de type {@link ResourcesBase} qui sera le "this" des fonctions membres de l'instance.
   * @param {ResourcesBase} resourceBase
   * @frommoduleparam Resources resourceBase
   * @override
   */
  constructor(resourceBase) {
    this.main(resourceBase);
  }

  /**
   * Change le "this" des fonctions de cette instance de classe.
   * @package
   * @param {ResourcesBase} resourceBase
   * @frommoduleparam Resources resourceBase
   * @override
   */
  main(resourceBase) {
    this._resourceBase = resourceBase;

    for (const iterator of Object.getOwnPropertyNames(this.__proto__)) {
      this[iterator] = new FunctionFrom(
        this[iterator],
        this._resourceBase,
      ).get();
    }
  }

  /**
   * Action lorsque l'on clique sur le bouton étoile d'une ressource, ce qui permet de la mettre en favoris ou non.
   * @param {Event} e Evènement envoyer par le click
   * @this ResourcesBase
   * @see {@link ResourceBaseFunctions.resource_render}
   * @frommodulethis Resources
   */
  on_star_clicked(e) {
    BnumMessage.SetBusyLoading();

    const id = $(e.currentTarget).data('email');
    const favorite = !JSON.parse(
      $(e.currentTarget).attr('data-favorite') ?? 'false',
    );

    $(e.currentTarget)
      .attr('data-favorite', favorite)
      .addClass('disabled')
      .attr('disabled', 'disabled');

    this.http_internal_post({
      task: 'mel_cal_resources',
      action: 'set_favorite',
      params: {
        _favorite: favorite,
        _uid: id,
      },
      on_success: (data) => {
        if (!this.get_env('fav_resources'))
          this.rcmail().env.fav_resources = [];

        this.rcmail().env.fav_resources[id] = favorite;
        this._on_data_changed();

        data = JSON.parse(data);

        if (data) {
          const current_favorite = this._p_resources.find(
            (x) => x.data.email === id,
          )?.data;

          if (current_favorite)
            FavoriteLoader.Add(this._name, current_favorite);
        } else FavoriteLoader.Remove(this._name, id);

        return data;
      },
    }).always(BnumMessage.StopBusyLoading.bind(BnumMessage));
  }

  /**
   * Action lorsqu'une date est sélectionné au clique
   * @this ResourcesBase
   * @param {external:moment} start Date de début
   * @param {external:moment} end Date de fin
   * @param {*} jsEvent /
   * @param {*} view /
   * @param {import('./resource_base.js').ResourceObject} resource Ressource sélectionnée
   * @see {@link ResourcesBase._generate_ui}
   * @frommodulethis Resources
   */
  on_selected_date(start, end, jsEvent, view, resource) {
    this.start = start;
    this.end = end;

    $(`#radio-${resource.data.uid}-${this.location_id}`).click();

    this._$calendar.fullCalendar('refetchEvents');
  }

  /**
   * Action lorsqu'une ressource est sélectionné
   * @param {Event} e
   * @this ResourcesBase
   * @see {@link ResourceBaseFunctions.resource_render}
   * @frommodulethis Resources
   */
  on_resource_selected(e) {
    e = $(e.currentTarget);

    const id = e
      .attr('id')
      .replace('radio-', EMPTY_STRING)
      .replace(`-${this.location_id}`, EMPTY_STRING);

    this.selected_resource = MelEnumerable.from(this._p_resources)
      .where((x) => x.data.uid === id)
      .firstOrDefault()?.data;

    for (let i = 0; i < this._p_resources.length; ++i) {
      if (this._p_resources[i].data.uid === id)
        this._p_resources[i].data.selected = true;
      else this._p_resources[i].data.selected = false;
    }

    this._$calendar.fullCalendar('refetchEvents');
  }

  /**
   * Action lorsqu'un label de ressource est cliqué
   * @param {Event} e
   * @this ResourcesBase
   * @see {@link ResourceBaseFunctions.resource_render}
   * @frommodulethis Resources
   * @deprecated
   */
  on_resource_label_clicked(e) {
    // for (let i = 0; i < this._p_resources.length; ++i) {
    //   this._p_resources[i].data.selected = false;
    // }
    // e = $(e.currentTarget);
    // if (!e.attr('for'))
    //   e = $(`label[for="${e.attr('id').replace('radio', EMPTY_STRING)}"`);
    // const id = e.data('id');
    // const index = MelEnumerable.from(this._p_resources)
    //   .select((x, i) => ({ x, i }))
    //   .where((x) => x.x.id === id)
    //   .first().i;
    // this._p_resources[index].data.selected = true;
  }

  /**
   * Action à faire lors du rendu de la ressource
   * @param {import('./resource_base.js').ResourceObject} resourceObj Ressourceà rendre
   * @param {external:jQuery} labelTds Div qui contient l'affichage
   * @this ResourcesBase
   * @see {@link ResourcesBase._generate_ui}
   * @frommodulethis Resources
   */
  resource_render(resourceObj, labelTds) {
    if (resourceObj.id !== 'resources') {
      labelTds
        .css('display', 'flex')
        .prepend(
          $(
            `<input type="radio" class="resource-radio" data-email="${resourceObj.data.email}" id="radio-${resourceObj.data.uid}-${this.location_id}" value="${resourceObj.data.email}" name="resa" ${resourceObj.data.selected ? 'checked' : EMPTY_STRING} />`,
          ).click(this._functions.on_resource_selected),
        )
        .append(
          MelHtml.start
            .div({ class: 'star-button-parent' })
            .button({
              class: 'star-button',
              id: `button-${resourceObj.data.uid}-${this.location_id}`,
              onclick: this._functions.on_star_clicked,
              'data-favorite':
                this.get_env('fav_resources')[resourceObj.data.email] ?? false,
              'data-email': resourceObj.data.email,
            })
            .icon('star')
            .end()
            .end()
            .end()
            .generate(),
        );

      labelTds = labelTds.find('.fc-cell-text');
      let parent = labelTds.parent();
      let text = labelTds.text();
      labelTds.remove();
      parent
        .html(
          $(
            `<label for="radio-${resourceObj.data.uid}-${this.location_id}"></label>`,
          )
            .data('id', resourceObj.data.uid)
            .text(text)
            .css('margin', '0 0 0 5px')
            .css('padding', 0)
            .click(this._functions.on_resource_label_clicked),
        )
        .css({
          height: '100%',
          display: 'flex',
          'align-items': 'center',
          padding: 0,
        });
    }
  }

  /**
   * Charge les évènements et es affiches dans `fullcalendar`
   * @this ResourcesBase
   * @param {external:moment} start Date de départ
   * @param {external:moment} end Date de fin
   * @param {number} timezone /
   * @param {Function} callback A appeller et à donner la liste des évènements en argument.
   * @frommodulethis Resources
   */
  event_loader(start, end, timezone, callback) {
    this._functions.event_loader_async(start, end, timezone, callback);
  }

  /**
   * Charge les évènements et es affiches dans `fullcalendar`.
   *
   * Ajoute les évènements au fur et à mesure. Cela ne permet de charger les évènements que lors de certaines actions et de ne pas
   * recharger ceux que l'on connaît déjà.
   * @async
   * @this ResourcesBase
   * @param {external:moment} start Date de départ
   * @param {external:moment} end Date de fin
   * @param {number} timezone /
   * @param {Function} callback A appeller et à donner la liste des évènements en argument.
   * @returns {Promise}
   * @frommodulethis Resources
   */
  async event_loader_async(start, end, timezone, callback) {
    const key = `${start.format()}-${end.format()}`;

    let emails = this._p_resources
      .map((x) => x.data.email)
      .filter((x) => !this._cache[key]?.includes?.(x));

    if (emails.length > 0) {
      if (!this._cache[key]) this._cache[key] = [];

      this._cache[key].push(...emails);

      let rcs = FreeBusyLoader.Instance.generate(emails, {
        start,
        end,
        interval: FreeBusyLoader.Instance.interval,
      });

      for await (const slots of rcs) {
        for (const slot of slots) {
          if (!slot.isFree) {
            this._p_events.push(new ResourceEvent(slot, slots.email));
          }
        }
      }
    }

    let rcs = MelEnumerable.from(this._p_events)
      .aggregate({
        title: 'Moi',
        start: this.start,
        end: this.end,
        allDay: this.all_day,
        resourceId: this.selected_resource?.email,
        color: 'green',
      })
      .toArray();

    callback(rcs);

    if (!this.itemloaded && this._p_events.length) {
      this.itemloaded = true;

      setTimeout(() => {
        this._$calendar.fullCalendar('refetchEvents');
      }, 100);
    }
  }

  /**
   * Lorsque une date a changée, met à jours les autres inputs identiques et gère les comportements de dates.
   * @this ResourcesBase
   * @frommodulethis Resources
   * @param {external:jQuery} $e Elément qui à changer
   * @param {string} html_class Classe de l'élément qui a changé
   */
  on_date_changed($e, html_class) {
    const val = $e.val();
    $(html_class).each((i, e) => {
      $(e).val(val);
    });

    const start = `${$('.input-date-start').val()} ${$('.input-time-start').val()}`;
    const end = `${$('.input-date-end').val()} ${$('.input-time-end').val()}`;
    this.start = moment(start, DATE_TIME_FORMAT);
    this.end = moment(end, DATE_TIME_FORMAT);

    if (this.end <= this.start) {
      this.end = moment(this.start).add(1, 'h');
    }

    this._$calendar.fullCalendar('gotoDate', this.start);
    this._$calendar.fullCalendar('refetchEvents');

    this.refresh_calendar_date();
  }

  /**
   * Lorsque la date de départ est changée.
   * @this ResourcesBase
   * @frommodulethis Resources
   * @param {Event} e Reçu lors du changement de date
   */
  on_date_start_changed(e) {
    this._functions.on_date_changed($(e.currentTarget), '.input-date-start');
  }

  /**
   * Lorsque la date de fin est changée.
   * @this ResourcesBase
   * @frommodulethis Resources
   * @param {Event} e Reçu lors du changement de date
   */
  on_date_end_changed(e) {
    this._functions.on_date_changed($(e.currentTarget), '.input-date-end');
  }

  /**
   * Lorsque l'heure de départ est changée.
   * @this ResourcesBase
   * @frommodulethis Resources
   * @param {Event} e Reçu lors du changement d'horaire
   */
  on_time_start_changed(e) {
    this._functions.on_date_changed($(e.currentTarget), '.input-time-start');
  }

  /**
   * Lorsque l'heure de fin est changée.
   * @this ResourcesBase
   * @frommodulethis Resources
   * @param {Event} e Reçu lors du changement d'horaire
   */
  on_time_end_changed(e) {
    this._functions.on_date_changed($(e.currentTarget), '.input-time-end');
  }
}

/**
 * @class
 * @classdesc Mise en forme des évènemant fullcalendar
 */
class ResourceEvent {
  /**
   * Constructeur de la classe
   * @param {Slot} slot Slot qui contient les données
   * @param {string} email Email qui correspond à l'id
   */
  constructor(slot, email) {
    /**
     * Titre de l'évènement
     * @type {string}
     * @default 'Occupé'
     */
    this.title = rcmail.gettext('mel_cal_resources.busy');
    /**
     * Date de début de l'évènement
     * @type {external:moment}
     */
    this.start = slot.start;
    /**
     * Date de fin de l'évènement
     * @type {external:moment}
     */
    this.end = slot.end;
    /**
     * Resource parente
     * @type {string}
     */
    this.resourceId = email;
  }
}