/**
 * 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 { addAttributeToken } from "../../dom/attributes.mjs";
import {
	ATTRIBUTE_ERRORMESSAGE,
	ATTRIBUTE_ROLE,
} from "../../dom/constants.mjs";
import { CustomControl } from "../../dom/customcontrol.mjs";
import {
	assembleMethodSymbol,
	attributeObserverSymbol,
	registerCustomElement,
} from "../../dom/customelement.mjs";
import { findTargetElementFromEvent } from "../../dom/events.mjs";
import { isFunction } from "../../types/is.mjs";
import { ATTRIBUTE_BUTTON_CLASS } from "./constants.mjs";
import { ButtonStyleSheet } from "./stylesheet/button.mjs";
import { RippleStyleSheet } from "../stylesheet/ripple.mjs";
import { AccessibilityStyleSheet } from "../stylesheet/accessibility.mjs";
import { fireCustomEvent } from "../../dom/events.mjs";
import { addErrorAttribute } from "../../dom/error.mjs";

export { Button };

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

/**
 * A button
 *
 * @fragments /fragments/components/form/button/
 *
 * @example /examples/components/form/button-simple Simple Button
 * @example /examples/components/form/button-with-click-event Button with event
 *
 * @issue https://localhost.alvine.dev:8440/development/issues/closed/282.html
 * @issue https://localhost.alvine.dev:8440/development/issues/closed/283.html
 *
 * @copyright Volker Schukai
 * @summary A form-aware button component for primary actions, event handling and consistent Monster styling.
 * @fires monster-button-clicked this event is triggered when the button is clicked. It contains the field {button} with the button instance.
 */
class Button extends CustomControl {
	/**
	 * This method is called by the <code>instanceof</code> operator.
	 * @return {symbol}
	 * @since 2.1.0
	 */
	static get [instanceSymbol]() {
		return Symbol.for("@schukai/monster/components/form/button@@instance");
	}

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

	/**
	 * The <code>Button.click()</code> 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();
		}
	}

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

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

	/**
	 * The current value of the button.
	 *
	 * ```javascript
	 * e = document.querySelector('monster-button');
	 * console.log(e.value)
	 * ```
	 *
	 * @return {string} The value of the button
	 */
	get value() {
		return this.getOption("value");
	}

	/**
	 * Set the value of the button.
	 *
	 * ```javascript
	 * e = document.querySelector('monster-button');
	 * e.value=1
	 * ```
	 *
	 * @param {string} value
	 * @return {void}
	 * @throws {Error} unsupported type
	 */
	set value(value) {
		this.setOption("value", value);
		try {
			this?.setFormValue(this.value);
		} catch (e) {
			addAttributeToken(this, ATTRIBUTE_ERRORMESSAGE, e.message);
		}
	}

	/**
	 * 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} labels Labels
	 * @property {string} labels.button="<slot></slot>" Button label
	 * @property {Object} actions Callbacks
	 * @property {string} actions.click="throw Error" Callback when clicked
	 * @property {Object} classes CSS classes
	 * @property {string} classes.button="monster-button-primary" CSS class for the button
	 * @property {boolean} disabled=false Disabled state
	 * @property {Object} effects Effects
	 * @property {boolean} effects.ripple=true Ripple effect
	 * @property {string} type="button" The default behavior of the button. Possible values are: submit, reset, button
	 * @property {Object} aria Aria attributes
	 * @property {string} aria.role The role of the button, should only be set if the button is not a button
	 * @property {string} aria.label="click me!" The label of the button
	 */
	get defaults() {
		return Object.assign({}, super.defaults, {
			templates: {
				main: getTemplate(),
			},
			labels: {
				button: "<slot></slot>",
			},
			classes: {
				button: "monster-button-outline-primary",
			},
			disabled: false,
			actions: {
				click: () => {},
			},
			effects: {
				ripple: true,
			},
			value: null,
			type: "button",

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

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

	/**
	 *
	 * @return {CSSStyleSheet[]}
	 */
	static getCSSStyleSheet() {
		return [RippleStyleSheet, ButtonStyleSheet, AccessibilityStyleSheet];
	}
}

/**
 * @private
 * @return {initEventHandler}
 */
function initEventHandler() {
	const self = this;
	const button = this[buttonElementSymbol];

	const type = "click";

	button.addEventListener(type, function (event) {
		const callback = self.getOption("actions.click");

		fireCustomEvent(self, "monster-button-clicked", {
			button: self,
		});

		if (!isFunction(callback)) {
			return;
		}

		const element = findTargetElementFromEvent(
			event,
			ATTRIBUTE_ROLE,
			"control",
		);

		if (!(element instanceof Node && self.hasNode(element))) {
			return;
		}

		callback.call(self, event);
	});

	if (self.getOption("effects.ripple")) {
		button.addEventListener("click", createRipple.bind(self));
	}

	// data-monster-options
	self[attributeObserverSymbol][ATTRIBUTE_BUTTON_CLASS] = function (value) {
		self.setOption("classes.button", value);
	};

	return this;
}

/**
 * @private
 */
function initControlReferences() {
	this[buttonElementSymbol] = this.shadowRoot.querySelector(
		`[${ATTRIBUTE_ROLE}=button]`,
	);
}

/**
 * @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,
                                             aria-role path:aria.role,
                                             path:aria.label | if:true,
                                             type path:type"
                    data-monster-role="button"
                    part="button"
					aria-labelledby="monster-button-aria-label"
                    data-monster-replace="path:labels.button"></button>
			<div id="monster-button-aria-label" 
				 class="visually-hidden" data-monster-replace="path:aria.label">click me</div>
        </div>`;
}

/**
 * @private
 * @param event
 */
function createRipple(event) {
	const button = this[buttonElementSymbol];

	const circle = document.createElement("span");
	const diameter = Math.max(button.clientWidth, button.clientHeight);
	const radius = diameter / 2;

	circle.style.width = circle.style.height = `${diameter}px`;
	circle.style.left = `${event.clientX - button.offsetLeft - radius}px`;
	circle.style.top = `${event.clientY - button.offsetTop - radius}px`;
	circle.classList.add("monster-fx-ripple");

	const ripples = button.getElementsByClassName("monster-fx-ripple");
	for (const ripple of ripples) {
		ripple.remove();
	}

	button.appendChild(circle);
}

registerCustomElement(Button);
