/**
 * 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, internalSymbol } from "../../constants.mjs";
import { CustomControl } from "../../dom/customcontrol.mjs";
import { Observer } from "../../types/observer.mjs";
import { ProxyObserver } from "../../types/proxyobserver.mjs";

import { addAttributeToken } from "../../dom/attributes.mjs";
import {
	assembleMethodSymbol,
	registerCustomElement,
	updaterTransformerMethodsSymbol,
} from "../../dom/customelement.mjs";
import { isFunction, isObject } from "../../types/is.mjs";
import { ToggleSwitchStyleSheet } from "./stylesheet/toggle-switch.mjs";
import {
	ATTRIBUTE_ERRORMESSAGE,
	ATTRIBUTE_ROLE,
} from "../../dom/constants.mjs";
import { getWindow } from "../../dom/util.mjs";
import { fireCustomEvent, fireEvent } from "../../dom/events.mjs";
import { addErrorAttribute } from "../../dom/error.mjs";

export { ToggleSwitch };

/**
 * @private
 * @type {symbol}
 */
const switchElementSymbol = Symbol("switchElement");
const invalidDisabledSymbol = Symbol("invalidDisabled");

/**
 * @type {string}
 */
export const STATE_ON = "on";

/**
 * @type {string}
 */
export const STATE_OFF = "off";

/**
 * A simple toggle switch
 *
 * @fragments /fragments/components/form/toggle-switch
 *
 * @example /examples/components/form/toggle-switch-simple Simple example
 * @example /examples/components/form/toggle-switch-custom-values Custom values and labels
 *
 * @since 3.57.0
 * @copyright Volker Schukai
 * @summary A toggle switch for binary settings, feature flags and compact on-off decisions.
 * @fires monster-options-set
 * @fires monster-selected
 * @fires monster-change
 * @fires monster-changed
 */
class ToggleSwitch 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}
	 *
	 * The individual configuration values can be found in the table.
	 *
	 * @property {string} value=current value of the element
	 * @property {Boolean} disabled Disabled state
	 * @property {Object} classes
	 * @property {string} classes.on specifies the class for the on state.
	 * @property {string} classes.off specifies the class for the off state.
	 * @property {Object} values
	 * @property {string} values.off specifies the value of the element if it is not selected
	 * @property {Object} labels
	 * @property {string} labels.on specifies the label for the on state.
	 * @property {string} labels.off specifies the label for the off state.
	 * @property {string} actions
	 * @property {string} actions.on specifies the action for the on state.
	 * @property {string} actions.off specifies the action for the off state.
	 * @property {Object} templates
	 * @property {string} templates.main the main template used by the control.
	 */
	get defaults() {
		return Object.assign({}, super.defaults, {
			value: null,
			disabled: false,
			classes: {
				on: "monster-theme-on",
				off: "monster-theme-off",
				handle: "monster-theme-primary-1",
				error: "monster-theme-error-1",
			},
			values: {
				on: "on",
				off: "off",
			},
			labels: {
				toggleSwitchOn: "✔",
				toggleSwitchOff: "×",
			},
			templates: {
				main: getTemplate(),
			},
			actions: {
				on: () => {},
				off: () => {},
			},
		});
	}

	/**
	 * @return {void}
	 */
	[assembleMethodSymbol]() {
		const self = this;
		super[assembleMethodSymbol]();

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

		setTimeout(() => {
			/**
			 * init value to off
			 * if the value was not defined before inserting it into the HTML
			 */
			if (self.getOption("value") === null) {
				self.setOption("value", self.getOption("values.off"));
			}

			/**
			 * value from attribute
			 */
			if (self.hasAttribute("value")) {
				self.setOption("value", self.getAttribute("value"));
			}

			/**
			 * validate value
			 */
			validateAndSetValue.call(self);

			// this state is a getter
			if (this.state === STATE_ON) {
				toggleOn.call(self);
			} else {
				toggleOff.call(self);
			}
		}, 0);
	}

	/**
	 * updater transformer methods for pipe
	 *
	 * @return {function}
	 */
	[updaterTransformerMethodsSymbol]() {
		return {
			"state-callback": () => {
				return this.state;
			},
		};
	}

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

	/**
	 * toggle switch
	 *
	 * ```
	 * e = document.querySelector('monster-toggle-switch');
	 * e.click()
	 * ```
	 */
	click() {
		this.toggle();
	}

	/**
	 * toggle switch on/off
	 *
	 * ```
	 * e = document.querySelector('monster-toggle-switch');
	 * e.toggle()
	 * ```
	 *
	 * @return {ToggleSwitch}
	 */
	toggle() {
		if (this.hasAttribute("disabled") || this.getOption("disabled", false)) {
			return this;
		}
		if (this.getOption("value") === this.getOption("values.on")) {
			return this.toggleOff();
		}
		return this.toggleOn();
	}

	/**
	 * toggle switch on
	 *
	 * ```
	 * e = document.querySelector('monster-toggle-switch');
	 * e.toggleOn()
	 * ```
	 *
	 * @return {ToggleSwitch}
	 */
	toggleOn() {
		this.setOption("value", this.getOption("values.on"));
		fireEvent(this, "change");
		fireCustomEvent(this, "monster-change", { value: this.value });
		fireCustomEvent(this, "monster-changed", { value: this.value });
		return this;
	}

	/**
	 * toggle switch off
	 *
	 * ```
	 * e = document.querySelector('monster-toggle-switch');
	 * e.toggleOff()
	 * ```
	 *
	 * @return {ToggleSwitch}
	 */
	toggleOff() {
		this.setOption("value", this.getOption("values.off"));
		fireEvent(this, "change");
		fireCustomEvent(this, "monster-change", { value: this.value });
		fireCustomEvent(this, "monster-changed", { value: this.value });
		return this;
	}

	/**
	 * returns the status of the element
	 *
	 * ```
	 * e = document.querySelector('monster-toggle-switch');
	 * console.log(e.state)
	 * // ↦ off
	 * ```
	 *
	 * @return {string}
	 */
	get state() {
		return this.getOption("value") === this.getOption("values.on")
			? STATE_ON
			: STATE_OFF;
	}

	/**
	 * 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 normalized = normalizeToggleValue.call(this, value);
		if (
			normalized === this.getOption("values.on") ||
			normalized === this.getOption("values.off")
		) {
			if (this.getOption("value") !== normalized) {
				this.setOption("value", normalized);
			} else {
				validateAndSetValue.call(this);
			}
			return;
		}

		addErrorAttribute(
			this,
			'The value "' +
				value +
				'" must be "' +
				this.getOption("values.on") +
				'" or "' +
				this.getOption("values.off"),
		);

		showError.call(this);
	}

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

	/**
	 *
	 * @returns {string}
	 */
	static getTag() {
		return "monster-toggle-switch";
	}
}

/**
 * @private
 */
function initControlReferences() {
	this[switchElementSymbol] = this.shadowRoot.querySelector(
		`[${ATTRIBUTE_ROLE}=switch]`,
	);
}

/**
 * @private
 */
function toggleOn() {
	if (!this[switchElementSymbol]) {
		return;
	}

	this[switchElementSymbol].classList.remove(this.getOption("classes.error"));
	this[switchElementSymbol].classList.remove(this.getOption("classes.off")); // change color
	this[switchElementSymbol].classList.add(this.getOption("classes.on")); // change color

	const callback = this.getOption("actions.on");
	if (isFunction(callback)) {
		callback.call(this);
	}

	if (typeof this.setFormValue === "function") {
		this.setFormValue(this.getOption("values.on"));
	}
}

/**
 * @private
 */
function toggleOff() {
	if (!this[switchElementSymbol]) {
		return;
	}

	this[switchElementSymbol].classList.remove(this.getOption("classes.error"));
	this[switchElementSymbol].classList.remove(this.getOption("classes.on")); // change color
	this[switchElementSymbol].classList.add(this.getOption("classes.off")); // change color

	const callback = this.getOption("actions.off");
	if (isFunction(callback)) {
		callback.call(this);
	}

	if (typeof this.setFormValue === "function") {
		this.setFormValue(this.getOption("values.off"));
	}
}

/**
 * @private
 */
function showError() {
	if (!this[switchElementSymbol]) {
		return;
	}

	this[switchElementSymbol].classList.remove(this.getOption("classes.on"));
	this[switchElementSymbol].classList.remove(this.getOption("classes.off"));
	this[switchElementSymbol].classList.add(this.getOption("classes.error"));
}

/**
 * @private
 */
function clearError() {
	if (this[switchElementSymbol]) {
		this[switchElementSymbol].classList.remove(this.getOption("classes.error"));
	}

	this.removeAttribute(ATTRIBUTE_ERRORMESSAGE);
}

/**
 * @private
 */
function validateAndSetValue() {
	const rawValue = this.getOption("value");
	const value = normalizeToggleValue.call(this, rawValue);
	if (value !== rawValue) {
		this.setOption("value", value);
	}

	const offValue = this.getOption("values.off");
	if (value === undefined || value === null || value === "") {
		clearError.call(this);
		if (this[invalidDisabledSymbol]) {
			this.setOption("disabled", false);
			this.formDisabledCallback(false);
			this[invalidDisabledSymbol] = false;
		}
		if (this.getOption("value") !== offValue) {
			this.setOption("value", offValue);
		}
		toggleOff.call(this);
		return;
	}

	const validatedValues = [];
	validatedValues.push(this.getOption("values.on"));
	validatedValues.push(this.getOption("values.off"));

	if (validatedValues.includes(value) === false) {
		addAttributeToken(
			this,
			ATTRIBUTE_ERRORMESSAGE,
			'The value "' +
				value +
				'" must be "' +
				this.getOption("values.on") +
				'" or "' +
				this.getOption("values.off"),
		);
		this[invalidDisabledSymbol] =
			!!this.getOption("disabled", false) !== true &&
			this.hasAttribute("disabled") === false;
		if (this[invalidDisabledSymbol]) {
			this.setOption("disabled", true);
		}
		this.formDisabledCallback(true);
		showError.call(this);
		return;
	}
	if (this[invalidDisabledSymbol]) {
		this.setOption("disabled", false);
		this.formDisabledCallback(false);
		this[invalidDisabledSymbol] = false;
	}
	clearError.call(this);

	if (value === this.getOption("values.on")) {
		toggleOn.call(this);
		return;
	}

	toggleOff.call(this);
}

/**
 * @private
 * @param {*} value
 * @return {*}
 */
function normalizeToggleValue(value) {
	const onValue = this.getOption("values.on");
	const offValue = this.getOption("values.off");

	if (value === onValue || value === offValue) {
		return value;
	}

	const token = normalizeToggleToken(value);
	if (!token) {
		return value;
	}

	const onToken = normalizeToggleToken(onValue);
	const offToken = normalizeToggleToken(offValue);

	if (token === "on") {
		if (onToken === "on" || offToken === "on") {
			return onToken === "on" ? onValue : offValue;
		}
		return onValue;
	}

	if (token === "off") {
		if (onToken === "off" || offToken === "off") {
			return onToken === "off" ? onValue : offValue;
		}
		return offValue;
	}

	if (token === "true") {
		if (onToken === "true" || offToken === "true") {
			return onToken === "true" ? onValue : offValue;
		}
		return onValue;
	}

	if (token === "false") {
		if (onToken === "false" || offToken === "false") {
			return onToken === "false" ? onValue : offValue;
		}
		return offValue;
	}

	return value;
}

/**
 * @private
 * @param {*} value
 * @return {string|undefined}
 */
function normalizeToggleToken(value) {
	if (value === true || value === "true" || value === "TRUE") {
		return "true";
	}
	if (value === false || value === "false" || value === "FALSE") {
		return "false";
	}
	if (value === "on" || value === "ON") {
		return "on";
	}
	if (value === "off" || value === "OFF") {
		return "off";
	}
	return undefined;
}

/**
 * @private
 * @return {initEventHandler}
 */
function initEventHandler() {
	const self = this;

	let lastValue = self.value;
	self[internalSymbol].attachObserver(
		new Observer(function () {
			if (isObject(this) && this instanceof ProxyObserver) {
				const n = this.getSubject()?.options?.value;
				if (lastValue !== n) {
					lastValue = n;
					validateAndSetValue.call(self);
				}
			}
		}),
	);

	self.addEventListener("keyup", (event) => {
		if (event.keyCode === 32) {
			self.toggle();
		}
	});

	self.addEventListener("click", (event) => {
		self.toggle();
	});

	self.addEventListener("touch", (event) => {
		self.toggle();
	});

	return this;
}

/**
 * @private
 * @return {ToggleSwitch}
 */
function initDisabledSync() {
	const self = this;
	const syncDisabled = () => {
		const disabledOption = !!self.getOption("disabled", false);
		const hasDisabledAttr = self.hasAttribute("disabled");
		const optionAttr = self.getAttribute("data-monster-option-disabled");
		const optionAttrDisabled =
			optionAttr !== null && optionAttr.toLowerCase() !== "false";
		const desiredDisabled =
			disabledOption || hasDisabledAttr || optionAttrDisabled;

		if (desiredDisabled && !disabledOption) {
			self.setOption("disabled", true);
		}

		if (desiredDisabled && !hasDisabledAttr) {
			self.setAttribute("disabled", "");
		}

		if (!desiredDisabled) {
			if (hasDisabledAttr) {
				self.removeAttribute("disabled");
			}
			if (disabledOption) {
				self.setOption("disabled", false);
			}
		}
	};

	syncDisabled();
	self.attachObserver(new Observer(syncDisabled));

	return this;
}

/**
 * @private
 * @return {string}
 */
function getTemplate() {
	// language=HTML
	return `
        <div data-monster-role="control" part="control" tabindex="0">
            <div class="switch" data-monster-role="switch"
                 data-monster-attributes="data-monster-state path:value | call:state-callback">
                <div class="label on" data-monster-replace="path:labels.toggleSwitchOn"></div>
                <div class="label off" data-monster-replace="path:labels.toggleSwitchOff"></div>
                <div data-monster-attributes="class path:classes.handle | suffix:\\ switch-slider"></div>
            </div>
        </div>`;
}

registerCustomElement(ToggleSwitch);
