/**
 * 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.
 */

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

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 nameSymbol = Symbol("name");

/**
 * The Collapse component is used to show a details.
 *
 * <img src="./images/collapse.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-collapse />` directly in the HTML or using
 * Javascript via the `document.createElement('monster-collapse');` method.
 *
 * ```html
 * <monster-collapse></monster-collapse>
 * ```
 *
 * Or you can create this CustomControl directly in Javascript:
 *
 * ```js
 * import '@schukai/monster/source/components/host/collapse.mjs';
 * document.createElement('monster-collapse');
 * ```
 *
 * The Body should have a class "hidden" to ensure that the styles are applied correctly.
 *
 * ```css
 * body.hidden {
 *    visibility: hidden;
 * }
 * ```
 *
 * @startuml collapse.png
 * skinparam monochrome true
 * skinparam shadowing false
 * HTMLElement <|-- CustomElement
 * CustomElement <|-- Collapse
 * @enduml
 *
 * @copyright schukai GmbH
 * @memberOf Monster.Components.Host
 * @summary A simple collapse component
 * @fires Monster.Components.Host.Collapse.event:monster-collapse-before-open
 * @fires Monster.Components.Host.Collapse.event:monster-collapse-open
 * @fires Monster.Components.Host.Collapse.event:monster-collapse-before-close
 * @fires Monster.Components.Host.Collapse.event:monster-collapse-closed
 * @fires Monster.Components.Host.Collapse.event:monster-collapse-adjust-height
 */
class Collapse extends CustomElement {
	/**
	 * This method is called by the `instanceof` operator.
	 * @returns {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";
	}

	/**
	 * 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,
		});
	}

	/**
	 *
	 * @returns {Monster.Components.Host.Collapse}
	 */
	[assembleMethodSymbol]() {
		super[assembleMethodSymbol]();
		initControlReferences.call(this);
		initStateFromHostConfig.call(this);
		initResizeObserver.call(this);
		initEventHandler.call(this);

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

	/**
	 *
	 */
	connectedCallback() {
		super.connectedCallback();
		updateResizeObserverObservation.call(this);
		//  this[resizeObserverSymbol].observe(getDocument().body);
	}

	/**
	 *
	 */
	disconnectedCallback() {
		super.disconnectedCallback();
		//this[resizeObserverSymbol].disconnect();
	}

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

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

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

	/**
	 *
	 * @returns {Monster.Components.Host.Collapse}
	 * @fires Monster.Components.Host.Collapse.event:monster-collapse-before-open
	 * @fires Monster.Components.Host.Collapse.event:monster-collapse-open
	 */
	open() {
		let node;
		if (this[detailsElementSymbol].classList.contains("active")) {
			return this;
		}

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

		return this;
	}

	/**
	 *
	 * @returns {Monster.Components.Host.Collapse}
	 * @fires Monster.Components.Host.Collapse.event:monster-collapse-before-close
	 * @fires Monster.Components.Host.Collapse.event:monster-collapse-closed
	 */
	close() {
		if (!this[detailsElementSymbol].classList.contains("active")) {
			return this;
		}

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

		return this;
	}

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

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

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

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

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
 * @returns {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
 * @returns {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);