/**
 * Copyright schukai GmbH and contributors 2023. All Rights Reserved.
 * Node module: @schukai/monster
 * This file is licensed under the AGPLv3 License.
 * License text available at https://www.gnu.org/licenses/agpl-3.0.en.html
 */

import { getGlobal } from "../types/global.mjs";
import { validateString } from "../types/validate.mjs";

export { getDocument, getWindow, getDocumentFragmentFromString, findElementWithIdUpwards, getContainingDocument };

/**
 * This method fetches the document object
 *
 * In nodejs this functionality can be performed with [jsdom](https://www.npmjs.com/package/jsdom).
 *
 * ```
 * import {JSDOM} from "jsdom"
 * if (typeof window !== "object") {
 *    const {window} = new JSDOM('', {
 *        url: 'http://example.com/',
 *        pretendToBeVisual: true
 *    });
 *
 *    [
 *        'self',
 *        'document',
 *        'Document',
 *        'Node',
 *        'Element',
 *        'HTMLElement',
 *        'DocumentFragment',
 *        'DOMParser',
 *        'XMLSerializer',
 *        'NodeFilter',
 *        'InputEvent',
 *        'CustomEvent'
 *    ].forEach(key => (getGlobal()[key] = window[key]));
 * }
 * ```
 *
 * @returns {object}
 * @license AGPLv3
 * @since 1.6.0
 * @copyright schukai GmbH
 * @memberOf Monster.DOM
 * @throws {Error} not supported environment
 */
function getDocument() {
    let document = getGlobal()?.["document"];
    if (typeof document !== "object") {
        throw new Error("not supported environment");
    }

    return document;
}

/**
 * This method fetches the window object
 *
 * In nodejs this functionality can be performed with [jsdom](https://www.npmjs.com/package/jsdom).
 *
 * ```
 * import {JSDOM} from "jsdom"
 * if (typeof window !== "object") {
 *    const {window} = new JSDOM('', {
 *        url: 'http://example.com/',
 *        pretendToBeVisual: true
 *    });
 *
 *    getGlobal()['window']=window;
 *
 *    [
 *        'self',
 *        'document',
 *        'Document',
 *        'Node',
 *        'Element',
 *        'HTMLElement',
 *        'DocumentFragment',
 *        'DOMParser',
 *        'XMLSerializer',
 *        'NodeFilter',
 *        'InputEvent',
 *        'CustomEvent'
 *    ].forEach(key => (getGlobal()[key] = window[key]));
 * }
 * ```
 *
 * @returns {object}
 * @license AGPLv3
 * @since 1.6.0
 * @copyright schukai GmbH
 * @memberOf Monster.DOM
 * @throws {Error} not supported environment
 */
function getWindow() {
    let window = getGlobal()?.["window"];
    if (typeof window !== "object") {
        throw new Error("not supported environment");
    }

    return window;
}

/**
 * This method fetches the document object
 *
 * In nodejs this functionality can be performed with [jsdom](https://www.npmjs.com/package/jsdom).
 *
 * ```
 * import {JSDOM} from "jsdom"
 * if (typeof window !== "object") {
 *    const {window} = new JSDOM('', {
 *        url: 'http://example.com/',
 *        pretendToBeVisual: true
 *    });
 *
 *    [
 *        'self',
 *        'document',
 *        'Document',
 *        'Node',
 *        'Element',
 *        'HTMLElement',
 *        'DocumentFragment',
 *        'DOMParser',
 *        'XMLSerializer',
 *        'NodeFilter',
 *        'InputEvent',
 *        'CustomEvent'
 *    ].forEach(key => (getGlobal()[key] = window[key]));
 * }
 * ```
 *
 * @returns {DocumentFragment}
 * @license AGPLv3
 * @since 1.6.0
 * @copyright schukai GmbH
 * @memberOf Monster.DOM
 * @throws {Error} not supported environment
 * @throws {TypeError} value is not a string
 */
function getDocumentFragmentFromString(html) {
    validateString(html);

    const document = getDocument();
    const template = document.createElement("template");
    template.innerHTML = html;

    return template.content;
}

/**
 * Recursively searches upwards from a given element to find an ancestor element
 * with a specified ID, considering both normal DOM and shadow DOM.
 *
 * @param {HTMLElement|ShadowRoot} element - The starting element or shadow root to search from.
 * @param {string} targetId - The ID of the target element to find.
 * @returns {HTMLElement|null} - The ancestor element with the specified ID, or null if not found.
 * @memberOf Monster.DOM
 * @since 3.29.0
 * @license AGPLv3
 * @copyright schukai GmbH
 */
function findElementWithIdUpwards(element, targetId) {
    if (!element) {
        return null;
    }

    // Check if the current element has the target ID
    if (element.id === targetId) {
        return element;
    }

    // Search within the current element's shadow root, if it exists
    if (element.shadowRoot) {
        const target = element.shadowRoot.getElementById(targetId);
        if (target) {
            return target;
        }
    }

    // If the current element is the document.documentElement, search within the main document
    if (element === document.documentElement) {
        const target = document.getElementById(targetId);
        if (target) {
            return target;
        }
    }

    // If the current element is inside a shadow root, search its host's ancestors
    const rootNode = element.getRootNode();
    if (rootNode && rootNode instanceof ShadowRoot) {
        return findElementWithIdUpwards(rootNode.host, targetId);
    }

    // Otherwise, search the current element's parent
    return findElementWithIdUpwards(element.parentElement, targetId);
}

/**
 * @private
 * @param {HTMLElement} element
 * @returns {HTMLElement|null}
 */
function traverseShadowRoots(element) {
    let currentRoot = element.shadowRoot;
    let currentParent = element.parentNode;

    while (
        currentParent &&
        currentParent.nodeType !== Node.DOCUMENT_NODE &&
        currentParent.nodeType !== Node.DOCUMENT_FRAGMENT_NODE
    ) {
        if (currentRoot && currentRoot.parentNode) {
            currentParent = currentRoot.parentNode;
            currentRoot = currentParent.shadowRoot;
        } else if (currentParent.parentNode) {
            currentParent = currentParent.parentNode;
            currentRoot = null;
        } else if (currentRoot && currentRoot.host && currentRoot.host.nodeType === Node.DOCUMENT_NODE) {
            currentParent = currentRoot.host;
            currentRoot = null;
        } else {
            currentParent = null;
            currentRoot = null;
        }
    }

    return currentParent;
}

/**
 * Recursively searches upwards from a given element to find an ancestor element
 *
 * @param {HTMLElement} element
 * @returns {*}
 * @throws {Error} Invalid argument. Expected an HTMLElement.
 * @memberOf Monster.DOM
 * @since 3.36.0
 */
function getContainingDocument(element) {
    if (
        !element ||
        !(element instanceof HTMLElement || element instanceof element.ownerDocument.defaultView.HTMLElement)
    ) {
        throw new Error("Invalid argument. Expected an HTMLElement.");
    }

    return traverseShadowRoots(element) || null;
}