/**
 * 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 { CustomControl } from "../../dom/customcontrol.mjs";
import {
	assembleMethodSymbol,
	registerCustomElement,
} from "../../dom/customelement.mjs";
import { fireCustomEvent } from "../../dom/events.mjs";

import { QuantityStyleSheet } from "./stylesheet/quantity.mjs";
import "./input-group.mjs";

export { Quantity };
/**
 * @private
 */
const controlElementSymbol = Symbol("quantityControl");
/**
 * @private
 */
const decrementButtonSymbol = Symbol("decrementButton");
/**
 * @private
 */
const incrementButtonSymbol = Symbol("incrementButton");
/**
 * @private
 */
const inputElementSymbol = Symbol("quantityInput");
/**
 * @private
 */
const holdTimerSymbol = Symbol("holdTimer");
/**
 * @private
 */
const holdIntervalSymbol = Symbol("holdInterval");

/**
 * This Control shows an input field with increment and decrement buttons.
 *
 * @fragments /fragments/components/form/quantity/
 *
 * @example /examples/components/form/quantity-simple Basic quantity control
 * @example /examples/components/form/quantity-decimal Decimal pricing stepper
 *
 * @since 4.41.0
 * @copyright Volker Schukai
 * @summary A quantity stepper with increment and decrement controls for counts, amounts and order quantities.
 * @fires monster-quantity-change
 */
class Quantity extends CustomControl {
	static get [instanceSymbol]() {
		return Symbol.for("@schukai/monster/components/form/quantity@@instance");
	}

	[assembleMethodSymbol]() {
		super[assembleMethodSymbol]();

		initControlReferences.call(this);
		initEventHandler.call(this);
		applyEditableState.call(this);
		clampAndRender.call(this, this.getOption("value"));

		return this;
	}

	/**
	 * Current numeric value
	 * @return {number|null}
	 */
	get value() {
		return this.getOption("value");
	}

	/**
	 * Sets the value programmatically (including clamping & FormValue)
	 * @param {number|string|null} v
	 */
	set value(v) {
		const n = normalizeNumber(v, this.getOption("precision"));
		clampAndRender.call(this, n);
	}

	/**
	 * Options
	 *
	 * @property {Object} templates
	 * @property {string} templates.main Main template
	 * @property {Object} templateMapping
	 * @property {string} templateMapping.plus Icon (SVG-Path) Plus
	 * @property {string} templateMapping.minus Icon (SVG-Path) Minus
	 * @property {Object} classes CSS classes
	 * @property {string} classes.button Button class (e.g. monster-button-outline-primary)
	 * @property {string} classes.input Additional class for input
	 * @property {Object} features Feature toggles
	 * @property {boolean} features.editable Allow manual input
	 * @property {boolean} features.hold Press-and-hold accelerates
	 * @property {boolean} features.enforceBounds Clamp value when manual input is out of bounds
	 * @property {number} value Current value
	 * @property {number} min Use Number.NEGATIVE_INFINITY and Number.POSITIVE_INFINITY for no bounds
	 * @property {number} max Use Number.NEGATIVE_INFINITY and Number.POSITIVE_INFINITY for no bounds
	 * @property {number} step Increment/decrement step
	 * @property {number} precision Round to N decimal places (null = no explicit rounding)
	 * @property {boolean} disabled Disable the input field (also disables manual input)
	 * @property {string} placeholder Placeholder text
	 * @property {string} inputmode For mobile keyboards
	 */
	get defaults() {
		return Object.assign({}, super.defaults, {
			templates: { main: getTemplate() },
			templateMapping: {
				plus: `
          <path d="M8 1a1 1 0 0 1 1 1v5h5a1 1 0 1 1 0 2H9v5a1 1 0 1 1-2 0V9H2a1 1 0 1 1 0-2h5V2a1 1 0 0 1 1-1z"/>`,
				minus: `
          <path d="M2 7.5a1 1 0 0 0 0 1H14a1 1 0 1 0 0-2H2a1 1 0 0 0 0 1z"/>`,
			},
			classes: {
				button: "monster-button-outline-primary",
				input: "",
			},
			features: {
				editable: true,
				hold: true,
				enforceBounds: true,
			},
			value: 0,
			min: 0,
			max: Number.POSITIVE_INFINITY,
			step: 1,
			precision: null,

			disabled: false,
			placeholder: "",
			inputmode: "decimal",
		});
	}

	static getTag() {
		return "monster-quantity";
	}

	// If you want a stylesheet, return it here.
	static getCSSStyleSheet() {
		return [QuantityStyleSheet];
	}
}

function getFiniteNumberOr(optionValue, fallback) {
	const n = Number(optionValue);
	return Number.isFinite(n) ? n : fallback;
}

/**
 * @private
 * @description Initialize references to important control elements
 * @this {Quantity}
 * @return {void}
 */
function initControlReferences() {
	this[controlElementSymbol] = this.shadowRoot.querySelector(
		`[${ATTRIBUTE_ROLE}="control"]`,
	);
	this[decrementButtonSymbol] = this.shadowRoot.querySelector(
		`[${ATTRIBUTE_ROLE}="decrement"]`,
	);
	this[incrementButtonSymbol] = this.shadowRoot.querySelector(
		`[${ATTRIBUTE_ROLE}="increment"]`,
	);
	this[inputElementSymbol] = this.shadowRoot.querySelector(
		`[${ATTRIBUTE_ROLE}="input"]`,
	);
}

/**
 * @private
 * @description Initialize event handlers
 * @this {Quantity}
 * @return {void}
 */
function initEventHandler() {
	const stepOnce = (dir) => {
		const step = Number(this.getOption("step")) || 1;
		const cur = toNumberOr(this.value, 0);
		const next = cur + (dir > 0 ? step : -step);
		clampAndRender.call(this, next, {
			fire: true,
			kind: dir > 0 ? "increment" : "decrement",
		});
	};

	const startHold = (dir) => {
		if (!this.getOption("features.hold")) return;
		clearTimeout(this[holdTimerSymbol]);
		clearInterval(this[holdIntervalSymbol]);

		// After a short delay, repeat faster
		this[holdTimerSymbol] = setTimeout(() => {
			this[holdIntervalSymbol] = setInterval(() => stepOnce(dir), 60);
		}, 300);
	};

	const stopHold = () => {
		clearTimeout(this[holdTimerSymbol]);
		clearInterval(this[holdIntervalSymbol]);
	};

	// Buttons
	this[decrementButtonSymbol].addEventListener("click", (e) => stepOnce(-1));
	this[incrementButtonSymbol].addEventListener("click", (e) => stepOnce(1));

	// Press & hold (Mouse/Touch)
	["mousedown", "pointerdown", "touchstart"].forEach((ev) => {
		this[decrementButtonSymbol].addEventListener(ev, () => startHold(-1));
		this[incrementButtonSymbol].addEventListener(ev, () => startHold(1));
	});
	["mouseup", "mouseleave", "pointerup", "touchend", "touchcancel"].forEach(
		(ev) => {
			this[decrementButtonSymbol].addEventListener(ev, stopHold);
			this[incrementButtonSymbol].addEventListener(ev, stopHold);
		},
	);

	// Keyboard on input
	this[inputElementSymbol].addEventListener("keydown", (e) => {
		if (e.key === "ArrowUp") {
			e.preventDefault();
			stepOnce(1);
		} else if (e.key === "ArrowDown") {
			e.preventDefault();
			stepOnce(-1);
		}
	});

	// Manual input
	this[inputElementSymbol].addEventListener("input", () => {
		if (!this.getOption("features.editable")) return;
		// Only store temporarily, clamp on blur/enter – but update FormValue immediately
		const raw = this[inputElementSymbol].value;
		const n = normalizeNumber(raw, this.getOption("precision"));
		this.setOption("value", n);
		this.setFormValue(n);
		fireChanged.call(this, "input");
	});

	this[inputElementSymbol].addEventListener("blur", () => {
		if (!this.getOption("features.editable")) return;
		const n = normalizeNumber(
			this[inputElementSymbol].value,
			this.getOption("precision"),
		);
		clampAndRender.call(this, n, { fire: true, kind: "blur" });
	});
}

function applyEditableState() {
	const editable = !!this.getOption("features.editable");
	this[inputElementSymbol].toggleAttribute("readonly", !editable);
	this[inputElementSymbol].toggleAttribute(
		"disabled",
		!!this.getOption("disabled"),
	);
}

function clampAndRender(n, opts = {}) {
	const min = getFiniteNumberOr(
		this.getOption("min"),
		Number.NEGATIVE_INFINITY,
	);
	const max = getFiniteNumberOr(
		this.getOption("max"),
		Number.POSITIVE_INFINITY,
	);

	let value = n;
	if (this.getOption("features.enforceBounds")) {
		value = Math.min(max, Math.max(min, toNumberOr(n, 0)));
	}

	// Precision
	const p = this.getOption("precision");
	if (Number.isInteger(p) && p >= 0) {
		value = Number(toFixedSafe(value, p));
	}

	// Render into input
	this[inputElementSymbol].value =
		value === null || Number.isNaN(value) ? "" : String(value);

	// Options + FormValue
	this.setOption("value", value);
	this.setFormValue(value);

	if (opts.fire) fireChanged.call(this, opts.kind || "programmatic");
}

function fireChanged(kind) {
	fireCustomEvent(this, "monster-quantity-change", {
		element: this,
		value: this.value,
		kind, // 'increment' | 'decrement' | 'input' | 'blur' | 'programmatic'
	});
}

function normalizeNumber(v, precision) {
	if (v === null || v === undefined || v === "") return null;
	let n = Number(v);
	if (!Number.isFinite(n)) return null;
	if (Number.isInteger(precision) && precision >= 0) {
		n = Number(toFixedSafe(n, precision));
	}
	return n;
}

function toNumberOr(v, dflt) {
	const n = Number(v);
	return Number.isFinite(n) ? n : dflt;
}

function toFixedSafe(n, p) {
	// Prevents 1.00000000000002 effects
	return (Math.round(n * Math.pow(10, p)) / Math.pow(10, p)).toFixed(p);
}

function getTemplate() {
	// language=HTML
	return `
    <div data-monster-role="control" part="control">
      <monster-input-group part="input-group">
        <button type="button"
                part="decrement-button"
                data-monster-attributes="class path:classes.button"
                data-monster-role="decrement"
                aria-label="decrement">
          <svg viewBox="0 0 16 16" aria-hidden="true" fill="currentColor"
                        >\${minus}</svg>
        </button>

        <input data-monster-role="input"
               part="input"
               data-monster-attributes="
                  class path:classes.input,
                  placeholder path:placeholder,
                  inputmode path:inputmode"
               autocomplete="off" />

        <button type="button"
                part="increment-button"
                data-monster-attributes="class path:classes.button"
                data-monster-role="increment"
                aria-label="increment">
          <svg viewBox="0 0 16 16" aria-hidden="true" fill="currentColor">\${plus}</svg>
        </button>
      </monster-input-group>
    </div>
  `;
}

registerCustomElement(Quantity);
