/**
 * EGroupware eTemplate2 - Date widget (WebComponent)
 *
 * @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
 * @package etemplate
 * @subpackage api
 * @link https://www.egroupware.org
 * @author Nathan Gray
 */


import {css, html, nothing, PropertyValueMap} from "lit";
import 'lit-flatpickr';
import {dateStyles} from "./DateStyles";
import type {Instance} from 'flatpickr/dist/types/instance';
import {default as scrollPlugin} from "flatpickr/dist/plugins/scrollPlugin.js";
import {default as ShortcutButtonsPlugin} from "shortcut-buttons-flatpickr/dist/shortcut-buttons-flatpickr.js";
import flatpickr from "flatpickr";
import {egw} from "../../jsapi/egw_global";
import {Et2Textbox} from "../Et2Textbox/Et2Textbox";
import {LitFlatpickr} from "lit-flatpickr";
import {Et2InputWidget} from "../Et2InputWidget/Et2InputWidget";
import shoelace from "../Styles/shoelace";
import {classMap} from "lit/directives/class-map.js";

// list of existing localizations from node_modules/flatpicker/dist/l10n directory:
const l10n = [
	'ar', 'at', 'az', 'be', 'bg', 'bn', 'bs', 'cat', 'cs', 'cy', 'da', 'de', 'eo', 'es', 'et', 'fa', 'fi', 'fo',
	'fr', 'ga', 'gr', 'he', 'hi', 'hr', 'hu', 'id', 'index', 'is', 'it', 'ja', 'ka', 'km', 'ko', 'kz', 'lt', 'lv', 'mk',
	'mn', 'ms', 'my', 'nl', 'no', 'pa', 'pl', 'pt', 'ro', 'ru', 'si', 'sk', 'sl', 'sq', 'sr-cyr', 'sr', 'sv', 'th', 'tr',
	'uk', 'uz', 'uz_latn', 'vn', 'zh-tw', 'zh',
];
const lang = egw && egw.preference ? <string>egw.preference('lang') || "" : "";
let localizePromise = Promise.resolve();
// only load localization if we have one
if (l10n.indexOf(lang) >= 0)
{
	localizePromise = import(egw.webserverUrl + "/node_modules/flatpickr/dist/l10n/" + lang + ".js").then(() =>
	{
		// @ts-ignore
		flatpickr.localize(flatpickr.l10ns[lang]);
	});
}

/**
 * Parse a date string into a Date object
 * Time will be 00:00:00 UTC
 *
 * @param {string} dateString
 * @returns {Date | undefined}
 */
export function parseDate(dateString, formatString?)
{
	// First try the server format
	if(dateString.substr(-1) === "Z")
	{
		try
		{
			let date = new Date(dateString);
			if(date instanceof Date)
			{
				return date;
			}
		}
		catch(e)
		{
			// Nope, that didn't parse directly
		}
	}

	formatString = formatString || <string>(window.egw.preference("dateformat") || 'Y-m-d');
	//@ts-ignore replaceAll() does not exist
	formatString = formatString.replaceAll(new RegExp('[-/\.]', 'ig'), '-');
	let parsedString = "";
	switch(formatString)
	{
		case 'd-m-Y':
			parsedString = `${dateString.slice(6, 10)}/${dateString.slice(3, 5,)}/${dateString.slice(0, 2)}`;
			break;
		case 'm-d-Y':
			parsedString = `${dateString.slice(6, 10)}/${dateString.slice(0, 2,)}/${dateString.slice(3, 5)}`;
			break;
		case 'Y-m-d':
			parsedString = `${dateString.slice(0, 4)}/${dateString.slice(5, 7,)}/${dateString.slice(8, 10)}`;
			break;
		case 'Y-d-m':
			parsedString = `${dateString.slice(0, 4)}/${dateString.slice(8, 10)}/${dateString.slice(5, 7)}`;
			break;
		case 'd-M-Y':
			parsedString = `${dateString.slice(6, 10)}/${dateString.slice(3, 5,)}/${dateString.slice(0, 2)}`;
			break;
		case 'Ymd':
			// Not a preference option, but used by some dates
			parsedString = `${dateString.slice(0, 4)}/${dateString.slice(4, 6,)}/${dateString.slice(6, 8)}`;
			break;
		default:
			parsedString = '0000/00/00';
	}

	const [year, month, day] = parsedString.split('/').map(Number);
	const parsedDate = new Date(`${year}-${month < 10 ? "0" + month : month}-${day < 10 ? "0" + day : day}T00:00:00Z`);

	// Check if parsedDate is not `Invalid Date` or that the date has changed (e.g. the not existing 31.02.2020)
	if(
		year > 0 &&
		month > 0 &&
		day > 0 &&
		parsedDate.getUTCDate() === day &&
		parsedDate.getUTCMonth() === month - 1
	)
	{
		return parsedDate;
	}
	return undefined;
}

/**
 * To parse a time into a Date object
 * Date will be 1970-01-01, time is in UTC to avoid browser issues
 *
 * @param {string} timeString
 * @returns {Date | undefined}
 */
export function parseTime(timeString)
{
	// First try the server format
	if(timeString.substr(-1) === "Z")
	{
		try
		{
			let date = new Date(timeString);
			if(date instanceof Date)
			{
				return date;
			}
		}
		catch(e)
		{
			// Nope, that didn't parse directly
		}
	}

	let am_pm = timeString.endsWith("pm") || timeString.endsWith("PM") ? 12 : 0;

	let strippedString = timeString.replaceAll(/[^0-9:]/gi, '');

	if(timeString.startsWith("12") && strippedString != timeString)
	{
		// 12:xx am -> 0:xx, 12:xx pm -> 12:xx
		am_pm -= 12;
	}

	const [hour, minute] = strippedString.split(':').map(Number);

	const parsedDate = new Date("1970-01-01T00:00:00Z");
	parsedDate.setUTCHours(hour + am_pm);
	parsedDate.setUTCMinutes(minute);

	// Check if parsedDate is not `Invalid Date` or that the time has changed
	if(
		parsedDate.getUTCHours() === hour + am_pm &&
		parsedDate.getUTCMinutes() === minute
	)
	{
		return parsedDate;
	}
	return undefined;
}

/**
 * To parse a date+time into an object
 * Time is in UTC to avoid browser issues
 *
 * @param {string} dateTimeString
 * @returns {Date | undefined}
 */
export function parseDateTime(dateTimeString)
{
	// First try some common invalid values
	if(dateTimeString === "" || dateTimeString === "0" || dateTimeString === 0)
	{
		return undefined;
	}

	// Next try server format
	if(typeof dateTimeString === "string" && dateTimeString.substr(-1) === "Z" || !isNaN(dateTimeString))
	{
		if(!isNaN(dateTimeString) && parseInt(dateTimeString) == dateTimeString)
		{
			console.warn("Invalid date/time string: " + dateTimeString);
			dateTimeString *= 1000;
		}
		try
		{
			let date = new Date(dateTimeString);
			if(date instanceof Date)
			{
				return date;
			}
		}
		catch(e)
		{
			// Nope, that didn't parse directly
		}
	}

	const date = parseDate(dateTimeString);

	let explody = dateTimeString.split(" ");
	explody.shift();
	const time = parseTime(explody.join(" "));

	if(typeof date === "undefined" || typeof time === "undefined")
	{
		return undefined;
	}
	date.setUTCHours(time.getUTCHours());
	date.setUTCMinutes(time.getUTCMinutes());
	date.setUTCSeconds(time.getUTCSeconds());
	return date;
}

/**
 * Format dates according to user preference
 *
 * @param {Date} date
 * @param [options] Intl options are available
 * 	set 'dateFormat': "Y-m-d" to specify a particular format
 * @returns {string}
 */
export function formatDate(date : Date, options = {dateFormat: ""}) : string
{
	if(!date || !(date instanceof Date))
	{
		return "";
	}
	let _value = '';
	let dateformat = options.dateFormat || <string>window.egw.preference("dateformat") || 'Y-m-d';

	let replace_map = {
		d: (date.getUTCDate() < 10 ? "0" : "") + date.getUTCDate(),
		m: (date.getUTCMonth() < 9 ? "0" : "") + (date.getUTCMonth() + 1),
		Y: "" + date.getUTCFullYear(),
		H: ("0" + date.getUTCHours()).slice(-2),
		i: ("0" + date.getUTCMinutes()).slice(-2)
	}
	if(dateformat.indexOf("M") != -1)
	{
		replace_map["M"] = flatpickr.formatDate(date, "M");
	}

	let re = new RegExp(Object.keys(replace_map).join("|"), "g");
	_value = dateformat.replace(re, function(matched)
	{
		return replace_map[matched];
	});
	return _value;
}

/**
 * Format dates according to user preference
 *
 * @param {Date} date
 * @param  [options] Intl options are available
 * 	set 'timeFormat': "12" to specify a particular format
 * @returns {string}
 */
export function formatTime(date : Date, options = {timeFormat: ""}) : string
{
	if(!date || !(date instanceof Date))
	{
		return "";
	}
	let _value = '';

	let timeformat = options.timeFormat || <string>window.egw.preference("timeformat") || "24";
	let hours = (timeformat == "12" && date.getUTCHours() > 12) ? (date.getUTCHours() - 12) : date.getUTCHours();
	if(timeformat == "12" && hours == 0)
	{
		// 00:00 is 12:00 am
		hours = 12;
	}

	_value = (timeformat == "24" && hours < 10 ? "0" : "") + hours + ":" +
		(date.getUTCMinutes() < 10 ? "0" : "") + (date.getUTCMinutes()) +
		(timeformat == "24" ? "" : (date.getUTCHours() < 12 ? " am" : " pm"));

	return _value;
}

/**
 * Format date+time according to user preference
 *
 * @param {Date} date
 * @param {import('@lion/localize/types/LocalizeMixinTypes').FormatDateOptions} [options] Intl options are available
 * 	set 'dateFormat': "Y-m-d", 'timeFormat': "12" to specify a particular format
 * @returns {string}
 */
export function formatDateTime(date : Date, options = {dateFormat: "", timeFormat: ""}) : string
{
	if(!date || !(date instanceof Date))
	{
		return "";
	}
	return formatDate(date, options) + " " + formatTime(date, options);
}

export class Et2Date extends Et2InputWidget(LitFlatpickr)
{
	static get styles()
	{
		return [
			...(super.styles ? (Array.isArray(super.styles) ? super.styles : [super.styles]) : []),
			shoelace,
			dateStyles,
			css`
			  :host {
				width: auto;
			  }

				/* Scroll buttons */
				.form-control-input {
					position: relative;
					display: flex;

					et2-textbox {
						flex: 1 1 auto;
						min-width: 14ex;
					}
				}

				.form-control-input:hover .et2-date-time__scrollbuttons {
				display: flex;
			  }

			  .et2-date-time__scrollbuttons {
				display: none;
				flex-direction: column;
				width: calc(var(--sl-input-height-medium) / 2);
				position: absolute;
				right: 0px;
				  margin-inline-end: 0px;
			  }

			  .et2-date-time__scrollbuttons > * {
				font-size: var(--sl-font-size-2x-small);
				height: calc(var(--sl-input-height-medium) / 2);
			  }
			.et2-date-time__scrollbuttons > *::part(base) {
				padding: 3px;
			}
            `,
		];
	}

	static get properties()
	{
		return {
			...super.properties,
			/**
			 * Display the calendar inline instead of revealed as needed
			 */
			inline: {type: Boolean},
			/**
			 * Placeholder text for input
			 */
			placeholder: {type: String},

			/**
			 * Allow value that is not a multiple of minuteIncrement
			 *
			 * eg: 11:23 with default 5 minuteIncrement = 11:25
			 * 16:47 with 30 minuteIncrement = 17:00
			 * If false (default), it is impossible to have a time that is not a multiple of minuteIncrement.
			 * Does not affect scroll, which always goes to nearest multiple.
			 */
			freeMinuteEntry: {type: Boolean},

			/**
			 * The preferred placement of the calendar popup can be set with the placement attribute.  The default
			 * is "auto".  Note that the actual position may vary to ensure the calendar remains in the viewport.
			 * Valid placements are "top", "bottom" or "auto".
			 */
			placement: {type: String, noAccessor: true}
		}
	}

	private _boundTooltipElements : HTMLElement[] = [];

	constructor()
	{
		super();

		// By default, 5 minute resolution (see minuteIncrement to change resolution)
		this.freeMinuteEntry = false;

		this._onDayCreate = this._onDayCreate.bind(this);
		this._handleInputChange = this._handleInputChange.bind(this);
		this._onReady = this._onReady.bind(this);
		this.handleScroll = this.handleScroll.bind(this);
	}


	connectedCallback()
	{
		super.connectedCallback();
		this._updateValueOnChange = this._updateValueOnChange.bind(this);
		this._handleShortcutButtonClick = this._handleShortcutButtonClick.bind(this);

		this.updateComplete.then(() =>
		{
			this.init();
		});
	}

	disconnectedCallback()
	{
		super.disconnectedCallback();
		this._inputNode?.removeEventListener('change', this._onChange);
		delete this._inputElement?.flatpickr;
		this.findInputField()?.removeEventListener("input", this._handleInputChange);

		this._boundTooltipElements.forEach(tooltipped =>
		{
			this.egw().tooltipUnbind(tooltipped)
		});
		this._boundTooltipElements.splice(0, this._boundTooltipElements.length);

		this._instance?.destroy && this._instance.destroy();
		if(this._instance)
		{
			delete this._inputNode?._flatpickr;
			this._instance = undefined;
		}
	}

	async getUpdateComplete()
	{
		const more = await super.getUpdateComplete();
		await localizePromise;
		if(this.getOptions().allowInput)
		{
			await this._inputNode.updateComplete;
		}
		return more;
	}

	update(changedProperties : PropertyValueMap<any>)
	{
		super.update(changedProperties);

		// Flatpickr puts some inputs we don't have direct control over
		if(changedProperties.has("disabled") && this._inputNode)
		{
			this._inputNode.disabled = this.disabled;
			if(typeof this._inputNode.requestUpdate == "function")
			{
				this._inputNode?.requestUpdate("disabled");
				this._inputNode.shadowRoot.querySelectorAll("input").forEach(i => i.disabled = this.disabled);
			}
		}
	}

	/**
	 * Override parent to skip call to CDN
	 * @returns {Promise<void>}
	 */
	async init()
	{
		// Plain input for mobile, unless it's inline
		if(!this.getOptions().inline && typeof egwIsMobile == "function" && egwIsMobile())
		{
			return;
		}

		// Wait for language to be loaded
		await localizePromise;

		if(typeof this._instance === "undefined")
		{
			if(this.getOptions().allowInput)
			{
				// Change this so it uses findInputField() to get the input
				this._hasSlottedElement = true;

				// Wait for everything to be there before we start flatpickr
				await this.updateComplete;
				(typeof this._inputNode.requestUpdate == "function") && this._inputNode.requestUpdate();
				await this._inputNode.updateComplete;

				// Set flag attribute on _internal_ input - flatpickr needs an <input>
				if(this._inputNode?.shadowRoot?.querySelectorAll("input[type='text']").length == 1)
				{
					this.findInputField().setAttribute("data-input", "");
				}
				if(this.defaultDate && this._inputNode)
				{
					this._inputNode.value = flatpickr.formatDate(<Date>this.defaultDate, this.getOptions().dateFormat);
				}
			}

			this.initializeComponent();

			// This has to go in init() rather than connectedCallback() because flatpickr creates its nodes in
			// initializeComponent() so this._inputNode is not available before this
			this.findInputField().addEventListener('change', this._updateValueOnChange);
			this.findInputField().addEventListener("input", this._handleInputChange);

			// pass aria(Label|Description) to real input node
			const input = this.getInputNode();
			if (input)
			{
				input.ariaLabel = this.ariaLabel || this.placeholder;
				input.ariaDescription = this.ariaDescription || this.statustext ||
					this.egw().lang('Format')+' '+this.getOptions().altFormat.split('').map(c => {
						switch(c)
						{
							case 'Y': return this.egw().lang('Year');
							case 'm': return this.egw().lang('Month');
							case 'd': return this.egw().lang('Day');
							case 'h':
							case 'H': return this.egw().lang('Hour');
							case 'i': return this.egw().lang('Minute');
							default: return c;
						}
					}).join(' ');
			}

			// Flatpickr calendar is usually child of body, but theme is set on  so needs theme

		}
	}

	/**
	 * Override some flatpickr defaults to get things how we like it
	 *
	 * @see https://flatpickr.js.org/options/
	 * @returns {any}
	 */
	public getOptions()
	{
		let options = super.getOptions();

		options.altFormat = <string>this.egw()?.preference("dateformat") || "Y-m-d";
		options.altInput = true;
		options.allowInput = true;
		options.dateFormat = "Y-m-dT00:00:00\\Z";
		options.weekNumbers = true;
		// Wrap needs to be false because flatpickr can't look inside et2-textbox and find the <input> it wants
		// We provide it directly through findInputField()
		options.wrap = false;

		options.onDayCreate = this._onDayCreate;

		this._localize(options);

		if(this.inline)
		{
			options.inline = this.inline;
		}
		if(this.placement)
		{
			options.position = this._convert_placement(this.placement);
		}

		options.plugins = [
			// Turn on scroll wheel support
			// @ts-ignore TypeScript can't find scrollPlugin, but rollup does
			new scrollPlugin()
		];
		// Add "Ok" and "today" buttons
		const buttons = this._buttonPlugin();
		if(buttons)
		{
			options.plugins.push(buttons)
		}


		// Listen for flatpickr change so we can update internal value, needed for validation
		options.onChange = this._updateValueOnChange;
		options.onReady = this._onReady;

		// Remove inert attribute so we can work in Et2Dialog
		options.onOpen = [() =>
		{
			this._instance.calendarContainer?.removeAttribute("inert")
		}];

		options.theme = "dark";

		return options;
	}

	/**
	 * Handle click on shortcut button(s) like "Today"
	 *
	 * @param button_index
	 * @param fp Flatpickr instance
	 */
	public _handleShortcutButtonClick(button_index, fp)
	{
		switch(button_index)
		{
			case 0: // OK
				fp.close();
				break;
			default:
				fp.setDate(new Date());
		}
	}

	/**
	 * Set localize options & translations
	 * @param options
	 * @protected
	 */
	protected _localize(options)
	{
		let first_dow = <string>this.egw()?.preference('weekdaystarts', 'calendar') || 'Monday';
		const DOW_MAP = {Monday: 1, Sunday: 0, Saturday: 6};
		options.locale = {
			firstDayOfWeek: DOW_MAP[first_dow]
		};
	}

	/**
	 * For mobile, we use a plain input of the proper type
	 * @returns {string}
	 */
	_mobileInputType() : string
	{
		return "date";
	}

	/**
	 * Add "today" button below calendar
	 * @protected
	 */
	protected _buttonPlugin()
	{
		// @ts-ignore TypeScript can't find ShortcutButtonsPlugin, but rollup does
		return ShortcutButtonsPlugin({
			button: [
				{label: this.egw().lang("ok")},
				{label: this.egw().lang("Today")}
			],
			onClick: this._handleShortcutButtonClick
		})
	}

	set value(value)
	{
		if(!value || value == 0 || value == "0")
		{
			value = "";
			this.clear();
			if(typeof egwIsMobile == "function" && egwIsMobile() && this._inputNode)
			{
				this._inputNode.value = '';
			}
			return;
		}
		let date;
		// handle relative time (eg. "+3600" or "-3600") used in calendar
		if(typeof value === 'string' && (value[0] === '+' || value[0] === '-'))
		{
			date = new Date(this.getValue());
			date.setSeconds(date.getSeconds() + parseInt(value));
		}
		else
		{
			date = new Date(value);
		}
		if(typeof egwIsMobile == "function" && egwIsMobile())
		{
			if(this._inputNode)
			{
				this._inputNode.value = isNaN(date) ? "" : this.format(date);
			}
			else
			{
				this.updateComplete.then(() =>
				{
					this._inputNode.value = isNaN(date) ? "" : this.format(date);
				});
			}
			return;
		}
		// Handle timezone offset, flatpickr uses local time
		let formatDate = new Date(date.valueOf() + date.getTimezoneOffset() * 60 * 1000);
		if(!this._instance)
		{
			this.defaultDate = formatDate;
		}
		else
		{
			this.setDate(formatDate);
		}

		this.requestUpdate("value", this._oldValue);
	}

	get value()
	{
		if(!this._inputElement)
		{
			if(this.defaultDate)
			{
				return flatpickr.formatDate(<Date>this.defaultDate, this.getOptions().dateFormat);
			}
			if(typeof egwIsMobile == "function" && egwIsMobile() && this._inputNode?.value)
			{
				return this._inputNode?.value + "Z";
			}
			return this._inputNode?.value || '';
		}
		let value = this._inputElement.value;

		// Empty field, return ''
		if(!value)
		{
			return '';
		}

		return value;
	}

	get parse() : Function
	{
		return parseDate;
	}

	get format() : Function
	{
		// Mobile is specific about the date format, no time allowed
		if(typeof egwIsMobile == "function" && egwIsMobile())
		{
			return (date) => {return formatDate(date, {dateFormat: "Y-m-d"});};
		}
		return formatDate;
	}

	set format(_format)
	{
		// ignored, trying to fix TypeError opening a new contact
	}

	/**
	 * Inline calendars need a slot
	 *
	 * @return {TemplateResult}
	 * @protected
	 */
	// eslint-disable-next-line class-methods-use-this
	_inputGroupAfterTemplate()
	{
		return html`
            <div class="input-group__after">
                <slot name="after"></slot>
                <slot/>
            </div>
		`;
	}

	/**
	 * Update the calendar when the input value changes
	 * Otherwise, user's change will be overwritten by calendar popup when the input loses focus
	 *
	 * @param e
	 */
	_handleInputChange(e : InputEvent)
	{
		if(this.disabled || this.getOptions().inline)
		{
			return;
		}

		// Update
		const value = this.findInputField().value;

		if(value === "" && this._instance.selectedDates.length > 0)
		{
			// Update the et2-textbox so it will fail a required validation check
			this._inputNode.value = '';
			this._instance.clear();
			this.updateComplete.then(() =>
			{
				this.dispatchEvent(new Event("change", {bubbles: true}));
			});
			return;
		}
		let parsedDate = null
		try
		{
			parsedDate = this._instance.parseDate(value, this.getOptions().altFormat)
		}
		catch(e)
		{
			// Invalid date string
		}
		// If they typed a valid date/time, try to update flatpickr
		if(parsedDate)
		{
			const formattedDate = flatpickr.formatDate(parsedDate, this.getOptions().altFormat)
			if(value === formattedDate &&
				// Avoid infinite loop of setting the same value back triggering another change
				this._instance.input.value !== flatpickr.formatDate(parsedDate, this.getOptions().dateFormat))
			{
				try
				{
					// Can error for time-only due to missing calendar
					this._instance.setDate(value, true, this._instance.config.altFormat)
				}
				catch(e)
				{
					// Just do it again, it works the second time
					this._instance.setDate(value, true, this._instance.config.altFormat);
				}
			}
			if(this._inputNode.value !== formattedDate)
			{
				// Update the et2-textbox so it has current value for any (required) validation
				this._inputNode.value = formattedDate;
				// @ts-ignore
				this._inputNode.validate && (<Et2Textbox>this._inputNode).validate();
				this.updateComplete.then(() =>
				{
					this.dispatchEvent(new Event("change", {bubbles: true}));
				});
				return;
			}
		}
		this.dispatchEvent(new Event("input", {bubbles: true}));
	}

	/**
	 * Change handler setting modelValue for validation
	 *
	 * @param _ev
	 * @returns
	 */
	_updateValueOnChange(selectedDates : Date[], dateStr : string, instance : Instance)
	{
		this.modelValue = this.getValue();
	}

	_onReady(selectedDates : Date[], dateStr : string, instance : Instance)
	{
		this._updateValueOnChange(selectedDates, dateStr, instance);

		// Add any classes we have to the instance
		instance.calendarContainer.classList.add(...this.classList.values())
	}

	/**
	 * Customise date rendering
	 *
	 * @see https://flatpickr.js.org/events/
	 *
	 * @param {Date} dates Currently selected date(s)
	 * @param dStr a string representation of the latest selected Date object by the user. The string is formatted as per the dateFormat option.
	 * @param inst flatpickr instance
	 * @param dayElement
	 * @protected
	 */
	protected _onDayCreate(dates : Date[], dStr : string, inst, dayElement : HTMLElement)
	{
		//@ts-ignore flatpickr adds dateObj to days
		let date = new Date(dayElement.dateObj);
		let f_date = new Date(date.valueOf() - date.getTimezoneOffset() * 60 * 1000);
		if(!f_date)
		{
			return;
		}

		this.egw().holidays(f_date.getFullYear()).then((h) => this.set_holiday(f_date, h, dayElement));
	}

	private set_holiday(f_date : Date, holidays, element : HTMLElement)
	{
		let day_holidays = holidays[formatDate(f_date, {dateFormat: "Ymd"})]
		let tooltip = '';
		if(typeof day_holidays !== 'undefined' && day_holidays.length)
		{
			for(var i = 0; i < day_holidays.length; i++)
			{
				if(typeof day_holidays[i]['birthyear'] !== 'undefined')
				{
					element.classList.add('calBirthday');
				}
				else
				{
					element.classList.add('calHoliday');
				}
				tooltip += day_holidays[i]['name'] + "\n";
			}
		}
		if(tooltip)
		{
			this.egw().tooltipBind(element, tooltip);
			this._boundTooltipElements.push(element);
		}
	}

	/**
	 * Set the minimum allowed date
	 * @param {string | Date} min
	 */
	set_min(min : string | Date)
	{
		this.minDate = min;
	}

	set minDate(min : string | Date)
	{
		if(this._instance)
		{
			if(min)
			{
				// Handle timezone offset, flatpickr uses local time
				let date = new Date(min);
				let formatDate = new Date(date.valueOf() + date.getTimezoneOffset() * 60 * 1000);
				this._instance.set("minDate", formatDate)
			}
			else
			{
				this._instance.set("minDate", "")
			}
		}
	}

	/**
	 * Set the minimum allowed date
	 * @param {string | Date} max
	 */
	set_max(max : string | Date)
	{
		this.maxDate = max;
	}

	set maxDate(max : string | Date)
	{
		if(this._instance)
		{
			if(max)
			{
				// Handle timezone offset, flatpickr uses local time
				let date = new Date(max);
				let formatDate = new Date(date.valueOf() + date.getTimezoneOffset() * 60 * 1000);
				this._instance.set("maxDate", formatDate)
			}
			else
			{
				this._instance.set("maxDate", "")
			}
		}
	}


	private _convert_placement(position : "top" | "bottom" | "auto" | "") : "above" | "below" | "auto"
	{
		let placement = "auto";
		switch(position)
		{
			case "top":
				placement = "above";
				break;
			case "bottom":
				placement = "below";
				break;
		}
		return placement;
	}

	get placement() : "top" | "bottom" | "auto"
	{
		return this.__placement;
	}

	set placement(new_placement : "top" | "bottom" | "auto")
	{
		if(this._instance)
		{
			this._instance.set("position", this._convert_placement(new_placement))
		}
		this.__placement = new_placement;
	}

	/**
	 * Override from flatpickr - This is the node we tell flatpickr to use
	 * It must be an <input>, flatpickr doesn't understand anything else
	 * @returns {any}
	 */
	findInputField() : HTMLInputElement
	{
		return <HTMLInputElement>this._inputNode?.shadowRoot?.querySelector('input:not([type="hidden"])');
	}

	getInputNode()
	{
		if(typeof egwIsMobile == "function" && egwIsMobile())
		{
			return super.getInputNode();
		}
		return this.findInputField();
	}

	focus()
	{
		if(!(typeof egwIsMobile == "function" && egwIsMobile()))
		{
			this.open();
		}
		this.getInputNode()?.focus();
	}

	/**
	 * The interactive (form) element.
	 * This is an et2-textbox, which causes some problems with flatpickr
	 * @protected
	 */
	get _inputNode() : Et2Textbox | HTMLInputElement
	{
		if(typeof egwIsMobile == "function" && egwIsMobile())
		{
			return this.shadowRoot?.querySelector("input[type='" + this._mobileInputType() + "']");
		}
		else
		{
			return this.shadowRoot?.querySelector('et2-textbox');
		}
	}

	/**
	 * The holder of value for flatpickr
	 */
	get _valueNode() : Et2Textbox
	{
		return this.shadowRoot?.querySelector('et2-textbox');
	}

	/**
	 * Handle clicks on scroll buttons
	 *
	 * @param e
	 */
	public handleScroll(e)
	{
		if(e.target && !e.target.dataset.direction)
		{
			return;
		}
		e.stopPropagation();

		const direction = parseInt(e.target.dataset.direction, 10) || 1;
		this.increment(direction, "day", true);

		this.updateComplete.then(() =>
		{
			this.dispatchEvent(new Event("change", {bubbles: true}));
		});
	}

	/**
	 * Increment the current value
	 *
	 * @param {number} delta Amount of change, positive or negative
	 * @param {"day" | "hour" | "minute"} field
	 * @param {boolean} roundToDelta Round the current value to a multiple of delta before incrementing
	 * 	Useful for keeping things to a multiple of 5, for example.
	 */
	public increment(delta : number, field : "day" | "hour" | "minute", roundToDelta = true)
	{
		let date;
		if(this._inputElement.value)
		{
			date = new Date(this._inputElement.value);
			// Handle timezone offset, flatpickr uses local time
			date = new Date(date.valueOf() + date.getTimezoneOffset() * 60 * 1000);
		}
		else
		{
			// No current value - start with "now", but don't increment at all
			date = new Date();
			delta = 0;
		}
		const fieldMap = {day: "UTCDate", hour: "UTCHours", minute: "UTCMinutes"};
		const original = date["get" + fieldMap[field]]();
		// Avoid divide by 0 in case we have no current value, or delta of 0 passed in
		const roundResolution = delta || {
			day: 1,
			hour: this.getOptions().hourIncrement,
			minute: this.getOptions().minuteIncrement
		}[field];

		let bound = roundToDelta ? (Math.round(original / roundResolution) * roundResolution) : original;
		date["set" + fieldMap[field]](bound + delta);


		this.setDate(date, false, null);
	}


	protected _inputTemplate()
	{
		if(typeof egwIsMobile == "function" && egwIsMobile())
		{
			// Plain input for mobile
			return html`<input type=${this._mobileInputType()} ?disabled=${this.disabled}></input>`;
		}
		// This element gets hidden and used for value, but copied by flatpickr and used for input
		return html`
            <slot name="prefix"></slot>
            <et2-textbox exportparts="base" type="text" placeholder=${this.placeholder}
                         ?required=${this.required}
                         ?disabled=${this.disabled}
            >
                ${this._incrementButtonTemplate()}
            </et2-textbox>
            <slot name="suffix"></slot>
		`;
	}

	protected _incrementButtonTemplate()
	{
		// No increment buttons on mobile
		if(typeof egwIsMobile == "function" && egwIsMobile() || this.disabled)
		{
			return nothing;
		}

		return html`
            <div slot="suffix" class="et2-date-time__scrollbuttons" part="scrollbuttons" @click=${this.handleScroll}>
                <et2-button-icon
                        noSubmit
                        image="chevron-up"
                        data-direction="1"
                >↑
                </et2-button-icon>
                <et2-button-icon
                        noSubmit
                        image="chevron-down"
                        data-direction="-1"
                >↓
                </et2-button-icon>
            </div>`;
	}

	render()
	{

		const labelTemplate = this._labelTemplate();
		const helpTemplate = this._helpTextTemplate();

		return html`
            ${(this.getOptions().inline || typeof egwIsMobile != "function" || !egwIsMobile()) ? super.render() : nothing}
            <div
                    part="form-control"
                    class=${classMap({
                        'form-control': true,
                        'form-control--small': this.size === 'small',
                        'form-control--medium': this.size === 'medium' || typeof this.size === 'undefined',
                        'form-control--large': this.size === 'large',
                        'form-control--has-label': labelTemplate !== nothing,
                        'form-control--has-help-text': helpTemplate !== nothing
                    })}
            >
                ${labelTemplate}
                <div part="form-control-input" class="form-control-input">
                    ${this._inputTemplate()}
                </div>
                ${helpTemplate}
            </div>
		`;
	}
}

// @ts-ignore TypeScript is not recognizing that Et2Date is a LitElement
customElements.define("et2-date", Et2Date);