/**
 * 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 { generateRangeComparisonExpression };

/**
 * The `generateRangeComparisonExpression()` function is a function that generates a string representation
 * of a comparison expression based on a range of values. It takes three arguments:
 *
 * - expression (required): a string representation of a range of values in the format of start1-end1,start2-end2,value3....
 * - valueName (required): a string representing the name of the value that is being compared to the range of values.
 * - options (optional): an object containing additional options to customize the comparison expression.
 *
 * The generateRangeComparisonExpression() function returns a string representation of the comparison expression.
 *
 * ## Options
 * The option parameter is an object that can have the following properties:
 *
 * urlEncode (boolean, default: false): if set to true, URL encodes the comparison operators.
 * andOp (string, default: '&&'): the logical AND operator to use in the expression.
 * orOp (string, default: '||'): the logical OR operator to use in the expression.
 * eqOp (string, default: '=='): the equality operator to use in the expression.
 * geOp (string, default: '>='): the greater than or equal to operator to use in the expression.
 * leOp (string, default: '<='): the less than or equal to operator to use in the expression.
 * gtOp (string, default: '>'): the greater than operator to use in the expression.
 * ltOp (string, default: '<'): the less than operator to use in the expression.
 *
 * Examples
 *
 * ```javascript
 * const expression = '0-10,20-30';
 * const valueName = 'age';
 * const options = { urlEncode: true, andOp: 'and', orOp: 'or', eqOp: '=', geOp: '>=', leOp: '<=' };
 * const comparisonExpression = generateRangeComparisonExpression(expression, valueName, options);
 *
 * console.log(comparisonExpression); // age%3E%3D0%20and%20age%3C%3D10%20or%20age%3E%3D20%20and%20age%3C%3D30
 * ```
 *
 * In this example, the generateRangeComparisonExpression() function generates a string representation of the comparison
 * expression for the expression and valueName parameters with the specified options. The resulting comparison
 * expression is 'age>=0 and age<=10 or age>=20 and age<=30', URL encoded according to the urlEncode option.
 *
 * @param {string} expression - The string expression to generate the comparison for.
 * @param {string} valueName - The name of the value to compare against.
 * @param {Object} [options] - The optional parameters.
 * @param {boolean} [options.urlEncode=false] - Whether to encode comparison operators for use in a URL.
 * @param {string} [options.andOp='&&'] - The logical AND operator to use.
 * @param {string} [options.orOp='||'] - The logical OR operator to use.
 * @param {string} [options.eqOp='=='] - The comparison operator for equality to use.
 * @param {string} [options.geOp='>='] - The comparison operator for greater than or equal to to use.
 * @param {string} [options.leOp='<='] - The comparison operator for less than or equal to to use.
 * @param {string} [options.gtOp='>'] - The comparison operator for greater than to use.
 * @param {string} [options.ltOp='<'] - The comparison operator for less than to use.
 * @return {string} The generated comparison expression.
 * @throws {Error} If the input is invalid.
 * @summary Generates a comparison expression based on a range of values.
 */
/**
 * Copyright © Volker Schukai and all contributing authors, {{copyRightYear}}. All rights reserved.
 * SPDX-License-Identifier: AGPL-3.0
 */
function generateRangeComparisonExpression(
	expression,
	valueName,
	options = {},
) {
	const {
		urlEncode = false,
		andOp = "&&",
		orOp = "||",
		eqOp = "==",
		geOp = ">=",
		leOp = "<=",
		gtOp = ">",
		ltOp = "<",
	} = options;

	if (
		typeof expression !== "string" ||
		typeof valueName !== "string" ||
		!expression.trim()
	) {
		throw new Error("Invalid input");
	}

	const encode = (s) => (urlEncode ? encodeURIComponent(s) : s);
	const and = encode(andOp);
	const or = encode(orOp);
	const space = urlEncode ? "%20" : " ";

	function parseRange(range) {
		range = range.trim();
		if (!range) throw new Error("Empty range");

		// Sonderfall: nur Bindestriche wie "--"
		if (/^-+$/.test(range)) {
			throw new Error(`Invalid range '${range}'`);
		}

		// Bereich: z.B. -10--5, 4-6
		const rangeMatch = range.match(
			/^(-?\d+(?:\.\d+)?)\s*-\s*(-?\d+(?:\.\d+)?)/,
		);
		if (rangeMatch) {
			const sNum = parseFloat(rangeMatch[1]);
			const eNum = parseFloat(rangeMatch[2]);
			if (isNaN(sNum) || isNaN(eNum))
				throw new Error(`Invalid value '${range}'`);
			if (sNum > eNum) throw new Error(`Invalid range '${range}'`);
			return `${valueName}${encode(geOp)}${sNum}${space}${and}${space}${valueName}${encode(leOp)}${eNum}`;
		}

		// Exklusiver Bereich: 4~6 → x>4 && x<6
		const exclMatch = range.match(/^(-?\d+(?:\.\d+)?)\s*~\s*(-?\d+(?:\.\d+)?)/);
		if (exclMatch) {
			const sNum = parseFloat(exclMatch[1]);
			const eNum = parseFloat(exclMatch[2]);
			if (isNaN(sNum) || isNaN(eNum))
				throw new Error(`Invalid value '${range}'`);
			return `${valueName}${encode(gtOp)}${sNum}${space}${and}${space}${valueName}${encode(ltOp)}${eNum}`;
		}

		// Offene Intervalle exklusiv: 4~ → x>4, ~6 → x<6
		const openRightExclMatch = range.match(/^(-?\d+(?:\.\d+)?)~$/);
		if (openRightExclMatch) {
			const sNum = parseFloat(openRightExclMatch[1]);
			if (isNaN(sNum)) throw new Error(`Invalid value in '${range}'`);
			return `${valueName}${encode(gtOp)}${sNum}`;
		}
		const openLeftExclMatch = range.match(/^~(-?\d+(?:\.\d+)?)$/);
		if (openLeftExclMatch) {
			const eNum = parseFloat(openLeftExclMatch[1]);
			if (isNaN(eNum)) throw new Error(`Invalid value in '${range}'`);
			return `${valueName}${encode(ltOp)}${eNum}`;
		}

		// Offene Intervalle inklusiv: 4- → x>=4, -6 → x<=6
		const openRightInclMatch = range.match(/^(-?\d+(?:\.\d+)?)\-$/);
		if (openRightInclMatch) {
			const sNum = parseFloat(openRightInclMatch[1]);
			if (isNaN(sNum)) throw new Error(`Invalid value in '${range}'`);
			return `${valueName}${encode(geOp)}${sNum}`;
		}
		const openLeftInclMatch = range.match(/^\-(-?\d+(?:\.\d+)?)$/);
		if (openLeftInclMatch) {
			const eNum = parseFloat(openLeftInclMatch[1]);
			if (isNaN(eNum)) throw new Error(`Invalid value in '${range}'`);
			return `${valueName}${encode(leOp)}${eNum}`;
		}

		// <n oder >n
		if (range.startsWith("<")) {
			const n = parseFloat(range.slice(1));
			if (isNaN(n)) throw new Error(`Invalid value in '${range}'`);
			return `${valueName}${encode(ltOp)}${n}`;
		}
		if (range.startsWith(">")) {
			const n = parseFloat(range.slice(1));
			if (isNaN(n)) throw new Error(`Invalid value in '${range}'`);
			return `${valueName}${encode(gtOp)}${n}`;
		}

		// Einzelwert NUR, wenn wirklich reine Zahl
		if (/^-?\d+(\.\d+)?$/.test(range)) {
			const value = parseFloat(range);
			return `${valueName}${encode(eqOp)}${value}`;
		}

		throw new Error(`Invalid value '${range}'`);
	}

	const parts = expression
		.split(",")
		.map((r) => r.trim())
		.filter(Boolean)
		.map(parseRange);

	const sep = space + or + space;
	return parts.length > 1 ? parts.map((p) => `(${p})`).join(sep) : parts[0];
}
