/**
 * 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,
	getSlottedElements,
	registerCustomElement,
} from "../../dom/customelement.mjs";
import { CollapseStyleSheet } from "./stylesheet/collapse.mjs";
import { fireCustomEvent } from "../../dom/events.mjs";
import { getDocument } from "../../dom/util.mjs";
import { addAttributeToken } from "../../dom/attributes.mjs";
import { ATTRIBUTE_ERRORMESSAGE } from "../../dom/constants.mjs";
import { Host } from "../host/host.mjs";
import { generateUniqueConfigKey } from "../host/util.mjs";
import { DeadMansSwitch } from "../../util/deadmansswitch.mjs";
import { instanceSymbol } from "../../constants.mjs";
import { Queue } from "../../types/queue.mjs";

export { Collapse, nameSymbol };

/**
 * @private
 * @type {symbol}
 */
const timerCallbackSymbol = Symbol("timerCallback");

/**
 * @private
 * @type {symbol}
 */
const detailsElementSymbol = Symbol("detailsElement");

/**
 * @private
 * @type {symbol}
 */
const controlElementSymbol = Symbol("controlElement");

/**
 * local symbol
 * @private
 * @type {symbol}
 */
const resizeObserverSymbol = Symbol("resizeObserver");

/**
 * @private
 * @type {symbol}
 */
const detailsSlotElementSymbol = Symbol("detailsSlotElement");

/**
 * @private
 * @type {symbol}
 */
const detailsContainerElementSymbol = Symbol("detailsContainerElement");

/**
 * @private
 * @type {symbol}
 */
const detailsDecoElementSymbol = Symbol("detailsDecoElement");

/**
 * @private
 * @type {symbol}
 */
const eventQueueSymbol = Symbol("eventQueue");

/**
 * @private
 * @type {symbol}
 */
const isTransitioningSymbol = Symbol("isTransitioning");

/**
 * @private
 * @type {symbol}
 */
const nameSymbol = Symbol("name");

/**
 * A Collapse component
 *
 * @fragments /fragments/components/layout/collapse/
 *
 * @example /examples/components/layout/collapse-simple
 * @example /examples/components/layout/collapse-with-events Collapse With Events
 *
 * @since 3.74.0
 * @copyright Volker Schukai
 * @summary A collapse component for expandable sections, progressive disclosure and accordion-style content.
 */
class Collapse extends CustomElement {
	/**
	 * This method is called by the `instanceof` operator.
	 * @return {symbol}
	 */
	static get [instanceSymbol]() {
		return Symbol.for("@schukai/monster/components/layout/collapse@@instance");
	}

	/**
	 *
	 */
	constructor() {
		super();
		// the name is only used for the host config and the event name
		this[nameSymbol] = "collapse";
		this[eventQueueSymbol] = new Queue();
	}

	/**
	 * 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} classes CSS classes
	 * @property {string} classes.container CSS class for the container
	 * @property {Object} features Feature configuration
	 * @property {boolean} features.accordion Enable accordion mode
	 * @property {boolean} features.persistState Enable persist state (Host and Config-Manager required)
	 * @property {boolean} features.useScrollValues Use scroll values (scrollHeight) instead of clientHeight for the height calculation
	 * @property {boolean} openByDefault Open the details by default
	 */
	get defaults() {
		return Object.assign({}, super.defaults, {
			templates: {
				main: getTemplate(),
			},
			classes: {
				container: "padding",
			},
			features: {
				accordion: true,
				persistState: true,
				useScrollValues: false,
			},
			openByDefault: false,
		});
	}

	/**
	 *
	 * @return {void}
	 */
	[assembleMethodSymbol]() {
		super[assembleMethodSymbol]();
		initControlReferences.call(this);
		initStateFromHostConfig.call(this);
		initResizeObserver.call(this);
		initEventHandler.call(this);

		if (this.getOption("openByDefault")) {
			this.open();
		}
	}

	/**
	 * @return {void}
	 */
	connectedCallback() {
		super.connectedCallback();
		updateResizeObserverObservation.call(this);
	}

	/**
	 * @return {void}
	 */
	disconnectedCallback() {
		super.disconnectedCallback();
	}

	/**
	 * @return {Collapse}
	 */
	toggle() {
		if (this[isTransitioningSymbol]) return this;
		if (this[detailsElementSymbol].classList.contains("active")) {
			this.close();
		} else {
			this.open();
		}
		return this;
	}

	/**
	 * @return {boolean}
	 */
	isClosed() {
		return !this[detailsElementSymbol].classList.contains("active");
	}

	/**
	 * @return {boolean}
	 */
	isOpen() {
		return !this.isClosed();
	}

	/**
	 * Open the collapse
	 * @return {Collapse}
	 * @fires monster-collapse-before-open This event is fired before the collapse is opened.
	 * @fires monster-collapse-open This event is fired after the collapse is opened.
	 */
	open() {
		this[eventQueueSymbol].add("open");
		runEventQueue.call(this);
		return this;
	}

	/**
	 * Close the collapse
	 * @return {Collapse}
	 * @fires monster-collapse-before-close This event is fired before the collapse is closed.
	 * @fires monster-collapse-closed This event is fired after the collapse is closed.
	 */
	close() {
		this[eventQueueSymbol].add("close");
		runEventQueue.call(this);
		return this;
	}

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

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

	/**
	 * This method is called when the element is inserted into a document, including into a shadow tree.
	 * @return {Collapse}
	 * @fires monster-collapse-adjust-height This event is fired when the height is adjusted. As a detail, the height is passed.
	 */
	adjustHeight() {
		adjustHeight.call(this);
		return this;
	}
}

function runEventQueue() {
	if (this[isTransitioningSymbol]) {
		return;
	}
	if (this[eventQueueSymbol].isEmpty()) {
		return;
	}

	const command = this[eventQueueSymbol].peek();
	if (command === "open") {
		this[eventQueueSymbol].poll();
		handleOpenCommand.call(this);
	} else if (command === "close") {
		this[eventQueueSymbol].poll();
		handleCloseCommand.call(this);
	} else {
		this[eventQueueSymbol].remove();
		throw new Error("Unknown command: " + command);
	}
}

/**
 * @private
 * @returns {handleCloseCommand}
 */
function handleCloseCommand() {
	if (!this[detailsElementSymbol].classList.contains("active")) {
		return this;
	}

	this[isTransitioningSymbol] = true;

	fireCustomEvent(this, "monster-" + this[nameSymbol] + "-before-close", {});
	this[controlElementSymbol].classList.add("overflow-hidden");

	setTimeout(() => {
		this[detailsElementSymbol].classList.remove("active");
		setTimeout(() => {
			updateStateConfig.call(this);
			fireCustomEvent(this, "monster-" + this[nameSymbol] + "-closed", {});
			this[isTransitioningSymbol] = false; // <<< Sperre freigeben
			runEventQueue.call(this);
		}, 0);
	}, 0);
}

/**
 * @private
 * @returns {handleOpenCommand}
 */
function handleOpenCommand() {
	let node;
	if (this[detailsElementSymbol].classList.contains("active")) {
		return this;
	}

	this[isTransitioningSymbol] = true;

	fireCustomEvent(this, "monster-" + this[nameSymbol] + "-before-open", {});

	adjustHeight.call(this);
	this[detailsElementSymbol].classList.add("active");

	if (this.getOption("features.accordion") === true) {
		node = this;
		while (node.nextElementSibling instanceof Collapse) {
			node = node.nextElementSibling;
			node.close();
		}

		node = this;
		while (node.previousElementSibling instanceof Collapse) {
			node = node.previousElementSibling;
			node.close();
		}
	}

	setTimeout(() => {
		setTimeout(() => {
			updateStateConfig.call(this);
			fireCustomEvent(this, "monster-" + this[nameSymbol] + "-open", {});
			setTimeout(() => {
				this[controlElementSymbol].classList.remove("overflow-hidden");
				this[isTransitioningSymbol] = false;
				runEventQueue.call(this);
			}, 500);
		}, 0);
	}, 0);

	return this;
}

/**
 * @private
 * @return {void}
 */
function adjustHeight() {
	let height = 0;

	if (this[detailsContainerElementSymbol]) {
		if (this.getOption("features.useScrollValues")) {
			height += this[detailsContainerElementSymbol].scrollHeight;
		} else {
			height += this[detailsContainerElementSymbol].clientHeight;
		}
	}

	if (this[detailsDecoElementSymbol]) {
		if (this.getOption("features.useScrollValues")) {
			height += this[detailsDecoElementSymbol].scrollHeight;
		} else {
			height += this[detailsDecoElementSymbol].clientHeight + 1;
		}
	}

	if (height === 0) {
		if (this.getOption("features.useScrollValues")) {
			height = this[detailsElementSymbol].scrollHeight;
		} else {
			height = this[detailsElementSymbol].clientHeight;
		}

		if (height === 0) {
			height = "auto";
		}
	} else {
		height += "px";
	}

	this[detailsElementSymbol].style.setProperty(
		"--monster-height",
		height,
		"important",
	);

	fireCustomEvent(this, "monster-" + this[nameSymbol] + "-adjust-height", {
		height,
	});
}

function updateResizeObserverObservation() {
	this[resizeObserverSymbol].disconnect();

	const slottedNodes = getSlottedElements.call(this);
	slottedNodes.forEach((node) => {
		this[resizeObserverSymbol].observe(node);
	});

	if (this[detailsContainerElementSymbol]) {
		this[resizeObserverSymbol].observe(this[detailsContainerElementSymbol]);
	}

	this.adjustHeight();
}

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

	initSlotChangedHandler.call(this);
	return this;
}

function initSlotChangedHandler() {
	this[detailsSlotElementSymbol].addEventListener("slotchange", () => {
		updateResizeObserverObservation.call(this);
	});
}

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

	this[controlElementSymbol] = this.shadowRoot.querySelector(
		"[data-monster-role=control]",
	);
	this[detailsElementSymbol] = this.shadowRoot.querySelector(
		"[data-monster-role=detail]",
	);
	this[detailsSlotElementSymbol] = this.shadowRoot.querySelector("slot");
	this[detailsContainerElementSymbol] = this.shadowRoot.querySelector(
		"[data-monster-role=container]",
	);
	this[detailsDecoElementSymbol] = this.shadowRoot.querySelector(
		"[data-monster-role=deco]",
	);
}

/**
 * @private
 * @return {string}
 */
function getConfigKey() {
	return generateUniqueConfigKey(this[nameSymbol], this.id, "state");
}

/**
 * @private
 */
function updateStateConfig() {
	if (!this.getOption("features.persistState")) {
		return;
	}

	if (!this[detailsElementSymbol]) {
		return;
	}

	const document = getDocument();
	const host = document.querySelector("monster-host");
	if (!(host && this.id)) {
		return;
	}

	const configKey = getConfigKey.call(this);

	try {
		host.setConfig(configKey, this.isOpen());
	} catch (error) {
		addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, String(error));
	}
}

/**
 * @private
 * @return {Promise}
 */
function initStateFromHostConfig() {
	if (!this.getOption("features.persistState")) {
		return Promise.resolve({});
	}

	const document = getDocument();
	const host = document.querySelector("monster-host");

	if (!(host && this.id)) {
		return Promise.resolve({});
	}

	const configKey = getConfigKey.call(this);
	return host
		.getConfig(configKey)
		.then((state) => {
			if (state === true) {
				this.open();
			} else {
				this.close();
			}
		})
		.catch((error) => {
			addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, error.toString());
		});
}

/**
 * @private
 */
function initResizeObserver() {
	// against flickering
	this[resizeObserverSymbol] = new ResizeObserver((entries) => {
		if (this[timerCallbackSymbol] instanceof DeadMansSwitch) {
			try {
				this[timerCallbackSymbol].touch();
				return;
			} catch (e) {
				delete this[timerCallbackSymbol];
			}
		}

		this[timerCallbackSymbol] = new DeadMansSwitch(200, () => {
			checkAndRearrangeContent.call(this);
		});
	});
}

function checkAndRearrangeContent() {
	this.adjustHeight();
}

/**
 * @private
 * @return {string}
 */
function getTemplate() {
	// language=HTML
	return `
        <div data-monster-role="control" part="control" class="overflow-hidden">
            <div data-monster-role="detail">
                <div data-monster-attributes="class path:classes.container" part="container"
                     data-monster-role="container">
                    <slot></slot>
                </div>
                <div class="deco-line" data-monster-role="deco" part="deco"></div>
            </div>
        </div>`;
}

registerCustomElement(Collapse);
