/* Binder.ts

	Purpose:
		
	Description:
		
	History:
		Wed, Jan 07, 2015 12:08:17 PM, Created by jumperchen

Copyright (C)  Potix Corporation. All Rights Reserved.
*/
var _zkMatchMediaRegexPattern = /ZKMatchMedia=([^;]*)/,
	_portrait: Record<string | number, boolean> = { '0': true, '180': true }, //default portrait definition
	_initLandscape = jq.innerWidth() > jq.innerHeight(), // initial orientation is landscape or not
	// eslint-disable-next-line zk/preferStrictBooleanType
	_initDefault = _portrait[window.orientation] as boolean | undefined; //default orientation

declare module '@zk/widget' {
	interface Widget {
		/** @internal */
		_$binder?: zkbind.Binder;
		$ZKBINDER$?: boolean;
		$ZKMATCHMEDIA$?: string[];
		$binder(): zkbind.Binder | undefined;
	}
}
const _WidgetX = zk.augment(zk.Widget.prototype, {
	$binder(): zkbind.Binder | undefined {
		// eslint-disable-next-line @typescript-eslint/no-this-alias
		var w: zk.Widget | undefined = this;
		for (; w; w = w.parent) {
			if (w.$ZKBINDER$)
				break;
		}
		if (w) {
			if (!w._$binder)
				w._$binder = new zkbind.Binder(w, this);
			return w._$binder;
		}
		return;
	},
	$afterCommand(command: string, args?: unknown[]): void {
		const binder = this.$binder();
		if (binder)
			binder.$doAfterCommand(command, args);
	},
	/** @internal */
	unbind_(skipper?: zk.Skipper, after?: CallableFunction[], keepRod?: boolean): void {
		if (this._$binder) {
			this._$binder.destroy();
			this._$binder = undefined;
		}
		_WidgetX.unbind_.call(this, skipper, after, keepRod);
	}
});

export interface BinderOptions {
	exact?: boolean;
	strict?: boolean;
	child?: boolean;
}
/**
 * Retrieves the binder if any.
 * @param n - the object to look for. If it is a string,
 * it is assumed to be UUID, unless it starts with '$'.
 * For example, `zkbind.$('uuid')` is the same as `zkbind.$('#uuid')`,
 * and both look for a widget whose ID is 'uuid'. On the other hand,
 * `zkbind.$('$id')` looks for a widget whose ID is 'id'.<br/>
 * and `zkbind.$('.className')` looks for a widget whose CSS selector is 'className'.<br/>
 * If it is an DOM element ({@link DOMElement}), it will look up
 * which widget it belongs to.<br/>
 * If the object is not a DOM element and has a property called
 * `target`, then `target` is assumed.
 * Thus, you can pass an instance of {@link jq.Event} or {@link zk.Event},
 * and the target widget will be returned.
 * @param opts - the options. Allowed values:
 * <ul>
 * <li>exact - id must exactly match uuid (i.e., uuid-xx ignored).
 * It also implies strict</li>
 * <li>strict - whether not to look up the parent node.(since 5.0.2)
 * If omitted, false is assumed (and it will look up parent).</li>
 * <li>child - whether to ensure the given element is a child element
 * of the widget's main element ({@link zk.Widget.$n}). In most cases, if ID
 * of an element is xxx-yyy, the the element must be a child of
 * the element whose ID is xxx. However, there is some exception
 * such as the shadow of a window.</li>
 * </ul>
 * @since 8.0.0
 */
export function $(n: string | HTMLElement | zk.Event | JQuery.Event, opts?: BinderOptions): Binder | undefined {
	var widget = zk.Widget.$(n, opts);
	if (widget)
		return widget.$binder();
	zk.error('Not found ZK Binder with [' + String(n) + ']');
}
zkbind.$ = $;

function _fixCommandName(prefix: string, cmd: string, opts: zk.EventOptions, prop: string): void {
	if (opts[prop]) {
		var ignores = {};
		ignores[prefix + cmd] = true;
		opts[prop] = ignores;
	}
}

// see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent
function encodeRFC5987ValueChars(str: string | string[]): string {
	return encodeURIComponent(str).replace(/['()]/g, escape).replace(/\*/g, '%2A').replace(/%(?:7C|60|5E)/g, unescape);
}

//ZK-3133
function _matchMedia(event: MediaQueryListEvent | MediaQueryList, binder: Binder, value: string): void {
	var cookies = binder._cookies!;
	if (event.matches) {
		var orient = '',
			dpr = 1;
		if (zk.mobile) {
			if ((_initLandscape && _initDefault) || (!_initLandscape && !_initDefault))
				_portrait = { '-90': true, '90': true };
			orient = _portrait[window.orientation] ? 'portrait' : 'landscape';
		} else {
			orient = jq.innerWidth() > jq.innerHeight() ? 'landscape' : 'portrait';
		}
		if (window.devicePixelRatio)
			dpr = window.devicePixelRatio;
		// 16 is the length of string MATCHMEDIAVALUE_PREFIX in BinderCtrl.java
		var ci = [new Date().getTimezoneOffset(), screen.width, screen.height, screen.colorDepth, jq.innerWidth(),
		jq.innerHeight(), jq.innerX(), jq.innerY(), dpr.toFixed(1), orient, zk.mm.tz.guess(), event.matches, value.substring(16)];
		// $ZKCLIENTINFO$ refers to CLIENT_INFO string in BinderCtrl.java
		binder.command(value, { '$ZKCLIENTINFO$': ci });
		if (!cookies.$contains(value)) cookies.push(value);
		// eslint-disable-next-line @microsoft/sdl/no-cookies
		document.cookie = 'ZKMatchMedia=' + encodeRFC5987ValueChars(cookies);
		// eslint-disable-next-line @microsoft/sdl/no-cookies
		document.cookie = 'ZKClientInfo=' + encodeRFC5987ValueChars(JSON.stringify(ci));
	} else {
		cookies.$remove(value);
		// eslint-disable-next-line @microsoft/sdl/no-cookies
		document.cookie = 'ZKMatchMedia=' + encodeRFC5987ValueChars(cookies);
	}
}

export interface MediaQueryListWithHandler {
	mql: MediaQueryList;
	handler: (evt: MediaQueryListEvent) => void;
}

/**
 * A data binder utile widget.
 * @import _global_.File
 * @since 8.0.0
 */
@zk.WrapClass('zkbind.Binder')
export class Binder extends zk.Object {
	/** @internal */
	_cookies?: string[];
	/** @internal */
	_lastcmd?: string;
	/** @internal */
	_aftercmd?: Record<string, CallableFunction[]>;
	/** @internal */
	_mediaQueryLists?: MediaQueryListWithHandler[];
	/** @internal */
	_processingAfterCommand?: boolean;
	$view?: zk.Widget;
	/** @internal */
	_toDoUnAftercmd: Record<string, CallableFunction[]>;
	$currentTarget?: object;

	constructor(widget: zk.Widget, currentTarget: object) {
		super(); // FIXME: params?
		this.$view = widget;
		this.$currentTarget = currentTarget;
		this._aftercmd = {};
		this._toDoUnAftercmd = {};
		//ZK-3133
		if (widget.$ZKMATCHMEDIA$) {
			var cookies: string[] = [],
				// eslint-disable-next-line @microsoft/sdl/no-cookies
				matched = _zkMatchMediaRegexPattern.exec(document.cookie);
			if (matched) {
				var m = matched[1];
				if (m) {
					cookies = decodeURIComponent(m).trim().split(',');
				}
			}
			this._cookies = cookies;
			var binder = this,
				mqls: MediaQueryListWithHandler[] = [];
			// eslint-disable-next-line @typescript-eslint/prefer-for-of
			for (var i = 0; i < widget.$ZKMATCHMEDIA$.length; i++) {
				var media = widget.$ZKMATCHMEDIA$[i],
					mql = window.matchMedia(media.substring(16)),
					handler = (function (s) {
						return function (event: MediaQueryListEvent | MediaQueryList) {
							_matchMedia(event, binder, s);
						};
					})(media);
				mql.addListener(handler);
				handler(mql);
				mqls.push({ mql: mql, handler: handler });
			}
			this._mediaQueryLists = mqls;
		}
	}

	/**
	 * Registers a callback after some command executed.
	 * @param cmd - the name of the command
	 * @param fn - the function to execute
	 */
	after(cmd: string | CallableFunction, fn: CallableFunction): this {
		if (!fn && jq.isFunction(cmd)) {
			fn = cmd;
			cmd = this._lastcmd!;
		}

		var ac = this._aftercmd![cmd as string];
		if (!ac) this._aftercmd![cmd as string] = [fn];
		else
			ac.push(fn);
		return this;
	}

	/**
	 * Unregisters a callback after some command executed.
	 * @param cmd - the name of the command
	 * @param fn - the function to execute
	 */
	unAfter(cmd: string, fn: CallableFunction): this {
		var ac = this._aftercmd![cmd];
		for (var j = ac ? ac.length : 0; j--;) {
			if (ac[j] == fn) {
				if (!this._processingAfterCommand)
					ac.splice(j, 1);
				else { // ZK-4482: queue unAfter if $doAfterCommand still processing
					var tduac = this._toDoUnAftercmd[cmd];
					if (!tduac) this._toDoUnAftercmd[cmd] = [fn];
					else
						tduac.push(fn);
				}
			}
		}
		return this;
	}

	/**
	 * Destroy this binder.
	 */
	destroy(): void {
		this._aftercmd = undefined;
		if (this._mediaQueryLists != null) {
			var mqls = this._mediaQueryLists;
			// eslint-disable-next-line @typescript-eslint/prefer-for-of
			for (var i = 0; i < mqls.length; i++) {
				mqls[i].mql.removeListener(mqls[i].handler);
			}
			this._mediaQueryLists = undefined;
			this._cookies = undefined;
		}
		this.$view = undefined;
		this.$currentTarget = undefined;
	}

	/**
	 * Post a command to the binder
	 * @param cmd - the name of the command
	 * @param args - the arguments for this command. (the value should be json type)
	 * @param opts - a map of options to zk.Event, if any.
	 * @param timeout - the time (milliseconds) to wait before sending the request.
	 */
	command(cmd: string, args?: Record<string, unknown>, opts?: zk.EventOptions, timeout?: number): this {
		var wgt = this.$view;
		if (opts) {
			if (opts.duplicateIgnore)
				_fixCommandName('onBindCommand$', cmd, opts, 'duplicateIgnore');
			if (opts.repeatIgnore)
				_fixCommandName('onBindCommand$', cmd, opts, 'repeatIgnore');
		}
		zAu.send(new zk.Event(wgt, 'onBindCommand$' + cmd, { cmd: cmd, args: args }, zk.copy({ toServer: true }, opts)), timeout != undefined ? timeout : 38);
		this._lastcmd = cmd;
		return this;
	}

	/**
	 * Post a global command from the binder.
	 * @param cmd - the name of the command
	 * @param args - the arguments for this command. (the value should be json type)
	 * @param opts - a map of options to zk.Event, if any.
	 * @param timeout - the time (milliseconds) to wait before sending the request.
	 */
	globalCommand(cmd: string, args?: Record<string, unknown>, opts?: zk.EventOptions, timeout?: number): this {
		var wgt = this.$view;
		if (opts) {
			if (opts.duplicateIgnore)
				_fixCommandName('onBindGlobalCommand$', cmd, opts, 'duplicateIgnore');
			if (opts.repeatIgnore)
				_fixCommandName('onBindGlobalCommand$', cmd, opts, 'repeatIgnore');
		}
		zAu.send(new zk.Event(wgt, 'onBindGlobalCommand$' + cmd, { cmd: cmd, args: args }, zk.copy({ toServer: true }, opts)), timeout != undefined ? timeout : 38);
		this._lastcmd = cmd;
		return this;
	}

	$doAfterCommand(cmd: string, args?: unknown[]): void {
		var ac = this._aftercmd![cmd],
			tduac = this._toDoUnAftercmd[cmd];
		this._processingAfterCommand = true; // ZK-4482
		for (var i = 0, j = ac ? ac.length : 0; i < j; i++)
			ac[i].bind(this)(args);
		this._processingAfterCommand = false;
		for (var i = 0, j = tduac ? tduac.length : 0; i < j; i++) { // ZK-4482: do unAfter
			this.unAfter(cmd, tduac[i]);
		}
		this._toDoUnAftercmd[cmd] = [];
	}

	/**
	 * Post a upload command to the binder
	 * @param cmd - the name of the command
	 * @param file - the file to upload. (the value should be a file type)
	 * @since 9.0.1
	 */
	upload(cmd: string, file: File): void {
		this.$view!.fire('onBindCommandUpload$' + cmd, { cmd: cmd }, { file: file });
	}

	/**
	 * Post a command to the binder from the give dom element.
	 * @param dom - the target of the dom element.
	 * @param command - the name of the command
	 * @param args - the arguments for this command. (the value should be json type)
	 * @param opt - a map of options to zk.Event, if any.
	 * @param timeout - the time (milliseconds) to wait before sending the request.
	 */
	static postCommand(dom: HTMLElement, command: string, args?: Record<string, unknown>, opt?: zk.EventOptions, timeout?: number): void {
		var w = zk.Widget.$(dom);
		if (w) {
			var binder = w.$binder();
			if (binder) {
				binder.command(command, args, opt, timeout);
			}
		}
	}

	/**
	 * Post a global command from the binder of the give dom element.
	 * @param dom - the target of the dom element.
	 * @param command - the name of the command
	 * @param args - the arguments for this command. (the value should be json type)
	 * @param timeout - the time (milliseconds) to wait before sending the request.
	 */
	static postGlobalCommand(dom: HTMLElement, command: string, args?: Record<string, unknown>, opt?: zk.EventOptions, timeout?: number): void {
		var w = zk.Widget.$(dom);
		if (w) {
			var binder = w.$binder();
			if (binder) {
				binder.globalCommand(command, args, opt, timeout);
			}
		}
	}
}