/**
 * 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 { CustomControl } from "../../dom/customcontrol.mjs";
import {
	assembleMethodSymbol,
	registerCustomElement,
} from "../../dom/customelement.mjs";
import { fireCustomEvent, fireEvent } from "../../dom/events.mjs";
import { isArray, isObject, isString, isNumber } from "../../types/is.mjs";
import { Observer } from "../../types/observer.mjs";
import { clone } from "../../util/clone.mjs";
import { ChoiceCardsStyleSheet } from "./stylesheet/choice-cards.mjs";

export { ChoiceCards };

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

/**
 * @private
 * @type {symbol}
 */
const itemsElementSymbol = Symbol("itemsElement");

/**
 * A card-based single choice control.
 *
 * @fragments /fragments/components/form/choice-cards
 *
 * @example /examples/components/form/choice-cards-simple Simple card choices
 *
 * @since 4.137.0
 * @copyright Volker Schukai
 * @summary A visual radio-card picker for choosing one option from a compact set.
 * @fires monster-selected
 * @fires monster-change
 * @fires monster-changed
 */
class ChoiceCards extends CustomControl {
	/**
	 * 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}
	 *
	 * @property {Array<Object|string|number>} items Choice items.
	 * @property {string} items[].contentSlot Slot name for custom card content.
	 * @property {string|null} value Current selected value.
	 * @property {boolean} disabled Disabled state.
	 * @property {Object} labels Accessible labels.
	 * @property {string} labels.group Radio group label.
	 * @property {Object} templates Template definitions.
	 * @property {string} templates.main Main template.
	 */
	get defaults() {
		return Object.assign({}, super.defaults, {
			items: [],
			value: null,
			disabled: false,
			eventProcessing: false,
			labels: {
				group: "Choices",
			},
			templates: {
				main: getTemplate(),
			},
		});
	}

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

		this.attachObserver(
			new Observer(() => {
				render.call(this);
				syncFormValue.call(this);
			}),
		);
	}

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

	/**
	 * This method is called by the `instanceof` operator.
	 * @return {symbol}
	 */
	static get [instanceSymbol]() {
		return Symbol.for(
			"@schukai/monster/components/form/choice-cards@@instance",
		);
	}

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

	/**
	 * @return {string}
	 */
	get value() {
		return this.getOption("value");
	}

	/**
	 * @param {string|null|undefined} value
	 */
	set value(value) {
		const normalized = normalizeValue(value);
		if (this.getOption("value") !== normalized) {
			this.setOption("value", normalized);
		}
		syncValueAttribute.call(this, normalized);
		render.call(this);
		syncFormValue.call(this);
	}

	/**
	 * @return {Array}
	 */
	getItems() {
		return clone(this.getOption("items", []));
	}

	/**
	 * @param {Array} items
	 * @return {ChoiceCards}
	 */
	setItems(items) {
		this.setOption("items", normalizeItems(items));
		render.call(this);
		return this;
	}

	/**
	 * Select a choice by value.
	 *
	 * @param {string|null|undefined} value
	 * @return {ChoiceCards}
	 */
	select(value) {
		if (this.getOption("disabled") === true || this.hasAttribute("disabled")) {
			return this;
		}

		const normalized = normalizeValue(value);
		const item = findItem.call(this, normalized);
		if (item?.disabled === true) {
			return this;
		}

		if (this.value === normalized) {
			return this;
		}

		this.value = normalized;
		const detail = {
			value: this.value,
			item: item || null,
		};

		fireEvent(this, "change");
		fireCustomEvent(this, "monster-selected", detail);
		fireCustomEvent(this, "monster-change", detail);
		fireCustomEvent(this, "monster-changed", detail);

		return this;
	}
}

/**
 * @private
 * @return {void}
 */
function initControlReferences() {
	this[controlElementSymbol] = this.shadowRoot?.querySelector(
		"[data-monster-role=control]",
	);
	this[itemsElementSymbol] = this.shadowRoot?.querySelector(
		"[data-monster-role=items]",
	);
}

/**
 * @private
 * @return {void}
 */
function initItems() {
	if (this.hasAttribute("value")) {
		this.setOption("value", normalizeValue(this.getAttribute("value")));
	}

	const normalized = normalizeItems(this.getOption("items", []));
	this.setOption("items", normalized);
}

/**
 * @private
 * @return {void}
 */
function initEventHandler() {
	this.addEventListener("click", (event) => {
		const button = findChoiceButton(event);
		if (!button) {
			return;
		}
		this.select(button.getAttribute("data-choice-value"));
	});

	this.addEventListener("keydown", (event) => {
		if (this.getOption("disabled") === true || this.hasAttribute("disabled")) {
			return;
		}

		const buttons = getEnabledButtons.call(this);
		if (buttons.length === 0) {
			return;
		}

		const current = this.shadowRoot?.activeElement;
		let index = buttons.indexOf(current);
		if (index < 0) {
			index = Math.max(
				0,
				buttons.findIndex(
					(button) => button.getAttribute("aria-checked") === "true",
				),
			);
		}

		if (event.key === " " || event.key === "Enter") {
			event.preventDefault();
			this.select(buttons[index].getAttribute("data-choice-value"));
			return;
		}

		if (event.key === "Home") {
			event.preventDefault();
			focusAndSelect.call(this, buttons[0]);
			return;
		}

		if (event.key === "End") {
			event.preventDefault();
			focusAndSelect.call(this, buttons[buttons.length - 1]);
			return;
		}

		if (["ArrowRight", "ArrowDown"].includes(event.key)) {
			event.preventDefault();
			focusAndSelect.call(this, buttons[(index + 1) % buttons.length]);
			return;
		}

		if (["ArrowLeft", "ArrowUp"].includes(event.key)) {
			event.preventDefault();
			focusAndSelect.call(
				this,
				buttons[(index - 1 + buttons.length) % buttons.length],
			);
		}
	});
}

/**
 * @private
 * @param {HTMLButtonElement} button
 * @return {void}
 */
function focusAndSelect(button) {
	button.focus();
	this.select(button.getAttribute("data-choice-value"));
}

/**
 * @private
 * @param {Event} event
 * @return {HTMLButtonElement|null}
 */
function findChoiceButton(event) {
	for (const node of event.composedPath()) {
		if (
			node instanceof HTMLButtonElement &&
			node.getAttribute("data-monster-role") === "choice"
		) {
			return node;
		}
	}
	return null;
}

/**
 * @private
 * @return {HTMLButtonElement[]}
 */
function getEnabledButtons() {
	return Array.from(
		this.shadowRoot?.querySelectorAll('[data-monster-role="choice"]') || [],
	).filter((button) => button.disabled !== true);
}

/**
 * @private
 * @return {void}
 */
function render() {
	if (!this[itemsElementSymbol]) {
		return;
	}

	const items = normalizeItems(this.getOption("items", []));
	const value = normalizeValue(this.getOption("value"));
	const disabled =
		this.getOption("disabled") === true || this.hasAttribute("disabled");
	const hasSelectedItem = items.some((item) => item.value === value);
	const firstEnabledIndex = items.findIndex((item) => item.disabled !== true);

	this[itemsElementSymbol].replaceChildren();
	this[controlElementSymbol]?.setAttribute(
		"aria-label",
		this.getOption("labels.group", "Choices"),
	);

	for (const [index, item] of items.entries()) {
		const selected = value !== null && item.value === value;
		const tabbable =
			selected || (hasSelectedItem === false && index === firstEnabledIndex);
		const button = document.createElement("button");
		button.type = "button";
		button.setAttribute("data-monster-role", "choice");
		button.setAttribute("data-choice-value", item.value);
		button.setAttribute("part", "choice");
		button.setAttribute("role", "radio");
		button.setAttribute("aria-label", item.label);
		button.setAttribute("aria-checked", selected ? "true" : "false");
		button.tabIndex = tabbable ? 0 : -1;
		button.disabled = disabled || item.disabled === true;
		button.classList.toggle("selected", selected);
		button.classList.toggle("custom-content", !!item.contentSlot);

		if (item.id) {
			button.setAttribute("data-choice-id", item.id);
		}

		const indicator = document.createElement("span");
		indicator.setAttribute("data-monster-role", "indicator");
		indicator.setAttribute("part", "indicator");
		button.appendChild(indicator);

		if (item.contentSlot) {
			const content = document.createElement("span");
			content.setAttribute("data-monster-role", "content");
			content.setAttribute("part", "content");
			const slot = document.createElement("slot");
			slot.name = item.contentSlot;
			content.appendChild(slot);
			button.appendChild(content);
		} else {
			const visual = document.createElement("span");
			visual.setAttribute("data-monster-role", "visual");
			visual.setAttribute("part", "visual");
			if (item.iconSlot) {
				const slot = document.createElement("slot");
				slot.name = item.iconSlot;
				visual.appendChild(slot);
			} else {
				visual.classList.add("empty");
			}
			button.appendChild(visual);

			const label = document.createElement("span");
			label.setAttribute("data-monster-role", "label");
			label.setAttribute("part", "label");
			label.textContent = item.label;
			button.appendChild(label);
		}

		this[itemsElementSymbol].appendChild(button);
	}
}

/**
 * @private
 * @param {string|null} value
 * @return {object|null}
 */
function findItem(value) {
	return normalizeItems(this.getOption("items", [])).find(
		(item) => item.value === value,
	);
}

/**
 * @private
 * @param {*} value
 * @return {string|null}
 */
function normalizeValue(value) {
	if (value === undefined || value === null || value === "") {
		return null;
	}
	return String(value);
}

/**
 * @private
 * @param {*} items
 * @return {Array}
 */
function normalizeItems(items) {
	if (!isArray(items)) {
		return [];
	}

	return items.map((entry, index) => {
		if (isString(entry) || isNumber(entry)) {
			return {
				id: `item-${index}`,
				value: String(entry),
				label: String(entry),
				iconSlot: "",
				contentSlot: "",
				disabled: false,
			};
		}

		if (!isObject(entry)) {
			return {
				id: `item-${index}`,
				value: `item-${index}`,
				label: "",
				iconSlot: "",
				contentSlot: "",
				disabled: false,
			};
		}

		const value = normalizeValue(entry.value ?? entry.id ?? `item-${index}`);
		return {
			id: String(entry.id ?? value ?? `item-${index}`),
			value: value ?? `item-${index}`,
			label: String(entry.label ?? value ?? ""),
			iconSlot: String(entry.iconSlot ?? ""),
			contentSlot: String(entry.contentSlot ?? entry.slot ?? ""),
			disabled: entry.disabled === true,
		};
	});
}

/**
 * @private
 * @return {void}
 */
function syncFormValue() {
	if (typeof this.setFormValue === "function") {
		this.setFormValue(this.value ?? "");
	}
}

/**
 * @private
 * @param {string|null} value
 * @return {void}
 */
function syncValueAttribute(value) {
	if (value === null) {
		if (this.hasAttribute("value")) {
			this.removeAttribute("value");
		}
		return;
	}

	if (this.getAttribute("value") !== value) {
		this.setAttribute("value", value);
	}
}

/**
 * @private
 * @return {string}
 */
function getTemplate() {
	// language=HTML
	return `
        <div data-monster-role="control" part="control" role="radiogroup">
            <div data-monster-role="items" part="items"></div>
        </div>
    `;
}

registerCustomElement(ChoiceCards);
