/**
 * 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 { instanceSymbol } from "../../constants.mjs";
import { addAttributeToken } from "../../dom/attributes.mjs";
import {
	ATTRIBUTE_ERRORMESSAGE,
	ATTRIBUTE_ROLE,
} from "../../dom/constants.mjs";
import {
	assembleMethodSymbol,
	attributeObserverSymbol,
	CustomElement,
	initMethodSymbol,
	registerCustomElement,
} from "../../dom/customelement.mjs";
import { isArray, isString } from "../../types/is.mjs";
import { ATTRIBUTE_FORM_RELOAD, ATTRIBUTE_FORM_URL } from "./constants.mjs";
import { loadAndAssignContent } from "./util/fetch.mjs";

export { Template };

/**
 * @private
 * @type {symbol}
 */
const intersectionObserverWasInitialized = Symbol("wasInitialized");

/**
 * A Template control is a control that can be used to load content from a URL and display it in the ShadowRoot.
 *
 * @fragments /fragments/components/form/template
 *
 * @example /examples/components/form/template-simple
 * @example /examples/components/form/template-with-default
 * @example /examples/components/form/template-with-processor
 * @example /examples/components/form/template-onshow
 *
 * @since 1.11.0
 * @copyright Volker Schukai
 * @summary A template control
 * @fires monster-fetched
 */
class Template extends CustomElement {
	/**
	 * This method is called by the `instanceof` operator.
	 * @return {symbol}
	 * @since 2.1.0
	 */
	static get [instanceSymbol]() {
		return Symbol.for("@schukai/monster/components/form/template");
	}

	/**
	 * To set the options via the HTML tag, the attribute `data-monster-options` must be used.
	 * @see {@link https://monsterjs.org/en/doc/#configurate-a-monster-control}
	 *
	 * The individual configuration values can be found in the table.
	 *
	 * @property {Object} templates Template definitions
	 * @property {string} templates.main Main template
	 * @property {string} url=undefined
	 * @property {string} reload=undefined  currently the only value defined is `onshow`. Currently the only value defined is onshow. this removes the IntersectionObserver. this means that the content is only loaded once. reloading of the content does not occur.
	 * @property {Object[]} processors
	 * @property {Object} fetch Fetch [see Using Fetch mozilla.org](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch)
	 * @property {String} fetch.redirect=error
	 * @property {String} fetch.method=GET
	 * @property {String} fetch.mode=same-origin
	 * @property {String} fetch.credentials=same-origin
	 * @property {Object} fetch.headers={"accept":"text/html"}}
	 */
	get defaults() {
		return Object.assign(
			{},
			super.defaults,
			{
				templates: {
					main: getTemplate(),
				},
				url: null,
				reload: null,
				processors: [],
				fetch: {
					redirect: "error",
					method: "GET",
					mode: "same-origin",
					credentials: "same-origin",
					headers: {
						accept: "text/html",
					},
				},
			},
			initOptionsFromArguments.call(this),
		);
	}

	/**
	 * This method determines which attributes are to be monitored by `attributeChangedCallback()`.
	 *
	 * @return {string[]}
	 */
	static get observedAttributes() {
		const list = super.observedAttributes;
		list.push(ATTRIBUTE_FORM_URL);
		return list;
	}

	/**
	 *
	 */
	[initMethodSymbol]() {
		super[initMethodSymbol]();

		// data-monster-options
		this[attributeObserverSymbol][ATTRIBUTE_FORM_URL] = (url) => {
			if (this.hasAttribute(ATTRIBUTE_FORM_URL)) {
				this.setOption("url", new URL(url, document.location).toString());
			}
		};
	}

	/**
	 * This method is called internal and should not be called directly.
	 * @throws {Error} missing default slot
	 * @throws {Error} no shadow-root is defined
	 * @throws {Error} missing url
	 * @throws {Error} we won't be able to read the data
	 * @throws {Error} request failed
	 * @throws {Error} not found
	 * @throws {Error} undefined status or type
	 * @fires monster-fetched
	 * @return {void}
	 */
	[assembleMethodSymbol]() {
		super[assembleMethodSymbol]();
		initIntersectionObserver.call(this);
	}

	/**
	 * This method is called internal and should not be called directly.
	 *
	 * @return {CSSStyleSheet[]}
	 */
	static getCSSStyleSheet() {
		return [];
	}

	/**
	 * This method is called internal and should not be called directly.
	 *
	 * @return {string}
	 */
	static getTag() {
		return "monster-template";
	}

	/**
	 * load content from url
	 *
	 * It is important to know that with this function the loading is executed
	 * directly. it is loaded as well when the element is not visible.
	 *
	 * @param {string|undefined} url
	 */
	fetch(url) {
		if (isString(url) || url instanceof URL) {
			this.setAttribute(ATTRIBUTE_FORM_URL, `${url}`);
		}

		return loadContent.call(this);
	}
}

/**
 * This attribute can be used to pass a URL to this select.
 *
 * ```
 * <monster-select data-monster-url="https://example.com/"></monster-select>
 * ```
 *
 * @private
 * @return {object}
 */
function initOptionsFromArguments() {
	const options = {};

	const url = this.getAttribute(ATTRIBUTE_FORM_URL);

	if (isString(url)) {
		options["url"] = new URL(url, document.location).toString();
	}

	if (this.hasAttribute(ATTRIBUTE_FORM_RELOAD)) {
		options["reload"] = this.getAttribute(ATTRIBUTE_FORM_RELOAD).toLowerCase();
	}

	return options;
}

/**
 * @private
 * @throws {Error} missing default slot
 * @throws {Error} no shadow-root is defined
 * @throws {Error} missing url
 * @throws {Error} we won't be able to read the data
 * @throws {Error} request failed
 * @throws {Error} not found
 * @throws {Error} undefined status or type
 * @fires monster-fetched
 */
function initIntersectionObserver() {
	if (this[intersectionObserverWasInitialized] === true) {
		return;
	}

	this[intersectionObserverWasInitialized] = true;

	const options = {
		threshold: [0.5],
	};

	const callback = (entries, observer) => {
		for (const [, entry] of entries.entries()) {
			if (entry.isIntersecting === true) {
				if (this.getOption("reload") === "onshow") {
					observer.disconnect();
				}

				try {
					loadContent.call(this);
				} catch (e) {
					addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, e.toString());
				}
			}
		}
	};

	const observer = new IntersectionObserver(callback, options);
	observer.observe(this);
}

/**
 * @private
 * @throws {Error} missing default slot
 * @throws {Error} no shadow-root is defined
 * @throws {Error} missing url
 * @throws {Error} we won't be able to read the data
 * @throws {Error} request failed
 * @throws {Error} not found
 * @throws {Error} undefined status or type
 * @throws {Error} client error
 * @throws {Error} undefined status or type
 * @throws {TypeError} value is not an instance of
 * @throws {TypeError} value is not a string
 * @fires monster-fetched
 */
function loadContent() {
	if (!this.shadowRoot) {
		throw new Error("no shadow-root is defined");
	}

	let url = this.getOption("url", undefined);

	if (url instanceof URL) {
		url = url.toString();
	}

	if (!isString(url) || url === "") {
		throw new Error("missing url");
	}

	const options = this.getOption("fetch", {});

	const defaultSlot = this.shadowRoot.querySelector("slot[name=default]");
	if (!(defaultSlot instanceof HTMLElement)) {
		throw new Error("missing default slot");
	}

	defaultSlot.style.display = "block";

	let container = this.shadowRoot.querySelector(
		`[${ATTRIBUTE_ROLE}=container]`,
	);
	if (!(container instanceof HTMLElement)) {
		container = document.createElement("div");
		container.style.display = "none";
		container.setAttribute(ATTRIBUTE_ROLE, "container");
		this.shadowRoot.appendChild(container);
	}

	loadAndAssignContent(container, url, options)
		.then(() => {
			defaultSlot.style.display = "none";
			container.style.display = "block";
			runProcessors.call(this);
		})
		.catch((e) => {
			throw e;
		});
}

/**
 * @private
 * @return {runProcessors}
 */
function runProcessors() {
	const processors = this.getOption("processors");
	if (!isArray(processors)) return this;

	for (const [, processor] of processors.entries()) {
		const source = processor?.source;
		let destination = processor?.destination;

		if (source === null) {
			addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, "missing source");
			continue;
		}

		if (
			destination === null ||
			destination === undefined ||
			destination === ""
		) {
			destination = "[" + ATTRIBUTE_ROLE + "=container]";
		}

		if (isString(source) && isString(destination)) {
			const sourceNode = this.shadowRoot.querySelector(source);
			let destinationNode = document.querySelector(destination);

			if (destinationNode === null) {
				destinationNode = this.shadowRoot.querySelector(destination);
				if (destinationNode === null) {
					addAttributeToken(
						this,
						ATTRIBUTE_ERRORMESSAGE,
						"destination not found",
					);
					continue;
				}
			}

			if (
				sourceNode instanceof HTMLElement &&
				destinationNode instanceof HTMLElement
			) {
				destinationNode.innerHTML = sourceNode.cloneNode(true).innerHTML;
			}
		} else {
			addAttributeToken(
				this,
				ATTRIBUTE_ERRORMESSAGE,
				"invalid source or destination",
			);
		}
	}

	return this;
}

/**
 * @private
 * @return {string}
 */
function getTemplate() {
	// language=HTML
	return `
        <slot name="default"></slot>
    `;
}

registerCustomElement(Template);
