/**
 * 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 {
	assembleMethodSymbol,
	CustomElement,
	registerCustomElement,
} from "../../dom/customelement.mjs";
import { findElementWithSelectorUpwards } from "../../dom/util.mjs";
import { ThemeStyleSheet } from "../stylesheet/theme.mjs";
import { Datasource } from "./datasource.mjs";
import { SpinnerStyleSheet } from "../stylesheet/spinner.mjs";
import { isString } from "../../types/is.mjs";
import { instanceSymbol } from "../../constants.mjs";
import "../form/select.mjs";

import "./datasource/dom.mjs";
import "./datasource/rest.mjs";
import "../form/popper.mjs";
import "../form/context-error.mjs";
import { StatusStyleSheet } from "./stylesheet/status.mjs";
import { Formatter } from "../../text/formatter.mjs";

export { DatasourceStatus };

/**
 * @private
 * @type {symbol}
 */
const errorElementSymbol = Symbol.for("errorElement");

/**
 * @private
 * @type {symbol}
 */
const datasourceLinkedElementSymbol = Symbol("datasourceLinkedElement");
const spinnerElementSymbol = Symbol("spinnerElement");

/**
 * A simple dataset status component
 *
 * @fragments /fragments/components/datatable/datasource-status
 *
 * @example /examples/components/datatable/datasource-status-simple Simple dataset status
 *
 * @issue https://localhost.alvine.dev:8440/development/issues/closed/274.html
 *
 * @copyright Volker Schukai
 * @summary A datasource status indicator for loading, empty, error and ready states in table workflows.
 */
class DatasourceStatus extends CustomElement {
	/**
	 */
	constructor() {
		super();
	}

	/**
	 * This method is called by the `instanceof` operator.
	 * @return {symbol}
	 */
	static get [instanceSymbol]() {
		return Symbol.for("@schukai/monster/components/datatable/status@@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 Datasource configuration
	 * @property {string} datasource.selector The selector of the datasource
	 * @property {Object} callbacks Callbacks
	 * @property {Function} callbacks.onError Callback function for error handling <code>function(message: string, event: Event): string</code>
	 * @property {Object} timeouts Timeouts
	 * @property {number} timeouts.message Timeout for the message
	 * @property {Object} state State
	 */
	get defaults() {
		return Object.assign({}, super.defaults, {
			templates: {
				main: getTemplate(),
			},

			datasource: {
				selector: null,
			},

			callbacks: {
				onError: null,
			},

			timeouts: {
				message: 4000,
				spinnerMin: 200,
			},

			state: {
				spinner: "hide",
			},
		});
	}

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

	/**
	 * @private
	 */
	[assembleMethodSymbol]() {
		super[assembleMethodSymbol]();

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

	/**
	 *
	 * @param message
	 * @param timeout
	 * @returns {DatasourceStatus}
	 */
	setErrorMessage(message, timeout) {
		this[errorElementSymbol].setErrorMessage(message, timeout);
		return this;
	}

	/**
	 *
	 * @return [CSSStyleSheet]
	 */
	static getCSSStyleSheet() {
		return [StatusStyleSheet, SpinnerStyleSheet, ThemeStyleSheet];
	}
}

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

	this[errorElementSymbol] = this.shadowRoot.querySelector(
		"monster-context-error",
	);
	this[spinnerElementSymbol] =
		this.shadowRoot.querySelector(".monster-spinner");
}

/**
 * @private
 */
function initEventHandler() {
	const selector = this.getOption("datasource.selector", "");
	const self = this;

	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");
		}

		let hideTimer = null;
		let lastShowAt = 0;
		let requestVersion = 0;

		const setSpinnerState = (state) => {
			self.setOption("state.spinner", state);
			const spinner = self[spinnerElementSymbol];
			if (spinner) {
				spinner.setAttribute("data-monster-state-loader", state);
			}
		};
		const clearHideTimer = () => {
			if (hideTimer) {
				clearTimeout(hideTimer);
				hideTimer = null;
			}
		};
		const getSpinnerMinTimeout = () => {
			const value = Number(self.getOption("timeouts.spinnerMin", 0));
			return Number.isFinite(value) ? Math.max(0, value) : 0;
		};
		const scheduleHide = (version) => {
			clearHideTimer();
			const elapsed = Date.now() - lastShowAt;
			const delay = Math.max(0, getSpinnerMinTimeout() - elapsed);
			hideTimer = setTimeout(() => {
				hideTimer = null;
				if (version !== requestVersion) {
					return;
				}
				setSpinnerState("hide");
			}, delay);
		};
		const hideSpinner = () => {
			setSpinnerState("hide");
		};
		const showSpinner = () => {
			setSpinnerState("show");
		};

		this[datasourceLinkedElementSymbol] = element;
		hideSpinner();
		element.addEventListener("monster-datasource-fetched", function () {
			if (typeof self[errorElementSymbol]?.resetErrorMessage === "function") {
				self[errorElementSymbol].resetErrorMessage();
			}
			scheduleHide(requestVersion);
		});

		element.addEventListener("monster-datasource-fetch", function () {
			requestVersion += 1;
			lastShowAt = Date.now();
			clearHideTimer();
			if (typeof self[errorElementSymbol]?.resetErrorMessage === "function") {
				self[errorElementSymbol].resetErrorMessage();
			}

			showSpinner();
		});

		element.addEventListener("monster-datasource-error", function (event) {
			scheduleHide(requestVersion);

			const timeout = self.getOption("timeouts.message", 4000);
			let msg = "Cannot load data";

			try {
				if (event.detail.error instanceof Error) {
					msg = event.detail.error.message;
				} else if (event.detail.error instanceof Object) {
					msg = JSON.stringify(event.detail.error);
				} else if (event.detail.error instanceof String) {
					msg = event.detail.error;
				} else if (event.detail.error instanceof Number) {
					msg = event.detail.error.toString();
				} else {
					msg = event.detail.error;
				}
			} catch (e) {
			} finally {
				const callback = self.getOption("callbacks.onError", null);
				if (callback) {
					callback.call(self, msg, event);
				} else {
					self[errorElementSymbol].setErrorMessage(msg, timeout);
				}
			}
		});
	}
}

/**
 * @private
 * @return {string}
 */
function getTemplate() {
	// language=HTML
	return `
        <div data-monster-role="control" part="control"
             data-monster-attributes="disabled path:disabled | if:true">
            <monster-context-error
                    data-monster-option-classes-button="monster-theme-error-2 monster-theme-background-inherit"></monster-context-error>
            <div class="monster-spinner"
                 data-monster-attributes="data-monster-state-loader path:state.spinner"></div>
        </div>
    `;
}

registerCustomElement(DatasourceStatus);
