/**
 * Copyright © schukai GmbH 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 schukai GmbH.
 *
 * SPDX-License-Identifier: AGPL-3.0
 */

import { instanceSymbol } from "../../constants.mjs";
import { 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 { isObject, isFunction } from "../../types/is.mjs";
import { ToggleSwitchStyleSheet } from "./stylesheet/toggle-switch.mjs";
import {
	ATTRIBUTE_ERRORMESSAGE,
	ATTRIBUTE_ROLE,
} from "../../dom/constants.mjs";

export { ToggleSwitch };

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

/**
 * @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
 *
 * @since 3.57.0
 * @copyright schukai GmbH
 * @summary A beautiful switch element
 * @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=false 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=specifies 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",
			},
			values: {
				on: "on",
				off: "off",
			},
			labels: {
				toggleSwitchOn: "✔",
				toggleSwitchOff: "✖",
			},
			templates: {
				main: getTemplate(),
			},
			actions: {
				on: () => {
					throw new Error("the on action is not defined");
				},
				off: () => {
					throw new Error("the off action is not defined");
				},
			},
		});
	}

	/**
	 * @return {ToggleSwitch}
	 */
	[assembleMethodSymbol]() {
		const self = this;
		super[assembleMethodSymbol]();
		initControlReferences.call(this);
		initEventHandler.call(this);

		/**
		 * 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);

		if (this.state === STATE_ON) {
			toggleClassOn.call(self);
		} else {
			toggleClassOff.call(self);
		}

		/**
		 * is called when options changed
		 */
		self[internalSymbol].attachObserver(
			new Observer(function () {
				if (isObject(this) && this instanceof ProxyObserver) {
					validateAndSetValue.call(self);
					toggleClass.call(self);
				}
			}),
		);

		return this;
	}

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

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

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

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

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

	/**
	 * toggle switch off
	 *
	 * ```
	 * e = document.querySelector('monster-toggle-switch');
	 * e.toggleOff()
	 * ```
	 *
	 * @return {ToggleSwitch}
	 */
	toggleOff() {
		this.setOption("value", this.getOption("values.off"));
		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.state === STATE_ON
			? this.getOption("values.on")
			: this.getOption("values.off");
	}

	/**
	 * Set value
	 *
	 * ```
	 * e = document.querySelector('monster-toggle-switch');
	 * e.value="on"
	 * ```
	 *
	 * @property {string} value
	 */
	set value(value) {
		this.setOption("value", value);
	}

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

	static getTag() {
		return "monster-toggle-switch";
	}
}

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

/**
 * @private
 */
function toggleClassOn() {
	this[switchElementSymbol].classList.remove(this.getOption("classes.off")); // change color
	this[switchElementSymbol].classList.add(this.getOption("classes.on")); // change color
}

/**
 * @private
 */
function toggleClassOff() {
	this[switchElementSymbol].classList.remove(this.getOption("classes.on")); // change color
	this[switchElementSymbol].classList.add(this.getOption("classes.off")); // change color
}

/**
 * @private
 */
function toggleClass() {
	if (this.getOption("value") === this.getOption("values.on")) {
		toggleClassOn.call(this);
	} else {
		toggleClassOff.call(this);
	}
}

/**
 * @private
 */
function toggleValues() {
	if (this.getOption("disabled") === true) {
		return;
	}

	let callback, value;

	if (this.getOption("value") === this.getOption("values.on")) {
		value = this.getOption("values.off");
		callback = this.getOption("actions.off");
	} else {
		value = this.getOption("values.on");
		callback = this.getOption("actions.on");
	}

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

	if (isFunction(callback)) {
		callback.call(this);
	}

	this.setOption("state", this.state);
}

/**
 * @private
 */
function validateAndSetValue() {
	const value = this.getOption("value");

	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.setOption("disabled", true);
		this.formDisabledCallback(true);
	} else {
		this.setOption("disabled", false);
		this.formDisabledCallback(false);
	}
}

/**
 * @private
 * @return {initEventHandler}
 */
function initEventHandler() {
	const self = this;
	self.addEventListener("keyup", function (event) {
		if (event.code === "Space") {
			self[switchElementSymbol].click();
		}
	});
	self.addEventListener("click", function (event) {
		toggleValues.call(self);
	});
	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);