
/** Compare two possibly complex values for
 * loose equality, going as deep as required in the
 * case of complex objects.
 */
function deepCompare (a, b) {
  if (typeof a == "object" && typeof b == "object") {
    return !Object.entries(a).some( ([k, v]) => !deepCompare(v, b[k])) &&
      !Object.entries(b).some( ([k, v]) => !deepCompare(v, a[k]));
  } else {
    return a == b;
  }
}


/** Compare two possibly complex values for
 * strict equality.
 */
function deepEqual (a, b) {
  if (typeof a === "object" && typeof b === "object") {
    return !Object.entries(a).some( ([k, v]) => !deepEqual(v, b[k])) &&
      !Object.entries(b).some( ([k, v]) => !deepEqual(v, a[k]));
  } else {
    return a === b;
  }
}

/** Traverses an object and sets a nested value.
 *
 * Example:
 *
 * const obj = {a: {b: {c: "X"} } }
 * deepSet(obj, ["a", "b", "c"], "d")
 * → {a: {b: {c: "d"} } }
 *
 * This would be the equivalent of:
 *
 * obj?.a?.b?.c = "d";
 *
 * Except that the above is not a legal expression.
 *
 * If a non-leaf property does not exist, this function
 * creates it as an empty object ({}) and keeps traversing.
 *
 * The last member of `path` may be `null`, in which case,
 * if the object pointed to by the next to last member is
 * an array, an insert operation will take place.
 *
 */
function deepSet (obj, path, value) {
  const key = path.shift();
  if (!path.length) {
    if (key === null && Array.isArray(obj)) {
      obj.push(value);
    } else {
      obj[key] = value;
    }
  } else {
    if (!Object.hasOwn(obj, key)) {
      obj[key] = {};
    }
    deepSet(obj[key], path, value);
  }
}

/** Returns a nested property.
 *
 * Example:
 *
 * const obj = {a: {b: {c: "d"} } }
 * deepSet(obj, ["a", "b", "c"])
 * → "d"
 *
 * If `path` is known in advance, this is effectively
 * the same as:
 *
 * obj?.a?.b?.c
 *
 * This might be useful when `path` is dynamic.
 */
function deepValue (obj, path) {
  if (obj !== undefined) {
    const key = path.shift();
    if (!path.length) {
      if (key === undefined) {
        return obj;
      } else {
        return obj[key];
      }
    } else {
      return deepValue(obj[key], path);
    }
  }
}

// Copied from:
// https://gomakethings.com/how-to-deep-merge-arrays-and-objects-with-javascript/

/*!
 * Deep merge two or more objects or arrays.
 * (c) 2023 Chris Ferdinandi, MIT License, https://gomakethings.com
 * @param   {*} ...objs  The arrays or objects to merge
 * @returns {*}          The merged arrays or objects
 */
function deepMerge (...objs) {

	/**
	 * Get the object type
	 * @param  {*}       obj The object
	 * @return {String}      The object type
	 */
	function getType (obj) {
		return Object.prototype.toString.call(obj).slice(8, -1).toLowerCase();
	}

	/**
	 * Deep merge two objects
	 * @return {Object}
	 */
	function mergeObj (clone, obj) {
		for (let [key, value] of Object.entries(obj)) {
			let type = getType(value);
			if (clone[key] !== undefined && getType(clone[key]) === type && ['array', 'object'].includes(type)) {
				clone[key] = deepMerge(clone[key], value);
			} else {
				clone[key] = structuredClone(value);
			}
		}
	}

	// Create a clone of the first item in the objs array
	let clone = structuredClone(objs.shift());

	// Loop through each item
	for (let obj of objs) {

		// Get the object type
		let type = getType(obj);

		// If the current item isn't the same type as the clone, replace it
		if (getType(clone) !== type) {
			clone = structuredClone(obj);
			continue;
		}

		// Otherwise, merge
		if (type === 'array') {
			// Replace old array with new
			clone = [...structuredClone(obj)];
		} else if (type === 'object') {
			mergeObj(clone, obj);
		} else {
			clone = obj;
		}

	}

	return clone;

}

module.exports = {
	deepCompare,
	deepEqual,
	deepSet,
	deepValue,
	deepMerge
};
