/**
 * 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 {
	ATTRIBUTE_ERRORMESSAGE,
	ATTRIBUTE_PREFIX,
	ATTRIBUTE_ROLE,
} from "../../dom/constants.mjs";
import {
	assembleMethodSymbol,
	CustomElement,
	initMethodSymbol,
	registerCustomElement,
} from "../../dom/customelement.mjs";
import { findTargetElementFromEvent } from "../../dom/events.mjs";
import { isString } from "../../types/is.mjs";
import { MessageStyleSheet } from "./stylesheet/message.mjs";

export { Message };

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

/**
 * @private
 * @type {symbol}
 */
const removeElementSymbol = Symbol("removeElement");

/**
 * @private
 * @type {symbol}
 */
const timerSymbol = Symbol("timer");

/**
 * @private
 * @type {symbol}
 */
const mouseenterEventHandlerSymbol = Symbol("mouseenterEventHandler");

/**
 * @private
 * @type {symbol}
 */
const mouseleaveEventHandlerSymbol = Symbol("mouseleaveEventHandler");

/**
 * @private
 * @type {symbol}
 */
const removeEventHandlerSymbol = Symbol("removeEventHandler");

/**
 * A Message is a notification element that can be used to display messages to the user.
 *
 * @fragments /fragments/components/notify/message
 *
 * @example /examples/components/notify/message-simple Message
 *
 * @issue https://localhost.alvine.dev:8440/development/issues/closed/269.html
 *
 * @since 1.0.0
 * @copyright Volker Schukai
 * @summary A single notification message element, typically rendered inside the notify container.
 */
class Message extends CustomElement {
	/**
	 * The defaults can be set either directly in the object or via an attribute in the HTML tag.
	 * The value of the attribute `data-monster-options` in the HTML tag must be a JSON string.
	 *
	 * ```
	 * <monster-message data-monster-options="{}"></monster-message>
	 * ```
	 *
	 * Since 1.18.0 the JSON can be specified as a DataURI.
	 *
	 * ```
	 * new Monster.Types.DataUrl(btoa(JSON.stringify({
	 *        timeout: 3000,
	 *        features: {
	 *          close: true,
	 *          disappear: true
	 *        }
	 *    })),'application/json',true).toString()
	 * ```
	 *
	 * @property {Object} templates Template definitions
	 * @property {string} templates.main Main template
	 * @property {number} timeout The time in milliseconds after which the message disappears
	 * @property {Object} features The features of the message
	 * @property {boolean} features.close Whether the message can be closed
	 * @property {boolean} features.disappear Whether the message disappears after a certain time
	 * @property {string} content The content of the message
	 */
	get defaults() {
		return Object.assign({}, super.defaults, {
			timeout: 6000,
			features: {
				close: true,
				disappear: true,
			},
			content: "<slot></slot>",
			templates: {
				main: getTemplate(),
			},
		});
	}

	/**
	 *
	 * @return {Monster.Components.Notify.Message}
	 */
	[assembleMethodSymbol]() {
		super[assembleMethodSymbol]();
		initControlReferences.call(this);
		initEventHandler.call(this);
		return this;
	}

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

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

	/**
	 *
	 */
	[initMethodSymbol]() {
		super[initMethodSymbol]();
	}

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

		if (this.getOption("features.disappear") === true) {
			startFadeout.call(this);
			this.addEventListener("mouseenter", this[mouseenterEventHandlerSymbol]);
		}
	}

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

		if (this.getOption("features.disappear") === true) {
			this.removeEventListener(
				"mouseenter",
				this[mouseenterEventHandlerSymbol],
			);
			this.removeEventListener(
				"mouseenter",
				this[mouseleaveEventHandlerSymbol],
			);
		}
	}
}

/**
 * @private
 */
function startFadeout() {
	if (!this?.[timerSymbol]) {
		this[timerSymbol] = setTimeout(
			() => {
				removeSelf.call(this);
			},
			this.getOption("timeout", 1000),
		);
	}
}

function removeSelf() {
	stopFadeout();
	this.classList.add("fadeout");

	setTimeout(() => {
		this.remove();
	}, 200);
}

/**
 * @private
 */
function stopFadeout() {
	if (this?.[timerSymbol]) {
		clearTimeout(this[timerSymbol]);
		this[timerSymbol] = undefined;
	}
}

/**
 * @private
 * @return {Message}
 * @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(
		"[" + ATTRIBUTE_ROLE + "=control]",
	);
	this[removeElementSymbol] = this.shadowRoot.querySelector(
		"[" + ATTRIBUTE_ROLE + "=close]",
	);
}

/**
 * @private
 */
function initEventHandler() {
	/**
	 * @param {Event} event
	 */
	this[mouseenterEventHandlerSymbol] = (event) => {
		const element = findTargetElementFromEvent(
			event,
			ATTRIBUTE_ROLE,
			"control",
		);

		if (element instanceof HTMLElement) {
			this.removeEventListener(
				"mouseenter",
				this[mouseenterEventHandlerSymbol],
			);
			this.addEventListener("mouseleave", this[mouseleaveEventHandlerSymbol]);
			stopFadeout.call(this);
		}
	};

	/**
	 * @param {Event} event
	 */
	this[mouseleaveEventHandlerSymbol] = (event) => {
		const element = findTargetElementFromEvent(
			event,
			ATTRIBUTE_ROLE,
			"control",
		);
		if (element instanceof HTMLElement) {
			this.removeEventListener(
				"mouseleave",
				this[mouseleaveEventHandlerSymbol],
			);
			this.addEventListener("mouseenter", this[mouseenterEventHandlerSymbol]);
			startFadeout.call(this);
		}
	};

	/**
	 * @param {Event} event
	 */
	this[removeEventHandlerSymbol] = (event) => {
		const element = findTargetElementFromEvent(event, ATTRIBUTE_ROLE, "close");

		if (element instanceof HTMLElement) {
			removeSelf.call(this);
		}
	};

	if (this.getOption("features.close") === true) {
		this[removeElementSymbol].addEventListener(
			"click",
			this[removeEventHandlerSymbol],
		);
		this[removeElementSymbol].addEventListener(
			"touch",
			this[removeEventHandlerSymbol],
		);
	}

	return this;
}

/**
 * @private
 * @return {string}
 */
function getTemplate() {
	// language=HTML
	return `
        <div data-monster-role="control" part="control" class="center">
            <div data-monster-role="message" part="message"
                 data-monster-attributes="data-monster-orientation path:orientation">
                <div data-monster-replace="path:content" part="content"
                     data-monster-role="content">

                </div>
                <div part="remove"
                     data-monster-attributes="class path:features.close | ?::hidden "
                     data-monster-role="close" tabindex="-1"></div>
            </div>
        </div>
    `;
}

registerCustomElement(Message);
