/**
 * 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 {
	assembleMethodSymbol,
	CustomElement,
	registerCustomElement,
} from "../../dom/customelement.mjs";
import "../notify/notify.mjs";
import { fireCustomEvent } from "../../dom/events.mjs";
import { Observer } from "../../types/observer.mjs";
import { SplitPanelStyleSheet } from "./stylesheet/split-panel.mjs";
import { instanceSymbol } from "../../constants.mjs";
import { internalSymbol } from "../../constants.mjs";
import { getDocument } from "../../dom/util.mjs";

export { SplitPanel, TYPE_VERTICAL, TYPE_HORIZONTAL };

/**
 * @private
 * @type {symbol}
 */
const splitScreenElementSymbol = Symbol("splitScreenElement");

/**
 * @private
 * @type {symbol}
 */
const draggerElementSymbol = Symbol("draggerElement");
/**
 * @private
 * @type {symbol}
 */
const startPanelElementSymbol = Symbol("startPanelElement");
/**
 * @private
 * @type {symbol}
 */
const endPanelElementSymbol = Symbol("endPanelElement");
/**
 * @private
 * @type {symbol}
 */
const handleElementSymbol = Symbol("handleElement");

/**
 *
 * @type {string}
 */
const TYPE_VERTICAL = "vertical";
/**
 *
 * @type {string}
 */
const TYPE_HORIZONTAL = "horizontal";

/**
 * A SplitPanel Control
 *
 * @fragments /fragments/components/layout/split-panel/
 *
 * @issue https://localhost.alvine.dev:8440/development/issues/closed/184.html
 *
 * @example /examples/components/layout/split-panel-simple
 * @example /examples/components/layout/split-panel-presets Split Panel Presets
 *
 * @since 3.54.0
 * @copyright Volker Schukai
 * @summary A split-panel layout that divides available space into resizable primary and secondary areas.
 * into two parts. The split can be either vertical or horizontal. The control provides a
 * draggable handle that allows you to adjust the size of the two panels.
 */
class SplitPanel extends CustomElement {
	/**
	 * This method is called by the `instanceof` operator.
	 * @return {symbol}
	 */
	static get [instanceSymbol]() {
		return Symbol.for("@schukai/monster/components/layout/split-panel");
	}

	/**
	 * 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 {string} splitType Split type (vertical or horizontal)
	 * @property {string} dimension Dimension
	 * @property {string} dimension.initial Initial dimension of the start panel
	 * @property {string} dimension.max Maximum dimension of the start panel (in percentage)
	 * @property {string} dimension.min Minimum dimension of the start panel (in percentage)
	 */
	get defaults() {
		return Object.assign({}, super.defaults, {
			templates: {
				main: getTemplate(),
			},
			splitType: TYPE_VERTICAL,
			dimension: {
				initial: "60%",
				max: "80%",
				min: "20%",
			},
		});
	}

	fullStartScreen() {
		this.setDimension("100%");
		return this;
	}

	fullEndScreen() {
		this.setDimension("0%");
		return this;
	}

	isFullStartScreen() {
		return this[internalSymbol].getSubject().currentDimension === "100%";
	}

	isFullEndScreen() {
		return this[internalSymbol].getSubject().currentDimension === "0%";
	}

	isInitialScreen() {
		return (
			this[internalSymbol].getSubject().currentDimension ===
			this.getOption("dimension").initial
		);
	}

	resetScreen() {
		this.setDimension(this.getOption("dimension").initial);
		return this;
	}

	setContent(html) {
		this.setOption("content", html);
		return this;
	}

	/**
	 *
	 * @return {Monster.Components.Host.Viewer}
	 */
	[assembleMethodSymbol]() {
		super[assembleMethodSymbol]();

		initControlReferences.call(this);
		initEventHandler.call(this);
		//applyPanelDimensions.call(this);
		this.setDimension(this.getOption("dimension").initial);
	}

	/**
	 * Check if the dimension is a percentage and within a valid range, then set the dimension option.
	 *
	 * @param {string} dimension - The dimension to be set, can be in percentage or absolute value.
	 * @return {Object} - Returns the current object instance for chaining.
	 */
	setDimension(dimension) {
		// check if percent and greater than100
		if (dimension.includes("%")) {
			if (parseFloat(dimension) > 100) {
				throw new Error("dimension must be less than 100%");
			} else if (parseFloat(dimension) < 0) {
				throw new Error("dimension must be greater than 0%");
			}
		}

		this[internalSymbol].getSubject().currentDimension = dimension;
		if (
			this[startPanelElementSymbol] &&
			this[endPanelElementSymbol] &&
			this[draggerElementSymbol] &&
			this[splitScreenElementSymbol]
		) {
			applyPanelDimensions.call(this);
		}
		return this;
	}

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

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

/**
 * Set the dimensions of the panel based on the split type.
 * @fires monster-dimension-changed
 */
function applyPanelDimensions() {
	const splitType = this.getOption("splitType");
	const dimension = this[internalSymbol].getSubject().currentDimension;
	const draggerSize = getDraggerSize.call(this);

	if (splitType === TYPE_VERTICAL) {
		this[startPanelElementSymbol].style.width = dimension;
		this[endPanelElementSymbol].style.width =
			`calc(100% - ${dimension} - ${draggerSize})`;
		this[draggerElementSymbol].style.cursor = "ew-resize";
		this[splitScreenElementSymbol].classList.add("vertical");
		this[splitScreenElementSymbol].classList.remove("horizontal");
	} else {
		this[startPanelElementSymbol].style.height = dimension;
		this[endPanelElementSymbol].style.height =
			`calc(100% - ${dimension} - ${draggerSize})`;
		this[draggerElementSymbol].style.cursor = "ns-resize";
		this[splitScreenElementSymbol].classList.add("horizontal");
		this[splitScreenElementSymbol].classList.remove("vertical");
	}

	fireCustomEvent(this, "monster-dimension-changed", {
		controller: this,
		dimension: dimension,
	});
}

/**
 * @private
 * @return {Select}
 * @throws {Error} no shadow-root is defined
 */
function initControlReferences() {
	if (!this.shadowRoot) {
		throw new Error("no shadow-root is defined");
	}

	this[splitScreenElementSymbol] = this.shadowRoot.querySelector(
		"[data-monster-role=split-panel]",
	);
	this[draggerElementSymbol] = this.shadowRoot.querySelector(
		"[data-monster-role=dragger]",
	);
	this[handleElementSymbol] = this.shadowRoot.querySelector(
		"[data-monster-role=handle]",
	);

	this[startPanelElementSymbol] = this.shadowRoot.querySelector(
		"[data-monster-role=startPanel]",
	);
	this[endPanelElementSymbol] = this.shadowRoot.querySelector(
		"[data-monster-role=endPanel]",
	);
}

/**
 * @private
 * @return {string}
 */
function getDraggerSize() {
	if (!(this[draggerElementSymbol] instanceof Element)) {
		return "0px";
	}

	const inlineProperty =
		this[draggerElementSymbol] instanceof HTMLElement
			? this[draggerElementSymbol].style
					.getPropertyValue("--monster-dragger-width")
					.trim()
			: "";
	const computedProperty = window
		.getComputedStyle(this[draggerElementSymbol])
		.getPropertyValue("--monster-dragger-width")
		.trim();

	return inlineProperty || computedProperty || "0px";
}

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

	let lastDimension = this[internalSymbol].getSubject().currentDimension;
	let lastType = this.getOption("splitType");

	this[internalSymbol].getSubject().isDragging = false;

	const eventTypes = ["dblclick"];
	for (const eventType of eventTypes) {
		this[draggerElementSymbol].addEventListener(eventType, () => {
			self[internalSymbol].getSubject().isDragging = false;
			lastDimension = undefined;

			let currentDimension;
			if (self.getOption("splitType") === TYPE_VERTICAL) {
				const topPanel = self[startPanelElementSymbol];
				currentDimension = topPanel.style.width;
			} else {
				const topPanel = self[startPanelElementSymbol];
				currentDimension = topPanel.style.height;
			}

			if (currentDimension === self.getOption("dimension").initial) {
				self.setDimension(self.getOption("dimension").max);
			} else if (currentDimension === self.getOption("dimension").max) {
				self.setDimension(self.getOption("dimension").min);
			} else if (currentDimension === self.getOption("dimension").min) {
				self.setDimension(self.getOption("dimension").initial);
			} else {
				self.setDimension(self.getOption("dimension").initial);
			}
		});
	}

	this[draggerElementSymbol].addEventListener("touchstart", (startEvent) => {
		startEvent.preventDefault();
		self[internalSymbol].getSubject().isDragging = true;

		const dragTouchMove = (e) => {
			if (!self[internalSymbol].getSubject().isDragging) return;

			const touch = e.touches[0];
			if (!touch) return;

			// identical logic as in mousemove - but with touch.clientX/clientY
			const draggerWidth = getDraggerSize.call(self);

			if (self.getOption("splitType") === TYPE_HORIZONTAL) {
				const containerRect =
					self[splitScreenElementSymbol].getBoundingClientRect();
				let newTopHeight = touch.clientY - containerRect.top;

				const min = this.getOption("dimension").min;
				const max = this.getOption("dimension").max;
				const topAsPercent =
					(newTopHeight / this[splitScreenElementSymbol].offsetHeight) * 100;

				if (parseFloat(min) > topAsPercent) newTopHeight = min;
				else if (parseFloat(max) < topAsPercent) newTopHeight = max;
				else newTopHeight = `${topAsPercent}%`;

				const newTopHeightPx =
					(parseFloat(newTopHeight) / 100) *
					this[splitScreenElementSymbol].offsetHeight;

				self[startPanelElementSymbol].style.height = `${newTopHeightPx}px`;
				self[endPanelElementSymbol].style.height =
					`calc(100% - ${newTopHeightPx}px - ${draggerWidth})`;
			} else {
				const containerRect =
					self[splitScreenElementSymbol].getBoundingClientRect();
				let newLeftWidth = touch.clientX - containerRect.left;

				const min = this.getOption("dimension").min;
				const max = this.getOption("dimension").max;
				const leftAsPercent =
					(newLeftWidth / this[splitScreenElementSymbol].offsetWidth) * 100;

				if (parseFloat(min) > leftAsPercent) newLeftWidth = min;
				else if (parseFloat(max) < leftAsPercent) newLeftWidth = max;
				else newLeftWidth = `${leftAsPercent}%`;

				self[startPanelElementSymbol].style.width = `${newLeftWidth}`;
				self[endPanelElementSymbol].style.width =
					`calc(100% - ${newLeftWidth} - ${draggerWidth})`;
			}
		};

		const dragTouchEnd = () => {
			self[internalSymbol].getSubject().isDragging = false;
			document.removeEventListener("touchmove", dragTouchMove);
			document.removeEventListener("touchend", dragTouchEnd);
		};

		document.addEventListener("touchmove", dragTouchMove, { passive: false });
		document.addEventListener("touchend", dragTouchEnd);
	});

	const userSelectDefault = getDocument().body.style.userSelect;

	this[draggerElementSymbol].addEventListener("mousedown", () => {
		self[internalSymbol].getSubject().isDragging = true;

		const eventListener = (e) => {
			e.preventDefault();

			const draggerWidth = getDraggerSize.call(self);

			if (!self[internalSymbol].getSubject().isDragging) {
				return;
			}

			getDocument().body.style.userSelect = "none";

			if (self.getOption("splitType") === TYPE_HORIZONTAL) {
				const containerRect =
					self[splitScreenElementSymbol].getBoundingClientRect();
				const topPanel = self[startPanelElementSymbol];
				const bottomPanel = self[endPanelElementSymbol];
				let newTopHeight = e.clientY - containerRect.top;

				const min = this.getOption("dimension").min;
				const max = this.getOption("dimension").max;

				const topAsPercent =
					(newTopHeight / this[splitScreenElementSymbol].offsetHeight) * 100;
				if (parseFloat(min) > topAsPercent) {
					newTopHeight = min;
				} else if (parseFloat(max) < topAsPercent) {
					newTopHeight = max;
				} else {
					newTopHeight = `${topAsPercent}%`;
				}

				// calc new top height to pixel
				const newTopHeightPx =
					(parseFloat(newTopHeight) / 100) *
					this[splitScreenElementSymbol].offsetHeight;

				topPanel.style.height = `${newTopHeightPx}px`;
				bottomPanel.style.height = `calc(100% - ${newTopHeightPx}px - ${draggerWidth})`;
			} else {
				const containerRect =
					self[splitScreenElementSymbol].getBoundingClientRect();
				const leftPanel = self[startPanelElementSymbol];
				const rightPanel = self[endPanelElementSymbol];
				let newLeftWidth = e.clientX - containerRect.left;

				const min = this.getOption("dimension").min;
				const max = this.getOption("dimension").max;

				const leftAsPercent =
					(newLeftWidth / this[splitScreenElementSymbol].offsetWidth) * 100;

				if (parseFloat(min) > leftAsPercent) {
					newLeftWidth = min;
				} else if (parseFloat(max) < leftAsPercent) {
					newLeftWidth = max;
				} else {
					newLeftWidth = `${leftAsPercent}%`;
				}

				leftPanel.style.width = `${newLeftWidth}`;
				rightPanel.style.width = `calc(100% - ${newLeftWidth} - ${draggerWidth})`;
			}
		};

		const dragEventHandler = () => {
			self[internalSymbol].getSubject().isDragging = false;

			document.body.style.userSelect = userSelectDefault;

			document.removeEventListener("mousemove", eventListener);
			document.removeEventListener("mouseup", dragEventHandler);
		};

		document.addEventListener("mousemove", eventListener);
		document.addEventListener("mouseup", dragEventHandler);
	});

	this[internalSymbol].attachObserver(
		new Observer(() => {
			let apply = false;

			if (
				lastDimension !== this[internalSymbol].getSubject().currentDimension
			) {
				lastDimension = this[internalSymbol].getSubject().currentDimension;
				apply = true;
			}

			if (lastType !== this.getOption("splitType")) {
				lastType = this.getOption("splitType");
				apply = true;
			}

			if (apply) {
				applyPanelDimensions.call(this);
			}
		}),
	);

	return this;
}

/**
 * @private
 * @return {string}
 */
function getTemplate() {
	// language=HTML
	return `
        <div data-monster-role="split-panel" part="control">
            <div data-monster-role="startPanel" class="panel" part="startPanel">
                <slot name="start"></slot>
            </div>
            <div data-monster-role="dragger" part="dragger">
                <div data-monster-role="handle" part="handle"></div>
            </div>
            <div data-monster-role="endPanel" class="panel" part="endPanel">
                <slot name="end"></slot>
            </div>
        </div>`;
}

registerCustomElement(SplitPanel);
