/**
 * 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 { 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";

export { Reload };

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

/**
 * This CustomControl reloads the content of a url and embeds it into the dom.
 *
 * <img src="./images/reload.png">
 *
 * You can create this control either by specifying the HTML tag `<monster-reload />` directly in the HTML or using
 * Javascript via the `document.createElement('monster-reload');` method.
 *
 * ```html
 * <monster-reload></monster-reload>
 *
 * <script type="module">
 * import {Reload} from '@schukai/component-form/source/reload.js';
 * document.createElement('monster-reload');
 * </script>
 * ```
 *
 * A simple configuration can look like this
 *
 * ```html
 * <script id="config"
 *         type="application/json">
 *     {
 *         "url": "./content.html",
 *     }
 * </script>
 *
 * <monster-reload data-monster-options-selector="#config">
 * </monster-reload>
 * ```
 *
 * If you want to display a loader, you can insert a div with the attribute `data-monster-role="container"`.
 * The content of this div will be replaced by the loaded code.
 *
 * ```html
 * <monster-reload data-monster-options-selector="#config">
 * <div data-monster-role="container">
 * LOADER ...
 * </div>
 * </monster-reload>
 * ```
 *
 * If you need additional structure, you can simply specify it.
 *
 * ```html
 * <monster-reload data-monster-options-selector="#config">
 * <div class="row">
 *   <div class="col" data-monster-role="container">
 *       LOADER ...
 *   </div>
 * </div>
 * </monster-reload>
 * ```
 *
 * @startuml reload.png
 * skinparam monochrome true
 * skinparam shadowing false
 * HTMLElement <|-- CustomElement
 * CustomElement <|-- CustomControl
 * CustomControl <|-- Reload
 * @enduml
 *
 * @since 1.13.0
 * @copyright schukai GmbH
 * @memberOf Monster.Components.Form
 * @summary A reload control
 * @fires Monster.Components.Form.event:monster-fetched
 */
class Reload extends CustomElement {
	/**
	 * This method is called by the `instanceof` operator.
	 * @returns {symbol}
	 * @since 2.1.0
	 */
	static get [instanceSymbol]() {
		return Symbol.for("@schukai/component-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=undefined
	 * @property {string} reload=undefined currently the values defined are `onshow` and `always`. The default `onshow` removes the IntersectionObserver. This means that the content is only loaded once. reloading of the content does not occur.
	 * @property {string} filter=undefined dom selectors to search for elements, if undefined then everything is taken
	 * @property {Monster.Components.Form.Processor[]} 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.call(this),
				},
				shadowMode: false,
				url: undefined,
				reload: "onshow",
				filter: undefined,
				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());
			} 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.Components.Form.event: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}`);
		}

		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.Components.Form.event: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) => {
						addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, e.toString());
					});
				} 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.Components.Form.event: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) => {
			throw e;
		});
}

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

registerCustomElement(Reload);