/**
 * 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 { internalSymbol } from "../../constants.mjs";
import { Datasource } from "../../data/datasource.mjs";
import { RestAPI } from "../../data/datasource/server/restapi.mjs";
import { WebConnect } from "../../data/datasource/server/webconnect.mjs";
import { WriteError } from "../../data/datasource/server/restapi/writeerror.mjs";
import { LocalStorage } from "../../data/datasource/storage/localstorage.mjs";
import { SessionStorage } from "../../data/datasource/storage/sessionstorage.mjs";
import {
	ATTRIBUTE_DISABLED,
	ATTRIBUTE_ERRORMESSAGE,
	ATTRIBUTE_PREFIX,
	ATTRIBUTE_UPDATER_ATTRIBUTES,
	ATTRIBUTE_UPDATER_INSERT,
	ATTRIBUTE_UPDATER_REMOVE,
	ATTRIBUTE_UPDATER_REPLACE,
} from "../../dom/constants.mjs";
import {
	assembleMethodSymbol,
	CustomElement,
	registerCustomElement,
	getSlottedElements,
} from "../../dom/customelement.mjs";
import { addObjectWithUpdaterToElement } from "../../dom/updater.mjs";
import { isFunction, isString } from "../../types/is.mjs";
import { Observer } from "../../types/observer.mjs";
import { ProxyObserver } from "../../types/proxyobserver.mjs";
import { Processing } from "../../util/processing.mjs";
import { MessageStateButton } from "./message-state-button.mjs";
import {
	ATTRIBUTE_FORM_DATASOURCE,
	ATTRIBUTE_FORM_DATASOURCE_ARGUMENTS,
} from "./constants.mjs";
import { StateButton } from "./state-button.mjs";
import { FormStyleSheet } from "./stylesheet/form.mjs";

export { Form };

/**
 * @private
 * @since 3.1.0
 * @type {string}
 */
const ATTRIBUTE_FORM_DATASOURCE_ACTION = `${ATTRIBUTE_PREFIX}datasource-action`;

/**
 * Form data is the internal representation of the form data
 *
 * @private
 * @type {symbol}
 * @since 1.7.0
 */
const formDataSymbol = Symbol.for(
	"@schukai/monster/components/form/form@@formdata",
);

/**
 * @private
 * @type {symbol}
 * @since 2.8.0
 */
const formDataUpdaterSymbol = Symbol.for(
	"@schukai/component-form/form@@formdata-updater-link",
);

/**
 * @private
 * @type {symbol}
 * @since 1.7.0
 */
const formElementSymbol = Symbol.for(
	"@schukai/component-form/form@@form-element",
);

/**
 * @private
 * @type {symbol}
 * @since 2.5.0
 */
const registeredDatasourcesSymbol = Symbol.for(
	"@schukai/component-form/form@@registered-datasources",
);

/**
 * @private
 * @since 1.7.0
 * @type {string}
 */
const PROPERTY_VALIDATION_KEY = "__validation";

/**
 * This CustomControl creates a form element with a variety of options.
 *
 * <img src="./images/form.png">
 *
 * Dependencies: the system uses functions of the [monsterjs](https://monsterjs.org/) library.
 *
 * You can create this control either by specifying the HTML tag `<monster-form />` directly in the HTML or using
 * Javascript via the `document.createElement('monster-form');` method.
 *
 * ```html
 * <monster-form></monster-form>
 * ```
 *
 * Or you can create this CustomControl directly in Javascript:
 *
 * ```js
 * import {Form} from '@schukai/component-form/source/form.js';
 * document.createElement('monster-form');
 * ```
 *
 * @startuml form.png
 * skinparam monochrome true
 * skinparam shadowing false
 * HTMLElement <|-- CustomElement
 * CustomElement <|-- Form
 * @enduml
 *
 * @since 1.6.0
 * @copyright schukai GmbH
 * @memberOf Monster.Components.Form
 * @summary A configurable form control
 */
class Form extends CustomElement {
	/**
	 * @throws {Error} the options attribute does not contain a valid json definition.
	 * @since 1.7.0
	 */
	constructor() {
		super();
		this[formDataSymbol] = new ProxyObserver({});
	}

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

	/**
	 * 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 {Datasource} datasource data source
	 * @property {Object} reportValidity
	 * @property {string} reportValidity.selector which element should be used to report the validity
	 * @property {function} reportValidity.errorHandler function to handle the error
	 * @property {Object} classes
	 * @property {string} classes.button class for the form
	 */
	get defaults() {
		return Object.assign(
			{},
			super.defaults,
			{
				templates: {
					main: getTemplate(),
				},
				datasource: undefined,
				reportValidity: {
					selector: "input,select,textarea",
					errorHandler: undefined,
				},
				classes: {
					form: "monster-form",
				},
			},
			initOptionsFromArguments.call(this),
		);
	}

	/**
	 * Called every time the element is inserted into the DOM. Useful for running setup code, such as
	 * fetching resources or rendering. Generally, you should try to delay work until this time.
	 *
	 * @return {void}
	 */
	connectedCallback() {
		super["connectedCallback"]();
	}

	/**
	 * The refresh method is called to update the control after a change with fresh data.
	 *
	 * Therefore, the data source is called again and the data is updated.
	 *
	 * If you have updated the data source with `setOption('datasource',datasource), you must call this method.
	 *
	 * @return {Form}
	 * @throws {Error} undefined datasource
	 */
	refresh() {
		try {
			this.setAttribute(ATTRIBUTE_DISABLED, "");
			const datasource = this.getOption("datasource");

			if (!(datasource instanceof Datasource)) {
				throw new Error("undefined datasource");
			}

			return datasource
				.read()
				.then(() => {
					this[formDataSymbol].setSubject(datasource.get());
				})
				.then(() => {
					new Processing(() => {
						this.removeAttribute(ATTRIBUTE_DISABLED);
					}).run();
				})
				.catch((e) => {
					this.removeAttribute(ATTRIBUTE_DISABLED);
					this.setAttribute(ATTRIBUTE_ERRORMESSAGE, e.toString());
				});
		} catch (e) {
			this.setAttribute(ATTRIBUTE_ERRORMESSAGE, e.toString());
			this.removeAttribute(ATTRIBUTE_DISABLED);
			throw e;
		}
	}

	/**
	 *
	 * @return {Monster.Components.Form.Form}
	 */
	[assembleMethodSymbol]() {
		super[assembleMethodSymbol]();

		initControlReferences.call(this);
		initDatasource.call(this);
		initUpdater.call(this);
		initObserver.call(this);

		return this;
	}

	/**
	 *
	 * @return {*}
	 */
	getValues() {
		return this[formDataSymbol].getSubject();
	}

	/**
	 *
	 * @return {string}
	 */
	static getTag() {
		return "monster-form";
	}

	/**
	 *
	 * @return {CSSStyleSheet}
	 */
	static getCSSStyleSheet() {
		return [FormStyleSheet];
	}

	static [registeredDatasourcesSymbol] = new Map([
		["restapi", RestAPI],
		["localstorage", LocalStorage],
		["sessionstorage", SessionStorage],
		["webconnect", WebConnect],
	]);

	/**
	 * Register a new datasource
	 *
	 * @param {string} name
	 * @param {Monster.Data.Datasource} datasource
	 */
	static registerDatasource(name, datasource) {
		Form[registeredDatasourcesSymbol].set(name, datasource);
	}

	/**
	 * Unregister a registered datasource
	 *
	 * @param {string} name
	 */
	static unregisterDatasource(name) {
		Form[registeredDatasourcesSymbol].delete(name);
	}

	/**
	 * Get registered data sources
	 *
	 * @return {Map}
	 */
	static getDatasources() {
		return Form[registeredDatasourcesSymbol];
	}

	/**
	 * Run reportValidation on all child html form controls.
	 *
	 * @since 2.10.0
	 * @returns {boolean}
	 */
	reportValidity() {
		let valid = true;

		const selector = this.getOption("reportValidity.selector");
		const nodes = getSlottedElements.call(this, selector);
		nodes.forEach((node) => {
			if (typeof node.reportValidity === "function") {
				if (node.reportValidity() === false) {
					valid = false;
				}
			}
		});

		return valid;
	}
}

/**
 * @private
 */
function initUpdater() {
	if (!this.shadowRoot) {
		throw new Error("no shadow-root is defined");
	}

	const slots = this.shadowRoot.querySelectorAll("slot");
	for (const [, slot] of Object.entries(slots)) {
		for (const [, node] of Object.entries(slot.assignedNodes())) {
			if (!(node instanceof HTMLElement)) {
				continue;
			}

			const query = `[${ATTRIBUTE_UPDATER_ATTRIBUTES}],[${ATTRIBUTE_UPDATER_REPLACE}],[${ATTRIBUTE_UPDATER_REMOVE}],[${ATTRIBUTE_UPDATER_INSERT}]`;
			const controls = node.querySelectorAll(query);

			const list = new Set([...controls]);

			if (node.matches(query)) {
				list.add(node);
			}

			if (list.size === 0) {
				continue;
			}

			addObjectWithUpdaterToElement.call(
				node,
				list,
				formDataUpdaterSymbol,
				this[formDataSymbol],
			);
		}
	}
}

/**
 * @private
 */
function initDatasource() {
	if (!this.shadowRoot) {
		throw new Error("no shadow-root is defined");
	}

	const slots = this.shadowRoot.querySelectorAll("slot");
	for (const [, slot] of Object.entries(slots)) {
		for (const [, node] of Object.entries(slot.assignedNodes())) {
			if (!(node instanceof HTMLElement)) {
				continue;
			}

			const query = `[${ATTRIBUTE_FORM_DATASOURCE_ACTION}=write]`;
			const controls = node.querySelectorAll(query);

			const list = new Set([...controls]);

			if (node.matches(query)) {
				list.add(node);
			}

			if (list.size === 0) {
				continue;
			}

			initWriteActions.call(this, list);
		}
	}
}

/**
 * @private
 * @param elements
 */
function initWriteActions(elements) {
	elements.forEach((element) => {
		if (element instanceof HTMLElement) {
			element.addEventListener("click", () => {
				runWriteCallback.call(this, element);
			});

			const g = element?.getOption;
			if (!isFunction(g)) {
				return;
			}

			const s = element?.setOption;
			if (!isFunction(s)) {
				return;
			}

			const fn = element.getOption("actions.click");

			if (!isFunction(fn)) {
				return;
			}

			// disable console.log of standard click event
			element.setOption("actions.click", function () {
				// do nothing!
			});
		}
	});
}

function runWriteCallback(button) {
	if (typeof this.reportValidity === "function") {
		if (this.reportValidity() === false) {
			if (
				button instanceof StateButton ||
				button instanceof MessageStateButton
			) {
				button.setState("failed");
			}
			return;
		}
	}

	const datasource = this.getOption("datasource");
	if (!(datasource instanceof Datasource)) {
		return;
	}

	if (button instanceof StateButton || button instanceof MessageStateButton) {
		button.setState("activity");
	}

	//const data = form?.[formDataSymbol]?.getRealSubject();
	const writePromise = datasource
		.set(this[formDataSymbol].getRealSubject())
		.write();
	if (!(writePromise instanceof Promise)) {
		throw new Error("datasource.write() must return a promise");
	}

	writePromise
		.then((r) => {
			if (
				button instanceof StateButton ||
				button instanceof MessageStateButton
			) {
				button.setState("successful");
			}
			this[formDataSymbol].getSubject()[PROPERTY_VALIDATION_KEY] = {};
		})
		.catch((e) => {
			if (e instanceof WriteError) {
				this[formDataSymbol].getSubject()[PROPERTY_VALIDATION_KEY] =
					e.getValidation();
			}

			if (
				button instanceof StateButton ||
				button instanceof MessageStateButton
			) {
				button.setState("failed");
			}

			if (button instanceof MessageStateButton) {
				button.setMessage(e.message);
				button.showMessage();
			}
		});
}

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

	const datasource = this.getAttribute(ATTRIBUTE_FORM_DATASOURCE);
	if (isString(datasource)) {
		for (const [key, classObject] of Form.getDatasources()) {
			if (datasource === key) {
				let args = this.getAttribute(ATTRIBUTE_FORM_DATASOURCE_ARGUMENTS);

				try {
					args = JSON.parse(args);
				} catch (e) {
					this.setAttribute(ATTRIBUTE_ERRORMESSAGE, e.toString());
					continue;
				}

				try {
					options["datasource"] = new classObject(args);
				} catch (e) {
					this.setAttribute(ATTRIBUTE_ERRORMESSAGE, e.toString());
					continue;
				}

				break;
			}

			if (options["datasource"] instanceof Datasource) {
				break;
			}
		}
	}

	return options;
}

/**
 * @private
 * @this Form
 */
function initObserver() {
	const self = this;

	let lastDatasource = null;
	self[internalSymbol].attachObserver(
		new Observer(function () {
			const datasource = self.getOption("datasource");
			if (datasource !== lastDatasource) {
				new Processing(100, function () {
					self.refresh();
				}).run();
			}

			lastDatasource = datasource;
		}),
	);
}

/**
 * @private
 * @return {Monster.Components.Form.Form}
 */
function initControlReferences() {
	if (!this.shadowRoot) {
		throw new Error("no shadow-root is defined");
	}

	this[formElementSymbol] = this.shadowRoot.querySelector(
		"[data-monster-role=form]",
	);
	return this;
}

/**
 * @private
 * @return {string}
 */
function getTemplate() {
	// language=HTML
	return `
        <div data-monster-role="control" part="control">
            <form data-monster-attributes="disabled path:disabled | if:true, class path:classes.form"
                  data-monster-role="form"
                  part="form">
                <slot data-monster-role="slot"></slot>
            </form>
        </div>
    `;
}

registerCustomElement(Form);