/**
 * 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 { isString } from "../../types/is.mjs";
import { ATTRIBUTE_FORM_RELOAD, ATTRIBUTE_FORM_URL } from "./constants.mjs";
import { loadAndAssignContent } from "./util/fetch.mjs";
import { addErrorAttribute } from "../../dom/error.mjs";

export { Reload };

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

/**
 * This CustomControl reloads the content of an url and embeds it into the dom.
 *
 * @fragments /fragments/components/form/reload/
 *
 * @example /examples/components/form/reload-simple
 *
 * @since 1.13.0
 * @copyright Volker Schukai
 * @summary A beautiful reload control
 * @fires monster-fetched
 */
class Reload 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/reload");
	}

	/**
	 * 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 url to fetch
	 * @property {string} reload onshow, always (onshow is default and means that the content is loaded when the element is visible, always means that the content is always loaded)
	 * @property {string} filter css selector
	 * @property {Object} fetch fetch options for the request
	 * @property {string} fetch.redirect error, follow, manual
	 * @property {string} fetch.method GET, POST, PUT, DELETE
	 * @property {string} fetch.mode same-origin, cors, no-cors, navigate
	 * @property {string} fetch.credentials omit, same-origin, include
	 * @property {Object} fetch.headers
	 * @property {string} fetch.headers.accept text/html, application/json
	 */
	get defaults() {
		return Object.assign(
			{},
			super.defaults,
			{
				templates: {
					main: getTemplate.call(this),
				},
				shadowMode: false,
				url: null,
				reload: "onshow",
				filter: null,
				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;
	}

	/**
	 * @return {void}
	 */
	[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());
			} else {
				this.setOption("url", undefined);
			}
		};
	}

	/**
	 * 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 {Monster.Components.Form.Form}
	 */
	[assembleMethodSymbol]() {
		super[assembleMethodSymbol]();
		initIntersectionObserver.call(this);
	}

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

	/**
	 * 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}`);
		}

		try {
			return loadContent.call(this);
		} catch (e) {
			addErrorAttribute(this, e);
			return Promise.reject(e);
		}
	}
}

/**
 * @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) {
				// undefined or always do the same
				if (this.getOption("reload") === "onshow") {
					observer.disconnect();
				}

				try {
					loadContent.call(this).catch((e) => {
						addErrorAttribute(this, e);
					});
				} catch (e) {
					addErrorAttribute(this, e);
				}
			}
		}
	};

	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
 * @return {Promise}
 */
function loadContent() {
	const url = this.getOption("url", undefined);
	if (!isString(url) || url === "") {
		throw new Error("missing url");
	}

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

	let parentNode = this;
	if (this.shadowRoot) {
		parentNode = this.shadowRoot;
	}

	let container = parentNode.querySelector(`[${ATTRIBUTE_ROLE}=container]`);
	let currentDisplayMode = container?.style?.display;

	if (currentDisplayMode === undefined) {
		currentDisplayMode = "inherit";
	}

	if (!(container instanceof HTMLElement)) {
		container = document.createElement("div");
		container.style.display = "none";
		container.setAttribute(ATTRIBUTE_ROLE, "container");
		parentNode.appendChild(container);
	}

	return loadAndAssignContent(container, url, options, this.getOption("filter"))
		.then(() => {
			if (currentDisplayMode !== undefined) {
				container.style.display = currentDisplayMode;
			}
		})
		.catch((e) => {
			addErrorAttribute(this, e);
		});
}

/**
 * @private
 * @return {string}
 */
function getTemplate() {
	return this.innerHTML;
}

registerCustomElement(Reload);
