/* Timebox.ts

	Purpose:
		testing textbox.intbox.spinner,timebox,doublebox,longbox and decimalbox on zk5
	Description:

	History:
		Thu June 11 10:17:24     2009, Created by kindalu

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

This program is distributed under LGPL Version 2.1 in the hope that
it will be useful, but WITHOUT ANY WARRANTY.
*/
var LEGAL_CHARS = 'ahKHksmz',
	/* constant for MINUTE (m) field alignment. */
	MINUTE_FIELD = 1,
	/* constant for SECOND (s) field alignment. */
	SECOND_FIELD = 2,
	/* constant for AM_PM (a) field alignment. */
	AM_PM_FIELD = 3,
	/* constant for HOUR0 (H) field alignment. (Hour in day (0-23)) */
	HOUR0_FIELD = 4,
	/* constant for HOUR1 (k) field alignment. (Hour in day (1-24)) */
	HOUR1_FIELD = 5,
	/* constant for HOUR2 (h) field alignment. (Hour in am/pm (1-12)) */
	HOUR2_FIELD = 6,
	/* constant for HOUR3 (K) field alignment. (Hour in am/pm (0-11)) */
	HOUR3_FIELD = 7,
	globallocalizedSymbols: Record<string, zk.LocalizedSymbols> = {};

@zk.WrapClass('zul.db.Timebox')
export class Timebox extends zul.inp.FormatWidget<DateImpl> {
	/** @internal */
	_buttonVisible = true;
	/** @internal */
	override readonly _format = 'HH:mm';
	/** @internal */
	_timezoneAbbr = '';
	/** @internal */
	_fmthdler!: TimeHandler[];
	/** @internal */
	_timeZone?: string;
	/** @internal */
	_unformater?: string;
	/** @internal */
	static _unformater?: zul.db.Unformater;
	/** @internal */
	_changed?: boolean;
	type?: number;
	timerId?: number;
	lastPos?: number;
	/** @internal */
	_lastPos?: number;
	/** @internal */
	_constraint?: string;
	/** @internal */
	_currentbtn?: HTMLElement;

	constructor() {
		super();
		Timebox._updFormat(this, this._format);
	}

	getTimezoneAbbr(): string {
		return this._timezoneAbbr;
	}

	setTimezoneAbbr(timezoneAbbr: string, opts?: Record<string, boolean>): this {
		const o = this._timezoneAbbr;
		this._timezoneAbbr = timezoneAbbr;

		if (o !== timezoneAbbr || opts?.force) {
			Timebox._updFormat(this, this._format);
		}

		return this;
	}

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

		if (o !== timeZone || opts?.force) {
			this._value?.tz(timeZone);
		}

		return this;
	}

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

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

	/**
	 * 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;
	}

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

		if (o !== this._unformater || opts?.force) {
			Timebox._unformater = unformater;
		}

		return this;
	}

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

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

	setLocalizedSymbols(localizedSymbols?: [string, zk.LocalizedSymbols]): this {
		if (localizedSymbols) {
			if (!globallocalizedSymbols[localizedSymbols[0]])
				globallocalizedSymbols[localizedSymbols[0]] = localizedSymbols[1];
			this._localizedSymbols = globallocalizedSymbols[localizedSymbols[0]];
		}
		return this;
	}

	/**
	 * Sets the constraint.
	 * @defaultValue `null` (means no constraint all all).
	 */
	override setConstraint(constraint: string, 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.SimpleLocalTimeConstraint(constraint, this);
			else
				this._cst = constraint;
			if (this._cst)
				this._reVald = true; //revalidate required
			// FIXME: never assigned
			// 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;
	}

	/**
	 * 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);
	}

	/**
	 * Sets the time zone ID that this time box belongs to.
	 * @param timezone - the time zone's ID, such as "America/Los_Angeles".
	 * @deprecated Use {@link setTimeZone} instead.
	 */
	setTimezone(timezone: string): this {
		return this.setTimeZone(timezone);
	}

	/**
	 * @returns the time zone ID that this time box belongs to, such as "America/Los_Angeles"
	 * @since 8.5.1
	 * @deprecated Use {@link getTimeZone} instead.
	 */
	getTimezone(): string | undefined {
		return this._timeZone;
	}

	override inRoundedMold(): boolean {
		return true;
	}

	override setFormat(format: string, opts?: Record<string, boolean> | undefined): this {
		format = format ? format.replace(/'/g, '') : format;
		Timebox._updFormat(this, format);
		return super.setFormat(format, opts);
	}

	override setValue(value: DateImpl, fromServer?: boolean): this {
		var tz = this.getTimeZone();
		if (tz && value) value.tz(tz);
		if (fromServer && value == null) //Bug ZK-1322: if from server side, return empty string
			this._changed = false;
		return super.setValue(value, fromServer);
	}

	/** @internal */
	override coerceToString_(date?: DateImpl): string {
		if (!this._changed && !date && arguments.length) return '';
		var out = '', th: TimeHandler, text: string, offset: number | boolean;
		for (var i = 0, f = this._fmthdler, l = f.length; i < l; i++) {
			th = f[i];
			text = th.format(date);
			out += text;
			//sync handler index
			if (th.type && (offset = th.isSingleLength()) !== false
				&& ((offset as number) += text.length - 1))
				th._doShift(this, offset as number);
		}
		return out;
	}

	/** @internal */
	override coerceFromString_(val: string | undefined): DateImpl | undefined {
		var unf = Timebox._unformater,
			tz = this.getTimeZone();
		if (unf && jq.isFunction(unf)) {
			var cusv = unf(val);
			if (cusv) {
				this._shortcut = val;
				return cusv;
			}
		}
		if (!val) return undefined;

		// F65-ZK-1825: use this._value instead of "today"
		// We cannot use this._value in this case, which won't trigger onChange
		// event. Using clone date instead.
		var date = this._value ? Dates.newInstance(this._value) : zUtl.today(this._format, tz),
			hasAM, isAM,
			fmt: TimeHandler[] = [],
			fmtObeyCount: boolean[] = [],
			emptyCount = 0;
		date.setSeconds(0);
		date.setMilliseconds(0);

		for (var i = 0, f = this._fmthdler, l = f.length; i < l; i++) {
			var th = f[i];
			if (th.type == AM_PM_FIELD) {
				hasAM = true;
				isAM = th.unformat(date, val, {});
				if (!th.getText(val).trim().length)
					emptyCount++;
			} else if (th.type) {
				var shouldObeyCount = i + 1 < l && Timebox._shouldObeyCount(f[i + 1].type);
				fmt.push(th);
				fmtObeyCount.push(shouldObeyCount);
				th.parse(val, {obeyCount: shouldObeyCount}); // in order to shift if necessary
				if (!th.getText(val).trim().length) // ZK-4342: no need to pass obeyCount to determine if part is empty
					emptyCount++;
			}
		}

		if (fmt.length == (hasAM ? --emptyCount : emptyCount)) {
			this._changed = false;//for return empty string
			return;
		}

		for (var i = 0, l = fmt.length; i < l; i++) {
			if (!hasAM && (fmt[i].type == HOUR2_FIELD || fmt[i].type == HOUR3_FIELD))
				isAM = true;
			date = fmt[i].unformat(date, val, {am: isAM, obeyCount: fmtObeyCount[i]}) as DateImpl; // FIXME: inconsistent use of unformat
		}
		return date;
	}

	override onSize(): void {
		var inp = this.getInputNode();
		if (inp && this._value && !inp.value)
			inp.value = this.coerceToString_(this._value);
	}

	onHide = undefined;
	validate = undefined;

	/** @internal */
	override doClick_(evt: zk.Event, popupOnly?: boolean): void {
		if (evt.domTarget == this.getInputNode())
			this._doCheckPos();
		super.doClick_(evt, popupOnly);
	}

	/** @internal */
	override doKeyPress_(evt: zk.Event): void {
		if (zk.opera && evt.keyCode != 9) {
			evt.stop();
			return;
		}
		super.doKeyPress_(evt);
	}

	/** @internal */
	_doBeforeInput(evt: zk.Event): void {
		var inp = this.getInputNode()!;
		if (inp.disabled || inp.readOnly)
			return;

		// control input keys only when no custom unformater is given
		if (!Timebox._unformater) {
			var char = (evt.domEvent!.originalEvent as InputEvent).data!;
			if (char.length <= 1) {
				if (/\d/.test(char)) { // only accept 0~9
					this._doType(parseInt(char));
				}
				// ZK-4861, android only IME issue
				// In desktop browser, keys that has real input value will be stopped at doKeyDown_(),
				// but in android device, keys that has real input value will have the keyCode 229 (evt.key === 'Unidentified'),
				// so we have to block them here to prevent the further event propagation
				evt.stop();
			} else {
				// if char more than 1 character, might triggered by doPaste_(),
				// we only handle single-character here,
				// do nothing to let the event continue to propagate,
				// the further doInput_() will handle it
			}
		}
	}

	/** @internal */
	override doKeyDown_(evt: zk.Event): void {
		var inp = this.getInputNode()!;
		if (inp.disabled || inp.readOnly)
			return;

		// control input keys only when no custom unformater is given
		if (!Timebox._unformater) {
			var code = evt.keyCode;
			switch (code) {
			case 48: case 96://0
			case 49: case 97://1
			case 50: case 98://2
			case 51: case 99://3
			case 52: case 100://4
			case 53: case 101://5
			case 54: case 102://6
			case 55: case 103://7
			case 56: case 104://8
			case 57: case 105://9
				// should handle all the number input value at _doBeforeInput()
				break;
			case 229: // ZK-4861, android only IME issue
				// 229 (evt.key === 'Unidentified') means the input value is not ready yet, so we should continue the event propagation,
				// we'll get the actual input value and handle it in _doBeforeInput()
				break;
			case 35://end
				this.lastPos = inp.value.length;
				return;
			case 36://home
				this.lastPos = 0;
				return;
			case 37://left
				if (this.lastPos! > 0)
					this.lastPos!--;
				return;
			case 39://right
				if (this.lastPos! < inp.value.length)
					this.lastPos!++;
				return;
			case 38://up
				this._doUp();
				evt.stop();
				return;
			case 40://down
				this._doDown();
				evt.stop();
				return;
			case 46://del
				this._doDel();
				evt.stop();
				return;
			case 8://backspace
				this._doBack();
				evt.stop();
				return;
			case 9:
				// do nothing
				break;
			case 13: case 27://enter,esc,tab
				break;
			default:
				if (!(code >= 112 && code <= 123) //F1-F12
						&& !evt.ctrlKey && !evt.metaKey && !evt.altKey) {
					if (evt.shiftKey && code == 45) break; // Allow macOS paste (shift + insert)
					evt.stop();
				}
			}
		}
		super.doKeyDown_(evt);
	}

	/** @internal */
	override doPaste_(evt: zk.Event): void {
		this._updateChanged();
		super.doPaste_(evt);
	}

	/** @internal */
	_updateChanged(): void {
		var inp = this.getInputNode()!,
			self = this,
			curVal = inp.value;
		setTimeout(function () {
			var inpNode = self.getInputNode();
			if (inpNode) {
				if (curVal != inpNode.value) {
					self._changed = true;
				}
			}
		}, 0);
	}

	/** @internal */
	_ondropbtnup(evt: zk.Event): void {
		this.domUnlisten_(document.body, 'onZMouseup', '_ondropbtnup');
		this._stopAutoIncProc();
		this._currentbtn = undefined;
	}

	/** @internal */
	_btnDown(evt: zk.Event): void { // TODO: format the value first
		if (!this._buttonVisible || this._disabled) return;

		// cache it for IE and others to keep the same position at the first time being clicked.
		this._lastPos = this._getPos();

		var btn = this.$n('btn'),
			inp = this.getInputNode()!;

		if (!zk.dragging) {
			if (this._currentbtn) // just in case
				this._ondropbtnup(evt);

			this.domListen_(document.body, 'onZMouseup', '_ondropbtnup');
			this._currentbtn = btn;
		}

		// if btn down before blur, needs to convert to real time string first
		if (inp.value && Timebox._unformater)
			inp.value = this.coerceToString_(this.coerceFromString_(inp.value));
		if (!inp.value)
			inp.value = this.coerceToString_();

		var ofs = zk(btn).revisedOffset(),
			isOverUpBtn = (evt.pageY - ofs[1]) < btn!.offsetHeight / 2;
		if (zk.webkit) {
			zk(inp).focus(); //Bug ZK-1527: chrome and safari will trigger focus if executing setSelectionRange, focus it early here
		}

		var newLastPos = this._getPos();

		// Chrome and Firefox get wrong position at initial case
		if (this._lastPos != newLastPos)
			zk(inp).setSelectionRange(this._lastPos);

		if (isOverUpBtn) { //up
			this._doUp();
			this._startAutoIncProc(true);
		} else {
			this._doDown();
			this._startAutoIncProc(false);
		}

		this._changed = true;
		delete this._shortcut;

		zk.Widget.mimicMouseDown_(this); //set zk.currentFocus
		zk(inp).focus(); //we have to set it here; otherwise, if it is in popup of
			//datebox, datebox's onblur will find zk.currentFocus is null

		// disable browser's text selection
		evt.stop();
	}

	/** @internal */
	_btnUp(evt: zk.Event): void {
		if (!this._buttonVisible || this._disabled || zk.dragging) return;

		if (zk.opera) zk(this.getInputNode()).focus();
			//unfortunately, in opera, it won't gain focus if we set in _btnDown

		this._onChanging();
		this._stopAutoIncProc();

		if (zk.webkit && this._lastPos)
			zk(this.getInputNode()).setSelectionRange(this._lastPos, this._lastPos);
	}

	/** @internal */
	_getPos(): number {
		return zk(this.getInputNode()).getSelectionRange()[0];
	}

	/** @internal */
	_doCheckPos(): void {
		this.lastPos = this._getPos();
	}

	/** @internal */
	_doUp(): void {
		this._changed = true;
		this.getTimeHandler().increase(this, 1);
		this._onChanging();
	}

	/** @internal */
	_doDown(): void {
		this._changed = true;
		this.getTimeHandler().increase(this, -1);
		this._onChanging();
	}

	/** @internal */
	_doBack(): void {
		this._changed = true;
		this.getTimeHandler().deleteTime(this, true);
	}

	/** @internal */
	_doDel(): void {
		this._changed = true;
		this.getTimeHandler().deleteTime(this, false);
	}

	/** @internal */
	_doType(val: number): void {
		this._changed = true;
		this.getTimeHandler().addTime(this, val);
	}

	getTimeHandler(): TimeHandler {
		var sr = zk(this.getInputNode()).getSelectionRange(),
			start = sr[0],
			end = sr[1],
			//don't use [0] as the end variable, it may have a bug when the format is aHH:mm:ss
			//when use UP/Down key to change the time
			hdler: TimeHandler | undefined;

		// Bug ZK-434
		for (var i = 0, f = this._fmthdler, l = f.length; i < l; i++) {
			if (!f[i].type) continue;
			if (f[i].index[0] <= start) {
				hdler = f[i];
				if (f[i].index[1] + 1 >= end)
					return f[i];
			}
		}
		return hdler || this._fmthdler[0];
	}

	getNextTimeHandler(th: TimeHandler): TimeHandler {
		var f = this._fmthdler,
			index = f.$indexOf(th),
			lastHandler: TimeHandler;

		while ((lastHandler = f[++index])
				&& (!lastHandler.type || lastHandler.type == AM_PM_FIELD));

		return lastHandler;
	}

	/** @internal */
	_startAutoIncProc(up: boolean): void {
		if (this.timerId)
			clearInterval(this.timerId);
		var self = this,
			fn: '_doUp' | '_doDown' = up ? '_doUp' : '_doDown';
		this.timerId = setInterval(function () {
			if (zk.webkit && self._lastPos)
				zk(self.getInputNode()).setSelectionRange(self._lastPos, self._lastPos);
			self[fn]();
		}, 300);
		jq(this.$n('btn-' + (up ? 'up' : 'down'))).addClass(this.$s('active'));
	}

	/** @internal */
	_stopAutoIncProc(): void {
		if (this.timerId)
			clearTimeout(this.timerId);
		// this.currentStep = this.defaultStep; // FIXME: both properties are not initialized
		this.timerId = undefined;
		jq('.' + this.$s('icon'), this.$n('btn')).removeClass(this.$s('active'));
	}

	/** @internal */
	override doFocus_(evt: zk.Event): void {
		super.doFocus_(evt);
		var inp = this.getInputNode()!,
			selrng = zk(inp).getSelectionRange();

		if (!inp.value)
			inp.value = Timebox._unformater ? '' : this.coerceToString_();

		this._doCheckPos();

		// Bug 2688620
		if (selrng[0] !== selrng[1]) {
			zk(inp).setSelectionRange(selrng[0], selrng[1]);
			this.lastPos = selrng[1];
		}

		zul.inp.RoundUtl.doFocus_(this);
	}

	/** @internal */
	override doBlur_(evt: zk.Event): void {
		// skip onchange, Bug 2936568
		if (!this._value && !this._changed && !Timebox._unformater)
			this.getInputNode()!.value = this._defRawVal = '';

		super.doBlur_(evt);

		zul.inp.RoundUtl.doBlur_(this);
	}

	/** @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: HTMLElement | undefined;

		this.domListen_(this.getInputNode()!, 'onBeforeInput', '_doBeforeInput');
		if (btn = this.$n('btn'))
			this.domListen_(btn, 'onZMouseDown', '_btnDown')
				.domListen_(btn, 'onZMouseUp', '_btnUp');
		zWatch.listen({onSize: this});
	}

	/** @internal */
	override unbind_(skipper?: zk.Skipper, after?: CallableFunction[], keepRod?: boolean): void {
		if (this.timerId) {
			clearTimeout(this.timerId);
			this.timerId = undefined;
		}
		zWatch.unlisten({onSize: this});
		var btn = this.$n('btn');
		if (btn) {
			this.domUnlisten_(btn, 'onZMouseDown', '_btnDown')
				.domUnlisten_(btn, 'onZMouseUp', '_btnUp');
		}
		this.domUnlisten_(this.getInputNode()!, 'onBeforeInput', '_doBeforeInput');
		this._changed = false;
		super.unbind_(skipper, after, keepRod);
	}

	/** @internal */
	getBtnUpIconClass_(): string {
		return 'z-icon-angle-up';
	}

	/** @internal */
	getBtnDownIconClass_(): string {
		return 'z-icon-angle-down';
	}

	/** @internal */
	static _updFormat(wgt: Timebox, fmt: string): void {
		var index: TimeHandler[] = [],
			APM = wgt._localizedSymbols ? wgt._localizedSymbols.APM! : zk.APM;
		for (var i = 0, l = fmt.length; i < l; i++) {
			var c = fmt.charAt(i);
			switch (c) {
			case 'a':
				var len = APM[0].length;
				index.push(new zul.inp.AMPMHandler([i, i + len - 1], AM_PM_FIELD, wgt));
				break;
			case 'K':
				var start = i,
					end = fmt.charAt(i + 1) == 'K' ? ++i : i;
				index.push(new zul.inp.HourHandler2([start, end], HOUR3_FIELD, wgt));
				break;
			case 'h':
				var start = i,
					end = fmt.charAt(i + 1) == 'h' ? ++i : i;
				index.push(new zul.inp.HourHandler([start, end], HOUR2_FIELD, wgt));
				break;
			case 'H':
				var start = i,
					end = fmt.charAt(i + 1) == 'H' ? ++i : i;
				index.push(new zul.inp.HourInDayHandler([start, end], HOUR0_FIELD, wgt));
				break;
			case 'k':
				var start = i,
					end = fmt.charAt(i + 1) == 'k' ? ++i : i;
				index.push(new zul.inp.HourInDayHandler2([start, end], HOUR1_FIELD, wgt));
				break;
			case 'm':
				var start = i,
					end = fmt.charAt(i + 1) == 'm' ? ++i : i;
				index.push(new zul.inp.MinuteHandler([start, end], MINUTE_FIELD, wgt));
				break;
			case 's':
				var start = i,
					end = fmt.charAt(i + 1) == 's' ? ++i : i;
				index.push(new zul.inp.SecondHandler([start, end], SECOND_FIELD, wgt));
				break;
			case 'z':
				index.push({index: [i, i], format: (function (text) {
					return function () {
						return text;
					};
				})(wgt._timezoneAbbr)} as TimeHandler);
				break;
			default:
				var ary = '',
					start = i,
					end = i;

				while ((ary += c) && ++end < l) {
					c = fmt.charAt(end);
					if (LEGAL_CHARS.includes(c)) {
						end--;
						break;
					}
				}
				index.push({index: [start, end], format: (function (text) {
					return function () {
						return text;
					};
				})(ary)} as TimeHandler);
				i = end;
			}
		}
		for (var shift, i = 0, l = index.length; i < l; i++) {
			if (index[i].type == AM_PM_FIELD) {
				shift = index[i].index[1] - index[i].index[0];
				if (!shift) break; // no need to shift.
			} else if (shift) {
				index[i].index[0] += shift;
				index[i].index[1] += shift;
			}
		}
		wgt._fmthdler = index;
	}

	/** @internal */
	static _shouldObeyCount(nextType: number): boolean {
		switch (nextType) {
			case MINUTE_FIELD:
			case SECOND_FIELD:
			case HOUR0_FIELD:
			case HOUR1_FIELD:
			case HOUR2_FIELD:
			case HOUR3_FIELD:
				return true;
			default:
				return false;
		}
	}
}

@zk.WrapClass('zul.inp.TimeHandler')
export class TimeHandler extends zk.Object {
	maxsize = 59;
	minsize = 0;
	digits = 2;
	index: number[];
	type: number;
	wgt: Timebox;

	constructor(index: number[], type: number, wgt: Timebox) {
		super();
		this.index = index;
		this.type = type;
		if (index[0] == index[1])
			this.digits = 1;
		this.wgt = wgt;
	}

	format(date?: DateImpl): string {
		return '00';
	}

	unformat(date: DateImpl, val: string, opt: Record<string, unknown>): DateImpl | boolean {
		return date;
	}

	increase(wgt: Timebox, up: number): void {
		var inp = wgt.getInputNode()!,
			start = this.index[0],
			end = this.index[1] + 1,
			val = inp.value,
			text: string | number = this.getText(val),
			singleLen = this.isSingleLength() !== false,
			ofs: number | undefined;

		text = zk.parseInt(singleLen ? text :
				text.replace(/ /g, '0')) + up;

		var max = this.maxsize + 1;
		if (text < this.minsize) {
			text = this.maxsize;
			ofs = 1;
		} else if (text >= max) {
			text = this.minsize;
			ofs = -1;
		} else if (singleLen)
			ofs = (up > 0) ?
					(text == 10) ? 1 : 0 :
					(text == 9) ? -1 : 0;

		if (text < 10 && !singleLen)
				text = '0' + text;

		inp.value = val.substring(0, start) + text + val.substring(end);

		if (singleLen && ofs) {
			this._doShift(wgt, ofs);
			end += ofs;
		}

		zk(inp).setSelectionRange(start, end);
	}

	deleteTime(wgt: Timebox, backspace: boolean): void {
		var inp = wgt.getInputNode()!,
			sel = zk(inp).getSelectionRange(),
			pos = sel[0],
			val = inp.value,
			maxLength = TimeHandler._getMaxLen(wgt);

		// clean over text
		if (val.length > maxLength) {
			val = inp.value = val.substr(0, maxLength);
			sel = [Math.min(sel[0], maxLength), Math.min(sel[1], maxLength)];
			pos = sel[0];
		}

		if (pos != sel[1]) {
			//select delete
			inp.value = val.substring(0, pos) + TimeHandler._cleanSelectionText(wgt, this)
							+ val.substring(sel[1]);
		} else {
			var fmthdler = wgt._fmthdler,
				index = fmthdler.$indexOf(this),
				ofs = backspace ? -1 : 1,
				ofs2 = backspace ? 0 : 1,
				ofs3 = backspace ? 1 : 0,
				hdler: TimeHandler,
				posOfs: number | boolean | undefined;
			if (pos == this.index[ofs2] + ofs2) {// on start or end
				//delete by sibling handler
				if (hdler = fmthdler[index + ofs * 2])
					pos = hdler.index[ofs3] + ofs3 + ofs;
			} else {// delete self
				pos += ofs;
				hdler = this;
			}
			if (hdler) {
				posOfs = hdler.isSingleLength();
				inp.value = val.substring(0, (ofs3 += pos) - 1)
					+ (posOfs ? '' : ' ') + val.substring(ofs3);
				if (posOfs)
					hdler._doShift(wgt, posOfs as number);
			}
			if (posOfs && !backspace) pos--;
		}
		zk(inp).setSelectionRange(pos, pos);
	}

	/** @internal */
	_addNextTime(wgt: Timebox, num: number): void {
		var inp = wgt.getInputNode(),
			index: number,
			NTH: TimeHandler;
		if (NTH = wgt.getNextTimeHandler(this)) {
			index = NTH.index[0];
			zk(inp).setSelectionRange(index,
				Math.max(index,
					zk(inp).getSelectionRange()[1]));
			NTH.addTime(wgt, num);
		}
	}

	addTime(wgt: Timebox, num: number): void {
		var inp = wgt.getInputNode()!,
			sel = zk(inp).getSelectionRange(),
			val = inp.value,
			pos = sel[0],
			maxLength = TimeHandler._getMaxLen(wgt),
			posOfs = this.isSingleLength();

		// clean over text
		if (val.length > maxLength) {
			val = inp.value = val.substr(0, maxLength);
			sel = [Math.min(sel[0], maxLength), Math.min(sel[1], maxLength)];
			pos = sel[0];
		}

		if (pos == maxLength)
			return;

		// first number (hendle max bound)
		if (pos == this.index[0]) {
			var text = this.getText(val)
						.substring((posOfs === 0) ? 0 : 2).trim(),
				i;
			if (!text.length) text = '0';

			if ((i = zk.parseInt(num + text)) > this.maxsize) {
				if (posOfs !== 0) {
					val = inp.value = val.substring(0, pos) + (posOfs ? '0' : '00')
						+ val.substring(pos + 2);
					if (!posOfs) pos++;
					zk(inp).setSelectionRange(pos, Math.max(sel[1], pos));
					sel = zk(inp).getSelectionRange();
				}
				if (posOfs)
					this._doShift(wgt, posOfs as number);
			}
		} else if (pos == (this.index[1] + 1)) {//end of handler
			var i;
			if (posOfs !== false) {
				var text = this.getText(val);
				if ((i = zk.parseInt(text + num)) <= this.maxsize) {//allow add number
					if (i && i < 10) // 1-9
						pos--;
					else if (i || posOfs) { // 0 or larger then 10, except zero and non-posOfs
						val = inp.value = val.substring(0, (pos + (posOfs as number)))
								+ (posOfs ? '' : '0') + val.substring(pos);
						if (i) // larger then 10
							this._doShift(wgt, 1);
						else { // 0
							zk(inp).setSelectionRange(pos, Math.max(sel[1], pos));
							if (posOfs)//2 digits zero
								this._doShift(wgt, posOfs as number);
						}
					}
				}
			}

			if (!i || i > this.maxsize) {
				this._addNextTime(wgt, num);
				return;
			}
		}

		if (pos != sel[1]) {
			//select edit
			var s = TimeHandler._cleanSelectionText(wgt, this),
				ofs: number | undefined;
			//in middle position
			if (posOfs !== false && (ofs = pos - this.index[1]))
				this._doShift(wgt, ofs);

			inp.value = val.substring(0, pos++) + num
				+ s.substring(ofs ? 0 : 1)
				+ val.substring(sel[1]);
		} else {
			inp.value = val.substring(0, pos)
				+ num + val.substring(++pos);
		}
		wgt.lastPos = pos;
		zk(inp).setSelectionRange(pos, pos);
	}

	getText(val: string, obeyCount?: boolean): string {
		var start = this.index[0],
			end = this.index[1] + 1;
		return obeyCount !== false ? val.substring(start, end) : val.substring(start);
	}

	/** @internal */
	_doShift(wgt: Timebox, shift: number): void {
		var f = wgt._fmthdler,
			index = f.$indexOf(this),
			NTH: TimeHandler;
		this.index[1] += shift;
		while (NTH = f[++index]) {
			NTH.index[0] += shift;
			NTH.index[1] += shift;
		}
	}

	isSingleLength(): boolean | number {
		return this.digits == 1 && (this.index[0] - this.index[1]);
	}

	parse(val: string, opt: {obeyCount?: boolean}): number {
		var text = this.getText(val, opt.obeyCount),
			parsed = /^\s*\d*/.exec(text)!,
			offset = parsed.length ? parsed[0].length - (this.index[1] - this.index[0] + 1) : 0;
		if (offset) {
			this._doShift(this.wgt, offset);
		}
		return zk.parseInt(text);
	}

	/** @internal */
	static _getMaxLen(wgt: Timebox): number {
		var val = wgt.getInputNode()!.value,
			len = 0,
			th: TimeHandler,
			lastTh: TimeHandler | undefined;
		for (var i = 0, f = wgt._fmthdler, l = f.length; i < l; i++) {
			th = f[i];
			if (i == l - 1) {
				len += th.format().length;
			} else
				len += (th.type ? th.getText(val) : th.format()).length;
			if (th.type) lastTh = th;
		}
		return (lastTh!.digits == 1) ? ++len : len;
	}

	/** @internal */
	static _cleanSelectionText(wgt: Timebox, startHandler: TimeHandler): string {
		var inp = wgt.getInputNode(),
			sel = zk(inp).getSelectionRange(),
			pos = sel[0],
			selEnd = sel[1],
			fmthdler = wgt._fmthdler,
			index = fmthdler.$indexOf(startHandler),
			text = '',
			hdler = startHandler,
			isFirst = true,
			prevStart: number | undefined,
			ofs,
			hStart: number,
			hEnd,
			posOfs: number | boolean;

		//restore separator
		do {
			hStart = hdler.index[0];
			hEnd = hdler.index[1] + 1;

			if (hdler.type && (posOfs = hdler.isSingleLength())) {
				//sync handler index
				hdler._doShift(wgt, posOfs as number);
				selEnd--;
			}

			//latest one
			if (hEnd >= selEnd && hdler.type) {
				ofs = selEnd - hStart;
				while (ofs-- > 0) //replace by space (after)
					text += ' ';
				break;
			}

			if (hdler.type) {
				prevStart = isFirst ? pos : hStart;
				isFirst = false;
				continue;
			}
			ofs = hStart - prevStart!;
			while (ofs-- > 0) //replace by space (before)
				text += ' ';

			text += hdler.format();

		} while (hdler = fmthdler[++index]);
		return text;
	}
}

@zk.WrapClass('zul.inp.HourInDayHandler')
export class HourInDayHandler extends zul.inp.TimeHandler {
	override maxsize = 23;
	override minsize = 0;

	override format(date?: DateImpl): string {
		var singleLen = this.digits == 1;
		if (!date) return singleLen ? '0' : '00';
		else {
			var h: number | string = date.getHours();
			if (!singleLen && h < 10)
				h = '0' + h;
			return h.toString();
		}
	}

	override unformat(date: DateImpl, val: string, opt: Record<string, unknown>): DateImpl {
		date.setHours(this.parse(val, opt));
		return date;
	}
}

@zk.WrapClass('zul.inp.HourInDayHandler2')
export class HourInDayHandler2 extends zul.inp.TimeHandler {
	override maxsize = 24;
	override minsize = 1;

	override format(date?: DateImpl): string {
		if (!date) return '24';
		else {
			var h: number | string = date.getHours();
			if (h == 0)
				h = '24';
			else if (this.digits == 2 && h < 10)
				h = '0' + h;
			return h.toString();
		}
	}

	override unformat(date: DateImpl, val: string, opt: Record<string, unknown>): DateImpl {
		var hours = this.parse(val, opt);
		if (hours >= this.maxsize)
			hours = 0;
		date.setHours(hours);
		return date;
	}
}

@zk.WrapClass('zul.inp.HourHandler')
export class HourHandler extends zul.inp.TimeHandler {
	override maxsize = 12;
	override minsize = 1;

	override format(date?: DateImpl): string {
		if (!date) return '12';
		else {
			var h: number | string = date.getHours();
			h = (h % 12);
			if (h == 0)
				h = '12';
			else if (this.digits == 2 && h < 10)
				h = '0' + h;
			return h.toString();
		}
	}

	override unformat(date: DateImpl, val: string, opt: Record<string, unknown>): DateImpl {
		var hours = this.parse(val, opt);
		if (hours >= this.maxsize)
			hours = 0;
		date.setHours(opt.am ? hours : hours + 12);
		return date;
	}
}

@zk.WrapClass('zul.inp.HourHandler2')
export class HourHandler2 extends zul.inp.TimeHandler {
	override maxsize = 11;
	override minsize = 0;

	override format(date?: DateImpl): string {
		var singleLen = this.digits == 1;
		if (!date) return singleLen ? '0' : '00';
		else {
			var h: number | string = date.getHours();
			h = (h % 12);
			if (!singleLen && h < 10)
				h = '0' + h;
			return h.toString();
		}
	}

	override unformat(date: DateImpl, val: string, opt: Record<string, unknown>): DateImpl {
		var hours = this.parse(val, opt);
		date.setHours(opt.am ? hours : hours + 12);
		return date;
	}
}

@zk.WrapClass('zul.inp.MinuteHandler')
export class MinuteHandler extends zul.inp.TimeHandler {
	override format(date?: DateImpl): string {
		var singleLen = this.digits == 1;
		if (!date) return singleLen ? '0' : '00';
		else {
			var m: number | string = date.getMinutes();
			if (!singleLen && m < 10)
				m = '0' + m;
			return m.toString();
		}
	}

	override unformat(date: DateImpl, val: string, opt: Record<string, unknown>): DateImpl {
		date.setMinutes(this.parse(val, opt));
		return date;
	}
}

@zk.WrapClass('zul.inp.SecondHandler')
export class SecondHandler extends zul.inp.TimeHandler {
	override format(date?: DateImpl): string {
		var singleLen = this.digits == 1;
		if (!date) return singleLen ? '0' : '00';
		else {
			var s: number | string = date.getSeconds();
			if (!singleLen && s < 10)
				s = '0' + s;
			return s.toString();
		}
	}

	override unformat(date: DateImpl, val: string, opt: Record<string, unknown>): DateImpl {
		date.setSeconds(this.parse(val, opt));
		return date;
	}
}

@zk.WrapClass('zul.inp.AMPMHandler')
export class AMPMHandler extends zul.inp.TimeHandler {
	override format(date?: DateImpl): string {
		var APM = this.wgt._localizedSymbols ? this.wgt._localizedSymbols.APM! : zk.APM;
		if (!date)
			return APM[0];
		var h = date.getHours();
		return APM[h < 12 ? 0 : 1];
	}

	override unformat(date: DateImpl, val: string, opt: Record<string, unknown>): boolean {
		var text = this.getText(val).trim(),
			APM = this.wgt._localizedSymbols ? this.wgt._localizedSymbols.APM! : zk.APM;
		return (text.length == APM[0].length) ?
			APM[0] == text : true;
	}

	override addTime(wgt: Timebox, num: number): void {
		var inp = wgt.getInputNode()!,
			start = this.index[0],
			end = this.index[1] + 1,
			val = inp.value,
			text = val.substring(start, end),
			APM = wgt._localizedSymbols ? wgt._localizedSymbols.APM! : zk.APM;
		//restore A/PM text
		if (text != APM[0] && text != APM[1]) {
			text = APM[0];
			inp.value = val.substring(0, start) + text + val.substring(end);
		}
		this._addNextTime(wgt, num);
	}

	// Bug ZK-434, we have to delete a sets of "AM/PM", rather than a single word "A/P/M"
	override deleteTime(wgt: Timebox, backspace: boolean): void {
		var inp = wgt.getInputNode()!,
			sel = zk(inp).getSelectionRange(),
			pos = sel[0],
			pos1 = sel[1],
			start = this.index[0],
			end = this.index[1] + 1,
			val = inp.value;
		if (pos1 - pos > end - start)
			return super.deleteTime(wgt, backspace);

		var t = [''];
		for (var i = end - start; i > 0; i--)
			t.push(' ');

		inp.value = val.substring(0, start) + t.join('') + val.substring(end);
		zk(inp).setSelectionRange(start, start);
	}

	override increase(wgt: Timebox, up: number): void {
		var inp = wgt.getInputNode()!,
			start = this.index[0],
			end = this.index[1] + 1,
			val = inp.value,
			text = val.substring(start, end),
			APM = wgt._localizedSymbols ? wgt._localizedSymbols.APM! : zk.APM;

		text = APM[0] == text ? APM[1] : APM[0];
		inp.value = val.substring(0, start) + text + val.substring(end);
		zk(inp).setSelectionRange(start, end);
	}

	override parse(val: string, opt: {obeyCount?: boolean}): number {
		return zk.parseInt(this.getText(val).trim());
	}
}