/**
 * Copyright © Volker Schukai and all contributing authors, {{copyRightYear}}. All rights reserved.
 * Node module: @schukai/monster
 *
 * This source code is licensed under the GNU Affero General Public License version 3 (AGPLv3).
 * The full text of the license can be found at: https://www.gnu.org/licenses/agpl-3.0.en.html
 *
 * For those who do not wish to adhere to the AGPLv3, a commercial license is available.
 * Acquiring a commercial license allows you to use this software without complying with the AGPLv3 terms.
 * For more information about purchasing a commercial license, please contact Volker Schukai.
 *
 * SPDX-License-Identifier: AGPL-3.0
 */

export { parseBracketedKeyValueHash, createBracketedKeyValueHash };

/**
 * Parses a string containing bracketed key-value pairs and returns an object representing the parsed result.
 *
 * - The string starts with a hash symbol #.
 * - After the hash symbol, there are one or more selector strings, separated by a semicolon ;.
 * - Each selector string has the format selectorName(key1=value1,key2=value2,...).
 * - The selector name is a string of one or more alphanumeric characters.
 * - The key-value pairs are separated by commas , and are of the form key=value.
 * - The key is a string of one or more alphanumeric characters.
 * - The value can be an empty string or a string of one or more characters.
 * - If the value contains commas, it must be enclosed in double quotes ".
 * - The entire key-value pair must be URL-encoded.
 * - The closing parenthesis ) for each selector must be present, even if there are no key-value pairs.
 *
 * @example
 *
 * ```javascript
 * // Example 1:
 * const hashString = '#selector1(key1=value1,key2=value2);selector2(key3=value3)';
 * const result = parseBracketedKeyValueHash(hashString);
 * // result => { selector1: { key1: "value1", key2: "value2" }, selector2: { key3: "value3" } }
 * ```
 *
 * @example
 *
 * ```javascript
 * // Example 2:
 * const hashString = '#selector1(key1=value1,key2=value2);selector2(';
 * const result = parseBracketedKeyValueHash(hashString);
 * // result => {}
 * ```
 *
 * @since 3.37.0
 * @param {string} hashString - The string to parse, containing bracketed key-value pairs.
 * @return {Object} - An object representing the parsed result, with keys representing the selectors and values representing the key-value pairs associated with each selector.
 *                    - Returns an empty object if there was an error during parsing. */
function parseBracketedKeyValueHash(hashString) {
	const selectors = {};
	//const selectorStack = [];
	//const keyValueStack = [];

	const trimmedHashString = hashString.trim();
	const cleanedHashString =
		trimmedHashString.charAt(0) === "#"
			? trimmedHashString.slice(1)
			: trimmedHashString;

	//const selectors = (keyValueStack.length > 0) ? result[selectorStack[selectorStack.length - 1]] : result;
	let currentSelector = "";

	function addToResult(key, value) {
		if (currentSelector && key) {
			if (!selectors[currentSelector]) {
				selectors[currentSelector] = {};
			}

			selectors[currentSelector][key] = value;
		}
	}

	let currentKey = "";
	let currentValue = "";
	let inKey = true;
	let inValue = false;
	let inQuotedValue = false;
	let inSelector = true;
	let escaped = false;
	let quotedValueStartChar = "";

	for (let i = 0; i < cleanedHashString.length; i++) {
		const c = cleanedHashString[i];
		const nextChar = cleanedHashString?.[i + 1];

		if (c === "\\" && !escaped) {
			escaped = true;
			continue;
		}

		if (escaped) {
			if (inSelector) {
				currentSelector += c;
			} else if (inKey) {
				currentKey += c;
			} else if (inValue) {
				currentValue += c;
			}
			escaped = false;
			continue;
		}

		if (inQuotedValue && quotedValueStartChar !== c) {
			if (inSelector) {
				currentSelector += c;
			} else if (inKey) {
				currentKey += c;
			} else if (inValue) {
				currentValue += c;
			}

			continue;
		}

		if (c === ";" && inSelector) {
			inSelector = true;
			currentSelector = "";
			continue;
		}

		if (inSelector === true && c !== "(") {
			currentSelector += c;
			continue;
		}

		if (c === "(" && inSelector) {
			inSelector = false;
			inKey = true;

			currentKey = "";
			continue;
		}

		if (inKey === true && c !== "=") {
			currentKey += c;
			continue;
		}

		if (c === "=" && inKey) {
			inKey = false;
			inValue = true;

			if (nextChar === '"' || nextChar === "'") {
				inQuotedValue = true;
				quotedValueStartChar = nextChar;
				i++;
				continue;
			}

			currentValue = "";
			continue;
		}

		if (inValue === true) {
			if (inQuotedValue) {
				if (c === quotedValueStartChar) {
					inQuotedValue = false;
					continue;
				}

				currentValue += c;
				continue;
			}

			if (c === ",") {
				inValue = false;
				inKey = true;
				const decodedCurrentValue = decodeURIComponent(currentValue);
				addToResult(currentKey, decodedCurrentValue);
				currentKey = "";
				currentValue = "";
				continue;
			}

			if (c === ")") {
				inValue = false;
				//inKey = true;
				inSelector = true;

				const decodedCurrentValue = decodeURIComponent(currentValue);
				addToResult(currentKey, decodedCurrentValue);
				currentKey = "";
				currentValue = "";
				currentSelector = "";
				continue;
			}

			currentValue += c;

			continue;
		}
	}

	if (inSelector) {
		return selectors;
	}

	return {};
}

/**
 * Creates a hash selector string from an object.
 *
 * @param {Object} object - The object containing selectors and key-value pairs.
 * @param {boolean} addHashPrefix - Whether to add the hash prefix # to the beginning of the string.
 * @return {string} The hash selector string.
 * @since 3.37.0
 */
function createBracketedKeyValueHash(object, addHashPrefix = true) {
	if (!object) {
		return addHashPrefix ? "#" : "";
	}

	let hashString = "";

	function encodeKeyValue(key, value) {
		return encodeURIComponent(key) + "=" + encodeURIComponent(value);
	}

	for (const selector in object) {
		if (object.hasOwnProperty(selector)) {
			const keyValuePairs = object[selector];
			let selectorString = selector;
			let keyValueString = "";

			for (const key in keyValuePairs) {
				if (keyValuePairs.hasOwnProperty(key)) {
					const value = keyValuePairs[key];
					keyValueString += keyValueString.length === 0 ? "" : ",";
					keyValueString += encodeKeyValue(key, value);
				}
			}

			if (keyValueString.length > 0) {
				selectorString += "(" + keyValueString + ")";
				hashString += hashString.length === 0 ? "" : ";";
				hashString += selectorString;
			}
		}
	}

	if (hashString.length > 0 && addHashPrefix) {
		hashString = "#" + hashString;
	}

	return hashString;
}
