/* Datebox.ts

{{IS_NOTE
	Purpose:

	Description:

	History:
		Fri Jan 23 10:32:34 TST 2009, Created by Flyworld
}}IS_NOTE

Copyright (C) 2008 Potix Corporation. All Rights Reserved.

{{IS_RIGHT
}}IS_RIGHT
*/
var globallocalizedSymbols: Record<string, zk.LocalizedSymbols> = {},
	_quotePattern = /'/g, // move pattern string here to avoid jsdoc failure
	_innerDateFormat = 'yyyy/MM/dd ';

@zk.WrapClass('zul.db.Datebox')
export class Datebox extends zul.inp.FormatWidget<DateImpl> {
	/** @internal */
	_buttonVisible = true;
	/** @internal */
	_lenient = true;
	/** @internal */
	_strictDate = false;
	/** @internal */
	_selectLevel: 'day' | 'month' | 'year' | 'decade' | 'today' = 'day';
	/** @internal */
	_closePopupOnTimezoneChange = true;
	/** @internal */
	_pop: CalendarPop;
	/** @internal */
	_tm: CalendarTime;
	// defSet will generate `_timeZone` but actually `_timezone` is used in code.
	// Debugger in ZKDemo shows that both properties are created and agree.
	// However, the server should expect `_timeZone`.
	/** @internal */
	_timeZone?: string;
	/** @internal */
	_constraint?: string;
	/** @internal */
	_displayedTimeZones?: string;
	/** @internal */
	_dtzones?: string[];
	/** @internal */
	_unformater?: string;
	/** @internal */
	_weekOfYear?: boolean;
	/** @internal */
	_showTodayLink?: boolean;
	/** @internal */
	_todayLinkLabel?: string;
	/** @internal */
	_defaultDateTime?: DateImpl;
	/** @internal */
	_refDate?: DateImpl;
	/** @internal */
	_timeZonesReadonly?: boolean;
	/** @internal */
	static _unformater?: zul.db.Unformater;
	localizedFormat?: string;
	/** @internal */
	_position?: string;

	constructor() {
		super();
		this.listen({ onChange: this }, -1000);
		this._pop = new zul.db.CalendarPop();
		this._tm = new zul.db.CalendarTime();
		this.appendChild(this._pop);
		this.appendChild(this._tm);
	}

	setPosition(position: string): this {
		this._position = position;
		return this;
	}

	getPosition(): string | undefined {
		return this._position;
	}

	/**
	 * Sets whether the button (on the right of the textbox) is visible.
	 */
	setButtonVisible(buttonVisible: boolean, opts?: Record<string, boolean>): this {
		const o = this._buttonVisible;
		this._buttonVisible = buttonVisible;

		if (o !== buttonVisible || opts?.force) {
			zul.inp.RoundUtl.buttonVisible(this, buttonVisible);
		}

		return this;
	}

	/**
	 * @returns whether the button (on the right of the textbox) is visible.
	 * @defaultValue `true`.
	 */
	isButtonVisible(): boolean {
		return this._buttonVisible;
	}

	/**
	 * Sets the date format.
	 * <p>The following pattern letters are defined:
	 * <table border="0" cellspacing="3" cellpadding="0">
	 * <tr bgcolor="#ccccff">
	 * <th align="left">Letter
	 * <th align="left">Date or Time Component
	 * <th align="left">Presentation
	 * <th align="left">Examples
	 * <tr><td>`G`
	 * <td>Era designator
	 * <td>Text</a>
	 * <td>`AD`
	 * <tr bgcolor="#eeeeff">
	 * <td>`y`
	 * <td>Year
	 * <td>Year</a>
	 * <td>`1996`; `96`
	 * <tr><td>`M`
	 * <td>Month in year
	 * <td>Month</a>
	 * <td>`July`; `Jul`; `07`
	 * <tr bgcolor="#eeeeff">
	 * <td>`w`
	 * <td>Week in year (starting at 1)
	 * <td>Number</a>
	 * <td>`27`
	 * <tr><td>`W`
	 * <td>Week in month (starting at 1)
	 * <td>Number</a>
	 * <td>`2`
	 * <tr bgcolor="#eeeeff">
	 * <td>`D`
	 * <td>Day in year (starting at 1)
	 * <td>Number</a>
	 * <td>`189`
	 * <tr><td>`d`
	 * <td>Day in month (starting at 1)
	 * <td>Number</a>
	 * <td>`10`
	 * <tr bgcolor="#eeeeff">
	 * <td>`F`
	 * <td>Day of week in month
	 * <td>Number</a>
	 * <td>`2`
	 * <tr><td>`E`
	 * <td>Day in week
	 * <td>Text</a>
	 * <td>`Tuesday`; `Tue`
	 * </table>
	 * @param format - the pattern.
	 */
	override setFormat(format: string, opts?: Record<string, boolean>): this {
		const o = this._format;
		this._format = format;

		if (o !== format || opts?.force) {
			if (this._pop) {
				this._pop.setFormat(this._format);
				if (this._value)
					this._value = this._pop.getTime();
			}
			var inp = this.getInputNode();
			if (inp)
				inp.value = this.getText();
		}

		return this;
	}

	/**
	 * @returns the full date format of the specified format
	 */
	override getFormat(): string | undefined {
		return this._format;
	}

	/**
	 * Sets the constraint.
	 * @defaultValue `null` (means no constraint all all).
	 */
	override setConstraint(constraint: string | undefined, opts?: Record<string, boolean>): this {
		const o = this._constraint;
		this._constraint = constraint;

		if (o !== constraint || opts?.force) {
			if (typeof constraint == 'string' && !constraint.startsWith('[')/*by server*/)
				this._cst = new zul.inp.SimpleDateConstraint(constraint, this);
			else
				this._cst = constraint;
			if (this._cst)
				this._reVald = true; //revalidate required
			if (this._pop) {
				this._pop.setConstraint(this._constraint);
				this._pop.rerender();
			}
		}

		return this;
	}

	/**
	 * @returns the constraint, or null if no constraint at all.
	 */
	override getConstraint(): string | undefined {
		return this._constraint;
	}

	/**
	 * Sets the time zone that this date box belongs to.
	 * @param timezone - the time zone's ID, such as "America/Los_Angeles".
	 */
	setTimeZone(timeZone: string, opts?: Record<string, boolean>): this {
		const o = this._timeZone;
		this._timeZone = timeZone;

		if (o !== timeZone || opts?.force) {
			this._timeZone = timeZone;
			this._tm.setTimezone(timeZone);
			this._setTimeZonesIndex();
			this._value?.tz(timeZone);
			this._pop && this._pop._fixConstraint();
			var cst = this._cst;
			if (cst && cst instanceof zul.inp.SimpleConstraint)
				cst.reparseConstraint();
		}

		return this;
	}

	/**
	 * @returns the time zone that this date box belongs to, such as "America/Los_Angeles"
	 */
	override getTimeZone(): string | undefined {
		return this._timeZone;
	}

	/**
	 * Sets whether the list of the time zones to display is readonly.
	 * If readonly, the user cannot change the time zone at the client.
	 */
	setTimeZonesReadonly(timeZonesReadonly: boolean, opts?: Record<string, boolean>): this {
		const o = this._timeZonesReadonly;
		this._timeZonesReadonly = timeZonesReadonly;

		if (o !== timeZonesReadonly || opts?.force) {
			var select = this.$n<HTMLSelectElement>('dtzones');
			if (select) select.disabled = timeZonesReadonly;
		}

		return this;
	}

	/**
	 * @returns whether the list of the time zones to display is readonly.
	 * If readonly, the user cannot change the time zone at the client.
	 */
	isTimeZonesReadonly(): boolean {
		return !!this._timeZonesReadonly;
	}

	/**
	 * Sets a catenation of a list of the time zones' ID, separated by comma,
	 * that will be displayed at the client and allow user to select.
	 * @param dtzones - a catenation of a list of the timezones' ID, such as
	 * `"America/Los_Angeles,GMT+8"`
	 */
	setDisplayedTimeZones(displayedTimeZones: string, opts?: Record<string, boolean>): this {
		const o = this._displayedTimeZones;
		this._displayedTimeZones = displayedTimeZones;

		if (o !== displayedTimeZones || opts?.force) {
			this._dtzones = displayedTimeZones ? displayedTimeZones.split(',') : undefined;
		}

		return this;
	}

	/**
	 * @returns a list of the time zones that will be displayed at the
	 * client and allow user to select.
	 * @defaultValue `null`
	 */
	getDisplayedTimeZones(): string | undefined {
		return this._displayedTimeZones;
	}

	/**
	 * Sets the unformater function. This method is called from Server side.
	 * @param unf - the unformater function
	 */
	setUnformater(unformater: string, opts?: Record<string, boolean>): this {
		const o = this._unformater;
		this._unformater = unformater;

		if (o !== unformater || opts?.force) {
			eval('Datebox._unformater = ' + unformater); // eslint-disable-line no-eval
		}

		return this;
	}

	/**
	 * @returns the unformater.
	 */
	getUnformater(): string | undefined {
		return this._unformater;
	}

	/**
	 * Sets whether or not date/time parsing is to be lenient.
	 *
	 * <p>
	 * With lenient parsing, the parser may use heuristics to interpret inputs
	 * that do not precisely match this object's format. With strict parsing,
	 * inputs must match this object's format.
	 *
	 * @defaultValue `true`.
	 */
	setLenient(lenient: boolean): this {
		this._lenient = lenient;
		return this;
	}

	/**
	 * @returns whether or not date/time parsing is to be lenient.
	 *
	 * <p>
	 * With lenient parsing, the parser may use heuristics to interpret inputs
	 * that do not precisely match this object's format. With strict parsing,
	 * inputs must match this object's format.
	 */
	isLenient(): boolean {
		return this._lenient;
	}

	getLocalizedSymbols(): zk.LocalizedSymbols | undefined {
		return this._localizedSymbols;
	}

	setLocalizedSymbols(localizedSymbols?: [string, zk.LocalizedSymbols], opts?: Record<string, boolean>): this {
		const o = this._localizedSymbols;

		if (localizedSymbols) {
			if (!globallocalizedSymbols[localizedSymbols[0]])
				globallocalizedSymbols[localizedSymbols[0]] = localizedSymbols[1];
			this._localizedSymbols = globallocalizedSymbols[localizedSymbols[0]];
		}

		if (o !== localizedSymbols || opts?.force) {

			// in this case, we cannot use setLocalizedSymbols() for Timebox
			if (this._tm)
				this._tm._localizedSymbols = this._localizedSymbols;
			if (this._pop)
				this._pop.setLocalizedSymbols(this._localizedSymbols);
		}

		return this;
	}

	/**
	 * Sets whether enable to show the week number in the current calendar or
	 * not. [ZK EE]
	 * @since 6.5.0
	 */
	setWeekOfYear(weekOfYear: boolean, opts?: Record<string, boolean>): this {
		const o = this._weekOfYear;
		this._weekOfYear = weekOfYear;

		if (o !== weekOfYear || opts?.force) {
			if (this._pop)
				this._pop.setWeekOfYear(weekOfYear);
		}

		return this;
	}

	/**
	 * @returns whether enable to show the week number in the current calendar
	 * or not.
	 * @defaultValue `false`
	 * @since 6.5.0
	 */
	isWeekOfYear(): boolean {
		return !!this._weekOfYear;
	}

	/**
	 * Sets whether enable to show the link that jump to today in day view
	 * @since 8.0.0
	 */
	setShowTodayLink(showTodayLink: boolean, opts?: Record<string, boolean>): this {
		const o = this._showTodayLink;
		this._showTodayLink = showTodayLink;

		if (o !== showTodayLink || opts?.force) {
			if (this._pop) {
				this._pop.setShowTodayLink(showTodayLink);
			}
		}

		return this;
	}

	/**
	 * @returns whether enable to show the link that jump to today in day view
	 * @defaultValue `false`
	 * @since 8.0.0
	 */
	isShowTodayLink(): boolean {
		return !!this._showTodayLink;
	}

	/**
	 * Sets the label of the link that jump to today in day view
	 * @since 8.0.4
	 */
	setTodayLinkLabel(todayLinkLabel: string, opts?: Record<string, boolean>): this {
		const o = this._todayLinkLabel;
		this._todayLinkLabel = todayLinkLabel;

		if (o !== todayLinkLabel || opts?.force) {
			if (this._pop) {
				this._pop.setTodayLinkLabel(todayLinkLabel);
			}
		}

		return this;
	}

	/**
	 * @returns the label of the link that jump to today in day view
	 * @since 8.0.4
	 */
	getTodayLinkLabel(): string | undefined {
		return this._todayLinkLabel;
	}

	/**
	 * Sets whether or not date/time should be strict.
	 * If true, any invalid input like "Jan 0" or "Nov 31" would be refused.
	 * If false, it won't be checked and let lenient parsing decide.
	 * @since 8.6.0
	 */
	setStrictDate(strictDate: boolean): this {
		this._strictDate = strictDate;
		return this;
	}

	/**
	 * @returns whether or not date/time should be strict.
	 * @defaultValue `false`.
	 * @since 8.6.0
	 */
	isStrictDate(): boolean {
		return this._strictDate;
	}

	/**
	 * Sets the default datetime if the value is empty.
	 * @since 9.0.0
	 * @param defaultDateTime - Default datetime. null means current datetime.
	 */
	setDefaultDateTime(defaultDateTime: DateImpl): this {
		this._defaultDateTime = defaultDateTime;
		return this;
	}

	/**
	 * @returns the default datetime if the value is empty.
	 * @defaultValue `null` (means current datetime).
	 * @since 9.0.0
	 */
	getDefaultDateTime(): DateImpl | undefined {
		return this._defaultDateTime;
	}

	/**
	 * Sets the level that a user can select.
	 * The valid options are "day", "month", and "year".
	 *
	 * @param selectLevel - the level that a user can select
	 * @since 9.5.1
	 */
	setSelectLevel(selectLevel: 'day' | 'month' | 'year' | 'decade' | 'today'): this {
		this._selectLevel = selectLevel;
		return this;
	}

	/**
	 * @returns the level that a user can select.
	 * @defaultValue `"day"`
	 * @since 9.5.1
	 */
	getSelectLevel(): string {
		return this._selectLevel;
	}

	/**
	 * Sets whether to auto close the datebox popup after changing the timezone.
	 *
	 * @param closePopupOnTimezoneChange - shall close the datebox popup or not
	 * @since 9.6.0
	 */
	setClosePopupOnTimezoneChange(closePopupOnTimezoneChange: boolean): this {
		this._closePopupOnTimezoneChange = closePopupOnTimezoneChange;
		return this;
	}

	/**
	 * @returns whether to auto close the datebox popup after changing the timezone.
	 * @defaultValue `true`
	 * @since 9.6.0
	 */
	isClosePopupOnTimezoneChange(): boolean {
		return this._closePopupOnTimezoneChange;
	}

	/**
	 * A method for component getter symmetry, it will call getValue
	 * @since 10.0.0
	 */
	getValueInZonedDateTime(): DateImpl | undefined {
		return this.getValue();
	}

	/**
	 * A method for component setter symmetry, it will call setValue
	 * @since 10.0.0
	 */
	setValueInZonedDateTime(valueInZonedDateTime: DateImpl, fromServer?: boolean): this {
		return this.setValue(valueInZonedDateTime, fromServer);
	}

	/**
	 * A method for component getter symmetry, it will call getValue
	 * @since 10.0.0
	 */
	getValueInLocalDateTime(): DateImpl | undefined {
		return this.getValue();
	}

	/**
	 * A method for component setter symmetry, it will call setValue
	 * @since 10.0.0
	 */
	setValueInLocalDateTime(valueInLocalDateTime: DateImpl, fromServer?: boolean): this {
		return this.setValue(valueInLocalDateTime, fromServer);
	}

	/**
	 * A method for component getter symmetry, it will call getValue
	 * @since 10.0.0
	 */
	getValueInLocalDate(): DateImpl | undefined {
		return this.getValue();
	}

	/**
	 * A method for component setter symmetry, it will call setValue
	 * @since 10.0.0
	 */
	setValueInLocalDate(valueInLocalDate: DateImpl, fromServer?: boolean): this {
		return this.setValue(valueInLocalDate, fromServer);
	}

	/**
	 * A method for component getter symmetry, it will call getValue
	 * @since 10.0.0
	 */
	getValueInLocalTime(): DateImpl | undefined {
		return this.getValue();
	}

	/**
	 * A method for component setter symmetry, it will call setValue
	 * @since 10.0.0
	 */
	setValueInLocalTime(valueInLocalTime: DateImpl, fromServer?: boolean): this {
		return this.setValue(valueInLocalTime, fromServer);
	}

	/**
	 * @returns the iconSclass name of this Datebox.
	 * @since 8.6.2
	 */
	getIconSclass(): string {
		return 'z-icon-calendar';
	}

	override inRoundedMold(): boolean {
		return true;
	}

	/** @internal */
	_setTimeZonesIndex(): this {
		var select = this.$n<HTMLSelectElement>('dtzones');
		if (select && this._timeZone) {
			var opts = jq(select).children('option');
			for (var i = opts.length; i--;) {
				if (opts[i].text == this._timeZone) select.selectedIndex = i;
			}
		}
		return this;
	}

	override onSize(): void {
		if (this.isOpen())
			Datebox._reposition(this, true);
	}

	/**
	 * @returns the Time format of the specified format
	 */
	getTimeFormat(): string {
		//Note: S (milliseconds not supported yet)
		var fmt = this._format!,
			aa = fmt.indexOf('a'),
			hh = fmt.indexOf('h'),
			KK = fmt.indexOf('K'),
			HH = fmt.indexOf('H'), // ZK-2964: local nl time format has only one 'H'
			kk = fmt.indexOf('k'),
			mm = fmt.indexOf('m'),
			ss = fmt.indexOf('s'),
			hasAM = aa > -1,
			//bug 3284144: The databox format parse a wrong result with hh:mm:ss
			hasHour1 = (hasAM || hh) ? hh > -1 || KK > -1 : false,
			mv = mm > -1 ? 'mm' : '',
			sv = ss > -1 ? 'ss' : '';

		if (hasHour1) {
			var time = Datebox._prepareTimeFormat(hh < KK ? 'KK' : 'hh', mv, sv);
			if (aa == -1)
				return time;
			else if ((hh != -1 && aa < hh) || (KK != -1 && aa < KK))
				return 'a ' + time;
			else
				return time + ' a';
		} else
			return Datebox._prepareTimeFormat(HH < kk ? 'kk' : HH > -1 ? 'HH' : '', mv, sv);

	}

	/**
	 * @returns the Date format of the specified format
	 */
	getDateFormat(): string {
		return this._format!.replace(/[(s.S{1,3})ahKHksm]*:?/g, '').trim();
	}

	/**
	 * Drops down or closes the calendar to select a date.
	 */
	setOpen(open: boolean, _focus_: boolean): this {
		if (this.isRealVisible()) {
			var pop = this._pop;
			if (pop) {
				if (open) pop.open(!_focus_);
				else pop.close(!_focus_);
			}
		}
		return this;
	}

	isOpen(): boolean {
		return !!this._pop && this._pop.isOpen();
	}

	override setValue(value: DateImpl, fromServer?: boolean): this {
		var tz = this.getTimeZone();
		if (tz && value) value.tz(tz);
		return super.setValue(value, fromServer);
	}

	/** @internal */
	override coerceFromString_(val: string | undefined, pattern?: string): zul.inp.CoerceFromStringResult | DateImpl | undefined {
		var unf = Datebox._unformater,
			tz = this.getTimeZone();
		if (unf && jq.isFunction(unf)) {
			var cusv = unf(val);
			if (cusv) {
				this._shortcut = val;
				return cusv;
			}
		}
		if (val) {
			var refDate = this._refDate || this._value,
				format = this.getFormat()!,
				d = new zk.fmt.Calendar().parseDate(val, pattern || format, !this._lenient, refDate, this._localizedSymbols, tz, this._strictDate);
			this._refDate = undefined;
			if (!d) return { error: zk.fmt.Text.format(msgzul.DATE_REQUIRED + (this.localizedFormat!.replace(_quotePattern, ''))) };
			// B70-ZK-2382 escape shouldn't be used in format including hour
			if (!format.match(/[HkKh]/))
				d = new zk.fmt.Calendar().escapeDSTConflict(d, tz);
			return d;
		}
		return undefined;
	}

	/** @internal */
	override coerceToString_(val: DateImpl | undefined, pattern?: string): string {
		return val ? new zk.fmt.Calendar().formatDate(val, pattern || this.getFormat(), this._localizedSymbols) : '';
	}

	/** @internal */
	override doClick_(evt: zk.Event, popupOnly?: boolean): void {
		if (this._disabled) return;
		if (this._readonly && this._buttonVisible && this._pop && !this._pop.isOpen())
			this._pop.open();
		super.doClick_(evt, popupOnly);
	}

	/** @internal */
	override doKeyDown_(evt: zk.Event): void {
		this._doKeyDown(evt);
		if (!evt.stopped)
			super.doKeyDown_(evt);
	}

	/** @internal */
	_doKeyDown(evt: zk.Event): void {
		if (jq.nodeName(evt.domTarget, 'option', 'select'))
			return;

		var keyCode = evt.keyCode,
			bOpen = this._pop.isOpen();
		if (!evt.shiftKey && keyCode == 9 && evt.domTarget == this.$n('real') && bOpen) { // Tab focus to popup
			this._pop.focus();
			evt.stop();
			return;
		}
		if (keyCode == 9 || (zk.webkit && keyCode == 0)) { //TAB or SHIFT-TAB (safari)
			if (evt.target != this._tm) { // avoid closing the popup if moving focus to the timezone dropdown
				if (bOpen) this._pop.close();
				return;
			}
		}

		if (evt.altKey && (keyCode == 38 || keyCode == 40)) {//UP/DN
			if (bOpen) this._pop.close();
			else this._pop.open();

			//FF: if we eat UP/DN, Alt+UP degenerate to Alt (select menubar)
			var opts = { propagation: true };
			evt.stop(opts);
			return;
		}

		//Request 1537962: better responsive
		if (bOpen && (keyCode == 13 || keyCode == 27 || keyCode == 32)) { //ENTER or ESC or SPACE
			if (keyCode == 13) this.enterPressed_(evt);
			else if (keyCode == 27) this.escPressed_(evt);
			return;
		}

		if (keyCode == 18 || keyCode == 27 || keyCode == 13
			|| (keyCode >= 112 && keyCode <= 123)) //ALT, ESC, Enter, Fn
			return; //ignore it (doc will handle it)

		// ZK-2202: should not trigger too early when key code is ENTER
		// select current time
		if (this._pop.isOpen()) {
			this._pop.doKeyDown_(evt);
		}
	}

	/**
	 * Called when the user presses enter when this widget has the focus ({@link focus}).
	 * <p>call the close function
	 * @param evt - the widget event.
	 * The original DOM event and target can be retrieved by {@link zk.Event#domEvent} and {@link zk.Event#domTarget}
	 * @internal
	 */
	enterPressed_(evt: zk.Event): void {
		this._pop.close();
		this.updateChange_();
		evt.stop();
	}

	/**
	 * Called when the user presses escape key when this widget has the focus ({@link focus}).
	 * <p>call the close function
	 * @param evt - the widget event.
	 * The original DOM event and target can be retrieved by {@link zk.Event#domEvent} and {@link zk.Event#domTarget}
	 * @internal
	 */
	escPressed_(evt: zk.Event): void {
		this._pop.close();
		evt.stop();
	}

	/** @internal */
	override afterKeyDown_(evt: zk.Event, simulated?: boolean): boolean {
		if (!simulated && this._inplace)
			jq(this.$n_()).toggleClass(this.getInplaceCSS(), evt.keyCode == 13);

		return super.afterKeyDown_(evt, simulated);
	}

	/** @internal */
	override bind_(desktop?: zk.Desktop, skipper?: zk.Skipper, after?: CallableFunction[]): void {
		super.bind_(desktop, skipper, after);
		var btn = this.$n('btn');

		if (btn) {
			this.domListen_(btn, zk.android ? 'onTouchstart' : 'onClick', '_doBtnClick');
			if (this._inplace) this.domListen_(btn, 'onMouseDown', '_doBtnMouseDown');
		}

		zWatch.listen({ onSize: this, onScroll: this, onShow: this });
		this._pop.setFormat(this.getDateFormat());
	}

	/** @internal */
	override unbind_(skipper?: zk.Skipper, after?: CallableFunction[], keepRod?: boolean): void {
		var pop = this._pop;
		if (pop)
			pop.close(true);

		var btn = this.$n('btn');
		if (btn) {
			this.domUnlisten_(btn, zk.android ? 'onTouchstart' : 'onClick', '_doBtnClick');
			if (this._inplace) this.domUnlisten_(btn, 'onMouseDown', '_doBtnMouseDown');
		}

		zWatch.unlisten({ onSize: this, onScroll: this, onShow: this });
		super.unbind_(skipper, after, keepRod);
	}

	/** @internal */
	_doBtnClick(evt: zk.Event): void {
		this._inplaceIgnore = false;
		if (!this._buttonVisible) return;
		if (!this._disabled)
			this.setOpen(!jq(this.$n_('pp')).zk.isVisible(), zul.db.DateboxCtrl.isPreservedFocus(this));
		// Bug ZK-2544, B70-ZK-2849
		evt.stop((this._pop && this._pop._open ? { propagation: true } : undefined));
	}

	/** @internal */
	_doBtnMouseDown(): void {
		this._inplaceIgnore = true;
	}

	/** @internal */
	_doTimeZoneChange(evt: zk.Event): void {
		var select = this.$n_<HTMLSelectElement>('dtzones'),
			timezone = select.value;
		this.updateChange_();
		this.fire('onTimeZoneChange', { timezone: timezone }, { toServer: true }, 150);
		if (this._pop && this._closePopupOnTimezoneChange) this._pop.close();
	}

	// NOTE: data value will become string after event handler
	onChange(evt: zk.Event & { data: zul.db.CalendarOnChangeData }): void {
		var data = evt.data,
			inpValue = this.getInputNode()!.value;
		if (this._pop)
			this._pop.setValue(data.value!);
		// B50-ZK-631: Datebox format error message not shown with implements CustomConstraint
		// pass input value to server for showCustomError
		if (!data.value && inpValue
			&& this.getFormat() && this._cst == '[c')
			data.value = inpValue as unknown as DateImpl; // FIXME: type mismatch
	}

	onScroll(wgt?: zk.Widget): void {
		if (this.isOpen()) {
			var pp = this._pop;
			// ZK-1552: fix the position of popup when scroll
			if (wgt && pp) {
				// ZK-2211: should close when the input is out of view
				if (this.getInputNode() && zul.inp.InputWidget._isInView(this))
					Datebox._reposition(this, true);
				else
					pp.close();
			}
		}
	}

	override onShow(): void {
		if (this.__ebox) {
			this.setFloating_(true);
			this.__ebox.show();
		}
	}

	/**
	 * @returns the label of the time zone
	 */
	getTimeZoneLabel(): string {
		return '';
	}

	/** @internal */
	redrawpp_(out: string[]): void {
		out.push('<div id="', this.uuid, '-pp" class="', this.$s('popup'),
			'" style="display:none" role="dialog" aria-labelledby="', /*safe*/ this._pop.uuid, '-title" tabindex="-1">');
		for (var w = this.firstChild; w; w = w.nextSibling)
			w.redraw(out);

		this._redrawTimezone(out);
		out.push('</div>');
	}

	/** @internal */
	_redrawTimezone(out: string[]): void {
		var timezones = this._dtzones;
		if (timezones) {
			out.push('<div class="', this.$s('timezone'), '">',
				DOMPurify.sanitize(this.getTimeZoneLabel()),
				'<select id="', this.uuid, '-dtzones">');
			for (var i = 0, len = timezones.length; i < len; i++) {
				const timezoneHTML = zUtl.encodeXML(timezones[i]);
				out.push('<option value="', timezoneHTML, '">', timezoneHTML, '</option>');
			}
			out.push('</select></div>');
			// B50-ZK-577: Rendering Issue using Datebox with displayedTimeZones
		}
	}

	/** @internal */
	override updateChange_(clear?: boolean): boolean {
		if (!this.isOpen())
			super.updateChange_(clear);
		return false;
	}

	/** @internal */
	static _reposition(db: Datebox, silent?: boolean): void {
		if (!db.$n()) return;
		var pp = db.$n('pp'),
			n = db.$n_(),
			inp = db.getInputNode();

		if (pp) {
			// dodgeRef is only ever tested for truthiness, and only here is it set to a non-boolean value.
			zk(pp).position(n, db._position, { dodgeRef: n as unknown as boolean });
			db._pop.syncShadow();
			if (!silent)
				zk(inp).focus();
		}
	}

	/** @internal */
	static _prepareTimeFormat(h: string, m: string, s: string): string {
		var o: string[] = [];
		if (h) o.push(h);
		if (m) o.push(m);
		if (s) o.push(s);
		return o.join(':');
	}
}

@zk.WrapClass('zul.db.CalendarPop')
export class CalendarPop extends zul.db.Calendar {
	override parent!: Datebox;
	/** @internal */
	_shadow?: zk.eff.Shadow;
	/** @internal */
	_open?: boolean;

	constructor() {
		super();
		this.listen({ onChange: this }, -1000);
	}

	setFormat(format: string): this {
		this._fmt = format;
		return this;
	}

	setLocalizedSymbols(localizedSymbols?: zk.LocalizedSymbols): this {
		this._localizedSymbols = localizedSymbols;
		return this;
	}

	// ZK-2047: should sync shadow when shiftView
	override rerender(skipper?: number | zk.Skipper): this {
		super.rerender(skipper);
		if (this.desktop) this.syncShadow();
		return this;
	}

	close(silent?: boolean): void {
		var db = this.parent,
			pp = db.$n('pp');

		if (!pp || !zk(pp).isVisible()) return;

		if (db._inplace) {
			db._inplaceIgnore = false;
			db._inplaceTimerId = setTimeout(function () {
				if (db.desktop) jq(db.$n_()).addClass(db.getInplaceCSS());
			}, db._inplaceTimeout);
		}
		// firefox and safari only
		try {
			if ((zk.ff || zk.safari) && zk.currentFocus) {
				var n = zk.currentFocus.getInputNode ?
					zk.currentFocus.getInputNode()! : zk.currentFocus.$n_();
				if (jq.nodeName(n, 'input') && jq.isAncestor(pp, n)) // Bug ZK-2922, check ancestor first.
					jq(n).blur(); // trigger a missing blur event.
			}
		} catch (e) {
			zk.debugLog((e as Error).message || e as string);
		}

		if (this._shadow) {
			// B65-ZK-1904: Make shadow behavior the same as ComboWidget
			this._shadow.destroy();
			this._shadow = undefined;
		}
		pp.style.display = 'none';
		pp.className = db.$s('popup');

		jq(pp).zk.undoVParent();
		db.setFloating_(false);

		if (silent)
			db.updateChange_();
		else if (zul.db.DateboxCtrl.isPreservedFocus(this))
			zk(db.getInputNode()).focus();
		//remove extra CSS class
		var openClass = db.$s('open');
		jq(db.$n_()).removeClass(openClass);
		jq(pp).removeClass(openClass);
	}

	isOpen(): boolean {
		return zk(this.parent.$n('pp')).isVisible();
	}

	open(silent?: boolean): void {
		var db = this.parent,
			dbn = db.$n(), pp = db.$n('pp');
		if (!dbn || !pp)
			return;
		if (db._inplace) db._inplaceIgnore = true;
		db.setFloating_(true, { node: pp });
		zWatch.fire('onFloatUp', db); //notify all
		var topZIndex = this.setTopmost();
		this._setView(db._selectLevel);
		var /*safe*/ zcls = db.getZclass();

		pp.className = dbn.className + ' ' + pp.className;
		jq(pp).removeClass(zcls);

		pp.style.width = 'auto'; //reset
		pp.style.display = 'block';
		pp.style.zIndex = (topZIndex > 0 ? topZIndex : 1) as unknown as string;
		this.setDomVisible_(pp, true, { visibility: true });

		//FF: Bug 1486840
		//IE: Bug 1766244 (after specifying position:relative to grid/tree/listbox)
		jq(pp).zk.makeVParent();

		if (pp.offsetWidth < dbn.offsetWidth) {
			pp.style.width = `${dbn.offsetWidth}px`;
		} else {
			var wd = jq.innerWidth() - 20;
			if (wd < dbn.offsetWidth)
				wd = dbn.offsetWidth;
			if (pp.offsetWidth > wd)
				pp.style.width = wd as unknown as string;
		}
		delete db._shortcut;

		var fmt = db.getTimeFormat(),
			tz = db.getTimeZone(),
			value = db._value || db._defaultDateTime || zUtl.today(fmt, tz);
		if (value)
			this.setValue(value);
		if (fmt) {
			var tm = db._tm;
			tm.setVisible(true);
			tm.setFormat(fmt);
			tm.setValue(value || Dates.newInstance().tz(tz));
			tm.onSize();
		} else {
			db._tm.setVisible(false);
		}
		//add extra CSS class for easy customize
		var openClass = db.$s('open');
		jq(db.$n_()).addClass(openClass);
		jq(pp).addClass(openClass);

		Datebox._reposition(db, silent); //ZK-3217: only need to calculate position once during open
	}

	syncShadow(): void {
		if (!this._shadow)
			this._shadow = new zk.eff.Shadow(this.parent.$n_('pp'), {
				left: -4, right: 4, top: 2, bottom: 3
			});
		this._shadow.sync();
	}

	override onChange(evt: zk.Event & { data: zul.db.CalendarOnChangeData }): void {
		var date: DateImpl | undefined = this.getTime(),
			db = this.parent,
			fmt = db.getTimeFormat(),
			oldDate = db.getValue(),
			tz = db.getTimeZone(),
			cal = new zk.fmt.Calendar();

		if (fmt) {
			var tm = db._tm,
				time = tm.getValue()!;
			if (fmt.match(/[HK]/i))
				date.setHours(time.getHours());
			if (fmt.match(/[m]/))
				date.setMinutes(time.getMinutes());
			if (fmt.match(/[s]/))
				date.setSeconds(time.getSeconds());
			if (fmt.match(/[S]/))
				date.setMilliseconds(time.getMilliseconds());

			// B70-ZK-2382 escape shouldn't be used in format including hour
			if (!fmt.match(/[HkKh]/))
				date = cal.escapeDSTConflict(date, tz);
		} else if (oldDate) {
			date = Dates.newInstance([date.getFullYear(), date.getMonth(),
			date.getDate(), oldDate.getHours(),
			oldDate.getMinutes(), oldDate.getSeconds(), oldDate.getMilliseconds()], tz);
			//Note: we cannot call setFullYear(), setMonth(), then setDate(),
			//since Date object will adjust month if date larger than max one

			// B70-ZK-2382 escape shouldn't be used in format including hour
			if (!this.getFormat().match(/[HkKh]/))
				date = cal.escapeDSTConflict(date, tz);
		}

		//Bug ZK-1712: no need to set datebox input value when shift view
		if (!evt.data.shiftView)
			db.getInputNode()!.value = db.coerceToString_(date);

		// eslint-disable-next-line @typescript-eslint/no-unnecessary-boolean-literal-compare
		if (this._view == db._selectLevel && evt.data.shallClose !== false) {
			this.close();

			// Bug 3122159 and 3301374
			evt.data.value = date;
			if (!CalendarPop._equalDate(date, oldDate)) {
				db._refDate = date; // for coerceFromString_
				db.updateChange_();
			}
		}
		evt.stop();
	}

	onFloatUp(ctl: zk.ZWatchController): void {
		if (this.isOpen()) {
			var db = this.parent;
			if (!zUtl.isAncestor(db, ctl.origin)) {
				this.close(true);
			}
		}
	}

	/** @internal */
	override bind_(desktop?: zk.Desktop, skipper?: zk.Skipper, after?: CallableFunction[]): void {
		super.bind_(desktop, skipper, after);
		this._bindTimezoneEvt();

		zWatch.listen({ onFloatUp: this });
	}

	/** @internal */
	override unbind_(skipper?: zk.Skipper, after?: CallableFunction[], keepRod?: boolean): void {
		zWatch.unlisten({ onFloatUp: this });
		this._unbindfTimezoneEvt();
		if (this._shadow) {
			this._shadow.destroy();
			this._shadow = undefined;
		}
		super.unbind_(skipper, after, keepRod);
	}

	/** @internal */
	_bindTimezoneEvt(): void {
		var db = this.parent,
			select = db.$n<HTMLSelectElement>('dtzones');
		if (select) {
			select.disabled = (db.isTimeZonesReadonly() ? 'disable' : '') as unknown as boolean;
			db.domListen_(select, 'onChange', '_doTimeZoneChange');
			db._setTimeZonesIndex();
		}
	}

	/** @internal */
	_unbindfTimezoneEvt(): void {
		var db = this.parent,
			select = db.$n('dtzones');
		if (select)
			db.domUnlisten_(select, 'onChange', '_doTimeZoneChange');
	}

	/** @internal */
	override _setView(val: 'day' | 'month' | 'year' | 'decade' | 'today', force?: number): this {
		if (this.parent.getTimeFormat())
			this.parent._tm.setVisible(val == 'day');
		super._setView(val, force);

		return this;
	}

	// ZK-2308
	/** @internal */
	override doKeyDown_(evt: zk.Event): void {
		super.doKeyDown_(evt);
		if (evt.keyCode == 27) {
			this.parent.escPressed_(evt);
		}
	}

	/** @internal */
	override animationSpeed_(): number | 'slow' | 'fast' {
		return zk(this.parent).getAnimationSpeed();
	}

	/** @internal */
	override _chooseDate(target: HTMLTableCellElement | undefined, val: number): void {
		var db = this.parent,
			selectLevel = db._selectLevel;
		if (target && !jq(target).hasClass(this.$s('disabled'))) {
			var cell = target,
				dateobj = this.getTime();
			switch (this._view) {
				case 'day':
					var oldTime = this.getTime();
					this._setTime(undefined, cell._monofs != null && cell._monofs != 0 ?
						dateobj.getMonth() + cell._monofs : undefined, val, true /*fire onChange */);
					var newTime = this.getTime();
					if (oldTime.getYear() == newTime.getYear()
						&& oldTime.getMonth() == newTime.getMonth()) {
						this._markCal({ sameMonth: true }); // optimize
					} else {
						this.rerender(-1);
						this.focus();
					}
					break;
				case 'month':
					if (selectLevel == 'month') {
						this._setTime(undefined, val, 1, true);
						break;
					}
					this._setTime(undefined, val);
					this._setView('day');
					break;
				case 'year':
					if (selectLevel == 'year') {
						this._setTime(val, 0, 1, true);
						break;
					}
					this._setTime(val);
					this._setView('month');
					break;
				case 'decade':
					//Decade mode Set Year Also
					this._setTime(val);
					this._setView('year');
					break;
			}
		}
	}

	/** @internal */
	static _equalDate(d1: DateImpl | undefined, d2: DateImpl | undefined): boolean {
		return !!((d1 == d2) || (d1 && d2 && d1.getTime() == d2.getTime()));
	}
}

@zk.WrapClass('zul.db.CalendarTime')
export class CalendarTime extends zul.db.Timebox {
	override parent!: Datebox;

	constructor() {
		super();
		this.listen({ onChanging: this }, -1000);
	}

	onChanging(evt: zk.Event & { data: zul.db.CalendarOnChangeData }): void {
		var db = this.parent,
			oldDate = db.getValue() || db._pop.getValue(),
			// ZK-2382 we must do the conversion with date and time in the same time
			// otherwise the result may be affcted by DST adjustment
			dateTime = db.coerceToString_(oldDate, _innerDateFormat) + evt.data.value, //onChanging's data is string
			pattern = _innerDateFormat + db.getTimeFormat();

		// add 'AM' (or APM in localizedSymbols) by default, if pattern specified AMPM
		if (pattern.indexOf('a') > -1) {
			var localizedSymbols = db.getLocalizedSymbols();
			if (localizedSymbols && localizedSymbols.APM) {
				dateTime += dateTime.indexOf(localizedSymbols.APM[0]) < 0 && dateTime.indexOf(localizedSymbols.APM[1]) < 0 ? localizedSymbols.APM[0] : '';
			} else {
				dateTime += dateTime.indexOf('AM') < 0 && dateTime.indexOf('PM') < 0 ? 'AM' : '';
			}
		}
		var date = db.coerceFromString_(dateTime, pattern);

		// do nothing if date converted from String is not a valid Date object e.g. dateTime = "2014/10/10 1 :  :     "
		if (date instanceof DateImpl) {
			db.getInputNode()!.value = (evt.data.value as unknown as string)
				= db.coerceToString_(date);
			db.fire(evt.name, evt.data); //onChanging
		}

		evt.stop();
	}
}

/** @class zul.db.DateboxCtrl
 * @import zk.Widget
 * The extra control for the Datebox.
 * It is designed to be overriden
 * @since 6.5.0
 */
export var DateboxCtrl = {
	/**
	 * @returns whether to preserve the focus state.
	 * @param wgt - a widget
	 */
	isPreservedFocus(wgt: zk.Widget): boolean {
		return true;
	}
};
zul.db.DateboxCtrl = DateboxCtrl;