/**
 * @param {string} data 
 * @returns {Record<string, string | Record<string, string>>}
 * @private
 */
function parseINIString(data) {
	var regex = {
		section: /^\s*\[\s*([^\]]*)\s*\]\s*$/,
		param: /^\s*([^=]+?)\s*=\s*(.*?)\s*$/,
		comment: /^\s*;.*$/
	};
	/** @type {Record<string, string | Record<string, string>>} */
	var value = {};
	var lines = data.split(/[\r\n]+/);
	/** @type {string | null} */
	var section = null;
	lines.forEach(function (line) {
		if (regex.comment.test(line)) {
			return;
		} else if (regex.param.test(line)) {
			var match = line.match(regex.param);
			if (section) {
				// @ts-ignore (could refactor to store section as an object, and use match result instead of test)
				value[section][match[1]] = match[2];
			} else {
				// @ts-ignore (could refactor to use match result instead of test)
				value[match[1]] = match[2];
			}
		} else if (regex.section.test(line)) {
			var match = line.match(regex.section);
			// @ts-ignore (could refactor to use match result instead of test)
			value[match[1]] = {};
			// @ts-ignore (could refactor to use match result instead of test)
			section = match[1];
		} else if (line.length == 0 && section) {
			// REALLY?? An empty line resets the section?? Dubious!
			// What does Windows do?
			section = null;
		};
	});
	return value;
}

/**
 * @param {Record<string, string> | CSSStyleDeclaration} cssProperties
 * @returns {Record<string, string>}
 */
function renderThemeGraphics(cssProperties) {
	/**
	 * @param {string} propName 
	 * @returns {string}
	 */
	var getProp = (propName) => typeof cssProperties.getPropertyValue === "function" ? cssProperties.getPropertyValue(propName) : /** @type {Record<string, string>} */(cssProperties)[propName];

	var canvas = document.createElement("canvas");
	canvas.width = canvas.height = 2;
	var ctx = canvas.getContext("2d");
	if (!ctx) {
		console.error("Failed to get 2d context for canvas to render theme graphics");
		return {};
	}
	ctx.fillStyle = getProp("--ButtonFace");
	ctx.fillRect(0, 1, 1, 1);
	ctx.fillRect(1, 0, 1, 1);
	ctx.fillStyle = getProp("--ButtonHilight");
	ctx.fillRect(0, 0, 1, 1);
	ctx.fillRect(1, 1, 1, 1);
	var checker = `url("${canvas.toDataURL()}")`;

	var scrollbar_size = parseInt(getProp("--scrollbar-size"));
	if (!isFinite(scrollbar_size)) {
		scrollbar_size = 13;
	}
	var scrollbar_button_inner_size = scrollbar_size - 4;

	// I don't know the exact formula, so approximate and special-case it for now
	// (It may very well *be* special cased, tho)
	var arrow_size = Math.floor(0.3 * scrollbar_size);
	if (scrollbar_size < 16 && scrollbar_size > 13) arrow_size -= 1;

	var arrow_width = arrow_size * 2 - 1;

	var arrow_canvas = document.createElement("canvas");
	var arrow_ctx = arrow_canvas.getContext("2d");
	if (!arrow_ctx) {
		console.error("Failed to get 2d context for canvas to render arrow graphics for theme");
		return {};
	}
	arrow_canvas.width = arrow_width;
	arrow_canvas.height = arrow_size;
	arrow_ctx.fillStyle = "white";
	for (let y = 0; y < arrow_size; y += 1) {
		for (let x = y; x < arrow_width - y; x += 1) {
			arrow_ctx.fillRect(x, y, 1, 1);
		}
	}

	canvas.width = scrollbar_button_inner_size * 4;
	canvas.height = scrollbar_button_inner_size;
	let i = 0;
	for (let horizontal = 0; horizontal < 2; horizontal += 1) {
		for (let decrement = 0; decrement < 2; decrement += 1) {
			ctx.save();
			ctx.translate(i * scrollbar_button_inner_size, 0);
			ctx.translate(scrollbar_button_inner_size / 2, scrollbar_button_inner_size / 2);
			// ctx.rotate(i * Math.PI / 2);
			if (horizontal) {
				ctx.rotate(-Math.PI / 2);
			}
			if (decrement) {
				ctx.scale(1, -1);
			}
			ctx.translate(-scrollbar_button_inner_size / 2, -scrollbar_button_inner_size / 2);
			ctx.drawImage(arrow_canvas, ~~(scrollbar_button_inner_size / 2 - arrow_width / 2), ~~(scrollbar_button_inner_size / 2 - arrow_size / 2));
			ctx.restore();
			i += 1;
		}
	}

	ctx.save();
	ctx.globalCompositeOperation = "source-in";
	ctx.fillStyle = getProp("--ButtonText");
	ctx.fillRect(0, 0, canvas.width, canvas.height);
	var scrollbar_arrows_ButtonText = `url("${canvas.toDataURL()}")`;
	ctx.fillStyle = getProp("--GrayText");
	ctx.fillRect(0, 0, canvas.width, canvas.height);
	var scrollbar_arrows_GrayText = `url("${canvas.toDataURL()}")`;
	ctx.fillStyle = getProp("--ButtonHilight");
	ctx.fillRect(0, 0, canvas.width, canvas.height);
	var scrollbar_arrows_ButtonHilight = `url("${canvas.toDataURL()}")`;
	// ctx.fillStyle = "red";
	// ctx.fillRect(0, 0, canvas.width, canvas.height);
	// canvas.style.background = "rgba(0, 0, 0, 0.2)";
	// $("h1").append(arrow_canvas).append(canvas);
	ctx.restore();

	/**
	 * @param {number} border_size 
	 * @param {string} svg_contents
	 * @returns {string}
	 */
	function border_image(border_size, svg_contents) {
		var base_size = 8;
		var border_size = border_size;
		var scale = 32;
		var slice_size = border_size * scale;
		var view_size = base_size * scale;
		// transform causes janky buggy garbage
		// var svg = `<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="${view_size}px" height="${view_size}px" viewBox="0 0 ${view_size} ${view_size}">
		// 	<g transform="scale(${scale})">
		// 		${svg_contents}
		// 	</g>
		// </svg>`;
		var svg = `<svg version="1.1" xmlns="http://www.w3.org/2000/svg" width="${view_size}px" height="${view_size}px" viewBox="0 0 ${view_size} ${view_size}">
			${svg_contents.replace(/(d|x|y|width|height|stroke-width)="[^"]*"/g, (attr) => attr.replace(/\d+/g, (n) => `${Number(n) * scale}`))}
		</svg>`;
		var url = `data:image/svg+xml,${encodeURIComponent(svg)}`;
		return `url("${url}") ${slice_size} / ${border_size}px`;
	}

	var button_active_border_image = border_image(2, `
		<path d="M0 0h8v8h-8v-8z" fill="${getProp("--ButtonDkShadow")}"/>
		<path d="M1 1h6v6h-6v-6z" fill="${getProp("--ButtonShadow")}"/>
		<path d="M2 2h4v4h-4v-4z" fill="${getProp("--ButtonFace")}"/>
	`);
	var button_default_active_border_image = border_image(2, `
		<path d="M0 0h8v8h-8v-8z" fill="${getProp("--ButtonDkShadow")}"/>
		<path d="M1 1h6v6h-6v-6z" fill="${getProp("--ButtonShadow")}"/>
		<path d="M2 2h4v4h-4v-4z" fill="${getProp("--ButtonFace")}"/>
		<rect x="0" y="0" width="8" height="8" stroke-width="2" stroke="${getProp("--WindowFrame")}" fill="none"/>
	`);
	// TODO: rename
	var button_normal_border_image = border_image(2, `
		<path d="M0 0h7v1h-6v6h-1v-7z" fill="${getProp("--ButtonHilight")}"/>
		<path d="M7 0h1v8h-8v-1h7v-7z" fill="${getProp("--ButtonDkShadow")}"/>
		<path d="M1 1h5v1h-4v4h-1v-5z" fill="${getProp("--ButtonLight")}"/>
		<path d="M6 1h1v6h-6v-1h5v-5z" fill="${getProp("--ButtonShadow")}"/>
		<path d="M2 2h4v4h-4v-4z" fill="${getProp("--ButtonFace")}"/>
	`);
	var inset_deep_border_image = border_image(2, `
		<path d="M0 0h7v1h-6v6h-1v-7z" fill="${getProp("--ButtonDkShadow")}"/>
		<path d="M7 0h1v8h-8v-1h7v-7z" fill="${getProp("--ButtonHilight")}"/>
		<path d="M1 1h5v1h-4v4h-1v-5z" fill="${getProp("--ButtonShadow")}"/>
		<path d="M6 1h1v6h-6v-1h5v-5z" fill="${getProp("--ButtonLight")}"/>
		<path d="M2 2h4v4h-4v-4z" fill="${getProp("--ButtonFace")}"/>
	`);
	var button_default_border_image = border_image(3, `
		<path d="M0 0h8v8h-8v-8z" fill="${getProp("--ButtonDkShadow")}"/>
		<path d="M1 1h5v1h-4v4h-1v-5z" fill="${getProp("--ButtonHilight")}"/>
		<path d="M2 2h3v1h-2v2h-1v-3z" fill="${getProp("--ButtonLight")}"/>
		<path d="M5 2h1v4h-4v-1h3v-3z" fill="${getProp("--ButtonShadow")}"/>
		<path d="M3 3h2v2h-2v-2z" fill="${getProp("--ButtonFace")}"/>
		<rect x="0" y="0" width="8" height="8" stroke-width="2" stroke="${getProp("--WindowFrame")}" fill="none"/>
	`);

	return {
		"--checker": checker,
		"--button-active-border-image": button_active_border_image,
		"--button-normal-border-image": button_normal_border_image,
		"--inset-deep-border-image": inset_deep_border_image,
		"--button-default-border-image": button_default_border_image,
		"--button-default-active-border-image": button_default_active_border_image,
		"--scrollbar-arrows-ButtonText": scrollbar_arrows_ButtonText,
		"--scrollbar-arrows-GrayText": scrollbar_arrows_GrayText,
		"--scrollbar-arrows-ButtonHilight": scrollbar_arrows_ButtonHilight,
		"--scrollbar-size": `${scrollbar_size}px`,
		"--scrollbar-button-inner-size": `${scrollbar_button_inner_size}px`,
	};
}

/**
 * @param {HTMLElement} element 
 * @returns {Record<string, string>}
 * @private
 */
function getThemeCSSProperties(element) {
	const keys = [
		"--checker",
		"--button-active-border-image",
		"--button-normal-border-image",
		"--inset-deep-border-image",
		"--button-default-border-image",
		"--button-default-active-border-image",
		"--scrollbar-arrows-ButtonText",
		"--scrollbar-arrows-GrayText",
		"--scrollbar-arrows-ButtonHilight",
		"--scrollbar-size",
		"--scrollbar-button-inner-size",
		"--ActiveBorder",
		"--ActiveTitle",
		"--AppWorkspace",
		"--Background",
		"--ButtonAlternateFace",
		"--ButtonDkShadow",
		"--ButtonFace",
		"--ButtonHilight",
		"--ButtonLight",
		"--ButtonShadow",
		"--ButtonText",
		"--GradientActiveTitle",
		"--GradientInactiveTitle",
		"--GrayText",
		"--Hilight",
		"--HilightText",
		"--HotTrackingColor",
		"--InactiveBorder",
		"--InactiveTitle",
		"--InactiveTitleText",
		"--InfoText",
		"--InfoWindow",
		"--Menu",
		"--MenuText",
		"--Scrollbar",
		"--TitleText",
		"--Window",
		"--WindowFrame",
		"--WindowText",
	];
	const style = window.getComputedStyle(element);
	/** @type {Record<string, string>} */
	const result = {};
	for (const key of keys) {
		result[key] = style.getPropertyValue(key);
	}
	return result;
}

/**
 * @param {HTMLElement} target 
 * @param {HTMLElement} source 
 * @private
 */
function inheritTheme(target, source) {
	applyCSSProperties(getThemeCSSProperties(source), { element: target, recurseIntoIframes: true });
}

// Parse NonClientMetrics
// https://docs.microsoft.com/en-us/windows/win32/controls/themesfileformat-overview?redirectedfrom=MSDN#metrics-section
// https://docs.microsoft.com/en-us/windows/win32/winprog/windows-data-types

// using https://github.com/toji/js-struct

// var NonClientMetricsStruct = Struct.create(
//     Struct.uint32("cbSize"),
//     Struct.int32("iBorderWidth"),
//     Struct.int32("iScrollWidth"),
//     Struct.int32("iScrollHeight"),
//     Struct.int32("iCaptionWidth"),
//     Struct.int32("iCaptionHeight"),
// 	// after that, it may be W or A
// //   LOGFONTW lfCaptionFont;
// //   int      iSmCaptionWidth;
// //   int      iSmCaptionHeight;
// //   LOGFONTW lfSmCaptionFont;
// //   int      iMenuWidth;
// //   int      iMenuHeight;
// //   LOGFONTW lfMenuFont;
// //   LOGFONTW lfStatusFont;
// //   LOGFONTW lfMessageFont;
// //   int      iPaddedBorderWidth;
// );

// var NonClientMetrics_buffer = new Uint8Array(NonClientMetrics_string.split(" ").map((str)=> parseInt(str))).buffer;

// NonClientMetricsStruct.readStructs(NonClientMetrics_buffer, 0, 1)[0];

/**
 * @param {string} themeIni 
 * @returns {Record<string, string> | undefined}
 */
function parseThemeFileString(themeIni) {
	// .theme is a renamed .ini text file
	// .themepack is a renamed .cab file, and parsing it as .ini seems to work well enough for the most part, as the .ini data appears in plain,
	// but it may not if compression is enabled for the .cab file
	var theme = parseINIString(themeIni);
	var colors = theme["Control Panel\\Colors"];
	if (!colors) {
		alert("Invalid theme file, no [Control Panel\\Colors] section");
		console.log(theme);
		return;
	}
	if (typeof colors !== "object") {
		alert("Invalid theme file, 'Control Panel\\Colors' is not a section");
		console.log(theme);
		return;
	}
	for (var k in colors) {
		// for .themepack file support, just ignore bad keys that were parsed
		if (k.match(/\W/)) {
			delete colors[k];
		} else {
			colors[k] = `rgb(${colors[k].split(" ").join(", ")})`;
		}
	}

	/** @type {Record<string, string>} */
	var cssProperties = {};
	for (var k in colors) {
		cssProperties[`--${k}`] = colors[k];
	}

	cssProperties = Object.assign(renderThemeGraphics(cssProperties), cssProperties);

	return cssProperties;
}

/**
 * @param {Record<string, string> | CSSStyleDeclaration} cssProperties
 * @param {{ element?: HTMLElement, recurseIntoIframes?: boolean } | HTMLElement} [options]
 */
function applyCSSProperties(cssProperties, options = {}) {
	// @TODO: clean up deprecated argument handling
	/** @type {HTMLElement} */
	let element;
	/** @type {boolean} */
	let recurseIntoIframes;
	if ("tagName" in options) {
		console.warn("deprecated: use options argument to applyCSSProperties, e.g. applyCSSProperties(cssProperties, { element: document.documentElement, recurseIntoIframes: true })");
		element = options;
		recurseIntoIframes = false;
	} else {
		({ element = document.documentElement, recurseIntoIframes = false } = options);
	}

	/**
	 * @param {string} propName 
	 * @returns {string}
	 */
	var getProp = (propName) => typeof cssProperties.getPropertyValue === "function" ? cssProperties.getPropertyValue(propName) : /** @type {Record<string, string>} */(cssProperties)[propName];
	for (var k in cssProperties) {
		element.style.setProperty(k, getProp(k));
	}
	// iframe theme propagation
	if (recurseIntoIframes) {
		var iframes = element.querySelectorAll("iframe");
		for (var i = 0; i < iframes.length; i++) {
			try {
				applyCSSProperties(cssProperties, { element: iframes[i].contentDocument?.documentElement, recurseIntoIframes: true });
			} catch (error) {
				// ignore
				// @TODO: share warning with $Window's iframe handling
			}
		}
	}
}

/**
 * @param {Record<string, string>} cssProperties
 * @returns {string}
 */
function makeThemeCSSFile(cssProperties) {
	var css = `
/* This is a generated file. */
/* spell-checker: disable */
:root {
`;
	for (var k in cssProperties) {
		css += `\t${k}: ${cssProperties[k]};\n`;
	}
	css += `}
`;
	return css;
}

// @TODO: should this be part of theme graphics generation?
// I want to figure out a better way to do dynamic theme features,
// where it works with the CSS cascade as much as possible
function makeBlackToInsetFilter() {
	if (document.getElementById("os-gui-black-to-inset-filter")) {
		return;
	}
	const svg_xml = `
		<svg style="position: absolute; pointer-events: none; bottom: 100%;">
			<defs>
				<filter id="os-gui-black-to-inset-filter" x="0" y="0" width="1px" height="1px">
					<feColorMatrix
						in="SourceGraphic"
						type="matrix"
						values="
							1 0 0 0 0
							0 1 0 0 0
							0 0 1 0 0
							-1000 -1000 -1000 1 0
						"
						result="black-parts-isolated"
					/>
					<feFlood result="shadow-color" flood-color="var(--ButtonShadow)"/>
					<feFlood result="hilight-color" flood-color="var(--ButtonHilight)"/>
					<feOffset in="black-parts-isolated" dx="1" dy="1" result="offset"/>
					<feComposite in="hilight-color" in2="offset" operator="in" result="hilight-colored-offset"/>
					<feComposite in="shadow-color" in2="black-parts-isolated" operator="in" result="shadow-colored"/>
					<feMerge>
						<feMergeNode in="hilight-colored-offset"/>
						<feMergeNode in="shadow-colored"/>
					</feMerge>
				</filter>
			</defs>
		</svg>
	`;
	const $svg = $(svg_xml);
	$svg.appendTo("body");
}
