/**
 * Copyright © schukai GmbH 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 schukai GmbH.
 *
 * SPDX-License-Identifier: AGPL-3.0
 */

import { instanceSymbol, internalSymbol } from "../../constants.mjs";
import { diff } from "../../data/diff.mjs";
import { addAttributeToken } from "../../dom/attributes.mjs";
import { ATTRIBUTE_ERRORMESSAGE } from "../../dom/constants.mjs";
import {
	assembleMethodSymbol,
	CustomElement,
	attributeObserverSymbol,
	registerCustomElement,
} from "../../dom/customelement.mjs";
import { findElementWithSelectorUpwards } from "../../dom/util.mjs";
import { isString, isArray } from "../../types/is.mjs";
import { Observer } from "../../types/observer.mjs";
import { TokenList } from "../../types/tokenlist.mjs";
import { clone } from "../../util/clone.mjs";
import { State } from "../form/types/state.mjs";
import { ATTRIBUTE_DATASOURCE_SELECTOR } from "./constants.mjs";
import { Datasource } from "./datasource.mjs";
import { BadgeStyleSheet } from "../stylesheet/badge.mjs";
import { SaveButtonStyleSheet } from "./stylesheet/save-button.mjs";

import {
	handleDataSourceChanges,
	datasourceLinkedElementSymbol,
} from "./util.mjs";

export { SaveButton };

/**
 * @private
 * @type {symbol}
 */
const stateButtonElementSymbol = Symbol("stateButtonElement");

/**
 * @private
 * @type {symbol}
 */
const originValuesSymbol = Symbol("originValues");

/**
 * @private
 * @type {symbol}
 */
const badgeElementSymbol = Symbol("badgeElement");

class SaveButton extends CustomElement {
	/**
	 * This method is called by the `instanceof` operator.
	 * @return {symbol}
	 */
	static get [instanceSymbol]() {
		return Symbol.for(
			"@schukai/monster/components/datasource/save-button@@instance",
		);
	}

	/**
	 * 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 {object} datasource The datasource
	 * @property {string} datasource.selector The selector of the datasource
	 * @property {string} labels.button The button label
	 * @property {Object} classes The classes
	 * @property {string} classes.bar The bar class
	 * @property {string} classes.badge The badge class
	 * @property {Array} ignoreChanges The ignore changes (regex)
	 * @property {Array} data The data
	 * @return {Object}
	 */
	get defaults() {
		const obj = Object.assign({}, super.defaults, {
			templates: {
				main: getTemplate(),
			},

			labels: {
				button: "save",
			},

			classes: {
				bar: "monster-button-primary",
				badge: "monster-badge-secondary hidden",
			},

			datasource: {
				selector: null,
			},

			changes: "0",

			ignoreChanges: [],

			data: {},

			disabled: false,
		});

		updateOptionsFromArguments.call(this, obj);
		return obj;
	}

	/**
	 *
	 * @return {string}
	 */
	static getTag() {
		return "monster-datasource-save-button";
	}

	/**
	 * This method is responsible for assembling the component.
	 *
	 * It calls the parent's assemble method first, then initializes control references and event handlers.
	 * If the `datasource.selector` option is provided and is a string, it searches for the corresponding
	 * element in the DOM using that selector.
	 *
	 * If the selector matches exactly one element, it checks if the element is an instance of the `Datasource` class.
	 *
	 * If it is, the component's `datasourceLinkedElementSymbol` property is set to the element, and the component
	 * attaches an observer to the datasource's changes.
	 *
	 * The observer is a function that calls the `handleDataSourceChanges` method in the context of the component.
	 * Additionally, the component attaches an observer to itself, which also calls the `handleDataSourceChanges`
	 * method in the component's context.
	 */
	[assembleMethodSymbol]() {
		super[assembleMethodSymbol]();
		const self = this;

		initControlReferences.call(this);
		initEventHandler.call(this);

		const selector = this.getOption("datasource.selector");

		if (isString(selector)) {
			const element = findElementWithSelectorUpwards(this, selector);
			if (element === null) {
				throw new Error("the selector must match exactly one element");
			}

			if (!(element instanceof Datasource)) {
				throw new TypeError("the element must be a datasource");
			}

			this[datasourceLinkedElementSymbol] = element;
			element.datasource.attachObserver(
				new Observer(handleDataSourceChanges.bind(this)),
			);

			self[originValuesSymbol] = null;

			element.datasource.attachObserver(
				new Observer(function () {
					if (!self[originValuesSymbol]) {
						self[originValuesSymbol] = clone(
							self[datasourceLinkedElementSymbol].data,
						);
					}

					const currentValues = this.getRealSubject();
					const ignoreChanges = self.getOption("ignoreChanges");

					const result = diff(self[originValuesSymbol], currentValues);
					if (isArray(ignoreChanges) && ignoreChanges.length > 0) {
						const itemsToRemove = [];
						for (const item of result) {
							for (const ignorePattern of ignoreChanges) {
								const p = new RegExp(ignorePattern);
								if (p.test(item.path)) {
									itemsToRemove.push(item);
									break;
								}
							}
						}

						for (const itemToRemove of itemsToRemove) {
							const index = result.indexOf(itemToRemove);
							if (index > -1) {
								result.splice(index, 1);
							}
						}
					}

					if (isArray(result) && result.length > 0) {
						self[stateButtonElementSymbol].setState("changed");
						self[stateButtonElementSymbol].setOption("disabled", false);
						self.setOption("changes", result.length);
						self.setOption(
							"classes.badge",
							new TokenList(self.getOption("classes.badge"))
								.remove("hidden")
								.toString(),
						);
					} else {
						self[stateButtonElementSymbol].removeState();
						self[stateButtonElementSymbol].setOption("disabled", true);
						self.setOption("changes", 0);
						self.setOption(
							"classes.badge",
							new TokenList(self.getOption("classes.badge"))
								.add("hidden")
								.toString(),
						);
					}
				}),
			);
		}

		this.attachObserver(
			new Observer(() => {
				handleDataSourceChanges.call(this);
			}),
		);
	}

	/**
	 *
	 * @return [CSSStyleSheet]
	 */
	static getCSSStyleSheet() {
		return [SaveButtonStyleSheet, BadgeStyleSheet];
	}
}

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

	this[stateButtonElementSymbol] = this.shadowRoot.querySelector(
		"[data-monster-role=state-button]",
	);

	this[badgeElementSymbol] = this.shadowRoot.querySelector(
		"[data-monster-role=badge]",
	);

	if (this[stateButtonElementSymbol]) {
		queueMicrotask(() => {
			const states = {
				changed: new State(
					"changed",
					'<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-cloud-arrow-up" viewBox="0 0 16 16">\n' +
						'  <path fill-rule="evenodd" d="M7.646 5.146a.5.5 0 0 1 .708 0l2 2a.5.5 0 0 1-.708.708L8.5 6.707V10.5a.5.5 0 0 1-1 0V6.707L6.354 7.854a.5.5 0 1 1-.708-.708z"/>\n' +
						'  <path d="M4.406 3.342A5.53 5.53 0 0 1 8 2c2.69 0 4.923 2 5.166 4.579C14.758 6.804 16 8.137 16 9.773 16 11.569 14.502 13 12.687 13H3.781C1.708 13 0 11.366 0 9.318c0-1.763 1.266-3.223 2.942-3.593.143-.863.698-1.723 1.464-2.383m.653.757c-.757.653-1.153 1.44-1.153 2.056v.448l-.445.049C2.064 6.805 1 7.952 1 9.318 1 10.785 2.23 12 3.781 12h8.906C13.98 12 15 10.988 15 9.773c0-1.216-1.02-2.228-2.313-2.228h-.5v-.5C12.188 4.825 10.328 3 8 3a4.53 4.53 0 0 0-2.941 1.1z"/>\n' +
						"</svg>",
				),
			};

			this[stateButtonElementSymbol].removeState();
			this[stateButtonElementSymbol].setOption("disabled", "disabled");
			this[stateButtonElementSymbol].setOption("states", states);
			this[stateButtonElementSymbol].setOption(
				"labels.button",
				this.getOption("labels.button"),
			);
		});
	}

	return this;
}

/**
 * @private
 */
function initEventHandler() {
	queueMicrotask(() => {
		this[stateButtonElementSymbol].setOption("actions.click", () => {
			this[datasourceLinkedElementSymbol]
				.write()
				.then(() => {
					this[originValuesSymbol] = null;
					this[stateButtonElementSymbol].removeState();
					this[stateButtonElementSymbol].setOption("disabled", true);
					this.setOption("changes", 0);
					this.setOption(
						"classes.badge",
						new TokenList(this.getOption("classes.badge"))
							.add("hidden")
							.toString(),
					);
				})
				.catch((error) => {
					addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, error.toString());
				});
		});
	});
}

/**
 * @param {Object} options
 */
function updateOptionsFromArguments(options) {
	const selector = this.getAttribute(ATTRIBUTE_DATASOURCE_SELECTOR);
	if (selector) {
		options.datasource.selector = selector;
	}
}

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

registerCustomElement(SaveButton);