var bricks = window.bricks || {};
bricks.bug = false;
bricks.sleep = function(seconds){
	var ms = seconds * 1000;
	return new Promise(resolve => setTimeout(resolve, ms));
}

bricks.timeDiff = function(startTime) {
  const now = Date.now();
  const diff = now - (startTime instanceof Date ? startTime.getTime() : startTime); // 毫秒差值

  const hours = String(Math.floor(diff / (1000 * 60 * 60))).padStart(2, "0");
  const minutes = String(Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60))).padStart(2, "0");
  const seconds = String(Math.floor((diff % (1000 * 60)) / 1000)).padStart(2, "0");
  const milliseconds = String(diff % 1000).padStart(3, "0");

  return `${hours}:${minutes}:${seconds}.${milliseconds}`;
}

bricks.escapeSpecialChars = function(s){
  return s
    .replace(/\\/g, '\\\\')  // escape backslashes
    .replace(/"/g, '\\"')   // escape double quotes
    // .replace(/'/g, '\\\'')   // escape single quotes
    .replace(/\n/g, '\\n')   // escape newlines
    .replace(/\r/g, '\\r')   // escape carriage returns
    .replace(/\t/g, '\\t')   // escape tabs
    .replace(/\f/g, '\\f')   // escape form feeds
    .replace(/\v/g, '\\v')   // escape vertical tabs
    .replace(/\0/g, '\\0');  // escape null bytes
}

/* 
We use ResizeObserver to implements dom object resize event
*/
bricks.resize_observer = new ResizeObserver(entries => {
	for (let entry of entries){
		const cr = entry.contentRect;
		const ele = entry.target;
		const w = ele.bricks_widget;
		// console.log('size=', cr, 'element=', ele, w);
		if (w){
			w.dispatch('element_resize', cr);
		}
	}
});

/* MutationObserver for add to DOM or remove from DOM
event: 
	domon: add to dom
	domoff: remove from dom
*/
bricks.dom_on_off_observer=new MutationObserver((mutations)=>{
	function handleRemoved(node) {
		if (node.nodeType !== 1) return; // 只处理元素节点
		if (node.bricks_widget) {
			// console.log('**** widget removed', node.bricks_widget);
			node.bricks_widget.dispatch('domoff');
		}
		// 遍历后代
		for (let child of node.querySelectorAll('*')) {
			if (child.bricks_widget) {
				// console.log('**** widget removed (descendant)', child.bricks_widget);
				child.bricks_widget.dispatch('domoff');
			}
		}
	}
	for (let m of mutations) {
		for (let n of m.removedNodes) {
			handleRemoved(n);
		}
		for (let n of m.addedNodes) {
		  	if (n.bricks_widget){
				var w = n.bricks_widget;
				w.dispatch('domon');
			}
		}
	}
});
bricks.dom_on_off_observer.observe(document.body, 
								{ childList: true, subtree: true });
function addParamsToUrl(url, params, widget) {
  const urlObj = new URL(url, window.baseURI); // 处理相对和绝对路径
  Object.keys(params).forEach(key => {
    urlObj.searchParams.set(key, params[key]);
  });
  return urlObj.toString();
}
function isString(value) {
  return typeof value === 'string' || value instanceof String;
}

function parseRGB(colorStr) {
  const match = colorStr.match(/^rgb\s*\(\s*(\d+),\s*(\d+),\s*(\d+)\s*\)$/);
  if (!match) return null;
  const [, r, g, b] = match.map(Number);
  return { r, g, b };
}

function parseRGBA(colorStr) {
  const match = colorStr.match(/^rgba?\s*\(\s*(\d+),\s*(\d+),\s*(\d+)(?:,\s*([\d.]+))?\s*\)$/);
  if (!match) return null;
  const [, r, g, b, a] = match;
  return { r: +r, g: +g, b: +b, a: a !== undefined ? +a : 1 };
}

async function streamResponseJson(response, onJson) {
	const reader = response.body.getReader();
	const decoder = new TextDecoder("utf-8");
	let buffer = "";
	while (true) {
		const { value, done } = await reader.read();
		if (done) break;
		buffer += decoder.decode(value, { stream: true });
		// 按换行切分 NDJSON
		let lines = buffer.split("\n");
		buffer = lines.pop(); // 可能是不完整的一行，留到下一轮
		for (const line of lines) {
			if (line.trim()) {
				try {
					const json = JSON.parse(line);
					onJson(json); // 👈 回调处理每个 JSON 对象
				} catch (err) {
					console.warn("Failed to parse JSON line:", line);
				}
			}
		}
	}
	// 处理最后残留的一行
	if (buffer.trim()) {
		try {
			const json = JSON.parse(buffer);
			onJson(json);
		} catch (err) {
			console.warn("Failed to parse trailing JSON line:", buffer);
		}
	}
}

function base64_to_url(base64, mimeType = "audio/wav") {
	const binary = atob(base64); // 解码 Base64 成 binary 字符串
	const len = binary.length;
	const bytes = new Uint8Array(len);

	for (let i = 0; i < len; i++) {
		bytes[i] = binary.charCodeAt(i);
	}

	const blob = new Blob([bytes], { type: mimeType });
	const url = URL.createObjectURL(blob);
	return url;
}

function blobToBase64(blob) {
  return new Promise((resolve, reject) => {
    const reader = new FileReader();

    reader.onloadend = () => {
      resolve(reader.result); // This will be a base64 string prefixed with "data:*/*;base64,"
    };

    reader.onerror = reject;

    reader.readAsDataURL(blob);
  });
}

bricks.formdata_copy = function(fd){
	var cfd = new FormData();

	// 遍历 originalFormData 中的所有条目并添加到 clonedFormData 中
	for (var pair of fd.entries()) {
		cfd.append(pair[0], pair[1]);
	}
	return cfd;
}

bricks.map = function(data_source, mapping, need_others){
	ret = {};
	Object.entries(data_source).forEach(([key, value]) => {
		if (mapping.hasOwnProperty(key)){
			ret[mapping[key]] = data_source[key];
		} else if (need_others){
			ret[key] = data_source[key];
		}
	});
	return ret;
}

bricks.relocate_by_eventpos = function(event, widget){
	var ex,ey;
	var x,y;
	var xsize = bricks.Body.dom_element.clientWidth;
	var ysize = bricks.Body.dom_element.clientHeight;
	ex = event.clientX;
	ey = event.clientY;
	var mxs = widget.dom_element.offsetWidth;
	var mys = widget.dom_element.offsetHeight;
	if (ex < (xsize / 2)) {
		x = ex + bricks.app.charsize;
	} else {
		x = ex - mxs - bricks.app.charsize;
	}
	if (ey < (ysize / 2)) {
		y = ey + bricks.app.charsize;
	} else {
		y = ey - mys - bricks.app.charsize;
	}
	widget.set_style('left', x + 'px');
	widget.set_style('top', y + 'px');
}

var formdata2object = function(formdata){
	let result = {};
    formdata.forEach((value, key) => {
        result[key] = value;
    });
    return result;
}

var inputdata2dic = function(data){
	try {
		var d = {}
		for (let k of data.keys()){
			var x = data.get(k);
			if (k == 'prompt'){
				x = bricks.escapeSpecialChars(x);
			}	
			d[k] = x;
		}
		return d;
	} catch (e){
		return data;
	}
}
bricks.delete_null_values = function(obj) {
	for (let key in obj) {
		if (obj[key] === null) {
			delete obj[key];
		}
	}
	return obj;
}

bricks.is_empty = function(obj){
	if (obj === null) return true;
	return JSON.stringify(obj) === '{}';
}
bricks.serverdebug = async function(message){
	var jc = new bricks.HttpJson();
	await jc.post(url='/debug', {params:{
		message:message
	}});
	return;
}
bricks.debug = function(...args){
	if (! bricks.bug){
		return;
	}
	if (bricks.bug == 'server'){
		var message = args.join(" ");
		f = bricks.serverdebug.bind(null, message);
		schedule_once(f, 0.1);
		return;
	}
	var callInfo;
	try {
		throw new Error();
	} catch (e) {
		try {
			callInfo = e.stack.split('\n')[2].trim();
		} catch (e1) {
			callInfo = e.toString();
		}
	}
	console.log(callInfo, ...args);
}

bricks.is_mobile = function(){
		var userAgent = navigator.userAgent;
	if (/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(userAgent)) {
		return true;
	}
	if (('ontouchstart' in window) || (navigator.maxTouchPoints > 0) || (navigator.msMaxTouchPoints > 0)) {
		return true;
	}
	if (window.innerWidth <= 768 && window.innerHeight <= 1024) {
		return true;
	}
	return false;
}

class _TypeIcons {
	constructor(){
		this.kv = {}
	}
	get(n, defaultvalue){
		return objget(this.kv, n, defaultvalue);
	}
	register(n, icon){
		this.kv[n] = icon;
	}
}

TypeIcons = new _TypeIcons();

/**
 * Current Script Path
 *
 * Get the dir path to the currently executing script file
 * which is always the last one in the scripts array with
 * an [src] attr
 */
var currentScriptPath = function () {
	var currentScript;
	if (document.currentScript){
		currentScript = document.currentScript.src;
	} else {
		bricks.debug('has not currentScriot');
		var scripts = document.querySelectorAll( 'script[src]' );
		if (scripts.length < 1){
			return null;
		}
		currentScript = scripts[ scripts.length - 1 ].src;
	}
		var currentScriptChunks = currentScript.split( '/' );
		var currentScriptFile = currentScriptChunks[ currentScriptChunks.length - 1 ];
		return currentScript.replace( currentScriptFile, '' );
}

bricks.path = currentScriptPath();

var bricks_resource = function(name){
	return bricks.path + name;
}

/**
 * Finds all elements in the entire page matching `selector`, even if they are in shadowRoots.
 * Just like `querySelectorAll`, but automatically expand on all child `shadowRoot` elements.
 * @see https://stackoverflow.com/a/71692555/2228771
 */
function querySelectorAllShadows(selector, el = document.body) {
	// recurse on childShadows
	const childShadows = Array.from(el.querySelectorAll('*')).
		map(el => el.shadowRoot).filter(Boolean);

	bricks.debug('[querySelectorAllShadows]', selector, el, `(${childShadows.length} shadowRoots)`);

	const childResults = childShadows.map(child => querySelectorAllShadows(selector, child));
	
	// fuse all results into singular, flat array
	const result = Array.from(el.querySelectorAll(selector));
	return result.concat(childResults).flat();
}

var schedule_once = function(f, t){
	/* f: function
		t:time in second unit
	*/
	t = t * 1000
	return window.setTimeout(f, t);
}

var schedule_interval = function(f, t){
	t = t * 1000
	return window.setInterval(f, t);
}

var debug = function(){
	bricks.debug(...arguments);
}

var import_cache = new Map()

var import_css = async function(url){
	if (objget(import_cache, url)===1) return;
	var result = await (bricks.tget(url));
	debug('import_css():tget() return', result); 
	var s = document.createElement('style');
	s.setAttribute('type', 'text/javascript');
	s.innerHTML = result;
	document.getElementsByTagName("head")[0].appendChild(s);
	import_cache.set(url, 1);
}

var import_js = async function(url){
	if (objget(import_cache, url)===1) return;
	// var result = await (bricks.tget(url));
	// debug('import_js():tget() return', url, result); 
	var s = document.createElement('script');
	s.setAttribute('type', 'text/javascript');
	s.src=url;
	// s.innerHTML = result;
	document.body.appendChild(s);
	import_cache.set(url, 1);

}

bricks.extend = function(d, s){
	for (var p in s){
		if (! s.hasOwnProperty(p)){
			continue;
		}
		if (d[p] && (typeof(d[p]) == 'object') 
				&& (d[p].toString() == '[object Object]') && s[p]){
			bricks.extend(d[p], s[p]);
		} else {
			d[p] = s[p];
		}
	}
	return d;
}

var objget = function(obj, key, defval){
	if (obj.hasOwnProperty(key)){
		return obj[key];
	}
	return defval;
}

bricks.obj_fmtstr = function(obj, fmt){
	/* fmt like 
		'my name is ${name}, ${age=}'
		'${name:}, ${age=}'
	*/
	var s = fmt;
	s =	s.replace(/\${(\w+)([:=]*)}/g, (k, key, op) => {
		if (obj.hasOwnProperty(key)){
			if (op == ''){
				return obj[key];
			} else {
				return key + op + obj[key];
			}
		}
		return ''
	})
	return s;
}

var archor_at = function(archor){
	/* archor maybe one of the:
	"tl", "tc", "tr",
	"cl", "cc", "cr",
	"bl", "bc", "br"
	*/
	if (! archor)
		archor = 'cc';
	var v = archor[0];
	var h = archor[1];
	var y = "0%";
	switch(v){
		case 't':
			y = "0%";
			break;
		case 'b':
			y = '100%';
			break;
		case 'c':
			y = '50%';
			break;
		default:
			y = '50%';
			break;
	}
	var x = "0%";
	switch(h){
		case 'l':
			x = "0%";
			break;
		case 'r':
			x = '100%';
			break;
		case 'c':
			x = '50%';
			break;
		default:
			x = '50%';
			break;
	}
	return {
		x:x,
		y:y,
		top:y,
		left:x
	}
}

var archorize = function(ele,archor){
	var lt = archor_at(archor);
	ele.style.top = lt.top;
	ele.style.left = lt.left;
	var o = {
		'x':lt.x,
		'y':lt.y
	}
	var tsf = bricks.obj_fmtstr(o, 'translateY(-${y}) translateX(-${x})');
	ele.style.transform = tsf;
	ele.style.position = "absolute";
}

Array.prototype.insert = function ( index, ...items ) {
		this.splice( index, 0, ...items );
};

Array.prototype.remove = function(item){
	var idx = this.indexOf(item);
	if (idx >= 0){
		this.splice(idx, 1);
	}
	return this;
}

function removeArrayItems(array, itemsToRemove) {
  return array.filter(item => !itemsToRemove.includes(item));
}

bricks.absurl = function(url, widget){
	if (url.startsWith('http://') || url.startsWith('https://')){
		return url;
	}
	var base_uri = widget.baseURI;
	if (!base_uri){
		base_uri = bricks.Body.baseURI;
	}
	if (url.startsWith('/')){
		base_uri = bricks.Body.baseURI;
		url = url.substring(1);
	}
	paths = base_uri.split('/');
	delete paths[paths.length - 1];
	var ret_url = paths.join('/') + url;
	return ret_url;
}

var debug = function(...args){
	bricks.debug(...args);
}

var convert2int = function(s){
	if (typeof(s) == 'number') return s;
	var s1 = s.match(/\d+/);
	return parseInt(s1[0]);
}

function setCookie(name,value,days) {
		var expires = "";
		if (days) {
				var date = new Date();
				date.setTime(date.getTime() + (days*24*60*60*1000));
				expires = "; expires=" + date.toUTCString();
		}
		document.cookie = name + "=" + (value || "")	+ expires + "; path=/";
}
function getCookie(name) {
		var nameEQ = name + "=";
		var ca = document.cookie.split(';');
		for(var i=0;i < ca.length;i++) {
				var c = ca[i];
				while (c.charAt(0)==' ') c = c.substring(1,c.length);
				if (c.indexOf(nameEQ) == 0) return c.substring(nameEQ.length,c.length);
		}
		return null;
}
function eraseCookie(name) {	 
		document.cookie = name +'=; Path=/; Expires=Thu, 01 Jan 1970 00:00:01 GMT;';
}

var set_max_height = function(w1, w2){
	var v1 = w1.dom_element.offsetHeight;
	var v2 = w2.dom_element.offsetHeight;
	var v = v1 - v2;
	if (v < 0){
		w1.set_height(w2.dom_element.offsetHeight);
	} else if (v > 0) {
		w2.set_height(w1.dom_element.offsetHeight);
	}
}
var objcopy = function(obj){
	var s = JSON.stringify(obj);
	return JSON.parse(s);
}

bricks.set_stream_source = async function(target, url, params){
	var widget;
	if (typeof target == typeof ''){
		widget = bricks.getWidgetById(target);
	} else if (target instanceof bricks.JsWidget){
		widget = target;
	} else {
		widget = await bricks.widgetBuild(target);
	}
	if (! widget){
		bricks.debug('playResponseAudio():', target, 'can not found or build a widget');
		return;
	}
	const mediaSource = new MediaSource();
	mediaSource.addEventListener('sourceopen', handleSourceOpen);
	widget.set_url(URL.createObjectURL(mediaSource));
	function handleSourceOpen(){
		const sourceBuffer = mediaSource.addSourceBuffer('audio/wav; codecs=1');
		var ht = new bricks.HttpText();
		ht.bricks_fetch(url, {params:params})
			.then(response => response.body)
			.then(body => {
				const reader = body.getReader();
				const read = () => {
					reader.read().then(({ done, value }) => {
						if (done) {
							mediaSource.endOfStream();
							return;
						}
						sourceBuffer.appendBuffer(value);
						read();
					}).catch(error => {
						console.error('Error reading audio stream:', error);
					});
				};
				read();
			});
	}
}

bricks.playResponseAudio = async function(response, target){
	var widget = null;
	if (response.status != 200){
		bricks.debug('playResponseAudio(): response.status != 200', response.status); 
		return;
	}
	if (typeof target == typeof ''){
		widget = bricks.getWidgetById(target);
	} else {
		widget = bricks.widgetBuild(target);
	}
	if (! widget){
		bricks.debug('playResponseAudio():', target, 'can not found or build a widget');
		return;
	}
		const blob = await response.blob();
		const url = URL.createObjectURL(blob);
	widget.set_url(url);
	widget.play();
}

bricks.widgetBuildWithData = async function(desc_tmpl, from_widget, data){
	if (!desc_tmpl){
		bricks.debug('bricks.widgetBuildWithData():data=', data, 'desc_tmpl=', desc_tmpl);
	}
	var s = bricks.obj_fmtstr(data, desc_tmpl);
	var desc = JSON.parse(s);
	var w = await bricks.widgetBuild(desc, from_widget);
	if (! w){
		bricks.debug(desc, 'widgetBuild() failed...........');
		return;
	}
	w.row_data = data;
	return w;
}

bricks.Observable = class {
	constructor(owner, name, v){
		this.owner = owner;
		this.name = name;
		this.value = v;
	}
	set(v){
		var ov = this.value;
		this.value = v;
		if (this.value != ov){
			this.owner.dispatch(this.name, v);
		}
	}
	get(){
		return this.v;
	}
}

bricks.Queue = class {
	constructor() {
		this.items = [];
		this._done = false;
	}

	// 添加元素到队列尾部
	enqueue(element) {
		this.items.push(element);
	}
	done(){
		this._done = true;
	}
	is_done(){
		return this._done;
	}

	// 移除队列的第一个元素并返回
	dequeue() {
		if (this.isEmpty()) {
			return null;
		}
		return this.items.shift();
	}

	// 查看队列的第一个元素
	peek() {
		if (this.isEmpty()) {
			return null;
		}
		return this.items[0];
	}

	// 检查队列是否为空
	isEmpty() {
		return this.items.length === 0;
	}

	// 获取队列的大小
	size() {
		return this.items.length;
	}

	// 清空队列
	clear() {
		this.items = [];
	}

	// 打印队列元素
	print() {
		console.log(this.items.toString());
	}
}

function blobToBase64(blob) {
  return new Promise((resolve, reject) => {
    const reader = new FileReader();
    reader.onloadend = function() {
      resolve(reader.result);
    };
    reader.onerror = reject;
    reader.readAsDataURL(blob);
  });
}
/* 
opts = {
	css:
	id
}
*/
bricks.dom_create = function(tag, opts){
	var e = document.createElement(tag);
	if (opts.css){
		var arr = css.split(' ');
		arr.forEach(c =>{
			e.classList.add(c);
		});
	}
	if (opts.id){
		e.id = opts.id;
	}
	return e;
}
bricks.element_from_html = function(html){
	var e = document.createElement('div');
	e.outerHTML = html;
	return e;
}
/*
// 使用队列
const queue = new Queue();
queue.enqueue(1);
queue.enqueue(2);
queue.enqueue(3);
queue.print(); // 输出: 1,2,3
console.log(queue.dequeue()); // 输出: 1
queue.print(); // 输出: 2,3
*/
