/**
 * 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 { Observer } from "../../types/observer.mjs";
import { clone } from "../../util/clone.mjs";
import { isArray, isObject, isString, isNumber } from "../../types/is.mjs";
import {
	assembleMethodSymbol,
	registerCustomElement,
	attributeObserverSymbol,
} from "../../dom/customelement.mjs";
import {
	fireCustomEvent,
	fireEvent,
	findTargetElementFromEvent,
} from "../../dom/events.mjs";
import { ChecklistStyleSheet } from "./stylesheet/checklist.mjs";
import { addAttributeToken } from "../../dom/attributes.mjs";
import { ATTRIBUTE_ERRORMESSAGE } from "../../dom/constants.mjs";

export { Checklist };

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

/**
 * Checklist control for displaying and tracking a list of items.
 *
 * @summary A checklist control with change events and form integration.
 */
class Checklist extends CustomControl {
	/**
	 * To set the options via the HTML tag, the attribute `data-monster-options` must be used.
	 *
	 * @property {Array} items The checklist items.
	 * @property {boolean} disabled Disabled state.
	 * @property {Object} templates Template definitions.
	 * @property {string} templates.main Main template.
	 */
	get defaults() {
		return Object.assign({}, super.defaults, {
			items: [],
			disabled: false,
			templates: {
				main: getTemplate(),
			},
		});
	}

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

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

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

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

	/**
	 * @returns {Array}
	 */
	get value() {
		return getCheckedIds.call(this);
	}

	/**
	 * @param {Array} value
	 */
	set value(value) {
		if (!isArray(value)) {
			value = [];
		}
		setCheckedIds.call(this, value);
	}

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

	/**
	 * @return {Array}
	 */
	getCheckedItems() {
		return this.getItems().filter((item) => Boolean(item?.checked));
	}

	/**
	 * @param {Array} items
	 * @return {Checklist}
	 */
	setItems(items) {
		const normalized = normalizeItems(items);
		this.setOption("items", normalized.items);
		syncFormValue.call(this);
		return this;
	}
}

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

/**
 * @private
 */
function initEventHandler() {
	this.addEventListener("change", (event) => {
		const checkbox = findTargetElementFromEvent(
			event,
			"data-monster-role",
			"checkbox",
		);
		if (!(checkbox instanceof HTMLInputElement)) {
			return;
		}

		if (this.getOption("disabled") === true) {
			event.preventDefault();
			return;
		}

		const itemId = checkbox.getAttribute("data-item-id");
		if (!itemId) {
			return;
		}

		const items = this.getOption("items", []);
		const updated = clone(items).map((item) => {
			if (item?.id === itemId) {
				return Object.assign({}, item, { checked: checkbox.checked });
			}
			return item;
		});

		this.setOption("items", updated);
		syncFormValue.call(this);

		fireEvent(this, "change");
		fireCustomEvent(this, "monster-checklist-change", {
			id: itemId,
			checked: checkbox.checked,
			item: updated.find((item) => item?.id === itemId),
			items: clone(updated),
			value: getCheckedIds.call(this),
		});
	});
}

/**
 * @private
 */
function normalizeItemsOption() {
	if (applyItemsAttribute.call(this)) {
		return;
	}
	const normalized = normalizeItems(this.getOption("items", []));
	if (normalized.changed) {
		this.setOption("items", normalized.items);
	}
}

/**
 * @private
 * @param {Array} items
 * @return {{items: Array, changed: boolean}}
 */
function normalizeItems(items) {
	const normalized = [];
	let changed = false;

	if (!isArray(items)) {
		return { items: [], changed: true };
	}

	for (const [index, entry] of Object.entries(items)) {
		const idx = Number(index);
		let item = entry;

		if (isString(item) || isNumber(item)) {
			item = {
				id: `item-${idx}`,
				label: String(item),
				checked: false,
				disabled: false,
			};
			changed = true;
		} else if (isObject(item)) {
			item = Object.assign({}, item, {
				id: item.id ?? `item-${idx}`,
				label: item.label ?? "",
				checked: Boolean(item.checked),
				disabled: Boolean(item.disabled),
			});
			if (item.id !== entry.id || item.label !== entry.label) {
				changed = true;
			}
		} else {
			item = {
				id: `item-${idx}`,
				label: "",
				checked: false,
				disabled: false,
			};
			changed = true;
		}

		normalized.push(item);
	}

	return { items: normalized, changed };
}

/**
 * @private
 * @return {boolean}
 */
function applyItemsAttribute() {
	if (!this.hasAttribute("data-monster-option-items")) {
		return false;
	}

	const raw = this.getAttribute("data-monster-option-items");
	if (!raw) {
		this.setOption("items", []);
		return true;
	}

	try {
		const parsed = JSON.parse(raw);
		if (!isArray(parsed)) {
			addAttributeToken(
				this,
				ATTRIBUTE_ERRORMESSAGE,
				"data-monster-option-items must be a JSON array",
			);
			return true;
		}
		const normalized = normalizeItems(parsed);
		this.setOption("items", normalized.items);
		return true;
	} catch (error) {
		addAttributeToken(
			this,
			ATTRIBUTE_ERRORMESSAGE,
			error?.message || String(error),
		);
		return true;
	}
}

/**
 * @private
 */
function initAttributeObservers() {
	this[attributeObserverSymbol]["data-monster-option-items"] = () => {
		applyItemsAttribute.call(this);
		syncFormValue.call(this);
	};
}

/**
 * @private
 * @return {Array}
 */
function getCheckedIds() {
	const items = this.getOption("items", []);
	return items.filter((item) => item?.checked).map((item) => item.id);
}

/**
 * @private
 * @param {Array} ids
 */
function setCheckedIds(ids) {
	const items = this.getOption("items", []);
	const updated = clone(items).map((item) =>
		Object.assign({}, item, { checked: ids.includes(item?.id) }),
	);
	this.setOption("items", updated);
	syncFormValue.call(this);
}

/**
 * @private
 */
function syncFormValue() {
	const value = JSON.stringify(getCheckedIds.call(this));
	if (typeof this.setFormValue === "function") {
		this.setFormValue(value);
	}
}

/**
 * @private
 * @return {string}
 */
function getTemplate() {
	// language=HTML
	return `
        <template id="item">
            <label data-monster-role="item"
                   part="item"
                   data-monster-attributes="data-item-id path:item.id, class path:item.disabled | ?:disabled">
                <input type="checkbox"
                       data-monster-role="checkbox"
                       part="checkbox"
                       data-monster-attributes="data-item-id path:item.id, checked path:item.checked | ?:checked, disabled path:item.disabled | ?:disabled">
                <span data-monster-role="label"
                      part="label"
                      data-monster-replace="path:item.label | default: "></span>
            </label>
        </template>

        <div data-monster-role="control" part="control">
            <div data-monster-role="items"
                 part="items"
                 data-monster-insert="item path:items"></div>
        </div>
    `;
}

registerCustomElement(Checklist);
