export { loader as CalendarLoader };
import { MelEnumerable } from '../classes/enum.js';
import { MainNav } from '../classes/main_nav.js';
import { DATE_TIME_FORMAT } from '../constants/constants.dates.js';
import { MelObject } from '../mel_object.js';

/**
 * Sélecteur de la pastille agenda de la navigation principale
 * @type {string}
 */
const SELECTOR_CLASS_ROUND_CALENDAR = `${CONST_JQUERY_SELECTOR_CLASS}calendar`;

/**
 * @extends MelObject
 * Classe qui gère le chargement de l'agenda côté serveur
 *
 * Donne des fonctions utiles pour charger ces données.
 */
class CalendarLoader extends MelObject {
  constructor() {
    super();
  }

  main() {
    super.main();

    this.timeout = null;
    this.on_calendar_updated = new MelEvent();

    const now = moment();

    if (['bnum', 'webconf', 'chat'].includes(rcmail.env.task)) {
      MainNav.try_add_round(
        SELECTOR_CLASS_ROUND_CALENDAR,
        mel_metapage.Ids.menu.badge.calendar,
      ).update_badge(
        this.get_next_events_day(now, {}).count(),
        mel_metapage.Ids.menu.badge.calendar,
      );
    }

    if (
      this.load(mel_metapage.Storage.last_calendar_update) !==
        moment().format(CONST_DATE_FORMAT_BNUM) ||
      this.load(mel_metapage.Storage.calendar_all_events) === null
    ) {
      const force_refresh = true;
      this.update_agenda_local_datas(force_refresh);
    }
  }

  /**
   * Vérifie si un jour se trouve entre 2 dates
   * @param {moment} start Date de début
   * @param {moment} end Date de fin
   * @param {moment} day Jour de test
   * @returns {boolean}
   */
  is_date_okay(start, end, day) {
    return (
      (start <= day && day <= end) ||
      (start >= day &&
        moment(start).startOf('day').format() ===
          moment(day).startOf('day').format() &&
        day <= end)
    );
  }

  /**
   * Coupe une date de début et une date de fin si celles-ci dépassent le début ou la fin de journée
   * @param {moment} start Date de début
   * @param {moment} end Date de fin
   * @param {moment} day Jour de test
   * @returns {SplittedDate} Nouvelles dates de début et de fin si besoin.
   */
  get_date_splitted(start, end, day) {
    const o_start = start;
    const o_end = end;
    const start_of_day = moment(day).startOf('day');
    const end_of_day = moment(day).endOf('day');
    if (start < start_of_day) start = start_of_day;
    if (end > end_of_day) end = end_of_day;

    return new SplittedDate(start, end, o_start, o_end, day);
  }

  /**
   * Récupère les évènements d'une journée
   * @param {moment} day Jour
   * @param {Array<Object> | null} loaded_events Si ce paramètre vaut null, les données seront chargés depuis le stockage local
   * @returns {Array<Object>} Evènements
   */
  get_from_day(day, loaded_events = null) {
    let return_datas = [];
    if (!day.toDate) day = moment(day).startOf('day');

    const events = loaded_events ?? this.load_all_events();

    if (events.length !== 0) {
      return_datas = MelEnumerable.from(events)
        .where((x) => this.is_date_okay(moment(x.start), moment(x.end), day))
        .toArray();
    }

    return return_datas;
  }

  /**
   * Récupère les prochains évènements d'une journée.
   *
   * La date ne doit pas être une date inférieur à aujourd'hui.
   * @param {moment} day Date de traitement
   * @param {Object} options Options de cette fonction
   * @param {boolean} options.enumerable Si on récupère un énumerable ou non
   * @param {Array<Object> | null} options.loaded_events Si ce paramètre vaut null, les données seront chargés depuis le stockage local
   * @returns {MelEnumerable | Array<Object>}
   */
  get_next_events_day(day, { enumerable = true, loaded_events = null }) {
    const now = moment();
    let enum_var = MelEnumerable.from(this.get_from_day(day, loaded_events))
      .where(
        (x) =>
          moment(x.end) > now &&
          x.free_busy !== CONST_EVENT_DISPO_FREE &&
          x.free_busy !== CONST_EVENT_DISPO_TELEWORK,
      )
      .orderBy((x) => moment(x.start));

    return enumerable ? enum_var : enum_var.toArray();
  }

  /**
   * Charge les évènements depuis le stockage local.
   * @returns {Array<Object>} Evènements
   */
  load_all_events() {
    return this.load(mel_metapage.Storage.calendar_all_events, []);
  }

  /**
   * Charge les évènements depuis le stockage local.
   *
   * Si les évènements depuis le stockages local sont nuls, ils sont chargés depuis la base.
   * @async
   * @returns {Promise<Array<Object>>} Evènements
   */
  async force_load_all_events_from_storage() {
    let loaded = this.load(mel_metapage.Storage.calendar_all_events);

    if (!loaded) {
      const top = true;
      await this.rcmail(top).triggerEvent(
        mel_metapage.EventListeners.calendar_updated.get,
      );
      loaded = this.load_all_events();
    }

    return loaded;
  }

  /**
   * Met à jours les données du stockage local depuis le serveur.
   * @async
   * @param {boolean} force Force la mise à jours des données depuis le serveur. Par défaut : `'random'`
   * @returns {Promise<Array<Objet> | null>} Evènements
   */
  async update_agenda_local_datas(force = CONST_CALENDAR_UPDATED_DEFAULT) {
    const isTop = window === top;
    const url = mel_metapage.Functions.url(
      PLUGIN_MEL_METAPAGE,
      ACTION_MEL_METAPAGE_CALENDAR_LOAD_EVENT,
      {
        force,
        source: mceToRcId(rcmail.env.username),
        start: this._dateNow(new Date()),
        end: this._dateNow(
          new Date(
            new Date().getFullYear(),
            new Date().getMonth(),
            new Date().getDate() + 7,
          ),
        ),
        last: moment(
          this.load(mel_metapage.Storage.last_calendar_update),
          CONST_DATE_FORMAT_BNUM,
        ).unix(),
      },
    );

    this.rcmail(!isTop).triggerEvent(
      mel_metapage.EventListeners.calendar_updated.before,
    );
    this.trigger_event(mel_metapage.EventListeners.calendar_updated.before);

    let events = null;
    await this.http_call({
      url,
      on_success: (datas) => (events = this._on_success(datas, isTop)),
      on_error: (...args) => {
        console.error(...args);
      },
      type: 'GET',
    });

    return events;
  }

  _on_success(datas, ...args) {
    const { forced, events, encoded } = JSON.parse(datas);
    const [isTop] = args;
    const isForcedRefresh = forced === true;
    const haveNewDatas =
      (encoded && events !== EMPTY_STRING && events !== EMPTY_ARRAY_STRING) ||
      (!encoded && events.length !== 0);
    datas = null;

    let loadedEvents;

    if (isForcedRefresh) loadedEvents = [];
    else if (haveNewDatas)
      loadedEvents = this.load(mel_metapage.Storage.calendar_all_events, []);

    if (haveNewDatas || isForcedRefresh) {
      this.save(
        CalendarLoader.KEY_LAST_REFRESH,
        moment().format(DATE_TIME_FORMAT),
      );

      if (haveNewDatas) {
        let index;
        for (const current of this._generator_selected_events(
          encoded ? JSON.parse(events) : events,
        )) {
          index = this._getEventIndex(current.id, loadedEvents);

          if (NO_INDEX === index) loadedEvents.push(current);
          else loadedEvents[index] = current;
        }
      }

      const now = moment().startOf('day');
      let all_events = MelEnumerable.from(loadedEvents)
        .where((x) => x !== null)
        .orderBy((x) => x.order)
        .then((x) => moment(x.start));
      all_events = this._events_remove_moment(all_events).toArray();
      this.save(mel_metapage.Storage.calendar_all_events, all_events);
      this.save(
        mel_metapage.Storage.last_calendar_update,
        moment().format(CONST_DATE_FORMAT_BNUM),
      );

      const next_events = this.get_next_events_day(now, {
        loaded_events: all_events,
      });
      MainNav.try_add_round(
        SELECTOR_CLASS_ROUND_CALENDAR,
        mel_metapage.Ids.menu.badge.calendar,
      ).update_badge(next_events.count(), mel_metapage.Ids.menu.badge.calendar);

      this.rcmail(!isTop).triggerEvent(
        mel_metapage.EventListeners.calendar_updated.after,
      );
      this.trigger_event(mel_metapage.EventListeners.calendar_updated.after);

      this._set_timeout(next_events);

      this.on_calendar_updated.call();

      return all_events;
    }
  }

  _set_timeout(next_events) {
    if (
      ['all', 'page'].includes(
        rcmail.env['mel_metapage.tab.notification_style'],
      ) &&
      next_events.any()
    ) {
      if (this.timeout) clearTimeout(this.timeout);

      this.timeout = setTimeout(
        () => {
          const now = moment();

          MainNav.try_add_round(
            SELECTOR_CLASS_ROUND_CALENDAR,
            mel_metapage.Ids.menu.badge.calendar,
          ).update_badge(
            next_events.count(),
            mel_metapage.Ids.menu.badge.calendar,
          );

          window?.update_notification_title?.();

          next_events = this.get_next_events_day(now, {
            loaded_events: next_events.toArray(),
          });
          this.timeout = null;
          this._set_timeout(next_events);
        },
        Math.abs(moment() - moment(next_events.first().end)),
      );
    }
  }

  _getEventIndex(id, events) {
    for (let index = 0, len = events.length; index < len; ++index) {
      if (events[index].id === id) return index;
    }

    return NO_INDEX;
  }

  _dateNow(date) {
    let set = date;
    let getDate = set.getDate().toString();
    // eslint-disable-next-line eqeqeq
    if (getDate.length == 1) {
      //example if 1 change to 01
      getDate = '0' + getDate;
    }
    let getMonth = (set.getMonth() + 1).toString();
    // eslint-disable-next-line eqeqeq
    if (getMonth.length == 1) {
      getMonth = '0' + getMonth;
    }
    let getYear = set.getFullYear().toString();
    let dateNow = getYear + '-' + getMonth + '-' + getDate + 'T00:00:00';
    return dateNow;
  }

  *_generator_selected_events(events) {
    const now = moment().startOf(CONST_DATE_START_OF_DAY);
    const parse = window?.cal?.parseISO8601 ?? ((item) => item);
    const user_id = mceToRcId(rcmail.env.username);

    for (let index = 0, element, len = events.length; index < len; ++index) {
      element = events[index];

      if (
        user_id !== element.calendar ||
        CONST_EVENT_ATTENDEE_STATUS_CANCELLED === element.status
      )
        continue;

      if (
        element.allday !== undefined &&
        element.allday !== null &&
        (element.allDay === undefined || element.allDay === null)
      )
        element.allDay = element.allday;
      element.order = element.allDay ? 0 : 1;

      if (STRING === typeof element.end)
        element.end = moment(parse(element.end));
      else if (!!element.end.date && STRING === typeof element.end.date)
        element.end = moment(element.end.date);

      if (STRING === typeof element.start)
        element.start = moment(parse(element.start));
      else if (!!element.start.date && STRING === typeof element.start.date)
        element.start = moment(element.start.date);

      if (element.end < now) continue;

      if (element.allDay) {
        element.end = element.end.startOf(CONST_DATE_START_OF_DAY);

        if (
          element.end.format(CONST_DATE_FORMAT_EN) ===
            now.format(CONST_DATE_FORMAT_EN) &&
          element.start
            .startOf(CONST_DATE_START_OF_DAY)
            .format(CONST_DATE_FORMAT_EN) !==
            element.end.format(CONST_DATE_FORMAT_EN)
        ) {
          continue;
        } else {
          element.start = element.start.startOf(CONST_DATE_START_OF_DAY);
          //element.end = element.end.startOf(CONST_DATE_START_OF_DAY);
        }
      }

      if (element?.recurrence?.EXCEPTIONS) element.recurrence.EXCEPTIONS = [];

      yield element;
    }
  }

  _events_remove_moment(events) {
    return events.select((x) => {
      if (typeof x.start !== 'string') x.start = x.start.format();
      if (typeof x.end !== 'string') x.end = x.end.format();
      return x;
    });
  }
}

/**
 * Clé de la dernière mise à jours de l'agenda
 * @type {string}
 * @static
 * @readonly
 * @default 'agenda_last_refresh'
 */
CalendarLoader.KEY_LAST_REFRESH = 'agenda_last_refresh';

/**
 * Représente une date coupée.
 */
class SplittedDate {
  /**
   * Constructeur de la classe
   * @param {moment} start Nouvelle date coupée
   * @param {moment} end Nouvelle date coupée
   * @param {moment} original_start Date original
   * @param {moment} original_end Date original
   * @param {moment} day Jour de traitement
   */
  constructor(start, end, original_start, original_end, day) {
    this._init()._setup(start, end, original_start, original_end, day);
  }

  _init() {
    const now = moment();
    this.start = now;
    this.end = now;
    this.original = {
      start: now,
      end: now,
    };
    this.day = now;

    return this;
  }

  _setup(start, end, original_start, original_end, day) {
    Object.defineProperties(this, {
      start: {
        get: function () {
          return start;
        },
        configurable: true,
      },
      end: {
        get: function () {
          return end;
        },
        configurable: true,
      },
      original: {
        get: function () {
          return {
            start: original_start,
            end: original_end,
          };
        },
        configurable: true,
      },
      day: {
        get: function () {
          return day.startOf('day');
        },
        configurable: true,
      },
    });

    return this;
  }
}

/**
 * @typedef {Object} CalendarLoaderWrapper
 * @property {CalendarLoader} Instance Instance de CalendarLoader
 * @property {typeof CalendarLoader} Class Classe de CalendarLoader
 *
 */

/**
 * Instance de CalendarLoader
 * @type {CalendarLoaderWrapper}
 */
let loader = {
  _instance: null,
  Instance: null,
  Class: null,
};

Object.defineProperties(loader, {
  Instance: {
    get: function () {
      if (!loader._instance) loader._instance = new CalendarLoader();

      return loader._instance;
    },
    configurable: true,
  },
  Class: {
    get() {
      return CalendarLoader;
    },
  },
});