/**
 * 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
 */

import { isArray, isObject } from "../types/is.mjs";
import { typeOf } from "../types/typeof.mjs";

export { diff };

/**
 * With the diff function you can perform the change of one object to another. The result shows the changes of the second object to the first object.
 *
 * The operator `add` means that something has been added to the second object. `delete` means that something has been deleted from the second object compared to the first object.
 *
 * @externalExample ../../example/data/diff.mjs
 * @param {*} first
 * @param {*} second
 * @return {array}
 * @license AGPLv3
 * @since 1.6.0
 * @copyright Volker Schukai
 */
function diff(first, second) {
	return doDiff(first, second);
}

/**
 * @private
 * @param a
 * @param b
 * @param type
 * @return {Set<string>|Set<number>}
 */
function getKeys(a, b, type) {
	if (isArray(type)) {
		const keys =
			a.length > b.length ? new Array(a.length) : new Array(b.length);
		keys.fill(0);
		return new Set(keys.map((_, i) => i));
	}

	return new Set(Object.keys(a).concat(Object.keys(b)));
}

/**
 * @private
 * @param a
 * @param b
 * @param path
 * @param diff
 * @return {array}
 */
function doDiff(a, b, path, diff) {
	const typeA = typeOf(a);
	const typeB = typeOf(b);

	const currPath = path || [];
	const currDiff = diff || [];

	if (typeA === typeB && (typeA === "object" || typeA === "array")) {
		const keys = getKeys(a, b, typeA);
		if (keys.size === 0) {
			return currDiff;
		}

		keys.forEach((v) => {
			if (!Object.prototype.hasOwnProperty.call(a, v)) {
				currDiff.push(buildResult(a[v], b[v], "add", currPath.concat(v)));
			} else if (!Object.prototype.hasOwnProperty.call(b, v)) {
				currDiff.push(buildResult(a[v], b[v], "delete", currPath.concat(v)));
			} else {
				doDiff(a[v], b[v], currPath.concat(v), currDiff);
			}
		});
	} else {
		const o = getOperator(a, b, typeA, typeB);
		if (o !== undefined) {
			currDiff.push(buildResult(a, b, o, path));
		}
	}

	return currDiff;
}

/**
 *
 * @param {*} a
 * @param {*} b
 * @param {string} operator
 * @param {array} path
 * @return {{path: array, operator: string}}
 * @private
 */
function buildResult(a, b, operator, path) {
	const result = {
		operator,
		path,
	};

	if (operator !== "add") {
		result.first = {
			value: a,
			type: typeof a,
		};

		if (isObject(a)) {
			const name = Object.getPrototypeOf(a)?.constructor?.name;
			if (name !== undefined) {
				result.first.instance = name;
			}
		}
	}

	if (operator === "add" || operator === "update") {
		result.second = {
			value: b,
			type: typeof b,
		};

		if (isObject(b)) {
			const name = Object.getPrototypeOf(b)?.constructor?.name;
			if (name !== undefined) {
				result.second.instance = name;
			}
		}
	}

	return result;
}

/**
 * @private
 * @param {*} a
 * @param {*} b
 * @return {boolean}
 */
function isNotEqual(a, b) {
	if (typeof a !== typeof b) {
		return true;
	}

	if (a instanceof Date && b instanceof Date) {
		return a.getTime() !== b.getTime();
	}

	return a !== b;
}

/**
 * @private
 * @param {*} a
 * @param {*} b
 * @return {string|undefined}
 */
function getOperator(a, b) {
	/**
	 * @type {string|undefined}
	 */
	let operator;

	/**
	 * @type {string}
	 */
	const typeA = typeof a;

	/**
	 * @type {string}
	 */
	const typeB = typeof b;

	if (typeA === "undefined" && typeB !== "undefined") {
		operator = "add";
	} else if (typeA !== "undefined" && typeB === "undefined") {
		operator = "delete";
	} else if (isNotEqual(a, b)) {
		operator = "update";
	}

	return operator;
}
