/**
 * Copyright 2023 schukai GmbH
 * SPDX-License-Identifier: AGPL-3.0
 */

import { instanceSymbol, internalSymbol } from "../../constants.mjs";
import { Pathfinder } from "../../data/pathfinder.mjs";
import { getLinkedObjects, hasObjectLink } from "../../dom/attributes.mjs";
import { customElementUpdaterLinkSymbol } from "../../dom/constants.mjs";
import {
	assembleMethodSymbol,
	CustomElement,
	attributeObserverSymbol,
	registerCustomElement,
} from "../../dom/customelement.mjs";
import { isString } from "../../types/is.mjs";
import { Observer } from "../../types/observer.mjs";
import { clone } from "../../util/clone.mjs";
import {
	ATTRIBUTE_DATASOURCE_SELECTOR,
	ATTRIBUTE_DATATABLE_INDEX,
} from "./constants.mjs";
import { Datasource } from "./datasource.mjs";
import { DatasetStyleSheet } from "./stylesheet/dataset.mjs";
import {
	handleDataSourceChanges,
	datasourceLinkedElementSymbol,
} from "./util.mjs";
import { FormStyleSheet } from "../stylesheet/form.mjs";

export { DataSet };

/**
 * The data set component is used to show the data of a data source.
 *
 * <img src="./images/dataset.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-dataset />` directly in the HTML or using
 * Javascript via the `document.createElement('monster-dataset');` method.
 *
 * ```html
 * <monster-dataset></monster-dataset>
 * ```
 *
 * Or you can create this CustomControl directly in Javascript:
 *
 * ```js
 * import '@schukai/component-datatable/source/dataset.mjs';
 * document.createElement('monster-dataset');
 * ```
 *
 * The Body should have a class "hidden" to ensure that the styles are applied correctly.
 *
 * ```css
 * body.hidden {
 *    visibility: hidden;
 * }
 * ```
 *
 * @startuml dataset.png
 * skinparam monochrome true
 * skinparam shadowing false
 * HTMLElement <|-- CustomElement
 * CustomElement <|-- DataSet
 * @enduml
 *
 * @copyright schukai GmbH
 * @memberOf Monster.Components.Datatable
 * @summary A data set
 */
class DataSet extends CustomElement {
	/**
	 * This method is called by the `instanceof` operator.
	 * @returns {symbol}
	 */
	static get [instanceSymbol]() {
		return Symbol.for("@schukai/monster/components/dataset@@instance");
	}

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

	/**
	 * 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 {object} mapping The mapping
	 * @property {string} mapping.data The data
	 * @property {number} mapping.index The index
	 * @property {Array} data The data
	 */
	get defaults() {
		const obj = Object.assign({}, super.defaults, {
			templates: {
				main: getTemplate(),
			},

			datasource: {
				selector: null,
			},

			mapping: {
				data: "dataset",
				index: 0,
			},

			data: {},
		});

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

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

	write() {;

		return new Promise((resolve, reject) => {
			if (!this[datasourceLinkedElementSymbol]) {
				reject(new Error("No datasource"));
				return;
			}

			const internalUpdateCloneData = this.getInternalUpdateCloneData();
			if (!internalUpdateCloneData) {
				reject(new Error("No update data"));
				return;
			}

			const internalData = internalUpdateCloneData?.["data"];
			if (
				internalData === undefined ||
				internalData === null ||
				internalData === ""
			) {
				reject(new Error("No data"));
				return;
			}

			setTimeout(() => {
				const path = this.getOption("mapping.data");
				const index = this.getOption("mapping.index");

				let pathWithIndex;

				if (isString(path) && path !== "") {
					pathWithIndex = path + "." + index;
				} else {
					pathWithIndex = index;
				}

				const data = this[datasourceLinkedElementSymbol].data;
				const unref = JSON.stringify(data);
				const ref = JSON.parse(unref);

				new Pathfinder(ref).setVia(pathWithIndex, internalData);

				this[datasourceLinkedElementSymbol].data = ref;

				resolve();
			}, 0);
		});
	}

	/**
	 * 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]();

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

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

		if (isString(selector)) {
			const elements = document.querySelectorAll(selector);
			if (elements.length !== 1) {
				throw new Error("the selector must match exactly one element");
			}

			const element = elements[0];
			if (!(element instanceof Datasource)) {
				throw new TypeError("the element must be a datasource");
			}

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

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

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

/**
 * @private
 */
function initEventHandler() {
	this[attributeObserverSymbol][ATTRIBUTE_DATATABLE_INDEX] = () => {
		const index = this.getAttribute(ATTRIBUTE_DATATABLE_INDEX);
		if (index) {
			this.setOption("mapping.index", parseInt(index, 10));
		}
	};
}

/**
 *
 * @param {Object} options
 */
function updateOptionsFromArguments(options) {
	const index = this.getAttribute(ATTRIBUTE_DATATABLE_INDEX);

	if (index !== null && index !== undefined) {
		options.mapping.index = parseInt(index, 10);
	}

	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">
            <slot></slot>
        </div>
    `;
}

registerCustomElement(DataSet);