import { MelEnumerable } from '../../../mel_metapage/js/lib/classes/enum.js';
import {
  DialogPage,
  RcmailDialogButton,
} from '../../../mel_metapage/js/lib/classes/modal.js';
import {
  DATE_FORMAT,
  DATE_HOUR_FORMAT,
  DATE_TIME_FORMAT,
} from '../../../mel_metapage/js/lib/constants/constants.dates.js';
import { EMPTY_STRING } from '../../../mel_metapage/js/lib/constants/constants.js';
import { BnumEvent } from '../../../mel_metapage/js/lib/mel_events.js';
import { MelObject } from '../../../mel_metapage/js/lib/mel_object.js';
import { template_resource } from '../../skins/mel_elastic/js_template/resource.js';
import { FavoriteLoader } from './favorite_loader.js';
import { FilterBase, eInputType } from './filter_base.js';
import { ResourceBaseFunctions } from './resources_functions.js';

export { ResourcesBase, ResourceSettings };

/**
 * Contient les classes qui permet de gérer et afficher les ressources
 * @module Resources
 * @property {module:Resources/Filters} Filters
 * @property {module:Resources/Favorites/Loaders} FavoriteLoader
 * @property {module:Resources/Location} Location
 * @property {module:Resources/ResourceBaseFunctions/Functions} Functions
 * @local ResourceData
 * @local ResourcesBase
 * @local ResourceObject
 */

/**
 * @typedef ResourceData
 * @property {string} bal
 * @property {string} batiment
 * @property {?number} capacite
 * @property {?string} caracteristiques
 * @property {?string} description
 * @property {string} dn
 * @property {string} email
 * @property {string[]} email_list
 * @property {string} etage
 * @property {string} fullname
 * @property {string} locality
 * @property {string} name
 * @property {string} postal_code
 * @property {string} room_number
 * @property {?string} service
 * @property {string} street
 * @property {?string} title
 * @property {string} type
 * @property {string} uid
 */

/**
 * @typedef ResourceObject
 * @property {ResourceData} data
 */

/**
 * @class
 * @classdesc Représente une ressource
 * @extends MelObject
 */
class ResourcesBase extends MelObject {
  /**
   * Constructeur de la classe
   * @param {string} name Id de la ressource
   * @param {FilterConfig[]} filters Liste des filtres de la ressource. Ils seront convertit en {@link FilterBase}
   */
  constructor(name, filters) {
    super(name, filters);
  }

  /**
   * Fonction principale
   * @override
   * @private
   * @param  {...any} args
   */
  main(...args) {
    this._init()._setup(...args);
  }

  /**
   * Initialise les variables
   * @private
   * @returns {ResourcesBase} Chaînage
   */
  _init() {
    /**
     * @protected
     * @type {Array<FilterBase>}
     * @description Contient la liste des filtres qui est possible d'appliquer
     * @frommodule Resources/Filters {@linkto FilterBase}
     */
    this._p_filters = [];
    /**
     * @protected
     * @type {Array<ResourceObject>}
     * @description Ressources à afficher dans le tableau
     * @frommodule Resources {@linkto ResourceObject}
     * */
    this._p_resources = [];

    /**
     * Liste des évènements afficher dans l'agenda
     * @type {ResourceEvent[]}
     * @frommodule Resources/ResourceBaseFunctions/Functions
     * @protected
     */
    this._p_events = [];

    /**
     * @package
     * @type {Array<ResourceObject>}
     * @description Ressources à afficher dans le tableau. Ce sont les ressources de départ.
     * @frommodule Resources {@linkto ResourceObject}
     */
    this._init_resources = [];
    /**
     * Date de départ
     * @protected
     * @type {?string}
     */
    this._p_startDate = null;
    /**
     * Date de fin
     * @protected
     * @type {?string}
     */
    this._p_endDate = null;
    /**
     * Heure de départ
     * @protected
     * @type {?string}
     */
    this._p_startTime = null;
    /**
     * Heure de fin
     * @protected
     * @type {?string}
     */
    this._p_endTime = null;
    /**
     * Données en cache
     * @protected
     * @type {Object<string, *>}
     */
    this._p_internal_data = {};

    /**
     * Calendrier
     * @type {external:jQuery}
     */
    this._$calendar = null;

    /**
     * Données en cache
     * @private
     * @type {Object<string , *>}
     */
    this._cache = {};

    /**
     * @type {ResourceBaseFunctions}
     */
    this._functions = new ResourceBaseFunctions(this);

    /**
     * Date et heure de départ de la location
     * @type {?external:moment}
     */
    this.start = null;
    /**
     * Date et heure de fin de la location
     * @type {?external:moment}
     */
    this.end = null;

    /**
     * Journée entière ou non
     * @type {boolean}
     */
    this.all_day = false;

    /**
     * Ressource sélectionnées
     * @type {ResourceData}
     */
    this.selected_resource = null;

    /**
     * Action à faire lors du clique du bouton "Sauvegarder"
     * @type {BnumEvent<Function>}
     */
    this.event_on_save = new BnumEvent();

    /**
     * Id du mode d'évènement associé
     * @type {number}
     */
    this.location_id = null;
    return this;
  }

  /**
   * Assigne les variables
   * @private
   * @param  {...any} args
   */
  _setup(...args) {
    const [name, filters] = args;

    this._name = name;
    this._p_filters = filters.map((x) =>
      new FilterBase(x.name, x.size, {
        load_data_on_change: x.load_data_on_change,
        load_data_on_start: x.load_data,
        input_type: x.type,
        icon: x.icon,
        number: x.number,
      })
        .push_event(this._on_data_loaded.bind(this))
        .push_event_data_changed(this._on_data_changed.bind(this)),
    );

    let _allday;

    Object.defineProperties(this, {
      start: {
        get: () => {
          if (this.all_day) return moment(this._p_startDate, DATE_FORMAT);
          else
            return this._p_startDate && this._p_startTime
              ? moment(
                  `${this._p_startDate} ${this._p_startTime}`,
                  DATE_TIME_FORMAT,
                )
              : null;
        },
        set: (value) => {
          this._p_startDate = value.format(DATE_FORMAT);
          this._p_startTime = value.format(DATE_HOUR_FORMAT);

          $('.input-date-start').val(this._p_startDate);
          $('.input-time-start').val(this._p_startTime);
        },
      },
      end: {
        get: () => {
          if (this.all_day) return moment(this._p_endDate, DATE_FORMAT);
          else
            return this._p_endDate && this._p_endTime
              ? moment(
                  `${this._p_endDate} ${this._p_endTime}`,
                  DATE_TIME_FORMAT,
                )
              : null;
        },
        set: (value) => {
          this._p_endDate = value.format(DATE_FORMAT);
          this._p_endTime = value.format(DATE_HOUR_FORMAT);

          $('.input-date-end').val(this._p_endDate);
          $('.input-time-end').val(this._p_endTime);
        },
      },
      all_day: {
        get: () => $('#rc-allday')?.prop?.('checked') ?? _allday,
        set: (value) => {
          if ($('#rc-allday').length) $('#rc-allday').prop('checked', value);
          else _allday = value;

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

  /**
   * Génère l'agenda avec fullcalendar
   * @package
   * @param {external:jQuery} $fc Div qui contient le fullcalendar
   * @returns {external:jQuery} Div qui contient le fullcalendar
   */
  _generate_ui($fc) {
    if (this._p_resources.length) {
      this._init_resources = [...this._p_resources];
      this._p_resources.length = 0;
    }

    const settings = window.cal?.settings || top.cal.settings;
    const config = {
      resources: this._fetch_resources.bind(this),
      resourceRender: this._functions.resource_render,
      defaultView: 'timelineDay',
      schedulerLicenseKey: 'GPL-My-Project-Is-Open-Source',
      height: 200,
      firstHour: settings.first_hour,
      scrollTime: { hours: settings.first_hour },
      slotDuration: { minutes: 60 / settings.timeslots },
      locale: 'fr',
      axisFormat: DATE_HOUR_FORMAT,
      slotLabelFormat: DATE_HOUR_FORMAT,
      selectable: true,
      selectHelper: true,
      //stickyFooterScrollbar:true,
      slotWidth: 50,
      defaultDate: this.start,
      select: this._functions.on_selected_date,
      eventSources: [
        {
          events: this._functions.event_loader,
          id: 'resources',
        },
      ],
    };
    try {
      $fc.css('width', '100%').fullCalendar(config);
      $fc.fullCalendar('render');
      //$fc.fullCalendar('option', 'height', 200);
      //$fc.fullCalendar('render');
    } catch (error) {
      console.error(error);
    }

    return $fc;
  }

  /**
   * Récupère les données pour le fullcalendar
   * @package
   * @param {function} callback
   */
  _fetch_resources(callback) {
    FavoriteLoader.Load(this._name).then((values) => {
      this.try_add_resources(values);

      if (this._init_resources.length) {
        this.try_add_resources(
          this._init_resources.map((x) => (x.data ? x.data : x)),
        );
        this._init_resources.length = 0;
      }

      const data = this._format_resources(this._p_resources, this._p_filters);

      if (!this._first_loaded) this._first_loaded = true;

      if (data.length) callback(data);
      else
        callback([
          {
            id: 'resources',
            title: this.gettext('no-resources', 'mel_cal_resources'),
          },
        ]);
    });
  }

  /**
   * Formatte les ressources et récupère seulement celles qui sont filtrés ou non
   * @param {ResourceObject[]} resources
   * @param {FilterBase[]} filters
   * @returns {ResourceObject[]}
   * @frommoduleparam Resources/Filters filters {@linkto FilterBase}
   * @frommodulereturn Resources {@linkto ResourceObject}
   */
  _format_resources(resources, filters) {
    return MelEnumerable.from(resources)
      .where((x) => !MelEnumerable.from(filters).any((f) => !f.filter(x)))
      .orderBy((x) => (this.get_env('fav_resources')?.[x.data.email] ? 0 : 1))
      .toArray();
  }

  /**
   * Fonction get pour la page de dialog retournée
   * @package
   * @param {Function} old Ancienne fonction get bind
   * @returns {external:jQuery}
   */
  _get(old) {
    let $rtn = old();

    let $fc = $rtn.find('[fullcalendar="true"]');

    if ($fc.length) this._$calendar = this._generate_ui($fc);

    this.refresh_calendar_date();

    return $rtn;
  }

  /**
   * Action appelé lorsque les données d'un filtre on été chargés
   * @package
   * @param {ResourceData[]} rcs
   * @param {FilterBase} filter
   * @frommoduleparam Resources/Filters filter
   */
  _on_data_loaded(rcs, filter) {
    let values;
    let resources;
    for (let index = 0, len = this._p_filters.length; index < len; ++index) {
      if (this._p_filters[index]._name !== filter._name) {
        //Réinitialise le filtre
        this._p_filters[index]._$filter
          .html(
            $('<option value="/"></option>')
              .text(
                rcmail.gettext(
                  this._p_filters[index]._name,
                  'mel_cal_resources',
                ),
              )
              .css('display', 'none'),
          )
          .append($('<option value=""></option>').text(EMPTY_STRING));

        if (this._p_filters[index]._input_type === eInputType.multi_select)
          this._p_filters[index]._$filter.html(EMPTY_STRING);

        values = {};
        resources = rcs;

        if (this._p_filters[index]._input_type !== eInputType.multi_select) {
          resources = MelEnumerable.from(rcs);
          if (this._p_filters[index].has_only_number_values()) {
            resources = resources.orderBy(
              (x) => +x[this._p_filters[index]._name],
            );
          } else
            resources = resources.orderBy(
              (x) => x[this._p_filters[index]._name],
            );
        }

        for (const iterator of resources) {
          //Si la données éxiste et qu'elle n'a pas déjà été traitée
          if (
            !values[iterator[this._p_filters[index]._name]] &&
            iterator[this._p_filters[index]._name]
          ) {
            //En multiselect, ce sont les clés des valeurs possible qui servent de filtre
            if (
              this._p_filters[index]._input_type === eInputType.multi_select
            ) {
              for (const current_filter of Object.keys(
                JSON.parse(iterator[this._p_filters[index]._name]),
              )) {
                if (!values[current_filter]) {
                  values[current_filter] = true;
                }
              }
            } else {
              values[iterator[this._p_filters[index]._name]] = true;
              this._p_filters[index]._$filter.append(
                $(
                  `<option value="${iterator[this._p_filters[index]._name]}">${iterator[this._p_filters[index]._name]}</option>`,
                ),
              );
            }
          }
        }

        if (this._p_filters[index]._$filter.children().length > 1)
          this._p_filters[index]._$filter
            .removeAttr('disabled')
            .removeClass('disabled');
        else
          this._p_filters[index]._$filter
            .attr('disabled', 'disabled')
            .removeClass('disabled');
      }

      if (this._p_filters[index]._input_type === eInputType.multi_select) {
        if (Object.keys(values).length) {
          for (const current_filter of MelEnumerable.from(
            Object.keys(values),
          ).orderBy((x) => x)) {
            this._p_filters[index]._$filter.append(
              $(`<option value="${current_filter}">${current_filter}</option>`),
            );
          }

          this._p_filters[index]._$filter.multiselect('enable');
        } else this._p_filters[index]._$filter.multiselect('disable');

        this._p_filters[index]._$filter.multiselect('rebuild');
      }
    }

    this._p_resources.length = 0;
    for (const iterator of rcs) {
      this.try_add_resource(iterator, false);
    }

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

  /**
   * Action à faire lorsqe'un filtre à changer de valeur
   * @package
   */
  _on_data_changed() {
    this._$calendar.fullCalendar('refetchResources');
    this._$calendar.fullCalendar('refetchEvents');
  }

  /**
   * Créer une page de dialog à partir de cette ressource
   * @returns {Promise<DialogPage>}
   * @frommodulereturn Modal {@linkto DialogPage}
   * @async
   */
  async create_page() {
    const button_save = RcmailDialogButton.ButtonSave({
      click: this.event_on_save,
    });
    const button_cancel = RcmailDialogButton.ButtonCancel({
      click: () => {
        let $parent = this._$calendar;

        while ($parent && !$parent.hasClass('ui-dialog'))
          $parent = $parent.parent();

        if ($parent) {
          $parent.find('.ui-dialog-titlebar-close').click();
        } else $('.ui-dialog-titlebar-close').click();
      },
    });
    let page = new DialogPage(this._name, {
      title: this._name,
      buttons: [button_cancel, button_save],
    });

    page = template_resource.get_page(
      page,
      (await Promise.allSettled(this._p_filters.map((x) => x.generate()))).map(
        (x) => x.value,
      ),
      this,
    );
    this._last_get = page.get.bind(page);
    page.get = this._get.bind(this, this._last_get);

    return page;
  }

  /**
   * Ajoute une ressource à la liste des ressources si elle n'existe pas
   * @param {ResourceData} rc Ressource à ajouter
   * @param {boolean} [refetch=true] Si vrai, récupère les ressources et le évènements
   * @returns {ResourcesBase} Chaînage
   */
  try_add_resource(rc, refetch = true) {
    if (!this._p_resources.filter((x) => x.data.email === rc.email).length) {
      rc.selected = rc.email === this.selected_resource?.email;
      this._p_resources.push({
        id: rc.email,
        title: rc.name,
        parentid: 'resources',
        data: rc,
      });

      if (refetch) {
        this._$calendar.fullCalendar('refetchResources');
        this._$calendar.fullCalendar('refetchEvents');
      }
    }

    return this;
  }

  /**
   * Ajoute plusieurs ressources si elles éxistent
   * @param {ResourceData[]} rcs Liste des ressources à ajouter
   * @returns {ResourcesBase} Chaînage
   */
  try_add_resources(rcs) {
    for (const iterator of rcs) {
      this.try_add_resource(iterator, false);
    }

    return this;
  }

  /**
   * Met à jours le texte de la date du planning
   * @returns {string}
   */
  refresh_calendar_date() {
    let $div = this._$calendar.parent();
    const text = $div.find('.fc-left h2').text();
    $div.find('.fo-date').text(text);
    return text;
  }

  /**
   * Dessine le calendrier
   */
  render() {
    this._$calendar.fullCalendar('rerender');
  }
}

/**
 * @class
 * @classdesc Représentation d'un paramètre de ressource
 */
class ResourceSettings {
  /**
   * Constructeur de la classe
   * @param {string} setting Valeur du filtre
   */
  constructor(setting) {
    /**
     * @private
     * @type {string}
     * @description Valeur du paramètre
     */
    this._setting = typeof setting === 'string' ? JSON.parse(setting) : setting;
  }

  /**
   * Change le paramètre en string
   * @returns {string}
   */
  toString() {
    return ResourceSettings.ToString(this._setting);
  }

  /**
   * Check si la ressource correspond au filtre
   * @param {ResourceObject} ressource Ressource à chacker
   * @returns {boolean}
   */
  is(ressource) {
    if (!this._setting?.length) return true;
    else if (!ressource.data.caracteristiques) return false;
    else {
      return MelEnumerable.from(this._setting)
        .select((x) =>
          // eslint-disable-next-line quotes
          ressource.data.caracteristiques.includes(x.replaceAll("'", '"')),
        )
        .all((x) => x);
    }
  }

  /**
   * Change le paramètre en string
   * @static
   * @param {string} setting
   * @returns {string}
   */
  static ToString(setting) {
    if (typeof setting === 'string') setting = JSON.parse(setting);

    let str = [];
    for (const key in setting) {
      if (Object.hasOwnProperty.call(setting, key)) {
        const element = setting[key];

        str.push(`${key} x${element}`);
      }
    }

    return str.join(' + ');
  }
}