// SPDX-FileCopyrightText: 2022 Felix Hamann <felix.hamann@campus.lmu.de>,Gregor Kleen <gregor.kleen@ifi.lmu.de>,Sarah Vaupel <sarah.vaupel@ifi.lmu.de>,Sarah Vaupel <vaupel.sarah@campus.lmu.de>
//
// SPDX-License-Identifier: AGPL-3.0-or-later

import { Utility } from '../../core/utility';
import moment from 'moment';
import { EventManager, EventWrapper, EVENT_TYPE } from '../../lib/event-manager/event-manager';

import defer from 'lodash.defer';


// INTERNAL (Uni2work specific) formats for formatting dates and/or times
const FORM_DATE_FORMAT = {
  'date': moment.HTML5_FMT.DATE,
  'time': moment.HTML5_FMT.TIME_SECONDS,
  'datetime-local': moment.HTML5_FMT.DATETIME_LOCAL_SECONDS,
};

// FANCY (moment specific) formats for displaying dates and/or times
const FORM_DATE_FORMAT_DATE_MOMENT = 'DD.MM.YYYY';
const FORM_DATE_FORMAT_TIME_MOMENT = 'HH:mm:ss';
const FORM_DATE_FORMAT_MOMENT = {
  'date': FORM_DATE_FORMAT_DATE_MOMENT,
  'time': FORM_DATE_FORMAT_TIME_MOMENT,
  'datetime-local': `${FORM_DATE_FORMAT_DATE_MOMENT} ${FORM_DATE_FORMAT_TIME_MOMENT}`,
};

const DATEPICKER_UTIL_SELECTOR = 'input[type="date"], input[type="time"], input[type="datetime-local"]';

const DATEPICKER_INITIALIZED_CLASS = 'datepicker--initialized';


@Utility({
  selector: DATEPICKER_UTIL_SELECTOR,
})
export class Datepicker {

  // singleton Map that maps a formID to a Map of Datepicker objects
  static datepickerCollections;

  _element;
  elementType;
  initialValue;
  _locale;

  _eventManager;

  _unloadIsDueToSubmit = false;

  constructor(element) {
    if (!element) {
      throw new Error('Datepicker utility needs to be passed an element!');
    }

    if (element.classList.contains(DATEPICKER_INITIALIZED_CLASS)) {
      return false;
    }

    this._locale = window.App.i18n.getDatetimeLocale();

    // initialize datepickerCollections singleton if not already done
    if (!Datepicker.datepickerCollections) {
      Datepicker.datepickerCollections = new Map();
    }

    this._element = element;

    this._eventManager = new EventManager();

    // store the previously set type to select the input format
    this.elementType = this._element.getAttribute('type');

    // store initial value prior to changing type
    this.initialValue = this._element.value || this._element.getAttribute('value');

    if (!FORM_DATE_FORMAT[this.elementType]) {
      throw new Error('Datepicker utility called on unsupported element!');
    }

    // register this datepicker instance with the formID of the given element in the datepicker collection
    const formID = this._element.form.id;
    const elemID = this._element.id;
    if (!Datepicker.datepickerCollections.has(formID)) {
      // insert a new key value pair if the formID key is not there already
      Datepicker.datepickerCollections.set(formID, new Map([[elemID, this]]));
    } else {
      // otherwise, insert this instance into the Map
      Datepicker.datepickerCollections.get(formID).set(elemID, this);
    }

    // mark the form input element as initialized
    this._element.classList.add(DATEPICKER_INITIALIZED_CLASS);
  }

  start() {
    // format the date value of the form input element of this datepicker before form submission
    const submitEvent = new EventWrapper(EVENT_TYPE.SUBMIT, this._submitHandler.bind(this), this._element.form);
    this._eventManager.registerNewListener(submitEvent);
  }

  destroy() {
    this._eventManager.cleanUp();
    this._element.classList.remove(DATEPICKER_INITIALIZED_CLASS);
  }

  
  //  FORMAT METHODS

  /**
   * Formats the value of this input element from datepicker format (i.e. DATEPICKER_CONFIG.dateFormat + " " + datetime.defaults.timeFormat) to Uni2work internal date format (i.e. FORM_DATE_FORMAT) required for form submission
   * @param {*} toFancy optional target format switch (boolean value; default is false). If set to a truthy value, formats the element value to fancy instead of internal date format.
   */
  formatElementValue(toFancy) {
    if (this._element.value) {
      this._element.value = this.unformat(toFancy);
    }
  }

  _submitHandler() {
    this._unloadIsDueToSubmit = true;
    this._element.setAttribute('type', 'text');
    this.formatElementValue(false);

    defer(() => { // Restore state after event loop is settled
      this._unloadIsDueToSubmit = false;
      this.formatElementValue(true);
      this._element.setAttribute('type', this.elementType);
    });
  }

  /**
   * Returns a datestring in internal format from the current state of the input element value.
   * @param {*} toFancy Format date from internal to fancy or vice versa. When omitted, toFancy is falsy and results in fancy -> internal
   */
  unformat(toFancy) {
    const formatIn = toFancy ? FORM_DATE_FORMAT[this.elementType] : FORM_DATE_FORMAT_MOMENT[this.elementType];
    const formatOut = toFancy ? FORM_DATE_FORMAT_MOMENT[this.elementType] : FORM_DATE_FORMAT[this.elementType];
    return reformatDateString(this._element.value, formatIn, formatOut);
  }

  /**
   * Takes a Form and a FormData and returns a new FormData with all dates formatted to uni2work date format. This function will not change the value of the date input elements.
   * @param {*} form Form for which all dates will be formatted in the FormData
   * @param {*} formData Initial FormData
   */
  static unformatAll(form, formData) {
    // only proceed if there are any datepickers and if both form and formData are defined
    if (Datepicker.datepickerCollections && form && formData) {
      // if the form has no id, assign one randomly
      if (!form.id) {
        form.id = `f${Math.floor(Math.random() * 100000)}`;
      }

      const formId = form.id;

      if (Datepicker.datepickerCollections.has(formId)) {
        const datepickerInstances = Datepicker.datepickerCollections.get(formId);
        datepickerInstances.forEach(instance => {
          formData.set(instance._element.name, instance.unformat());
        });
      }
    }

    // return the (possibly changed) FormData
    return formData;
  }
}


//  HELPER FUNCTIONS

/**
 * Takes a string representation of a date, an input ('previous') format and a desired output format and returns a reformatted date string.
 * If the date string is not valid (i.e. cannot be parsed with the given input format string), returns the original date string;
 * @param {*} dateStr string representation of a date (needs to be in format formatIn)
 * @param {*} formatIn input format string
 * @param {*} formatOut format string of the desired output date string
 */
function reformatDateString(dateStr, formatIn, formatOut) {
  const parsedMomentDate = moment(dateStr, [formatIn, formatOut]);
  return parsedMomentDate.isValid() ? parsedMomentDate.format(formatOut) : dateStr;
}
