/* eslint-disable no-fallthrough */
/**
 * @module EventView/Parts/Alarm
 */

import { custom_alarm_dialog } from '../../../../../skins/mel_elastic/js_templates/custom_alarm.js';
import { MelEnumerable } from '../../../classes/enum.js';
import { RcmailDialog, RcmailDialogButton } from '../../../classes/modal.js';
import { EMPTY_STRING } from '../../../constants/constants.js';
import { MelHtml } from '../../../html/JsHtml/MelHtml.js';
import { isDecimal } from '../../../mel.js';
import { Alarm } from '../../alarms.js';
import { FakePart, Parts } from './parts.js';

/**
 * Contient les données d'un rappel
 * @class
 * @classdesc Contient les données d'un rappel. Sa valeur et son "unitée" (minutes, heures, jours etc....)
 * @package
 */
class AlarmData {
	/**
	 *
	 * @param {number} value Durée en minutes
	 */
	constructor(value) {
		/**
		 * Durée en minutes
		 * @member
		 * @type {number}
		 */
		this.value = value;
		/**
		 * Unitée de la durée
		 *
		 * Peut être : ``-W`` pour semaine, ``-D`` pour jour, ``-H`` pour heure, ``-M`` pour minute
		 * @member
		 * @readonly
		 * @type {string}
		 */
		this.offset = EMPTY_STRING;

		Object.defineProperty(this, 'offset', {
			get: () => {
				if (this.value >= 60 * 24 * 7) {
					return AlarmData.OFFSETS.WEEK;
				} else if (this.value >= 60 * 24) {
					return AlarmData.OFFSETS.DAY;
				} else if (this.value >= 60) {
					return AlarmData.OFFSETS.HOUR;
				} else {
					return AlarmData.OFFSETS.MINUTE;
				}
			},
		});
	}

	/**
	 * Récupère le texte d'une valeur sous format : x unite et x sous unités.
	 *
	 * Prend en compte les pluriels.
	 *
	 * Dans la localization du plugin, il doit y avoir les clés suivantes : `text_key`, `text_key_plurial`, `text_key_after`, `text_key_after_plurial`.
	 *
	 * (Remplacez ``text_key`` par le nom de la clé que vous voulez utiliser, exemple : ``week`` pour la semaine, ``day`` pour la journée etc...)
	 *
	 * `_plurial` signifie le pluriel
	 *
	 * `_after` signifie l'affichage de la deuxième unitée de temps. Celle ci doit contenir `%0`.
	 *
	 * example : `hour` => heure, `hour_plurial` => heures, `hour_after` =>  et %0 minute, `hour_after_plurial` =>  et %0 minutes
	 *
	 * @private
	 * @param {number} multiplier Le multiplier qui permet de déduire le nombre de sous unitées sous forme entière (ex: 7 pour les semaines, 24 pour les jours etc...)
	 * @param {number} val Valeur du rappel en minute
	 * @param {string} text_key Clé qui sera utiliser par `rcmail.gettext()`
	 * @returns {{text:string, value:number}}
	 */
	_getText(val, multiplier, text_key) {
		let has_s = val > 1;
		let offset = rcmail.gettext(
			`${text_key}${has_s ? '_plurial' : EMPTY_STRING}`,
			'mel_metapage',
		);

		if (isDecimal(val)) {
			const tmp = Math.round((val - ~~val) * multiplier);
			val = ~~val;
			has_s = tmp > 1;

			if (1 === val) offset = offset.slice(0, offset.length - 1);

			offset += rcmail
				.gettext(
					`${text_key}_after${has_s ? '_plurial' : EMPTY_STRING}`,
					'mel_metapage',
				)
				.replaceAll('%0', tmp);
		}

		return { text: offset, value: val };
	}

	/**
	 * Récupère le temps convertit à partir de l'unitée.
	 * @param {boolean} include_week Par défaut : ``false``. Si ``true``, inclut les semaines dans le calcul.
	 * @returns {number}
	 */
	getTime(include_week = false) {
		let val;
		switch (this.offset) {
			case AlarmData.OFFSETS.WEEK:
				if (include_week) {
					val = this.value / 60 / 24 / 7;
					break;
				}
			case AlarmData.OFFSETS.DAY:
				val = this.value / 60 / 24;
				break;

			case AlarmData.OFFSETS.HOUR:
				val = this.value / 60;
				break;

			case AlarmData.OFFSETS.MINUTE:
				val = this.value;
				break;

			default:
				val = 0;
				break;
		}

		return val;
	}

	/**
	 * Affiche la donnée en texte lisible et compréhensible pour un être humain.
	 * @returns {string}
	 */
	toString() {
		let val = this.getTime(true);
		let text_key = EMPTY_STRING;
		let multiplier = 0;

		switch (this.offset) {
			case AlarmData.OFFSETS.WEEK:
				text_key = 'week';
				multiplier = 7;

				break;
			case AlarmData.OFFSETS.DAY:
				text_key = 'day';
				multiplier = 24;
				break;
			case AlarmData.OFFSETS.HOUR:
				text_key = 'hour';
				multiplier = 60;
				break;

			default:
				text_key = 'minutes';
				break;
		}

		const { text, value } = this._getText(val, multiplier, text_key);
		const offset = text;
		val = value;

		return `${val} ${offset}`;
	}

	/**
	 * Créer une instance de ``AlarmData`` à partir d'une durée et d'une unitée.
	 * @static
	 * @param {number} val Durée en minutes
	 * @param {string} offset Unitée de la durée. Peut être : ``-W`` pour semaine, ``-D`` pour jour, ``-H`` pour heure, ``-M`` pour minute
	 * @returns {AlarmData}
	 * @see {@link AlarmData.OFFSETS}
	 */
	static From(val, offset) {
		switch (offset) {
			case AlarmData.OFFSETS.WEEK:
				val *= 7;
			case AlarmData.OFFSETS.DAY:
				val *= 24;
			case AlarmData.OFFSETS.HOUR:
				val *= 60;

			default:
				break;
		}

		return new AlarmData(val);
	}
}

/**
 * Enumerable des unitées
 * @static
 * @readonly
 * @enum {string}
 * @see {@link AlarmData.From}
 */
AlarmData.OFFSETS = {
	WEEK: '-W',
	DAY: '-D',
	HOUR: '-H',
	MINUTE: '-M',
};

/**
 * Partie de la vue qui gère les rappels
 * @class
 * @classdesc Partie de la vue qui gère les rappels, elle fait le lien entre le champs de base qui est un ensemble de 3 champs et le champ visuel qui est un `select`
 * @extends FakePart
 * @frommodule EventView/Parts
 */
export class AlarmPart extends FakePart {
	/**
	 *
	 * @param {external:jQuery} $alarm_type Champ qui gère le type d'alarme
	 * @param {external:jQuery} $alarm_offset Champ qui gère la durée de l'alarme
	 * @param {external:jQuery} $alarf_offset_type Champ qui gère l'unitée de l'alarme
	 * @param {external:jQuery} $alarm Champ visuel qui sera afficher à la place du champ de base
	 */
	constructor($alarm_type, $alarm_offset, $alarf_offset_type, $alarm) {
		super($alarm_offset, $alarm, Parts.MODE.change);
		/**
		 * Champ qui gère le type d'alarme
		 * @type {external:jQuery}
		 * @package
		 */
		this._$fieldAlarmType = $alarm_type;
		/**
		 * Champ qui gère l'unitée de l'alarme
		 * @type {external:jQuery}
		 * @package
		 */
		this._$fieldAlarmOffsetType = $alarf_offset_type;

		//Génère le tooltip du champs
		this._$fakeField.tooltip({
			title: () => {
				const val = this._$fakeField.val();

				if (!!(val || false) && 0 !== val && '0' !== val)
					return rcmail
						.gettext('rappel_of', 'mel_metapage')
						.replaceAll('%0', this._$fakeField.find('option:selected').text());
				else return rcmail.gettext('no_rappel', 'mel_metapage');
			},
			trigger: 'hover',
		});
	}

	/**
	 * Initialise la classe par rapport à l'évènement
	 * @param {*} event Evènement de plugin `calendar`
	 * @returns {AlarmPart} Chaînage
	 */
	init(event) {
		this._$fakeField.html(EMPTY_STRING);
		let options_alarms = AlarmPart.PREDEFINED;
		let val = 0;

		// Ajoute une option si une alarm existe déjà
		if (event.alarms) {
			const alarm = new Alarm(event.alarms);

			if (0 !== alarm.getTime()) {
				const time = alarm.getTime() / 1000 / 60;
				val = time;

				if (
					!MelEnumerable.from(AlarmPart.PREDEFINED)
						.where(x => x.value === time)
						.any()
				) {
					const data = new AlarmData(time);
					const str = data.toString();
					options_alarms = MelEnumerable.from(options_alarms)
						.aggregate({
							value: data.value,
							label: str,
						})
						.orderBy(x =>
							-1 === x.value ? Number.POSITIVE_INFINITY : x.value,
						);
				}
			}
		} else if (rcmail.env.calendar_default_alarm_offset) {
			//Si il y a un rappel par défaut et pas de rappel dans l'évènement, on le rajoute
			event.alarms = rcmail.env.calendar_default_alarm_offset;
			event.alarms = `${event.alarms[0]}PT${event.alarms.slice(1)}:DISPLAY`;
			return this.init(event);
		}

		//Génère les options du select
		let $option;
		for (const alarm of options_alarms) {
			$option = MelHtml.start
				.option({ value: alarm.value })
				.text(alarm.label)
				.end()
				.generate()
				.appendTo(this._$fakeField);

			if (alarm.value === val) {
				$option.attr('selected', 'selected');
			}
		}

		$option = null;

		return this;
	}

	/**
	 * Action qui sera effectué lors de la mise à jour du champ visuel
	 * @param {string} val Valeur du select
	 * @override
	 */
	onUpdate(val) {
		switch (val) {
			case undefined:
			case null:
			case 0:
				this._$field.val(0);
				this._$fieldAlarmType.val(EMPTY_STRING).change();
				this._$fieldAlarmOffsetType.val(AlarmData.OFFSETS.MINUTE).change();
				break;

			case -1:
				this._startModalCustomAlarm(val);
				break;

			default:
				// eslint-disable-next-line no-case-declarations
				const alarm = new AlarmData(val);
				this._$fieldAlarmType.val('DISPLAY').change();
				this._$field.val(alarm.getTime());
				this._$fieldAlarmOffsetType
					.val(
						AlarmData.OFFSETS.WEEK === alarm.offset
							? AlarmData.OFFSETS.DAY
							: alarm.offset,
					)
					.change();
				break;
		}
	}

	/**
	 * Action qui sera appelé lors de la mise à jour du champ visuel
	 *
	 * Appele la fonction @see {@link AlarmPart~onUpdate}
	 * @param  {...any} args
	 * @override
	 */
	onChange(...args) {
		let $e = $(args[0].currentTarget);

		this.onUpdate(+$e.val());
	}

	/**
	 * Ouvre une boîte de dialogue pour choisir une alarme personnalisée
	 * @package
	 */
	_startModalCustomAlarm() {
		let $dialog = new RcmailDialog(custom_alarm_dialog, {
			title: rcmail.gettext('custom_alarm_title', 'mel_metapage'),
			buttons: [
				new RcmailDialogButton(rcmail.gettext('validate', 'mel_metapage'), {
					click: () => {
						let offset = $dialog._$dialog
							.find('select')
							.attr('disabled', 'disabled')
							.addClass('disabled')
							.val();
						let value = +$dialog._$dialog
							.find('input')
							.attr('disabled', 'disabled')
							.addClass('disabled')
							.val();

						if (AlarmData.OFFSETS.WEEK === offset) {
							offset = AlarmData.OFFSETS.DAY;
							value *= 7;
						}

						const alarm = AlarmData.From(value, offset);
						this.onUpdate(alarm.value);
						if (
							!MelEnumerable.from(AlarmPart.PREDEFINED)
								.where(x => x.value === alarm.value)
								.any()
						) {
							this._$fakeField.html(EMPTY_STRING);
							let enu = MelEnumerable.from(AlarmPart.PREDEFINED)
								.aggregate([
									{
										value: alarm.value,
										label: alarm.toString(),
									},
								])
								.orderBy(x =>
									-1 === x.value ? Number.POSITIVE_INFINITY : x.value,
								)
								.toArray();

							for (const iterator of enu) {
								MelHtml.start
									.option({ value: iterator.value })
									.text(iterator.label)
									.end()
									.generate()
									.appendTo(this._$fakeField);
							}
						}

						setTimeout(() => {
							this._$fakeField.val(alarm.value);
							$dialog.destroy();
						}, 10);
					},
				}),
			],
		});

		$($dialog._$dialog).on('dialogbeforeclose', () => {
			this._$fakeField.val(0);
		});
	}
}

/**
 * Structure qui contient le label de l'option et sa valeur en minute.
 * @typedef PredefinedOption
 * @property {string} label Sera affiché
 * @property {number} value Valeur en minute
 */

/**
 * Liste des rappels prédéfinis
 * @type {Array<PredefinedOption>}
 */
AlarmPart.PREDEFINED = [
	{ label: 'Aucune', value: 0 },
	{ label: '5 minutes', value: 5 },
	{ label: '10 minutes', value: 10 },
	{ label: '15 minutes', value: 15 },
	{ label: '30 minutes', value: 30 },
	{ label: '1 heure', value: 60 },
	{ label: '2 heures', value: 120 },
	{ label: '12 heures', value: 60 * 12 },
	{ label: '1 jour', value: 60 * 24 },
	{ label: '1 semaine', value: 60 * 24 * 7 },
	{ label: 'Personnalisé', value: -1 },
];