/**
 * 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.
 */

import { instanceSymbol } from "../../constants.mjs";
import { ATTRIBUTE_ROLE } from "../../dom/constants.mjs";
import {
	assembleMethodSymbol,
	registerCustomElement,
} from "../../dom/customelement.mjs";
import { DigitsStyleSheet } from "./stylesheet/digits.mjs";
import { addErrorAttribute } from "../../dom/error.mjs";
import { Observer } from "../../types/observer.mjs";
import { CustomControl } from "../../dom/customcontrol.mjs";
import { InvalidStyleSheet } from "./stylesheet/invalid.mjs";

export { Digits };

/**
 * @private
 * @type {symbol}
 */
const digitsElementSymbol = Symbol("digitsElement");

/**
 * A Digits
 *
 * @fragments /fragments/components/form/digits/
 *
 * @example /examples/components/form/digits-simple
 *
 * @since 3.113.0
 * @copyright Volker Schukai
 * @summary A beautiful Digits that can make your life easier and also looks good.
 */
class Digits extends CustomControl {
	/**
	 *
	 */
	constructor() {
		super();
		initOptionObserver.call(this);
	}

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

	/**
	 *
	 * @return {Components.Form.Digits
	 */
	[assembleMethodSymbol]() {
		super[assembleMethodSymbol]();

		setTimeout(() => {
			initControlReferences.call(this);
			initEventHandler.call(this);
			updateDigitControls.call(this);
		}, 0);

		return this;
	}

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

	/**
	 * Set value
	 *
	 * ```
	 * e = document.querySelector('monster-toggle-switch');
	 * e.value="on"
	 * ```
	 *
	 * @property {string} value
	 */
	set value(value) {
		const normalizedValue = value == null ? "" : String(value);
		const chars = normalizedValue.split("");

		this.setOption("value", normalizedValue);
		this.setFormValue(normalizedValue);

		if (chars.every(checkCharacter.bind(this))) {
			this.setValidity({ badInput: false }, "");
		} else {
			this.setValidity(
				{ badInput: true },
				"The value contains invalid characters",
			);
		}
	}

	/**
	 * 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 {number} digits Number of digits
	 * @property {string} characterSet Character set for the digits, which are allowed
	 */
	get defaults() {
		return Object.assign({}, super.defaults, {
			templates: {
				main: getTemplate(),
			},

			digits: 6,
			characterSet: "0123456789",

			digitsControls: [],
			value: null,
		});
	}

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

	/**
	 * @return {CSSStyleSheet[]}
	 */
	static getCSSStyleSheet() {
		return [DigitsStyleSheet, InvalidStyleSheet];
	}

	/**
	 * @return {void}
	 */
	focus(options) {
		const element = this[digitsElementSymbol].querySelector("input");
		if (element) {
			element.focus(options);
		}
	}

	/**
	 * @return {void}
	 */
	blur() {
		const elements = this[digitsElementSymbol].querySelectorAll("input");
		if (elements) {
			for (const element of elements) {
				element.blur();
			}
		}
	}
}

/**
 * @private
 */
function initOptionObserver() {
	const self = this;

	let lastValue = this.getOption("value");
	let lastDigits = this.getOption("digits");

	self.attachObserver(
		new Observer(function () {
			if (lastValue !== self.getOption("value")) {
				lastValue = self.getOption("value");
				updateDigitControls.call(self);
			}

			if (lastDigits !== self.getOption("digits")) {
				lastDigits = self.getOption("digits");
				updateDigitControls.call(self);
			}
		}),
	);
}

/**
 * @private
 */
function updateDigitControls() {
	const digits = this.getOption("digits") || this.setOption("digits", 4);

	const controls = [];

	const values =
		this.getOption("value") == null ? "" : String(this.getOption("value"));

	if (this[digitsElementSymbol]) {
		this[digitsElementSymbol].style.gridTemplateColumns =
			`repeat(${digits}, 1fr)`;
	}

	for (let i = 0; i < digits; i++) {
		controls.push({
			value: values[i] ?? "",
		});
	}

	this.setOption("digitsControls", controls);

	// Keep real input.value in sync with the option model. Attribute updates alone
	// can drift after user interaction because inputs keep an internal dirty value.
	setTimeout(() => {
		syncInputValues.call(this, controls);
	}, 0);
}

/**
 * @private
 * @param value
 * @returns {boolean}
 */
function checkCharacter(value) {
	const characterSet = this.getOption("characterSet");
	return characterSet.includes(value);
}

/**
 * @private
 * @returns {initEventHandler}
 */
function initEventHandler() {
	const self = this;
	const element = this[digitsElementSymbol];

	// this[attributeObserverSymbol]['data-monster-option-digits'] = () => {
	// 	this.setOption("digits", );
	// }

	element.addEventListener("keydown", function (event) {
		if (event.target.tagName !== "INPUT") return;
		const inputControl = event.target;

		const pressedKey = event.key;

		if ((event.ctrlKey || event.metaKey) && pressedKey === "v") {
			event.preventDefault();

			navigator.clipboard
				.readText()
				.then((clipText) => {
					self.value = clipText;
				})
				.catch(() => {
					addErrorAttribute(this, "Error while pasting");
				});

			return;
		}

		if (pressedKey === "ArrowRight") {
			const nextControl = inputControl.nextElementSibling;
			if (nextControl && nextControl.tagName === "INPUT") {
				event.preventDefault();
				nextControl.focus();
			}
			return;
		}

		if (pressedKey === "ArrowLeft") {
			const previousControl = inputControl.previousElementSibling;
			if (previousControl && previousControl.tagName === "INPUT") {
				event.preventDefault();
				previousControl.focus();
			}
			return;
		}

		if (pressedKey === "Backspace") {
			if (inputControl.value !== "") {
				event.preventDefault();
				inputControl.value = "";
				collectValues.call(self);
				return;
			} else {
				const previousControl = inputControl.previousElementSibling;
				if (previousControl && previousControl.tagName === "INPUT") {
					event.preventDefault();
					previousControl.focus();
				}
				return;
			}
		}

		if (pressedKey.length === 1) {
			if (!checkCharacter.call(self, pressedKey)) {
				event.preventDefault();
				inputControl.classList.add("invalid");

				setTimeout(() => {
					inputControl.classList.remove("invalid");
				}, 500);
				return;
			}

			event.preventDefault();
			inputControl.value = pressedKey;
			const nextControl = inputControl.nextElementSibling;
			if (nextControl && nextControl.tagName === "INPUT") {
				nextControl.focus();
			}
			collectValues.call(self);
			return;
		}
	});

	// Input event as a fallback: On successful input, the focus changes.
	element.addEventListener("input", function (event) {
		if (event.target.tagName !== "INPUT") return;
		if (event.target.value.length === 1) {
			const nextControl = event.target.nextElementSibling;
			if (nextControl && nextControl.tagName === "INPUT") {
				nextControl.focus();
			}
			collectValues.call(self);
		}
	});

	return this;
}

function collectValues() {
	const controlsValues = Array.from(
		this[digitsElementSymbol].querySelectorAll("input"),
	).map((input) => input.value || "");
	this.value = controlsValues.join("");
}

/**
 * @private
 * @param {Array<{value:string}>} controls
 * @returns {void}
 */
function syncInputValues(controls) {
	if (!this[digitsElementSymbol]) return;
	const inputs = this[digitsElementSymbol].querySelectorAll("input");
	if (!inputs.length) return;

	for (let i = 0; i < inputs.length; i++) {
		const nextValue = controls?.[i]?.value ?? "";
		if (inputs[i].value !== nextValue) {
			inputs[i].value = nextValue;
		}
	}
}

/**
 * @private
 * @return {void}
 */
function initControlReferences() {
	this[digitsElementSymbol] = this.shadowRoot.querySelector(
		`[${ATTRIBUTE_ROLE}="digits"]`,
	);
}

/**
 * @private
 * @return {string}
 */
function getTemplate() {
	// language=HTML
	return `
        <template id="digit">
            <input maxlength="1"
                   data-monster-attributes="
                 value path:digit.value">
        </template>

        <div data-monster-role="control" part="control" data-monster-attributes="class path:classes.control">
            <div part="digits" data-monster-role="digits"
                 data-monster-insert="digit path:digitsControls"
                 tabindex="-1"></div>
        </div>`;
}

registerCustomElement(Digits);
