import { isPrimitive } from "../../predicate/isPrimitive.mjs";
import { getTag } from "../_internal/getTag.mjs";
import "../_internal/tags.mjs";
import { eq } from "../util/eq.mjs";
import { isObject } from "./isObject.mjs";
//#region src/compat/predicate/isMatchWith.ts
/**
* Performs a deep comparison between a target value and a source pattern to determine if they match,
* using a custom comparison function for fine-grained control over the matching logic.
*
* This function recursively traverses both values, calling the custom compare function for each
* property/element pair. If the compare function returns a boolean, that result is used directly.
* If it returns undefined, the default matching behavior continues recursively.
*
* The matching behavior varies by data type:
* - **Objects**: Matches if all properties in the source exist in the target and match
* - **Arrays**: Matches if all elements in the source array can be found in the target array (order-independent)
* - **Maps**: Matches if all key-value pairs in the source Map exist and match in the target Map
* - **Sets**: Matches if all elements in the source Set can be found in the target Set
* - **Functions**: Matches using strict equality, or object comparison if the function has properties
* - **Primitives**: Matches using strict equality
*
* Special cases:
* - Empty arrays, Maps, and Sets always match any target
* - An empty object matches any target at the top level, but at nested levels it only matches object targets
* - `null` and `undefined` source values have specific matching rules
* - Circular references are handled using an internal stack to prevent infinite recursion
*
* @param target - The value to be tested for matching
* @param source - The pattern/template to match against
* @param [compare] - Optional custom comparison function that receives:
*   - `objValue` - The value from the target at the current path
*   - `srcValue` - The value from the source at the current path
*   - `key` - The property key or array index being compared
*   - `object` - The parent object/array from the target
*   - `source` - The parent object/array from the source
*   - `stack` - Internal Map used for circular reference detection
*   Should return `true` for a match, `false` for no match, or `undefined` to continue with default behavior
*
* @returns `true` if the target matches the source pattern, `false` otherwise
*
* @example
* // Basic matching without custom comparator
* isMatchWith({ a: 1, b: 2 }, { a: 1 }); // true
* isMatchWith([1, 2, 3], [1, 3]); // true
*
* @example
* // Custom comparison for case-insensitive string matching
* const caseInsensitiveCompare = (objVal, srcVal) => {
*   if (typeof objVal === 'string' && typeof srcVal === 'string') {
*     return objVal.toLowerCase() === srcVal.toLowerCase();
*   }
*   return undefined; // Use default behavior for non-strings
* };
*
* isMatchWith(
*   { name: 'JOHN', age: 30 },
*   { name: 'john' },
*   caseInsensitiveCompare
* ); // true
*
* @example
* // Custom comparison for range matching
* const rangeCompare = (objVal, srcVal, key) => {
*   if (key === 'age' && typeof srcVal === 'object' && srcVal.min !== undefined) {
*     return objVal >= srcVal.min && objVal <= srcVal.max;
*   }
*   return undefined;
* };
*
* isMatchWith(
*   { name: 'John', age: 25 },
*   { age: { min: 18, max: 30 } },
*   rangeCompare
* ); // true
*/
function isMatchWith(target, source, compare) {
	if (typeof compare !== "function") return isMatchWith(target, source, () => void 0);
	return isMatchWithInternal(target, source, function doesMatch(objValue, srcValue, key, object, source, stack) {
		const isEqual = compare(objValue, srcValue, key, object, source, stack);
		if (isEqual !== void 0) return Boolean(isEqual);
		return isMatchWithInternal(objValue, srcValue, doesMatch, stack, false);
	}, /* @__PURE__ */ new Map(), true);
}
function isMatchWithInternal(target, source, compare, stack, isRoot = false) {
	if (source === target) return true;
	switch (typeof source) {
		case "object": return isObjectMatch(target, source, compare, stack, isRoot);
		case "function":
			if (Object.keys(source).length > 0) return isMatchWithInternal(target, { ...source }, compare, stack, isRoot);
			return eq(target, source);
		default:
			if (!isObject(target)) return eq(target, source);
			if (isRoot) {
				if (typeof source === "string") return source === "";
				return true;
			}
			return eq(target, source);
	}
}
function isObjectMatch(target, source, compare, stack, isRoot = false) {
	if (source == null) return true;
	if (Array.isArray(source)) return isArrayMatch(target, source, compare, stack);
	if (source instanceof Map) return isMapMatch(target, source, compare, stack);
	if (source instanceof Set) return isSetMatch(target, source, compare, stack);
	const keys = Object.keys(source);
	if (target == null) return isRoot && keys.length === 0;
	if (isRoot) {
		if (isPrimitive(target)) target = Object(target);
	} else {
		const tag = getTag(target);
		if (tag !== "[object Object]" && tag !== "[object Arguments]") return false;
	}
	if (keys.length === 0) return true;
	if (stack?.has(source)) return stack.get(source) === target;
	stack?.set(source, target);
	try {
		for (let i = 0; i < keys.length; i++) {
			const key = keys[i];
			if (!(key in target)) return false;
			if (source[key] === void 0 && target[key] !== void 0) return false;
			if (source[key] === null && target[key] !== null) return false;
			if (!compare(target[key], source[key], key, target, source, stack)) return false;
		}
		return true;
	} finally {
		stack?.delete(source);
	}
}
function isMapMatch(target, source, compare, stack) {
	if (source.size === 0) return true;
	if (!(target instanceof Map)) return false;
	for (const [key, sourceValue] of source.entries()) if (compare(target.get(key), sourceValue, key, target, source, stack) === false) return false;
	return true;
}
function isArrayMatch(target, source, compare, stack) {
	if (source.length === 0) return true;
	if (!Array.isArray(target)) return false;
	const countedIndex = /* @__PURE__ */ new Set();
	for (let i = 0; i < source.length; i++) {
		const sourceItem = source[i];
		let found = false;
		for (let j = 0; j < target.length; j++) {
			if (countedIndex.has(j)) continue;
			const targetItem = target[j];
			let matches = false;
			if (compare(targetItem, sourceItem, i, target, source, stack)) matches = true;
			if (matches) {
				countedIndex.add(j);
				found = true;
				break;
			}
		}
		if (!found) return false;
	}
	return true;
}
function isSetMatch(target, source, compare, stack) {
	if (source.size === 0) return true;
	if (!(target instanceof Set)) return false;
	return isArrayMatch([...target], [...source], compare, stack);
}
//#endregion
export { isMatchWith };
