/**
 * 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 { instanceSymbol } from "../../constants.mjs";
import { ATTRIBUTE_ROLE } from "../../dom/constants.mjs";
import {
	assembleMethodSymbol,
	registerCustomElement,
} from "../../dom/customelement.mjs";
import {
	findElementWithSelectorUpwards,
	getDocument,
} from "../../dom/util.mjs";
import { isFunction } from "../../types/is.mjs";
import { DeadMansSwitch } from "../../util/deadmansswitch.mjs";
import { Popper } from "../layout/popper.mjs";
import { PopperButtonStyleSheet } from "./stylesheet/popper-button.mjs";
import { isPositionedPopperOpen, positionPopper } from "./util/floating-ui.mjs";
import { AccessibilityStyleSheet } from "../stylesheet/accessibility.mjs";
import "./button.mjs";
import { addErrorAttribute } from "../../dom/error.mjs";

export { PopperButton };

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

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

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

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

/**
 * @private
 * @type {symbol}
 */
const buttonElementSymbol = Symbol("buttonElement");

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

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

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

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

/**
 * A beautiful popper button that can make your life easier and also looks good.
 *
 * @fragments /fragments/components/form/popper-button/
 *
 * @example /examples/components/form/popper-button-simple
 *
 * @issue https://localhost.alvine.dev:8440/development/issues/closed/283.html
 *
 * @since 1.5.0
 * @copyright Volker Schukai
 * @summary A beautiful popper button
 * @fires monster-options-set
 * @fires monster-selected
 * @fires monster-change
 * @fires monster-changed
 */
class PopperButton extends Popper {
	/**
	 * This method is called by the `instanceof` operator.
	 * @return {symbol}
	 * @since 2.1.0
	 */
	static get [instanceSymbol]() {
		return Symbol.for(
			"@schukai/monster/components/form/popper-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 The templates for the control.
	 * @property {string} templates.main The main template.
	 * @property {Object} actions The actions for the control.
	 * @property {Function} actions.click The click action.
	 * @property {Object} classes The classes for the control.
	 * @property {string} classes.button The button class.
	 * @property {Object} labels The labels for the control.
	 * @property {string} labels.button The button label.
	 * @property {string} mode The mode of the control.
	 * @property {string} value The value of the control.
	 * @property {Object} aria The aria attributes for the control.
	 * @property {string} aria.role The role of the control, only if the control is not a button.
	 * @property {string} aria.label The label of the control.
	 */
	get defaults() {
		return Object.assign({}, super.defaults, {
			templates: {
				main: getTemplate(),
			},
			actions: {
				click: (e) => {
					this.toggleDialog();
				},
			},
			classes: {
				button: "monster-button-outline-primary",
			},
			labels: {
				button: '<slot name="button"></slot>',
			},
			mode: "click",
			value: null,
			popper: Object.assign({}, super.defaults.popper, {
				contentOverflow: "smart",
			}),

			aria: {
				role: null,
				label: null,
			},

			features: {
				closeOnOutsideClick: true,
			},
		});
	}

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

		return;
	}

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

	/**
	 * @return {CSSStyleSheet[]}
	 */
	static getCSSStyleSheet() {
		const styles = super.getCSSStyleSheet();
		styles.push(PopperButtonStyleSheet);
		styles.push(AccessibilityStyleSheet);
		return styles;
	}

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

		this[hostElementSymbol] = findElementWithSelectorUpwards(
			this,
			"monster-host",
		);
		this[usesHostDismissSymbol] =
			this[hostElementSymbol] &&
			typeof this[hostElementSymbol].registerDismissable === "function";

		const document = getDocument();

		if (
			this.getOption("features.closeOnOutsideClick", true) === true &&
			!this[usesHostDismissSymbol]
		) {
			for (const [, type] of Object.entries(["click", "touch"])) {
				// close on outside ui-events
				document.addEventListener(type, this[closeEventHandler]);
			}
		}

		updatePopper.call(this);
		attachResizeObserver.call(this);
	}

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

		if (
			this.getOption("features.closeOnOutsideClick", true) === true &&
			!this[usesHostDismissSymbol]
		) {
			// close on outside ui-events
			for (const [, type] of Object.entries(["click", "touch"])) {
				document.removeEventListener(type, this[closeEventHandler]);
			}
		}

		disconnectResizeObserver.call(this);
	}

	/**
	 * The Button.click() method simulates a click on the internal button element.
	 *
	 * @since 3.27.0
	 * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/click}
	 */
	click() {
		if (this.getOption("disabled") === true) {
			return;
		}

		if (
			this[buttonElementSymbol] &&
			isFunction(this[buttonElementSymbol].click)
		) {
			this[buttonElementSymbol].click();
		}
	}

	/**
	 * The Button.focus() method sets focus on the internal button element.
	 *
	 * @since 3.27.0
	 * @param {Object} options
	 * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus}
	 */
	focus(options) {
		if (this.getOption("disabled") === true) {
			return;
		}

		if (
			this[buttonElementSymbol] &&
			isFunction(this[buttonElementSymbol].focus)
		) {
			this[buttonElementSymbol].focus(options);
		}
	}

	/**
	 * The Button.blur() method removes focus from the internal button element.
	 */
	blur() {
		if (
			this[buttonElementSymbol] &&
			isFunction(this[buttonElementSymbol].blur)
		) {
			this[buttonElementSymbol].blur();
		}
	}

	/**
	 * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/attachInternals}
	 * @return {boolean}
	 */
	static get formAssociated() {
		return true;
	}

	/**
	 * The current selection of the Select
	 *
	 * ```
	 * e = document.querySelector('monster-select');
	 * console.log(e.value)
	 * // ↦ 1
	 * // ↦ ['1','2']
	 * ```
	 *
	 * @property {string|array}
	 */
	get value() {
		return this.getOption("value");
	}

	/**
	 * Set selection
	 *
	 * ```
	 * e = document.querySelector('monster-select');
	 * e.value=1
	 * ```
	 *
	 * @property {string|array} value
	 * @throws {Error} unsupported type
	 */
	set value(value) {
		this.setOption("value", value);
		try {
			this?.setFormValue(this.value);
		} catch (e) {
			addErrorAttribute(this, e);
		}
	}
}

/**
 * @private
 * @return {initEventHandler}
 */
function initEventHandler() {
	this[closeEventHandler] = (event) => {
		if (
			isEventInsidePopperButton(
				this,
				event,
				this[controlElementSymbol],
				this[buttonElementSymbol],
				this[popperElementSymbol],
			)
		) {
			return;
		}
		this.hideDialog();
	};

	return this;
}

function isEventInsidePopperButton(
	owner,
	event,
	controlElement,
	buttonElement,
	popperElement,
) {
	const path = event.composedPath?.() || [];

	for (const element of path) {
		if (
			element === owner ||
			element === controlElement ||
			element === buttonElement ||
			element === popperElement
		) {
			return true;
		}
	}

	const target = path[0] || event.target;
	if (!(target instanceof Node)) {
		return false;
	}

	if (owner instanceof HTMLElement && owner.contains(target)) {
		return true;
	}

	if (
		owner?.shadowRoot instanceof ShadowRoot &&
		owner.shadowRoot.contains(target)
	) {
		return true;
	}

	return false;
}

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

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

	requestAnimationFrame(() => {
		let parent = this.parentNode;
		while (!(parent instanceof HTMLElement) && parent !== null) {
			parent = parent.parentNode;
		}

		if (parent instanceof HTMLElement) {
			this[resizeObserverSymbol].observe(parent);
		}
	});
}

function disconnectResizeObserver() {
	if (this[resizeObserverSymbol] instanceof ResizeObserver) {
		this[resizeObserverSymbol].disconnect();
	}
}

/**
 * @private
 */
function updatePopper() {
	if (!isPositionedPopperOpen(this[popperElementSymbol])) {
		return;
	}

	if (this.getOption("disabled", false) === true) {
		return;
	}

	positionPopper.call(
		this,
		this[controlElementSymbol],
		this[popperElementSymbol],
		this.getOption("popper", {}),
	);
}

/**
 * @private
 * @return {Select}
 */
function initControlReferences() {
	this[controlElementSymbol] = this.shadowRoot.querySelector(
		`[${ATTRIBUTE_ROLE}=control]`,
	);

	this[buttonElementSymbol] = this.shadowRoot.querySelector(
		`[${ATTRIBUTE_ROLE}=button]`,
	);

	this[popperElementSymbol] = this.shadowRoot.querySelector(
		`[${ATTRIBUTE_ROLE}=popper]`,
	);

	this[arrowElementSymbol] = this.shadowRoot.querySelector(
		`[${ATTRIBUTE_ROLE}=arrow]`,
	);
}

/**
 * @private
 * @return {string}
 */
function getTemplate() {
	// language=HTML
	return `
        <div data-monster-role="control" part="control">
            <button data-monster-attributes="disabled path:disabled | if:true, class path:classes.button"
                    data-monster-role="button"
                    part="button"
					aria-labelledby="monster-popper-button-aria-label"
                    data-monster-replace="path:labels.button"></button>
			<div id="monster-popper-button-aria-label"
				 class="visually-hidden" data-monster-replace="path:aria.label">click me</div>

            <div data-monster-role="popper" part="popper" tabindex="-1" class="monster-color-primary-1">
                <div data-monster-role="arrow"></div>
                <div part="content" class="flex" data-monster-replace="path:content">
                </div>
            </div>
        </div>
    `;
}

registerCustomElement(PopperButton);
