import { StringEncoder, Utils as BaseUtils, MathUtils } from '@uino/base-thing';
import { System } from './System';

const _defaultOptions = {};

// Default threshold is 2, which matches the original logic: distance <= 2 triggers click
let _pointerMoveThreshold = 2;

// #region Internal Functions

let _codeMap = {};
let _codeId = 1;

window.__loadCodeComplete = function (key) {
	_codeMap[key] = { result: true };
}

window.__catchError = function (key, error) {
	_codeMap[key] = { result: false, error };
}

function _getDurationByMetric(measureName) {
	const performanceEntries = window.performance.getEntriesByName(measureName);
	const entry = performanceEntries[performanceEntries.length - 1];
	if (entry && entry.entryType === 'measure') {
		return entry.duration;
	}

	return -1;
}

function _performanceMeasure(measureName) {
	const startMark = `mark_${measureName}_start`;
	const endMark = `mark_${measureName}_end`;
	window.performance.measure(measureName, startMark, endMark);
	return _getDurationByMetric(measureName);
}

function _getPath(string) {
	let startIndex = (string.indexOf('\\') >= 0 ? string.lastIndexOf('\\') : string.lastIndexOf('/'));
	return string.substring(0, startIndex);
}

const isPerformanceSupported = window.performance && !!window.performance.getEntriesByType && !!window.performance.now && !!window.performance.mark;

function _appendTimestamp(name) {
	if (name.lastIndexOf("?") == -1) {
		return name + "?time=" + new Date().getTime();
	}
	else {
		return name + "&time=" + new Date().getTime();
	}
}

function _removeElementsByURL(tagName, attributeName, url) {
	// Remove existing CSS items
	let items = document.getElementsByTagName(tagName);
	for (let i = 0; i < items.length; i++) {
		let item = items[i];
		if (!item) {
			continue;
		}

		let src = item.getAttribute(attributeName);
		if (!src) {
			continue;
		}

		// Compare without '?' search name
		let name = src._removeSearchName();
		if (name != url) {
			continue;
		}

		item.parentNode.removeChild(item);
		i--;
	}
}

function _JSLoaderFromFile(url, cache, load, error) {
	// Remove existing script items
	_removeElementsByURL("script", "src", url);

	// If we do not use cache then append the timestamp to make sure download it
	if (!cache) {
		url = _appendTimestamp(url);
	}

	// Create JS element
	let script = document.createElement("script");
	script.type = "text/javascript";
	script.src = url;
	script.onload = function () { // chrome ff
		if (load) {
			load({ script });
		}
	};
	script.onreadystatechange = function () { // ie
		if (this.readyState == 'loaded' || this.readyState == 'complete') {
			if (load) {
				load({ script });
			}
		}
	};
	script.onerror = function () {
		if (error) {
			error();
		}
	}

	document.getElementsByTagName("head")[0].appendChild(script);
}

function _CSSLoaderFromFile(url, cache, load, error) {
	// Remove existing CSS items
	_removeElementsByURL("link", "href", url);

	// If we do not use cache then append the timestamp to make sure download it
	if (!cache) {
		url = _appendTimestamp(url);
	}

	// Create CSS element
	let head = document.getElementsByTagName('head')[0];
	let link = document.createElement('link');
	link.type = 'text/css';
	link.rel = 'stylesheet';
	link.href = url;
	head.appendChild(link);
	link.onreadystatechange = function () { // ie
		if (css.readyState == 'complete') {
			if (load) {
				load();
			}
		}
	};
	link.onload = function () { // chrome ff
		if (load) {
			load();
		}
	};
	link.onerror = function () {
		if (error) {
			error();
		}
	}
}

function _asyncLoadFile(url, cache, load, error) {
	if (!url) {
		return;
	}

	url = BaseUtils.resolveURL(url);

	// Get the loader
	let loader;
	let extension = url._getExtension();
	if (extension == "js") {
		loader = _JSLoaderFromFile;
	}
	else if (extension == "css") {
		loader = _CSSLoaderFromFile;
	}

	if (loader) {
		loader(url, cache, load, error);
	}
}

// #endregion

/**
 * @class Utils
 * Useful functions.
 * @memberof THING
 * @extends THING.UTILS.Utils
 */
class Utils extends BaseUtils {

	static mainScriptUrl = Utils.getCurrentScriptUrl();
	static hasPointerEvent = false;

	static #options = {};
	static system = new System();

	/**
	 * @typedef {object} DomWorkerInitOptions
	 * @property {string} rootPath The working root path.
	 * @property {boolean} basis True indicates to enable basis compressed texture encoder and loader.
	 */

	/**
	 * @typedef {object} DomInitOptions
	 * @property {DomWorkerInitOptions} worker The worker options.
	 */

	/**
	 * Initialize.
	 * @param {DomInitOptions} options The options.
	 * @example
	 * THING.DOM.Utils.init({
	 * 	worker: {
	 * 		rootPath: './js/thing.dom.dist/workers',
	 * 		basis: true
	 * 	}
	 * });
	 */
	static init(options = {}) {
		Utils.#options = Utils.cloneObject(options, false);
	}

	static getOptions() {
		return Utils.#options;
	}

	/**
	 * 设置鼠标移动判断的阈值距离。
	 * @param {number} threshold 阈值距离(像素),默认值为2。
	 * @example
	 * THING.DOM.Utils.setPointerMoveThreshold(5);
	 * @public
	 */
	static setPointerMoveThreshold(threshold) {
		if (Utils.isNumber(threshold) && threshold >= 0) {
			_pointerMoveThreshold = threshold;
		}
	}

	/**
	 * 获取鼠标移动判断的阈值距离。
	 * @returns {number} 阈值距离(像素)。
	 * @example
	 * let threshold = THING.DOM.Utils.getPointerMoveThreshold();
	 * @public
	 */
	static getPointerMoveThreshold() {
		return _pointerMoveThreshold;
	}

	static parseCSSColor(value, defaultValue) {
		if (Utils.isNull(value)) {
			return defaultValue;
		}
		else if (Utils.isString(value) && value._startsWith('#')) {
			return value;
		}
		else {
			let color = Utils.parseColor(value, defaultValue);

			let hex = (color[0] * 255) << 16 ^ (color[1] * 255) << 8 ^ (color[2] * 255) << 0;
			return '#' + (('000000' + hex.toString(16)).slice(-6));
		}
	}

	static parseMouseEventType(ev, type) {
		let eventType = type || ev.type;

		switch (eventType) {
			case 'pointerdown':
				eventType = 'mousedown';
				break;

			case 'pointermove':
				eventType = 'mousemove';
				break;

			case 'pointerup':
				eventType = 'mouseup';
				break;

			default:
				break;
		}

		return eventType;
	}

	static buildKeyboardEvent(ev) {
		// Because the keydown/up event only return uppercase code, so we need to fix it
		let keyCode;
		if (ev.key >= 'a' && ev.key <= 'z') {
			keyCode = Utils.atoi(ev.key);
		}
		else {
			keyCode = ev.keyCode || ev.which || ev.charCode;
		}

		return {
			"type": ev.type,
			"keyCode": keyCode, // We can use THING.KeyType.XXX to check it
			"code": ev.code,
			"location": ev.location,
			"altKey": ev.altKey,
			"ctrlKey": ev.ctrlKey,
			"shiftKey": ev.shiftKey,
		};
	}

	static buildMouseEvent(domElement, ev, type, mousePosition) {
		let _mousePosition = mousePosition || Utils.getMousePosition(domElement, ev);

		let mouseEvent = {
			type: Utils.parseMouseEventType(ev, type),
			x: _mousePosition.x,
			y: _mousePosition.y,
			button: ev.button || 0,
			buttons: ev.buttons || 0,
			altKey: ev.altKey,
			ctrlKey: ev.ctrlKey,
			shiftKey: ev.shiftKey,
		};

		if (Utils.isValid(ev.pointerId)) {
			mouseEvent.pointerId = ev.pointerId;
		}

		if (Utils.isValid(ev.pointerType)) {
			mouseEvent.pointerType = ev.pointerType;
		}

		return mouseEvent;
	}

	static buildTouchEvent(domElement, ev) {
		let createTouchEvent = function (domElement, touch) {
			let mousePosition = Utils.getMousePosition(domElement, touch);
			return {
				identifier: touch.identifier,
				force: touch.force,
				x: mousePosition.x,
				y: mousePosition.y,
				altKey: ev.altKey,
				ctrlKey: ev.ctrlKey,
				shiftKey: ev.shiftKey,
			};
		}

		let touchEvent = {
			type: ev.type,
			altKey: ev.altKey,
			ctrlKey: ev.ctrlKey,
			shiftKey: ev.shiftKey,
		};

		touchEvent['changedTouches'] = [];
		for (let i = 0; i < ev.changedTouches.length; i++) {
			let touch = ev.changedTouches[i];
			touchEvent['changedTouches'].push(createTouchEvent(domElement, touch));
		}

		touchEvent['targetTouches'] = [];
		for (let i = 0; i < ev.targetTouches.length; i++) {
			let touch = ev.targetTouches[i];
			touchEvent['targetTouches'].push(createTouchEvent(domElement, touch));
		}

		touchEvent['touches'] = [];
		for (let i = 0; i < ev.touches.length; i++) {
			let touch = ev.touches[i];
			touchEvent['touches'].push(createTouchEvent(domElement, touch));
		}

		return touchEvent;
	}

	static getMousePosition(domElement, ev, offset) {
		let x, y;

		// Thanks for sharing code: http://www.jacklmoore.com/notes/mouse-position/
		if (domElement) {
			let style = domElement.currentStyle || window.getComputedStyle(domElement, null);

			let borderLeftWidth = offset ? offset[0] : parseInt(style['borderLeftWidth'], 10);
			let borderTopWidth = offset ? offset[1] : parseInt(style['borderTopWidth'], 10);
			let rect = domElement.getBoundingClientRect();

			x = ev.clientX - borderLeftWidth - rect.left;
			y = ev.clientY - borderTopWidth - rect.top;
		}
		else {
			if (ev.layerX || ev.layerX == 0) {
				x = ev.layerX;
				y = ev.layerY;
			}
			else if (ev.offsetX || ev.offsetX == 0) { // Opera
				x = ev.offsetX;
				y = ev.offsetY;
			}
			else if (ev.clientX || ev.clientX == 0) {
				x = ev.clientX;
				y = ev.clientY;
			}
		}

		return { x: Math.round(x), y: Math.round(y) };
	}

	/**
	 * Start performance measurement.
	 * @param {string} name The measure name.
	 * @example
	 * THING.DOM.Utils.startMark('exampleMeasure');
	 */
	static startMark(name) {
		if (!isPerformanceSupported) {
			return;
		}

		window.performance.mark(`mark_${name}_start`);
	}

	/**
	 * End performance measurement.
	 * @param {string} name The measure name.
	 * @example
	 * THING.DOM.Utils.endMark('exampleMeasure');
	 */
	static endMark(name) {
		if (!isPerformanceSupported) {
			return;
		}

		// End Performance Mark
		window.performance.mark(`mark_${name}_end`);
		return _performanceMeasure(name);
	}

	static normalizeURL(url) {
		// 使用 BaseUtils.resolveURL 处理 protocol-relative URL
		url = BaseUtils.resolveURL(url);

		if (url === undefined) {
			url = '';
		}

		if (url._startsWith('http')) {
			return url;
		}

		if (url._startsWith('file://')) {
			return url;
		}

		if (url._startsWith('blob')) {
			return url;
		}

		if (url._startsWith('/')) {
			return window.location.origin + url;
		}

		if (url._startsWith('./')) {
			url = url.substring(2);
		}

		let origin = window.location.origin;
		let pathname = window.location.pathname;

		return _getPath(origin + pathname) + '/' + url;
	}

	static getUserAngent() {
		if (!window["navigator"]) {
			return '';
		}

		let ua = navigator.userAgent.toLowerCase();
		return ua;
	}

	/**
	 * Check whether the given value is a DOM element.
	 * @param {*} value The value to check.
	 * @return {boolean} True if the value is a DOM element, false otherwise.
	 * @example
	 * const div = document.createElement('div');
	 * console.log(THING.DOM.Utils.isDom(div)); // Output: true
	 * console.log(THING.DOM.Utils.isDom('div')); // Output: false
	 */
	static isDom(value) {
		return typeof value === 'object'
			&& typeof value.nodeType === 'number'
			&& typeof value.ownerDocument === 'object';
	}

	static isMobileDevice() {
		let ua = this.getUserAngent();
		return (ua.indexOf('mobile') != -1 || ua.indexOf('android') != -1 || ua.indexOf('iphone') != -1);
	}

	static loadImage(url, onLoad, onError) {
		let image = document.createElementNS('http://www.w3.org/1999/xhtml', 'img');
		image.crossOrigin = 'anonymous';

		function onImageLoad() {
			image.removeEventListener('load', onImageLoad, false);
			image.removeEventListener('error', onImageError, false);

			if (onLoad) {
				onLoad(this);
			}
		}

		function onImageError(event) {
			image.removeEventListener('load', onImageLoad, false);
			image.removeEventListener('error', onImageError, false);

			if (onError) {
				onError(event);
			}
		}

		image.addEventListener('load', onImageLoad, false);
		image.addEventListener('error', onImageError, false);

		image.src = url;

		return image;
	}

	// Code taken from MatthewCrumley (https://stackoverflow.com/a/934925/298479)
	static imageToBase64(img) {
		// Create an empty canvas element
		let canvas = document.createElement("canvas");
		canvas.width = img.width;
		canvas.height = img.height;

		// Copy the image contents to the canvas
		let ctx = canvas.getContext("2d");
		ctx.drawImage(img, 0, 0);

		// Get the data-URL formatted image
		// Firefox supports PNG and JPEG. You could check img.src to guess the
		// original format, but be aware the using "image/jpg" will re-encode the image.
		let dataURL = canvas.toDataURL("image/png");

		return dataURL;
	}

	static base64ToImage(src) {
		let image = new Image();
		image.src = src;

		return image;
	}

	static loadBitmapImageFromBase64Async(src) {
		function dataURItoBlob(dataURI) {
			let mimeString = dataURI.split(',')[0].split(':')[1].split(';')[0];
			let byteString = atob(dataURI.split(',')[1]);
			let arrayBuffer = new ArrayBuffer(byteString.length);
			let intArray = new Uint8Array(arrayBuffer);

			for (let i = 0; i < byteString.length; i++) {
				intArray[i] = byteString.charCodeAt(i);
			}

			return new Blob([intArray], { type: mimeString });
		}

		let blob = dataURItoBlob(src);
		return createImageBitmap(blob, { imageOrientation: 'flipY' });
	}

	static loadBitmapImageFromUrlAsync(url, options = _defaultOptions) {
		return new Promise(async (resolve, reject) => {
			try {
				url = Utils.normalizeURL(url);

				const res = await fetch(url, { mode: 'cors' });
				const blob = await res.blob();
				if (!res.ok) {
					reject('Image loading failed: ' + url);
					return;
				}

				let imageOptions = {
					premultiplyAlpha: 'none',
					colorSpaceConversion: 'none'
				};

				if (options['flipY']) {
					imageOptions.imageOrientation = 'flipY';
				}

				if (options['premultiplyAlpha']) {
					imageOptions.premultiplyAlpha = 'premultiply';
				}

				const bitmap = await createImageBitmap(blob, imageOptions);

				resolve(bitmap);
			}
			catch (ex) {
				reject(ex);
			}
		});
	}

	static canvasToDataURL(canvas, format, quality) {
		return canvas.toDataURL(format || 'image/jpeg', quality || 1.0);
	}

	static dataURLToCanvas(dataurl, cb) {
		let canvas = document.createElement('CANVAS');
		let ctx = canvas.getContext('2d');
		let img = new Image();
		img.onload = function () {
			canvas.width = img.width;
			canvas.height = img.height;
			ctx.drawImage(img, 0, 0);
			cb(canvas);
		};
		img.src = dataurl;
	}

	static imageToCanvas(src, cb) {
		let canvas = document.createElement('CANVAS');
		let ctx = canvas.getContext('2d');
		let img = new Image();
		img.src = src;
		img.onload = function () {
			canvas.width = img.width;
			canvas.height = img.height;
			ctx.drawImage(img, 0, 0);
			cb(canvas);
		};
	}

	static canvasToImage(canvas) {
		let img = new Image();
		img.src = canvas.toDataURL('image/jpeg', 1.0);
		return img;
	}

	static fileOrBlobToDataURL(obj, cb) {
		let a = new FileReader();
		a.readAsDataURL(obj);
		a.onload = function (e) {
			cb(e.target.result);
		};
	}

	static dataURLToBlob(dataurl) {
		let arr = dataurl.split(',');
		let mime = arr[0].match(/:(.*?);/)[1];
		let bstr = atob(arr[1]);
		let n = bstr.length;
		let u8arr = new Uint8Array(n);
		while (n--) {
			u8arr[n] = bstr.charCodeAt(n);
		}
		return new Blob([u8arr], { type: mime });
	}

	static blobToImage(blob, cb) {
		Utils.fileOrBlobToDataURL(blob, function (dataurl) {
			let img = new Image();
			img.src = dataurl;
			cb(img);
		});
	}

	static imageToBlob(src, cb) {
		Utils.imageToCanvas(src, function (canvas) {
			cb(Utils.dataURLToBlob(Utils.canvasToDataURL(canvas)));
		});
	}

	static blobToCanvas(blob, cb) {
		Utils.fileOrBlobToDataURL(blob, function (dataurl) {
			Utils.dataURLToCanvas(dataurl, cb);
		});
	}

	static canvasToBlob(canvas, cb) {
		cb(Utils.dataURLToBlob(Utils.canvasToDataURL(canvas)));
	}

	static imageToDataURL(src, cb) {
		Utils.imageToCanvas(src, function (canvas) {
			cb(Utils.canvasToDataURL(canvas));
		});
	}

	static dataURLToImage(dataurl) {
		let img = new Image();
		img.src = dataurl;
		return img;
	}

	static dataToImage(type, data) {
		let image = new Image();
		image.src = URL.createObjectURL(
			new Blob([data.buffer], { type: `image/${type}` })
		);

		return image;
	}

	/**
	 * Dynamically evaluate JavaScript-as-string in the browser.
	 * @param {string} js The JS code.
	 * @param {object | Array<string>} options The options.
	 * @param {boolean} options.es6Mode True indicates load code in es6 mode.
	 * @param {string} options.sourceURL The source URL in sync mode.
	 * @param {Function} options.onLoad The load finished callback function.
	 * @param {Function} options.onError The error occurred callback function.
	 * @example
	 * // Example usage in es6 mode
	 * THING.DOM.Utils.loadJSCode('console.log("Hello World");', {
	 *   es6Mode: true,
	 *   sourceURL: 'example.js',
	 *   onLoad: function() {
	 *     console.log('Code loaded successfully.');
	 *   },
	 *   onError: function(error) {
	 *     console.error('Error loading code:', error);
	 *   }
	 * });
	 * // Example usage in common mode
	 * THING.DOM.Utils.loadJSCode('console.log("Hello World");', {
	 *   onLoad: function() {
	 *     console.log('Code loaded successfully.');
	 *   },
	 *   onError: function() {
	 *     console.error('Error loading code.');
	 *   }
	 * });
	 */
	static loadJSCode(js, options = {}) {
		try {
			let es6Mode = options['es6Mode'];
			let load = options['onLoad'] || options['load'];
			let error = options['onError'] || options['error'];

			let script = document.createElement('script');

			// Load in es6 mode
			if (es6Mode) {
				// Add file key in order to find much easiler
				let sourceURL = options['sourceURL'] || MathUtils.generateUUID();
				sourceURL += '_' + _codeId++;

				let code = `//# sourceURL=${sourceURL}\n`;
				code += `try { \n`;
				code += js;

				// Link load function in order to invoke load callback function
				code += `\n\n__loadCodeComplete('${sourceURL}');\n`;
				code += `} \n catch (ex) { \n __catchError('${sourceURL}', ex); }`;

				// Use es6 mode to load script
				script.type = 'module';
				script.text = code;
				document.head.appendChild(script);

				// Wait for code loaded, check every 5ms
				let timer = setInterval(() => {
					let codeInfo = _codeMap[sourceURL];
					if (codeInfo) {
						clearInterval(timer);

						if (codeInfo.result) {
							if (load) {
								load();
							}
						}
						else {
							if (error) {
								error(codeInfo.error);
							}
						}

						delete _codeMap[sourceURL];
					}
				}, 5);
			}
			// Load in common mode
			else {
				script.onload = function () { // chrome ff
					if (load) {
						load();
					}
				};
				script.onreadystatechange = function () { // ie
					if (this.readyState == 'loaded' || this.readyState == 'complete') {
						if (load) {
							load();
						}
					}
				};
				script.onerror = function () {
					if (error) {
						error();
					}
				}

				script.type = 'text/javascript';
				script.src = 'data:text/javascript;charset=utf-8,' + escape(js);
				document.head.appendChild(script);
			}
		}
		catch (ex) {
			console.error(ex);
		}
	}

	/**
	 * Loads a file or an array of files.
	 *
	 * @param {(string | Array<string>)} resource The URL(s) of the file(s) to load.
	 * @param {(object | Array<string>)} options The options for loading the file(s).
	 * @param {Function} options.onLoad The callback function to call when the file(s) have finished loading.
	 * @param {Function} options.onError The callback function to call if an error occurs during loading.
	 * @param {boolean} [options.cache=true] Indicates whether to use the browser cache for loading.
	 * @param {boolean} [options.inOrder=true] Indicates whether to load files in the order they are specified.
	 * @returns {Promise<any>} A promise that resolves when all files have been loaded or an error occurs.
	 * @example
	 * THING.DOM.Utils.loadFile('path/to/file.js', {
	 *   onLoad: function() {
	 *     console.log('File loaded successfully.');
	 *   },
	 *   onError: function(error) {
	 *     console.error('Error loading file:', error);
	 *   }
	 * });
	 */
	static loadFile(resource, options = {}) {
		let cache = Utils.parseValue(options['cache'], true);
		let inOrder = Utils.parseValue(options['inOrder'], true);

		// Get callback functions
		let load = Utils.parseValue(options['onLoad'], options['load']);
		let error = Utils.parseValue(options['onError'], options['error']);

		// Use param as URLs
		let urls = [];
		if (Utils.isArray(resource)) {
			urls = resource;
		}
		// Use param as single URL
		else if (Utils.isString(resource)) {
			urls = [resource];
		}

		if (!urls.length) {
			return;
		}

		// Load one by one in order
		if (inOrder) {
			let _urls = urls.concat();

			let _loadFile = (ev) => {
				// Finished all files loading
				if (load && !_urls.length) {
					load(ev);
				}
				// Go on
				else {
					let url = _urls.shift();
					_asyncLoadFile(url, cache, _loadFile, error);
				}
			}

			let url = _urls.shift();
			_asyncLoadFile(url, cache, _loadFile, error);
		}
		// Load all in batch mode
		else {
			let loadedNumber = 0;

			let _loadFile = (ev) => {
				loadedNumber++;

				// Finished all files loading
				if (loadedNumber == urls.length) {
					if (load) {
						load(ev);
					}
				}
			}

			urls.forEach(url => {
				_asyncLoadFile(url, cache, _loadFile, error);
			});
		}
	}

	/**
	 * Load file in async mode.
	 * @param {string | Array<string>} resource The file(s) resource url.
	 * @param {object | Array<string>} options The options.
	 * @param {Function} options.onLoad The load finished callback function.
	 * @param {Function} options.onError The error occurred callback function.
	 * @param {boolean} [options.cache=true] True indicates try to use cache.
	 * @param {boolean} [options.inOrder=true] True indicates keep order to load files one by one.
	 * @example
	 * // Load a single file with default options
	 * THING.DOM.Utils.loadFileAsync('path/to/your/file.js');
	 *
	 * // Load multiple files in order with caching disabled
	 * THING.DOM.Utils.loadFileAsync(['file1.js', 'file2.js'], { inOrder: true, cache: false });
	 *
	 * // Load a single file with custom options
	 * THING.DOM.Utils.loadFileAsync('path/to/your/file.js', {
	 *   onLoad: function() {
	 *     console.log('File loaded successfully.');
	 *   },
	 *   onError: function() {
	 *     console.error('Error loading file.');
	 *   },
	 *   cache: false,
	 *   inOrder: false
	 * });
	 */
	static loadFileAsync(resource, options = {}) {
		return new Promise((resolve, reject) => {
			let load = Utils.parseValue(options['onLoad'], options['load']);
			let error = Utils.parseValue(options['onError'], options['error']);

			this.loadFile(resource, {
				onLoad: function () {
					resolve();

					if (load) {
						load();
					}
				},
				onError: function () {
					reject();

					if (error) {
						error();
					}
				}
			});
		});
	}

	static hasBitmapImageInterface() {
		return typeof createImageBitmap !== 'undefined'
			&& /Firefox/.test(navigator.userAgent) === false;
	}

	static getLoadedScripts() {
		if (document.getElementsByTagName) {
			const scriptTag = StringEncoder.toText("<@secret script>");
			const srcTag = StringEncoder.toText("<@secret src>");

			let scripts = document.getElementsByTagName(scriptTag);
			if (scripts.length) {
				let sources = [];
				for (let i = 0; i < scripts.length; i++) {
					let script = scripts[i];
					let src = document.querySelector ? script.src : script.getAttribute(srcTag);

					sources.push(src);
				}

				return sources;
			}
		}

		return [];
	}

	static getFileSize(url, callback) {
		const headMethod = StringEncoder.toText("<@secret HEAD>");
		const contentLengthKey = StringEncoder.toText("<@secret Content-Length>");

		let xhr = new XMLHttpRequest();
		xhr.open(headMethod, url, true);
		xhr.onreadystatechange = function () {
			if (this.readyState == this.DONE) {
				var size = Utils.parseValue(xhr.getResponseHeader(contentLengthKey), 0);
				callback(parseInt(size));
			}
		};

		xhr.send();
	}

	static getFileSizeAsync(url) {
		return new Promise((resolve, reject) => {
			Utils.getFileSize(url, (size) => {
				resolve(size);
			});
		});
	}

	/**
	 * Dynamically import a js module (es6)
	 * @param {string} url The file resource url.
	 * @param {object} options The options.
	 * @param {boolean} [options.cache=true] True indicates try to use cache.
	 * @returns {Promise<any>}
	 * @example
	 * THING.DOM.Utils.importScript('path/to/your/script.js', { cache: true })
	 *   .then(module => {
	 *     console.log(module);
	 *   })
	 *   .catch(error => {
	 *     console.error('Failed to import script:', error);
	 *   });
	 */
	static importScript(url, options) {
		if (Utils.isNull(options)) {
			options = {};
		}

		const useCache = Utils.parseBoolean(options['cache'], true);
		url = BaseUtils.resolveURL(url);

		if (!BaseUtils.isBlobUrl(url)) {
			url = Utils.normalizeURL(url);
		}

		if (!useCache) {
			url += '?' + Date.now()
		}

		let result = null;

		window.__importScriptCallback__ = (promise) => {
			result = promise;
		}

		var strCode = 'var promise = import("' + url + '"); window.__importScriptCallback__(promise);';
		Utils.eval(strCode);

		window.__importScriptCallback__ = null;
		return result;
	}

	static getCurrentScriptUrl() {
		let script = document.currentScript;
		if (!script) {
			return '';
		}

		let src = script.getAttribute("src");
		if (!src) {
			return '';
		}

		return src;
	}

	static isTouchDevice() {
		return Utils.hasPointerEvent;
	}

}

if (typeof matchMedia === 'function') {
	function monitorDeviceModeChange() {
		const mediaQuery = matchMedia('(pointer: coarse)');

		function handleChange(event) {
			Utils.hasPointerEvent = event.matches;
		}

		// 初始检查
		handleChange(mediaQuery);

		// 监听变化
		mediaQuery.addEventListener('change', handleChange);
	}

	// 调用该函数来开始监控
	monitorDeviceModeChange();
}

export { Utils };