/**
 * 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 { validateInstance, validateString } from "../types/validate.mjs";
import { getDocument } from "./util.mjs";

export { fireEvent, fireCustomEvent, findTargetElementFromEvent };

/**
 * The function sends an event
 *
 * @param {Element | Node | HTMLCollection | NodeList} element
 * @param {string} type
 * @return {void}
 * @license AGPLv3
 * @since 1.10.0
 * @copyright Volker Schukai
 * @throws {TypeError} value is not an instance of HTMLElement or HTMLCollection
 * @summary Construct and send and event
 */
function fireEvent(element, type) {
	const document = getDocument();

	if (element instanceof HTMLElement) {
		if (type === "click") {
			element.click();
			return;
		}

		// https://developer.mozilla.org/en-US/docs/Web/API/Event/Event
		const event = new Event(validateString(type), {
			bubbles: true,
			cancelable: true,
			composed: true,
		});

		element.dispatchEvent(event);
	} else if (element instanceof HTMLCollection || element instanceof NodeList) {
		for (const e of element) {
			fireEvent(e, type);
		}
	} else {
		throw new TypeError(
			"value is not an instance of HTMLElement or HTMLCollection",
		);
	}
}

/**
 * You can call the function via the monster namespace `new Monster.DOM.fireCustomEvent()`.
 *
 * @param {Element | Node | HTMLCollection | NodeList} element
 * @param {string} type
 * @param {object} detail
 * @return {void}
 * @license AGPLv3
 * @since 1.29.0
 * @copyright Volker Schukai
 * @throws {TypeError} value is not an instance of HTMLElement or HTMLCollection
 * @summary Construct and send and event
 */
function fireCustomEvent(element, type, detail) {
	if (
		element instanceof HTMLElement ||
		(element &&
			typeof element === "object" &&
			typeof element.dispatchEvent === "function")
	) {
		if (!isObject(detail)) {
			detail = { detail };
		}

		const event = new CustomEvent(validateString(type), {
			bubbles: true,
			cancelable: true,
			composed: true,
			detail,
		});

		element.dispatchEvent(event);
	} else if (element instanceof HTMLCollection || element instanceof NodeList) {
		for (const e of element) {
			fireCustomEvent(e, type, detail);
		}
	} else {
		throw new TypeError(
			"element is not an instance of HTMLElement or HTMLCollection",
		);
	}
}

/**
 * This function gets the path `Event.composedPath()` from an event and tries to find the next element
 * up the tree `element.closest()` with the attribute and value. If no value, or a value that is undefined or null,
 * is specified, only the attribute is searched.
 *
 * @license AGPLv3
 * @since 1.14.0
 * @param {Event} event
 * @param {string} attributeName
 * @param {string|null|undefined} attributeValue
 * @throws {Error} unsupported event
 * @throws {TypeError} value is not a string
 * @throws {TypeError} value is not an instance of HTMLElement
 * @summary Help function to find the appropriate control
 */
function findTargetElementFromEvent(event, attributeName, attributeValue) {
	validateInstance(event, Event);

	if (typeof event.composedPath !== "function") {
		throw new Error("unsupported event");
	}

	const path = event.composedPath();

	// closest cannot be used here, because closest is not correct for slotted elements
	if (isArray(path)) {
		for (let i = 0; i < path.length; i++) {
			const o = path[i];

			if (
				o instanceof HTMLElement &&
				o.hasAttribute(attributeName) &&
				(attributeValue === undefined ||
					o.getAttribute(attributeName) === attributeValue)
			) {
				return o;
			}
		}
	}

	return undefined;
}
