/**
 * 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 { extend } from "../data/extend.mjs";
import { addAttributeToken } from "./attributes.mjs";
import { ATTRIBUTE_ERRORMESSAGE } from "./constants.mjs";
import { CustomElement, attributeObserverSymbol } from "./customelement.mjs";
import { instanceSymbol } from "../constants.mjs";
import { DeadMansSwitch } from "../util/deadmansswitch.mjs";
import { addErrorAttribute } from "./error.mjs";
import { Observer } from "../types/observer.mjs";

export { CustomControl };

/**
 * @private
 * @type {symbol}
 */
const attachedInternalSymbol = Symbol("attachedInternal");

/**
 * This is a base class for creating custom controls using the power of CustomElement.
 *
 * After defining a `CustomElement`, the `registerCustomElement` method must be called with the new class name. Only then
 * will the tag defined via the `getTag` method be made known to the DOM.
 *
 * This control uses `attachInternals()` to integrate the control into a form. If the target environment does not support
 * this method, the [polyfill](https://www.npmjs.com/package/element-internals-polyfill) can be used.
 *
 * You can create the object using the function `document.createElement()`.
 *
 * This control uses `attachInternals()` to integrate the control into a form. If the target environment does not support
 * this method, the Polyfill for attachInternals() can be used: {@link https://www.npmjs.com/package/element-internals-polyfill|element-internals-polyfill}.
 *
 * Learn more about WICG Web Components: {@link https://github.com/WICG/webcomponents|WICG Web Components}.
 *
 * Read the HTML specification for Custom Elements: {@link https://html.spec.whatwg.org/multipage/custom-elements.html#custom-elements|Custom Elements}.
 *
 * Read the HTML specification for Custom Element Reactions: {@link https://html.spec.whatwg.org/dev/custom-elements.html#custom-element-reactions|Custom Element Reactions}.
 *
 * @fragments /fragments/libraries/dom/customcontrol/
 * @example /examples/libraries/dom/customcontrol/text-input/ Build a minimal form associated text control
 * @example /examples/libraries/dom/customcontrol/validation/ Add validation and reportValidity behaviour
 * @example /examples/libraries/dom/customcontrol/formdata-submit/ Submit structured FormData from a custom control
 * @example /examples/libraries/dom/customcontrol/reset-behaviour/ Restore the control state on form reset
 * @example /examples/libraries/dom/customcontrol/disabled-sync/ Sync disabled state from form containers into the control
 *
 * @summary A form-oriented base class for Monster controls with value handling, validation and control semantics.
 * @license AGPLv3
 * @since 1.14.0
 */
class CustomControl extends CustomElement {
	/**
	 * The constructor method of CustomControl, which is called when creating a new instance.
	 * It checks whether the element supports `attachInternals()` and initializes an internal form-associated element
	 * if supported. Additionally, it initializes a MutationObserver to watch for attribute changes.
	 *
	 * See the links below for more information:
	 * {@link https://html.spec.whatwg.org/multipage/custom-elements.html#dom-customelementregistry-define|CustomElementRegistry.define()}
	 * {@link https://html.spec.whatwg.org/multipage/custom-elements.html#dom-customelementregistry-get|CustomElementRegistry.get()}
	 * and {@link https://developer.mozilla.org/en-US/docs/Web/API/ElementInternals|ElementInternals}
	 *
	 * @inheritdoc
	 * @throws {Error} the ElementInternals is not supported and a polyfill is necessary
	 */
	constructor() {
		super();

		// check if element supports `attachInternals()`
		if (typeof this["attachInternals"] === "function") {
			this[attachedInternalSymbol] = this.attachInternals();
		} else {
			// `attachInternals()` is not supported, so a polyfill is necessary
			throw Error(
				"the ElementInternals is not supported and a polyfill is necessary",
			);
		}

		// watch for attribute value changes
		initValueAttributeObserver.call(this);
		initDisabledAttributeObserver.call(this);
		initDisabledOptionObserver.call(this);
	}

	/**
	 * This method is called by the `instanceof` operator.
	 * @return {symbol}
	 * @since 2.1.0
	 */
	static get [instanceSymbol]() {
		return Symbol.for("@schukai/monster/dom/custom-control@@instance");
	}

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

	/**
	 * Adding a static `formAssociated` property, with a true value, makes an autonomous custom element a form-associated custom element.
	 *
	 * @see [attachInternals()]{@link https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/attachInternals}
	 * @see [Custom Elements Face Example]{@link https://html.spec.whatwg.org/multipage/custom-elements.html#custom-elements-face-example}
	 * @return {boolean}
	 */
	static formAssociated = true;

	/**
	 * @inheritdoc
	 */
	connectedCallback() {
		super.connectedCallback();
		const disabledObserver = this[attributeObserverSymbol]?.["disabled"];
		if (typeof disabledObserver === "function") {
			disabledObserver();
		}
	}

	/**
	 * @inheritdoc
	 **/
	get defaults() {
		return extend({}, super.defaults);
	}

	/**
	 * Must be overridden by a derived class and return the value of the control.
	 *
	 * This is a method of [internal API](https://developer.mozilla.org/en-US/docs/Web/API/ElementInternals), which is a part of the web standard for custom elements.
	 *
	 * @throws {Error} the value getter must be overwritten by the derived class
	 */
	get value() {
		throw Error("the value getter must be overwritten by the derived class");
	}

	/**
	 * Must be overridden by a derived class and set the value of the control.
	 *
	 * This is a method of [internal API](https://developer.mozilla.org/en-US/docs/Web/API/ElementInternals), which is a part of the web standard for custom elements.
	 *
	 * @param {*} value The value to set.
	 * @throws {Error} the value setter must be overwritten by the derived class
	 */
	set value(value) {
		throw Error("the value setter must be overwritten by the derived class");
	}

	/**
	 * This is a method of [internal api](https://developer.mozilla.org/en-US/docs/Web/API/ElementInternals)
	 *
	 * @return {NodeList}
	 * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/ElementInternals/labels}
	 * @throws {Error} the ElementInternals is not supported and a polyfill is necessary
	 */
	get labels() {
		return getInternal.call(this)?.labels;
	}

	/**
	 * This is a method of [internal api](https://developer.mozilla.org/en-US/docs/Web/API/ElementInternals)
	 *
	 * @return {string|null}
	 */
	get name() {
		return this.getAttribute("name");
	}

	/**
	 * This is a method of [internal api](https://developer.mozilla.org/en-US/docs/Web/API/ElementInternals)
	 *
	 * @return {string}
	 */
	get type() {
		return this.constructor.getTag();
	}

	/**
	 * This is a method of [internal api](https://developer.mozilla.org/en-US/docs/Web/API/ElementInternals)
	 *
	 * @return {ValidityState}
	 * @throws {Error} the ElementInternals is not supported and a polyfill is necessary
	 * @see [ValidityState]{@link https://developer.mozilla.org/en-US/docs/Web/API/ValidityState}
	 * @see [validity]{@link https://developer.mozilla.org/en-US/docs/Web/API/validity}
	 */
	get validity() {
		return getInternal.call(this)?.validity;
	}

	/**
	 * This is a method of [internal api](https://developer.mozilla.org/en-US/docs/Web/API/ElementInternals)
	 *
	 * @return {string}
	 * @see https://developer.mozilla.org/en-US/docs/Web/API/ElementInternals/validationMessage
	 * @throws {Error} the ElementInternals is not supported and a polyfill is necessary
	 */
	get validationMessage() {
		return getInternal.call(this)?.validationMessage;
	}

	/**
	 * This is a method of [internal api](https://developer.mozilla.org/en-US/docs/Web/API/ElementInternals)
	 *
	 * @return {boolean}
	 * @see https://developer.mozilla.org/en-US/docs/Web/API/ElementInternals/willValidate
	 * @throws {Error} the ElementInternals is not supported and a polyfill is necessary
	 */
	get willValidate() {
		return getInternal.call(this)?.willValidate;
	}

	/**
	 * This is a method of [internal api](https://developer.mozilla.org/en-US/docs/Web/API/ElementInternals)
	 *
	 * @return {boolean}
	 * @see https://developer.mozilla.org/en-US/docs/Web/API/ElementInternals/states
	 * @throws {Error} the ElementInternals is not supported and a polyfill is necessary
	 */
	get states() {
		return getInternal.call(this)?.states;
	}

	/**
	 * This is a method of [internal api](https://developer.mozilla.org/en-US/docs/Web/API/ElementInternals)
	 *
	 * @return {HTMLFontElement|null}
	 * @see https://developer.mozilla.org/en-US/docs/Web/API/ElementInternals/form
	 * @throws {Error} the ElementInternals is not supported and a polyfill is necessary
	 */
	get form() {
		return getInternal.call(this)?.form;
	}

	/**
	 * This is a method of [internal api](https://developer.mozilla.org/en-US/docs/Web/API/ElementInternals)
	 *
	 * ```
	 * // Use the control's name as the base name for submitted data
	 * const n = this.getAttribute('name');
	 * const entries = new FormData();
	 * entries.append(n + '-first-name', this.firstName_);
	 * entries.append(n + '-last-name', this.lastName_);
	 * this.setFormValue(entries);
	 * ```
	 *
	 * @param {File|string|FormData} value
	 * @param {File|string|FormData} state
	 * @return {undefined}
	 * @throws {DOMException} NotSupportedError
	 * @throws {Error} the ElementInternals is not supported and a polyfill is necessary
	 * @see https://developer.mozilla.org/en-US/docs/Web/API/ElementInternals/setFormValue
	 */
	setFormValue(value, state) {
		getInternal.call(this).setFormValue(value, state);
	}

	/**
	 *
	 * @param {object} flags
	 * @param {string|undefined} message
	 * @param {HTMLElement} anchor
	 * @see https://developer.mozilla.org/en-US/docs/Web/API/ElementInternals/setValidity
	 * @return {undefined}
	 * @throws {DOMException} NotSupportedError
	 * @throws {Error} the ElementInternals is not supported and a polyfill is necessary
	 */
	setValidity(flags, message, anchor) {
		getInternal.call(this).setValidity(flags, message, anchor);
	}

	/**
	 * This is a method of [internal api](https://developer.mozilla.org/en-US/docs/Web/API/ElementInternals)
	 *
	 * @see https://developer.mozilla.org/en-US/docs/Web/API/ElementInternals/checkValidity
	 * @return {boolean}
	 * @throws {DOMException} NotSupportedError
	 * @throws {Error} the ElementInternals is not supported and a polyfill is necessary
	 */
	checkValidity() {
		return getInternal.call(this)?.checkValidity();
	}

	/**
	 * This is a method of [internal api](https://developer.mozilla.org/en-US/docs/Web/API/ElementInternals)
	 *
	 * @return {boolean}
	 * @see https://developer.mozilla.org/en-US/docs/Web/API/ElementInternals/reportValidity
	 * @throws {Error} the ElementInternals is not supported and a polyfill is necessary
	 * @throws {DOMException} NotSupportedError
	 */
	reportValidity() {
		return getInternal.call(this)?.reportValidity();
	}

	/**
	 * Sets the `form` attribute of the custom control to the `id` of the passed form element.
	 * If no form element is passed, removes the `form` attribute.
	 *
	 * @param {HTMLFormElement} form - The form element to associate with the control
	 */
	formAssociatedCallback(form) {
		if (form) {
			if (form.id) {
				this.setAttribute("form", form.id);
			}
		} else {
			this.removeAttribute("form");
		}
	}

	/**
	 * Sets or removes the `disabled` attribute of the custom control based on the passed value.
	 *
	 * @param {boolean} disabled - Whether or not the control should be disabled
	 */
	formDisabledCallback(disabled) {
		if (disabled) {
			if (!this.hasAttribute("disabled")) {
				this.setAttribute("disabled", "");
			}
		} else {
			if (this.hasAttribute("disabled")) {
				this.removeAttribute("disabled");
			}
		}
	}

	/**
	 * @param {string} state
	 * @param {string} mode
	 */
	formStateRestoreCallback(state, mode) {}

	/**
	 *
	 */
	formResetCallback() {
		this.value = this.getAttribute("value") ?? "";
	}
}

/**
 * @private
 * @return {object}
 * @throws {Error} the ElementInternals is not supported and a polyfill is necessary
 * @this CustomControl
 */
function getInternal() {
	if (!(attachedInternalSymbol in this)) {
		throw new Error(
			"ElementInternals is not supported and a polyfill is necessary",
		);
	}

	return this[attachedInternalSymbol];
}

const debounceValueSymbol = Symbol("debounceValue");

/**
 *
 * @issue https://gitlab.schukai.com/oss/libraries/javascript/monster/-/issues/290
 *
 * @private
 * @return {object}
 * @this CustomControl
 */
function initValueAttributeObserver() {
	const self = this;
	this[attributeObserverSymbol]["value"] = () => {
		if (self[debounceValueSymbol] instanceof DeadMansSwitch) {
			try {
				self[debounceValueSymbol].touch();
				return;
			} catch (e) {
				if (e.message !== "has already run") {
					throw e;
				}
				delete self[debounceValueSymbol];
			}
		}

		self[debounceValueSymbol] = new DeadMansSwitch(10, () => {
			const oldValue = self.getAttribute("value");
			const newValue = self.value;

			if (oldValue !== newValue) {
				setTimeout(() => {
					self.value = self.getAttribute("value");
				}, 0);
			}
		});
	};
}

const disabledSyncSymbol = Symbol("disabledSync");
const disabledAttributeSyncSymbol = Symbol("disabledAttributeSync");

/**
 * @private
 * @return {object}
 * @this CustomControl
 */
function initDisabledAttributeObserver() {
	const self = this;
	self[disabledAttributeSyncSymbol] = null;
	this[attributeObserverSymbol]["disabled"] = () => {
		const disabled = self.hasAttribute("disabled");
		if (self[disabledAttributeSyncSymbol] === disabled) {
			return;
		}

		const previous = self[disabledAttributeSyncSymbol];
		self[disabledAttributeSyncSymbol] = disabled;

		if (disabled) {
			if (self.getOption("disabled", false) !== true) {
				self.setOption("disabled", true);
			}
			return;
		}

		if (previous === true && self.getOption("disabled", false) !== false) {
			self.setOption("disabled", false);
		}
	};
	this[attributeObserverSymbol]["disabled"]();
}

/**
 * @private
 * @return {object}
 * @this CustomControl
 */
function initDisabledOptionObserver() {
	const self = this;

	const syncDisabledOptionToAttribute = () => {
		const disabled = !!self.getOption("disabled", false);
		if (self[disabledSyncSymbol].value === disabled) {
			return;
		}
		self[disabledSyncSymbol].value = disabled;

		if (disabled && !self.hasAttribute("disabled")) {
			self.setAttribute("disabled", "");
		} else if (!disabled && self.hasAttribute("disabled")) {
			self.removeAttribute("disabled");
		}
	};

	self[disabledSyncSymbol] = { value: null };
	syncDisabledOptionToAttribute();

	self.attachObserver(
		new Observer(() => {
			syncDisabledOptionToAttribute();
		}),
	);
}
