/**
 * 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 {
	assembleMethodSymbol,
	registerCustomElement,
} from "../../../dom/customelement.mjs";
import { FilterRangeStyleSheet } from "../stylesheet/filter-range.mjs";
import { FilterControlsDefaultsStyleSheet } from "../stylesheet/filter-controls-defaults.mjs";
import { AbstractBase } from "./abstract-base.mjs";
import { positionPopper } from "../../form/util/floating-ui.mjs";
import { ATTRIBUTE_ROLE } from "../../../dom/constants.mjs";
import { getDocument } from "../../../dom/util.mjs";
import { STYLE_DISPLAY_MODE_BLOCK } from "../constants.mjs";
import { DeadMansSwitch } from "../../../util/deadmansswitch.mjs";
import {
	addAttributeToken,
	removeAttributeToken,
} from "../../../dom/attributes.mjs";
import { findTargetElementFromEvent } from "../../../dom/events.mjs";
import { getLocaleOfDocument } from "../../../dom/locale.mjs";

export { Range };

/**
 * @private
 * @type {symbol}
 */
const timerCallbackSymbol = Symbol("timerCallback");

/**
 * local symbol
 * @private
 * @type {symbol}
 */
const resizeObserverSymbol = Symbol("resizeObserver");

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

/**
 * local symbol
 * @private
 * @type {symbol}
 */
const closeEventHandler = Symbol("closeEventHandler");

/**
 * @private
 * @type {symbol}
 */
const inputElementSymbol = Symbol("inputElement");

/**
 * @private
 * @type {symbol}
 */
const formContainerElementSymbol = Symbol("formContainerElement");

/**
 * local symbol
 * @private
 * @type {symbol}
 */
const popperElementSymbol = Symbol("popperElement");

/**
 * local symbol
 * @private
 * @type {symbol}
 */
const arrowElementSymbol = Symbol("arrowElement");

/**
 * The range filter control is used to filter a data set by a range.
 *
 * <img src="./images/range.png">
 *
 * Dependencies: the system uses functions of the [monsterjs](https://monsterjs.org/) library
 *
 * You can create this control either by specifying the HTML tag <monster-filter-range />` directly in the HTML or using
 * Javascript via the `document.createElement('monster-filter-range');` method.
 *
 * ```html
 * <monster-filter-range></monster-filter-range>
 * ```
 *
 * Or you can create this CustomControl directly in Javascript:
 *
 * ```js
 * import '@schukai/component-datatable/source/filter/range.mjs';
 * document.createElement('monster-filter-range');
 * ```
 *
 * @startuml range.png
 * skinparam monochrome true
 * skinparam shadowing false
 * HTMLElement <|-- CustomElement
 * CustomElement <|-- AbstractBase
 * AbstractBase <|-- Range
 * @enduml
 *
 * @copyright Volker Schukai
 * @summary A range filter control
 */
class Range extends AbstractBase {
	/**
	 * This method is called by the `instanceof` operator.
	 * @return {symbol}
	 */
	static get [instanceSymbol]() {
		return Symbol.for("@schukai/monster/components/filter/range@@instance");
	}

	/**
	 *
	 * @return {FilterButton}
	 */
	[assembleMethodSymbol]() {
		super[assembleMethodSymbol]();

		initControlReferences.call(this);
		initEventHandler.call(this);
	}

	/**
	 * This is a method of [internal api](https://developer.mozilla.org/en-US/docs/Web/API/ElementInternals)
	 *
	 * @param {*} value
	 */
	set value(value) {
		this[inputElementSymbol].value = value;
	}

	/**
	 * @return {*}
	 */
	get value() {
		return this[inputElementSymbol].value;
	}

	/**
	 * 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.
	 *
	 * @return {Object}
	 *
	 * @property {Object} templates
	 * @property {string} templates.main
	 * @property {Object} labels
	 * @property {Object} features
	 */
	get defaults() {
		const d = Object.assign({}, super.defaults, {
			templates: {
				main: getTemplate(),
			},

			labels: getTranslations(),

			features: {},

			popper: {
				placement: "bottom",
				middleware: ["flip", "offset:1"],
			},
		});

		return initOptionsFromArguments.call(this, d);
	}

	/**
	 *
	 * @return {CSSStyleSheet[]}
	 */
	static getCSSStyleSheet() {
		return [FilterControlsDefaultsStyleSheet, FilterRangeStyleSheet];
	}

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

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

		const document = getDocument();

		for (const [, type] of Object.entries(["click", "touch"])) {
			// close on outside ui-events
			document.addEventListener(type, this[closeEventHandler]);
		}

		updatePopper.call(this);
		attachResizeObserver.call(this);
	}

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

		// close on outside ui-events
		for (const [, type] of Object.entries(["click", "touch"])) {
			document.removeEventListener(type, this[closeEventHandler]);
		}

		disconnectResizeObserver.call(this);
	}

	/**
	 *
	 * @return {Range}
	 */
	showDialog() {
		show.call(this);
		return this;
	}

	/**
	 *
	 * @return {Range}
	 */
	hideDialog() {
		hide.call(this);
		return this;
	}

	/**
	 *
	 * @return {Range}
	 */
	toggleDialog() {
		if (this[popperElementSymbol].style.display === STYLE_DISPLAY_MODE_BLOCK) {
			this.hideDialog();
		} else {
			this.showDialog();
		}
		return this;
	}
}

/**
 * @private
 * @returns {object}
 */
function getTranslations() {
	const locale = getLocaleOfDocument();
	switch (locale.language) {
		case "de":
			return {
				singleValue: "Wert",
				fromValue: "Von",
				toValue: "Bis",
				rangeFrom: "Von",
				rangeTo: "Bis",
			};
		case "fr":
			return {
				singleValue: "Valeur",
				fromValue: "De",
				toValue: "À",
				rangeFrom: "De",
				rangeTo: "À",
			};
		case "sp":
			return {
				singleValue: "Valor",
				fromValue: "Desde",
				toValue: "Hasta",
				rangeFrom: "Desde",
				rangeTo: "Hasta",
			};
		case "it":
			return {
				singleValue: "Valore",
				fromValue: "Da",
				toValue: "A",
				rangeFrom: "Da",
				rangeTo: "A",
			};
		case "pl":
			return {
				singleValue: "Wartość",
				fromValue: "Od",
				toValue: "Do",
				rangeFrom: "Od",
				rangeTo: "Do",
			};
		case "no":
			return {
				singleValue: "Verdi",
				fromValue: "Fra",
				toValue: "Til",
				rangeFrom: "Fra",
				rangeTo: "Til",
			};
		case "dk":
			return {
				singleValue: "Værdi",
				fromValue: "Fra",
				toValue: "Til",
				rangeFrom: "Fra",
				rangeTo: "Til",
			};
		case "sw":
			return {
				singleValue: "Värde",
				fromValue: "Från",
				toValue: "Till",
				rangeFrom: "Från",
				rangeTo: "Till",
			};
		default:
		case "en":
			return {
				singleValue: "Value",
				fromValue: "From",
				toValue: "To",
				rangeFrom: "From",
				rangeTo: "To",
			};
	}
}

/**
 * @private
 * @param type
 * @param self
 * @param value
 * @param element
 */
function updateFilterValue(type, self, value, element) {
	if (type === "single") {
		self[inputElementSymbol].value = value;
	} else if (type === "from") {
		self[inputElementSymbol].value = value + "-";
	} else if (type === "to") {
		self[inputElementSymbol].value = "-" + value;
	} else if (type === "from-to") {
		// this option contain two input fields, we check which one was changed and read the other one
		// the other field is in the same form group, for simplification we get both controls from the group here
		const group = self[formContainerElementSymbol].querySelectorAll(
			"input[type=radio][value=" + type + "] ~ input[type=number]",
		);
		let from = group[0].value;

		if (from === "" || from === null || from < 0) {
			from = 0;
		}

		let to = group[1].value;
		if (to === "" || to === null || to < 0) {
			to = 0;
		}

		if (element.name === "rangeTo") {
			if (parseInt(from) > parseInt(to)) {
				group[0].value = to;
				from = to;
			}
		} else {
			if (parseInt(from) > parseInt(to)) {
				group[1].value = from;
				to = from;
			}
		}

		let range;
		if (from === 0 && to === 0) {
			range = "";
		} else if (from === 0) {
			range = "-" + to;
		} else if (to === 0) {
			range = from + "-";
		} else if (from === to) {
			range = from;
		} else {
			range = from + "-" + to;
		}

		self[inputElementSymbol].value = range;
	}
}

/**
 * @private
 * @return {initEventHandler}
 */
function initEventHandler() {
	this[closeEventHandler] = (event) => {
		const path = event.composedPath();

		for (const [, element] of Object.entries(path)) {
			if (element === this) {
				return;
			}
		}
		hide.call(this);
	};

	this[inputElementSymbol].addEventListener("click", () => {
		this.toggleDialog();
	});

	this[formContainerElementSymbol].addEventListener("click", (event) => {
		const element = findTargetElementFromEvent(
			event,
			"data-monster-role",
			"range-type",
		);
		if (!element) {
			return;
		}

		const type = element.getAttribute("data-monster-range-type");

		const radio = this[formContainerElementSymbol].querySelector(
			"input[type=radio][value=" + type + "]",
		);
		if (!radio) {
			return;
		}

		radio.checked = true;

		// enable input from this group and disable the other
		const group = this[formContainerElementSymbol].querySelectorAll(
			"input[type=radio][value=" + type + "] ~ input[type=number]",
		);
		for (const [, element] of Object.entries(group)) {
			element.disabled = false;
		}

		const otherGroup = this[formContainerElementSymbol].querySelectorAll(
			"input[type=radio]:not([value=" + type + "]) ~ input[type=number]",
		);
		for (const [, element] of Object.entries(otherGroup)) {
			element.disabled = true;
		}
	});

	// if the user change the value of the input field, we have to update the main input field
	this[formContainerElementSymbol].addEventListener("change", (event) => {
		const element = findTargetElementFromEvent(event, "type", "number");
		if (!element) {
			return;
		}

		const typeElement = findTargetElementFromEvent(
			event,
			"data-monster-role",
			"range-type",
		);
		if (!typeElement) {
			return;
		}

		const type = typeElement.getAttribute("data-monster-range-type");
		const value = element.value;
		updateFilterValue(type, this, value, element);
	});

	// we should watch the change event of self[inputElementSymbol] and call updateInputFromValue
	// if the value is changed from outside
	this[inputElementSymbol].addEventListener("change", () => {
		updateInputFromValue.call(this);
	});

	this.addEventListener("keydown", (event) => {
		// if key code esc than hide dialog
		if (event.key === "Escape") {
			hide.call(this);
			queueMicrotask(() => {
				this[inputElementSymbol].focus();
			});

			return;
		}

		const input = findTargetElementFromEvent(
			event,
			"data-monster-role",
			"input",
		);

		if (!input) {
			return;
		}

		// key code down should activate first radio with name="singleValue"
		if (event.key === "ArrowDown") {
			show.call(this);
			const radio = this[formContainerElementSymbol].querySelector(
				"input[type=radio][value=single]",
			);
			if (radio) {
				radio.checked = true;
			}
			queueMicrotask(() => {
				// focus the input field
				const input = this[formContainerElementSymbol].querySelector(
					"input[type=radio][value=single] ~ input[name=singleValue]",
				);
				if (input) {
					input.focus();
				}
			});
		}
	});

	return this;
}

/**
 * @private
 */
function attachResizeObserver() {
	// against flickering
	this[resizeObserverSymbol] = new ResizeObserver((entries) => {
		if (this[timerCallbackSymbol] instanceof DeadMansSwitch) {
			try {
				this[timerCallbackSymbol].touch();
				return;
			} catch (e) {
				delete this[timerCallbackSymbol];
			}
		}

		this[timerCallbackSymbol] = new DeadMansSwitch(200, () => {
			updatePopper.call(this);
		});
	});

	this[resizeObserverSymbol].observe(this.parentElement);
}

function disconnectResizeObserver() {
	if (this[resizeObserverSymbol] instanceof ResizeObserver) {
		this[resizeObserverSymbol].disconnect();
	}
}

/**
 * @private
 */
function hide() {
	this[popperElementSymbol].style.display = "none";
	removeAttributeToken(this[controlElementSymbol], "class", "open");
}

/**
 * @private
 * @this PopperButton
 */
function show() {
	if (this.getOption("disabled", false) === true) {
		return;
	}

	updateInputFromValue.call(this);

	if (this[popperElementSymbol].style.display === STYLE_DISPLAY_MODE_BLOCK) {
		return;
	}

	this[popperElementSymbol].style.visibility = "hidden";
	this[popperElementSymbol].style.display = STYLE_DISPLAY_MODE_BLOCK;

	addAttributeToken(this[controlElementSymbol], "class", "open");

	updatePopper.call(this);
}

/**
 * @private
 */
function updateInputFromValue() {
	const value = this[inputElementSymbol].value.trim();
	const formContainer = this[formContainerElementSymbol];
	const rangeTypes = ["single", "from", "to", "from-to"];
	const rangeValues = value.split("-");
	const hasDash = value.includes("-");

	if (!/^(-?\d+(-\d+)?|-)?$/.test(value)) {
		return; // Skip if input contains invalid characters
	}

	rangeTypes.forEach((rangeType) => {
		const radio = formContainer.querySelector(
			`input[type=radio][value=${rangeType}]`,
		);
		const inputs = formContainer.querySelectorAll(
			`input[type=radio][value=${rangeType}] ~ input[type=number]`,
		);

		inputs.forEach((input) => (input.value = ""));

		if (
			(rangeType === "single" && !hasDash) ||
			(rangeType === "to" && value.startsWith("-")) ||
			(rangeType === "from" && value.endsWith("-")) ||
			(rangeType === "from-to" && hasDash && rangeValues.length === 2)
		) {
			if (rangeType === "single") {
				inputs[0].value = value;
			} else if (rangeType !== "from-to") {
				inputs[0].value = rangeValues.pop();
			} else {
				if (isNaN(rangeValues[0]) || isNaN(rangeValues[1])) {
					rangeValues[0] = rangeValues[1] = "";
				} else if (parseInt(rangeValues[0]) > parseInt(rangeValues[1])) {
					rangeValues.reverse();
					this[inputElementSymbol].value = rangeValues.join("-");
				}
				inputs.forEach((input, index) => (input.value = rangeValues[index]));
			}
			radio.click();
			inputs[0].focus();
		}
	});
}

/**
 * @private
 * @return {Monster.Components.Datatable.Filter.Range}
 */
function initControlReferences() {
	if (!this.shadowRoot) {
		throw new Error("no shadow-root is defined");
	}

	this[controlElementSymbol] = this.shadowRoot.querySelector(
		`[${ATTRIBUTE_ROLE}=control]`,
	);
	this[inputElementSymbol] = this.shadowRoot.querySelector(
		`[${ATTRIBUTE_ROLE}=input]`,
	);
	this[popperElementSymbol] = this.shadowRoot.querySelector(
		`[${ATTRIBUTE_ROLE}=popper]`,
	);
	this[arrowElementSymbol] = this.shadowRoot.querySelector(
		`[${ATTRIBUTE_ROLE}=arrow]`,
	);
	this[formContainerElementSymbol] = this.shadowRoot.querySelector(
		`[${ATTRIBUTE_ROLE}=form]`,
	);

	return this;
}

/**
 * @private
 * @param {object} options
 * @return {object}
 */
function initOptionsFromArguments(options) {
	return options;
}

/**
 * @private
 */
function updatePopper() {
	if (this[popperElementSymbol].style.display !== STYLE_DISPLAY_MODE_BLOCK) {
		return;
	}

	if (this.getOption("disabled", false) === true) {
		return;
	}

	positionPopper.call(
		this,
		this[controlElementSymbol],
		this[popperElementSymbol],
		this.getOption("popper", {}),
	);
}

/**
 * @private
 * @return {string}
 */
function getTemplate() {
	// language=HTML
	return `
        <div data-monster-role="control" part="control">
            <input data-monster-attributes="disabled path:disabled | if:true, class path:classes.input"
                   data-monster-role="input"
                   part="button"
                   data-monster-replace="path:labels.button">

            <div data-monster-role="popper" part="popper" tabindex="-1" class="monster-color-primary-1">
                <div data-monster-role="arrow"></div>
                <div part="filter" class="flex" data-monster-replace="path:filter">
                    <div class="form-container" data-monster-role="form">

                        <div class="form-group" data-monster-role="range-type" data-monster-range-type="single">
                            <input type="radio" name="rangeType" value="single" tabindex="0">
                            <label for="single"><span data-monster-replace="path:labels.singleValue"></span></label>
                            <input type="number" min="0" name="singleValue" disabled tabindex="0">
                        </div>
                        <div class="form-group" data-monster-role="range-type" data-monster-range-type="from">
                            <input type="radio" name="rangeType" value="from" tabindex="0">
                            <label for="from"><span data-monster-replace="path:labels.fromValue"></span></label>
                            <input type="number" min="0" name="fromValue" disabled tabindex="0">
                        </div>
                        <div class="form-group" data-monster-role="range-type" data-monster-range-type="to">
                            <input type="radio" name="rangeType" value="to" tabindex="0">
                            <label for="to"><span data-monster-replace="path:labels.toValue"></span></label>
                            <input type="number" min="0" name="toValue" disabled tabindex="0">
                        </div>
                        <div class="form-group" data-monster-role="range-type" data-monster-range-type="from-to">
                            <input type="radio" name="rangeType" value="from-to" tabindex="0">
                            <label for="rangeFrom"><span data-monster-replace="path:labels.rangeFrom"></span></label>
                            <input type="number" min="0" name="rangeFrom" disabled tabindex="0">
                            <label for="rangeTo"><span data-monster-replace="path:labels.toValue"></span></label>
                            <input type="number" min="0" name="rangeTo" disabled tabindex="0">
                        </div>


                    </div>
                </div>
            </div>

    `;
}

registerCustomElement(Range);
