Source: UIBasic.js

import { Skin } from './Skin'
import { Util } from './Util'
import { Controller2D } from './Controller2D'
import { ControllerPanZoom } from './ControllerPanZoom'
import { Ruler } from "./Ruler"
import { ScaleBar } from './ScaleBar'
import { addSignals } from './Signals'

/**
 * @typedef {Object} UIAction
 * Action configuration for toolbar buttons
 * @property {string} title - Display title for the action
 * @property {boolean} display - Whether to show in toolbar
 * @property {string} [key] - Keyboard shortcut key
 * @property {Function} task - Callback function for action
 * @property {string} [icon] - Custom SVG icon path or content
 * @property {string} [html] - HTML content for help dialog
 */

/**
 * @typedef {Object} MenuEntry
 * Menu configuration item
 * @property {string} [title] - Large title text
 * @property {string} [section] - Section header text
 * @property {string} [html] - Raw HTML content
 * @property {string} [button] - Button text
 * @property {string} [group] - Button group identifier
 * @property {string} [layer] - Associated layer ID
 * @property {string} [mode] - Layer visualization mode
 * @property {Function} [onclick] - Click handler
 * @property {Function} [oninput] - Input handler for sliders
 * @property {MenuEntry[]} [list] - Nested menu entries
 */

/**
 * 
 * UIBasic implements a complete user interface for OpenLIME viewers.
 * Provides toolbar controls, layer management, and interactive features.
 * 
 * Core Features:
 * - Customizable toolbar
 * - Layer management
 * - Light direction control
 * - Camera controls
 * - Keyboard shortcuts
 * - Scale bar
 * - Measurement tools
 * 
 * Built-in Actions:
 * - home: Reset camera view
 * - fullscreen: Toggle fullscreen mode
 * - layers: Show/hide layer menu
 * - zoomin/zoomout: Camera zoom controls
 * - rotate: Rotate view
 * - light: Light direction control
 * - ruler: Distance measurement
 * - help: Show help dialog
 * - snapshot: Save view as image
 *
 * Implementation Details
 * 
 * Layer Management:
 * - Layers can be toggled individually
 * - Layer visibility affects associated controllers
 * - Overlay layers behave independently
 * - Layer state is reflected in menu UI
 * 
 * Mouse/Touch Interaction:
 * - Uses PointerManager for event handling
 * - Supports multi-touch gestures
 * - Handles drag operations for light control
 * - Manages tool state transitions
 * 
 * Menu System:
 * - Hierarchical structure
 * - Dynamic updates based on state
 * - Group-based selection
 * - Mode-specific entries
 * 
 * Controller Integration:
 * - Light direction controller
 * - Pan/zoom controller
 * - Measurement controller
 * - Priority-based event handling
 * 
 * Dialog System:
 * - Modal blocking of underlying UI
 * - Non-modal floating windows
 * - Content injection system
 * - Event-based communication
 * 
 * Skin System:
 * - SVG-based icons
 * - Dynamic loading
 * - CSS customization
 * - Responsive layout
 * 
 * Keyboard Support:
 * - Configurable shortcuts
 * - Action mapping
 * - Mode-specific keys
 * - Focus handling
 * 
 * See the complete example in: {@link https://github.com/cnr-isti-vclab/openlime/tree/main/dist/examples/ui-custom|GitHub ui-custom example}
 */
class UIBasic {
	/**
	 * Creates a new UIBasic instance
	 * @param {Viewer} viewer - OpenLIME viewer instance
	 * @param {UIBasic~Options} [options] - Configuration options
	 * 
	 * @fires UIBasic#lightdirection
	 * 
	 * @example
	 * ```javascript
	 * const ui = new UIBasic(viewer, {
	 *     // Enable specific actions
	 *     actions: {
	 *         light: { display: true },
	 *         zoomin: { display: true },
	 *         layers: { display: true }
	 *     },
	 *     // Add measurement support
	 *     pixelSize: 0.1,
	 *     // Add attribution
	 *     attribution: "© Example Source"
	 * });
	 * ```
	 */
	constructor(viewer, options) {
		//we need to know the size of the scene but the layers are not ready.
		let camera = viewer.camera;
		Object.assign(this, {
			viewer: viewer,
			camera: viewer.camera,
			skin: 'skin/skin.svg',
			autoFit: true, //FIXME to be moved in the viewer?
			//skinCSS: 'skin.css', // TODO: probably not useful
			actions: {
				home: { title: 'Home', display: true, key: 'Home', task: (event) => { if (camera.boundingBox) camera.fitCameraBox(250); } },
				fullscreen: { title: 'Fullscreen', display: true, key: 'f', task: (event) => { this.toggleFullscreen(); } },
				layers: { title: 'Layers', display: true, key: 'Escape', task: (event) => { this.toggleLayers(); } },
				zoomin: { title: 'Zoom in', display: false, key: '+', task: (event) => { camera.deltaZoom(250, 1.25, 0, 0); } },
				zoomout: { title: 'Zoom out', display: false, key: '-', task: (event) => { camera.deltaZoom(250, 1 / 1.25, 0, 0); } },
				rotate: { title: 'Rotate', display: false, key: 'r', task: (event) => { camera.rotate(250, -45); } },
				light: { title: 'Light', display: 'auto', key: 'l', task: (event) => { this.toggleLightController(); } },
				ruler: { title: 'Ruler', display: false, task: (event) => { this.toggleRuler(); } },
				help: { title: 'Help', display: false, key: '?', task: (event) => { this.toggleHelp(this.actions.help); }, html: '<p>Help here!</p>' }, //FIXME Why a boolean in toggleHelp?
				snapshot: { title: 'Snapshot', display: false, task: (event) => { this.snapshot() } }, //FIXME not work!
			},
			postInit: () => { },
			pixelSize: null,
			unit: null, //FIXME to be used with ruler
			attribution: null,     //image attribution
			lightcontroller: null,
			showLightDirections: false,
			enableTooltip: true,
			controlZoomMessage: null, //"Use Ctrl + Wheel to zoom instead of scrolling" ,
			menu: []
		});

		Object.assign(this, options);
		if (this.autoFit) //FIXME Check if fitCamera is triggered only if the layer is loaded. Is updateSize the right event?
			this.viewer.canvas.addEvent('updateSize', () => this.viewer.camera.fitCameraBox(0));

		this.panzoom = new ControllerPanZoom(this.viewer.camera, {
			priority: -1000,
			activeModifiers: [0, 1],
			controlZoom: this.controlZoomMessage != null
		});
		if (this.controlZoomMessage)
			this.panzoom.addEvent('nowheel', () => { this.showOverlayMessage(this.controlZoomMessage); });
		this.viewer.pointerManager.onEvent(this.panzoom); //register wheel, doubleclick, pan and pinch
		// this.viewer.pointerManager.on("fingerSingleTap", { "fingerSingleTap": (e) => { this.showInfo(e); }, priority: 10000 });

		/*let element = entry.element;
		let group = element.getAttribute('data-group');
		let layer = element.getAttribute('data-layer');
		let mode = element.getAttribute('data-mode');
		let active = (layer && this.viewer.canvas.layers[layer].visible) &&
			(!mode || this.viewer.canvas.layers[layer].getMode() == mode);
		entry.element.classList.toggle('active', active); */

		this.menu.push({ section: "Layers" });
		for (let [id, layer] of Object.entries(this.viewer.canvas.layers)) {
			let modes = []
			for (let m of layer.getModes()) {
				let mode = {
					button: m,
					mode: m,
					layer: id,
					onclick: () => { layer.setMode(m); },
					status: () => layer.getMode() == m ? 'active' : '',
				};
				if (m == 'specular' && layer.shader.setSpecularExp)
					mode.list = [{ slider: '', oninput: (e) => { layer.shader.setSpecularExp(e.target.value); } }];
				modes.push(mode);
			}

			let layerEntry = {
				button: layer.label || id,
				onclick: () => { this.setLayer(layer); },
				status: () => layer.visible ? 'active' : '',
				layer: id
			};
			if (modes.length > 1) layerEntry.list = modes;

			if (layer.annotations) {
				layerEntry.list = [];
				//setTimeout(() => { 
				layerEntry.list.push(layer.annotationsEntry());
				//this.updateMenu();
				//}, 1000);
				//TODO: this could be a convenience, creating an editor which can be
				//customized later using layer.editor.
				//if(layer.editable) 
				//	layer.editor = this.editor;
			}
			this.menu.push(layerEntry);
		}

		let controller = new Controller2D(
			(x, y) => {
				for (let layer of lightLayers)
					layer.setLight([x, y], 0);
				if (this.showLightDirections)
					this.updateLightDirections(x, y);
				this.emit('lightdirection', [x, y, Math.sqrt(1 - x * x + y * y)]);
			}, {
			// TODO: IS THIS OK? It was false before
			active: false,
			activeModifiers: [2, 4],
			control: 'light',
			onPanStart: this.showLightDirections ? () => {
				Object.values(this.viewer.canvas.layers).filter(l => l.annotations != null).forEach(l => l.setVisible(false));
				this.enableLightDirections(true);
			} : null,
			onPanEnd: this.showLightDirections ? () => {
				Object.values(this.viewer.canvas.layers).filter(l => l.annotations != null).forEach(l => l.setVisible(true));
				this.enableLightDirections(false);
			} : null,
			relative: true
		});

		controller.priority = 0;
		this.viewer.pointerManager.onEvent(controller);
		this.lightcontroller = controller;


		let lightLayers = [];
		for (let [id, layer] of Object.entries(this.viewer.canvas.layers))
			if (layer.controls.light) lightLayers.push(layer);

		if (lightLayers.length) {
			this.createLightDirections();
			for (let layer of lightLayers) {
				controller.setPosition(0.5, 0.5);
				//layer.setLight([0.5, 0.5], 0);
				layer.controllers.push(controller);
			}
		}

		if (queueMicrotask) queueMicrotask(() => { this.init() }); //allows modification of actions and layers before init.
		else setTimeout(() => { this.init(); }, 0);
	}

	/**
	 * Shows overlay message
	 * @param {string} msg - Message to display
	 * @param {number} [duration=2000] - Display duration in ms
	 */
	showOverlayMessage(msg, duration = 2000) {
		if (this.overlayMessage) {
			clearTimeout(this.overlayMessage.timeout);
			this.overlayMessage.timeout = setTimeout(() => this.destroyOverlayMessage(), duration);
			return;
		}


		let background = document.createElement('div');
		background.classList.add('openlime-overlaymsg');
		background.innerHTML = `<p>${msg}</p>`;
		this.viewer.containerElement.appendChild(background);

		this.overlayMessage = {
			background,
			timeout: setTimeout(() => this.destroyOverlayMessage(), duration)
		}
	}

	/**
	 * Removes the overlay message
	 * @private
	 */
	destroyOverlayMessage() {
		this.overlayMessage.background.remove();
		this.overlayMessage = null;
	}

	/**
	 * Retrieves menu entry for a specific layer
	 * @param {string} id - Layer identifier
	 * @returns {UIBasic~MenuEntry|undefined} Found menu entry or undefined
	 * @private
	 */
	getMenuLayerEntry(id) {
		const found = this.menu.find(e => e.layer == id);
		return found;
	}

	/**
	 * Creates SVG elements for light direction indicators
	 * @private
	 */
	createLightDirections() {
		this.lightDirections = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
		this.lightDirections.setAttribute('viewBox', '-100, -100, 200 200');
		this.lightDirections.setAttribute('preserveAspectRatio', 'xMidYMid meet');
		this.lightDirections.style.display = 'none';
		this.lightDirections.classList.add('openlime-lightdir');
		for (let x = -1; x <= 1; x++) {
			for (let y = -1; y <= 1; y++) {
				let line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
				line.pos = [x * 35, y * 35];
				//line.setAttribute('data-start', `${x} ${y}`);
				this.lightDirections.appendChild(line);
			}
		}
		this.viewer.containerElement.appendChild(this.lightDirections);
	}

	/**
	 * Updates light direction indicator positions
	 * @param {number} lx - Light X coordinate
	 * @param {number} ly - Light Y coordinate
	 * @private
	 */
	updateLightDirections(lx, ly) {
		let lines = [...this.lightDirections.children];
		for (let line of lines) {
			let x = line.pos[0];
			let y = line.pos[1];

			line.setAttribute('x1', 0.6 * x - 25 * 0 * lx);
			line.setAttribute('y1', 0.6 * y + 25 * 0 * ly);
			line.setAttribute('x2', x / 0.6 + 60 * lx);
			line.setAttribute('y2', y / 0.6 - 60 * ly);
		}
	}

	/**
	 * Toggles visibility of light direction indicators
	 * @param {boolean} show - Whether to show indicators
	 * @private
	 */
	enableLightDirections(show) {
		this.lightDirections.style.display = show ? 'block' : 'none';
	}

	/**
	 * Initializes UI components
	 * Sets up toolbar, menu, and controllers
	 * @private
	 * @async
	 */
	init() {
		(async () => {

			document.addEventListener('keydown', (e) => this.keyDown(e), false);
			document.addEventListener('keyup', (e) => this.keyUp(e), false);

			this.createMenu();
			this.updateMenu();
			this.viewer.canvas.addEvent('update', () => this.updateMenu());

			if (this.actions.light && this.actions.light.display === 'auto')
				this.actions.light.display = true;


			if (this.skin)
				await this.loadSkin();
			/* TODO: this is probably not needed
			if(this.skinCSS)
				await this.loadSkinCSS();
			*/

			this.setupActions();


			/* Get pixel size from options if provided or from layer metadata
			 */
			if (this.pixelSize) {
				this.scalebar = new ScaleBar(this.pixelSize, this.viewer);
			}
			else if (this.viewer.canvas.layers[Object.keys(this.viewer.canvas.layers)[0]].pixelSize) {
				let pixelSize = this.viewer.canvas.layers[Object.keys(this.viewer.canvas.layers)[0]].pixelSizePerMM();
				this.scalebar = new ScaleBar(pixelSize, this.viewer);
			}

			if (this.attribution) {
				var p = document.createElement('p');
				p.classList.add('openlime-attribution');
				p.innerHTML = this.attribution;
				this.viewer.containerElement.appendChild(p);
			}

			for (let l of Object.values(this.viewer.canvas.layers)) {
				this.setLayer(l);
				break;
			}

			if (this.actions.light && this.actions.light.active)
				this.toggleLightController();
			if (this.actions.layers && this.actions.layers.active)
				this.toggleLayers();

			this.postInit();

		})().catch(e => { console.log(e); throw Error("Something failed") });
	}

	/**
	 * Handles keyboard down events
	 * @param {KeyboardEvent} e - Keyboard event
	 * @private
	 */
	keyDown(e) {
	}

	/**
	 * Processes keyboard shortcuts
	 * @param {KeyboardEvent} e - Keyboard event
	 * @private
	 */
	keyUp(e) {
		if (e.target != document.body && e.target.closest('input, textarea') != null)
			return;

		if (e.defaultPrevented) return;

		for (const a of Object.values(this.actions)) {
			if ('key' in a && a.key == e.key) {
				e.preventDefault();
				a.task(e);
				return;
			}
		}
	}

	/**
	 * Loads and initializes skin SVG elements
	 * @returns {Promise<void>}
	 * @private
	 * @async
	 */
	async loadSkin() {
		let toolbar = document.createElement('div');
		toolbar.classList.add('openlime-toolbar');
		this.viewer.containerElement.appendChild(toolbar);

		//toolbar manually created with parameters (padding, etc) + css for toolbar positioning and size.
		if (1) {

			let padding = 10;
			let x = 0;
			let h = 0;
			for (let [name, action] of Object.entries(this.actions)) {

				if (action.display !== true)
					continue;

				if ('icon' in action) {
					if (typeof action.icon == 'string') {
						if (Util.isSVGString(action.icon)) {
							action.icon = Util.SVGFromString(action.icon);
						} else {
							action.icon = await Util.loadSVG(action.icon);
						}
						action.icon.classList.add('openlime-button');
					}
				} else {
					action.icon = '.openlime-' + name;
				}

				action.element = await Skin.appendIcon(toolbar, action.icon);
				if (this.enableTooltip) {
					let title = document.createElementNS('http://www.w3.org/2000/svg', 'title');
					title.textContent = action.title;
					action.element.appendChild(title);
				}
			}

		}

		if (0) {  //single svg toolbar
			let svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
			toolbar.appendChild(svg); ui.toggleLightController();
			let x = padding;
			let h = 0;
			for (let [name, action] of Object.entries(this.actions)) {
				if (action.display !== true)
					continue;
				let element = skin.querySelector('.openlime-' + name).cloneNode(true);
				if (!element) continue;
				svg.appendChild(element);
				let box = element.getBBox();
				h = Math.max(h, box.height);
				let tlist = element.transform.baseVal;
				if (tlist.numberOfItems == 0)
					tlist.appendItem(svg.createSVGTransform());
				tlist.getItem(0).setTranslate(-box.x + x, -box.y);
				x += box.width + padding;
			}

			svg.setAttribute('viewBox', `0 0 ${x} ${h}`);
			svg.setAttribute('preserveAspectRatio', 'xMidYMid meet');
		}



		//TODO: not needed, probably. Toolbar build from the skin directly
		if (0) {
			toolbar.appendChild(skin);

			let w = skin.getAttribute('width');
			let h = skin.getAttribute('height');
			let viewbox = skin.getAttribute('viewBox');
			if (!viewbox)
				skin.setAttribute('viewBox', `0 0 ${w} ${h}`);
		}
	}

	/**
	 * Initializes action buttons and their event handlers
	 * @private
	 */
	setupActions() {
		for (let [name, action] of Object.entries(this.actions)) {
			let element = action.element;
			if (!element)
				continue;
			// let pointerManager = new PointerManager(element);
			// pointerManager.onEvent({ fingerSingleTap: action.task, priority: -2000 });
			element.addEventListener('click', (e) => {
				action.task(e);
				e.preventDefault();
			});
		}
		let items = document.querySelectorAll('.openlime-layers-button');
		for (let item of items) {
			let id = item.getAttribute('data-layer');
			if (!id) continue;
			item.addEventListener('click', () => {
				this.setLayer(this.viewer.layers[id]);
			});
		}
	}

	/**
	 * Toggles light direction control mode
	 * @param {boolean} [on] - Force specific state
	 * @private
	 */
	toggleLightController(on) {
		let div = this.viewer.containerElement;
		let active = div.classList.toggle('openlime-light-active', on);
		this.lightActive = active;

		for (let layer of Object.values(this.viewer.canvas.layers))
			for (let c of layer.controllers)
				if (c.control == 'light') {
					c.active = true;
					c.activeModifiers = active ? [0, 2, 4] : [2, 4];  //nothing, shift and alt
				}
	}

	/**
	 * Toggles fullscreen mode
	 * Handles browser-specific fullscreen APIs
	 * @private
	 */
	toggleFullscreen() {
		let canvas = this.viewer.canvasElement;
		let div = this.viewer.containerElement;
		let active = div.classList.toggle('openlime-fullscreen-active');

		if (!active) {
			var request = document.exitFullscreen || document.webkitExitFullscreen ||
				document.mozCancelFullScreen || document.msExitFullscreen;
			request.call(document); document.querySelector('.openlime-scale > line');

			this.viewer.resize(canvas.offsetWidth, canvas.offsetHeight);
		} else {
			var request = div.requestFullscreen || div.webkitRequestFullscreen ||
				div.mozRequestFullScreen || div.msRequestFullscreen;
			request.call(div);
		}
		this.viewer.resize(canvas.offsetWidth, canvas.offsetHeight);
	}

	/**
	 * Toggles measurement ruler tool
	 * @private
	 */
	toggleRuler() {
		if (!this.ruler) {
			this.ruler = new Ruler(this.viewer, this.pixelSize);
			this.viewer.pointerManager.onEvent(this.ruler);
		}

		if (!this.ruler.enabled)
			this.ruler.start();
		else
			this.ruler.end();
	}

	/**
	 * Toggles help dialog
	 * @param {UIBasic~Action} help - Help action configuration
	 * @param {boolean} [on] - Force specific state
	 * @private
	 */
	toggleHelp(help, on) {
		if (!help.dialog) {
			help.dialog = new UIDialog(this.viewer.containerElement, { modal: true, class: 'openlime-help-dialog' });
			help.dialog.setContent(help.html);
		} else
			help.dialog.toggle(on);
	}

	/**
	 * Creates and downloads canvas snapshot
	 * @private
	 */
	snapshot() {
		var e = document.createElement('a');
		e.setAttribute('href', this.viewer.canvas.canvasElement.toDataURL());
		e.setAttribute('download', 'snapshot.png');
		e.style.display = 'none';
		document.body.appendChild(e);
		e.click();
		document.body.removeChild(e);
	}

	/* Layer management */

	/**
	 * Creates HTML for menu entry
	 * @param {UIBasic~MenuEntry} entry - Menu entry to create
	 * @returns {string} Generated HTML
	 * @private
	 */
	createEntry(entry) {
		if (!('id' in entry))
			entry.id = 'entry_' + (this.entry_count++);

		let id = `id="${entry.id}"`;
		let tooltip = 'tooltip' in entry ? `title="${entry.tooltip}"` : '';
		let classes = 'classes' in entry ? entry.classes : '';
		let html = '';
		if ('title' in entry) {
			html += `<h2 ${id} class="openlime-title ${classes}" ${tooltip}>${entry.title}</h2>`;

		} else if ('section' in entry) {
			html += `<h3 ${id} class="openlime-section ${classes}" ${tooltip}>${entry.section}</h3>`;

		} else if ('html' in entry) {
			html += `<div ${id} class="${classes}">${entry.html}</div>`;

		} else if ('button' in entry) {
			let group = 'group' in entry ? `data-group="${entry.group}"` : '';
			let layer = 'layer' in entry ? `data-layer="${entry.layer}"` : '';
			let mode = 'mode' in entry ? `data-mode="${entry.mode}"` : '';
			html += `<a href="#" ${id} ${group} ${layer} ${mode} ${tooltip} class="openlime-entry ${classes}">${entry.button}</a>`;
		} else if ('slider' in entry) {
			let value = ('value' in entry) ? entry['value'] : 50;
			html += `<input type="range" min="1" max="100" value="${value}" class="openlime-slider ${classes}" ${id}>`;
		}

		if ('list' in entry) {
			let ul = `<div class="openlime-list ${classes}">`;
			for (let li of entry.list)
				ul += this.createEntry(li);
			ul += '</div>';
			html += ul;
		}
		return html;
	}

	/**
	 * Attaches event handlers to menu entry elements
	 * @param {UIBasic~MenuEntry} entry - Menu entry to process
	 * @private
	 */
	addEntryCallbacks(entry) {
		entry.element = this.layerMenu.querySelector('#' + entry.id);
		if (entry.onclick)
			entry.element.addEventListener('click', (e) => {
				entry.onclick();
				//this.updateMenu();
			});
		if (entry.oninput)
			entry.element.addEventListener('input', entry.oninput);
		if (entry.oncreate)
			entry.oncreate();

		if ('list' in entry)
			for (let e of entry.list)
				this.addEntryCallbacks(e);
	}

	/**
	 * Updates menu entry state
	 * @param {UIBasic~MenuEntry} entry - Menu entry to update
	 * @private
	 */
	updateEntry(entry) {
		let status = entry.status ? entry.status() : '';
		entry.element.classList.toggle('active', status == 'active');

		if ('list' in entry)
			for (let e of entry.list)
				this.updateEntry(e);
	}

	/**
	 * Updates all menu entries
	 * @private
	 */
	updateMenu() {
		for (let entry of this.menu)
			this.updateEntry(entry);
	}

	/**
	 * Creates main menu structure
	 * @private
	 */
	createMenu() {
		this.entry_count = 0;
		let html = `<div class="openlime-layers-menu">`;
		for (let entry of this.menu) {
			html += this.createEntry(entry);
		}
		html += '</div>';


		let template = document.createElement('template');
		template.innerHTML = html.trim();
		this.layerMenu = template.content.firstChild;
		this.viewer.containerElement.appendChild(this.layerMenu);

		for (let entry of this.menu) {
			this.addEntryCallbacks(entry);
		}


		/*		for(let li of document.querySelectorAll('[data-layer]'))
					li.addEventListener('click', (e) => {
						this.setLayer(this.viewer.canvas.layers[li.getAttribute('data-layer')]);
					}); */
	}

	/**
	 * Toggles layer menu visibility
	 * @private
	 */
	toggleLayers() {
		this.layerMenu.classList.toggle('open');
	}

	/**
	 * Sets active layer and updates UI
	 * @param {Layer|string} layer_on - Layer or layer ID to activate
	 */
	setLayer(layer_on) {
		if (typeof layer_on == 'string')
			layer_on = this.viewer.canvas.layers[layer_on];

		if (layer_on.overlay) { //just toggle
			layer_on.setVisible(!layer_on.visible);

		} else {
			for (let layer of Object.values(this.viewer.canvas.layers)) {
				if (layer.overlay)
					continue;

				layer.setVisible(layer == layer_on);
				for (let c of layer.controllers) {
					if (c.control == 'light')
						c.active = this.lightActive && layer == layer_on;
				}
			}
		}
		this.updateMenu();
		this.viewer.redraw();
	}

	/**
	 * Hides layers menu
	 */
	// closeLayersMenu() {
	// 	this.layerMenu.style.display = 'none';
	// }
}

/**
 * A **UIDialog** is a top-level window used for communications with the user. It may be modal or modeless.
 * The content of the dialog can be either an HTML text or a pre-built DOM element.
 * When hidden, a dialog emits a 'closed' event.
 */
class UIDialog { //FIXME standalone class
	/**
	 * Instatiates a UIDialog object.
	 * @param {HTMLElement} container The HTMLElement on which the dialog is focused
	 * @param {Object} [options] An object literal with UIDialog parameters.
	 * @param {bool} options.modal Whether the dialog is modal. 
	 */
	constructor(container, options) {
		Object.assign(this, {
			dialog: null,
			content: null,
			container: container,
			modal: false,
			class: null,
			visible: false,
			backdropEvents: true
		}, options);
		this.create();
	}

	/**
	 * Creates dialog DOM structure
	 * @private
	 */
	create() {
		let background = document.createElement('div');
		background.classList.add('openlime-dialog-background');

		let dialog = document.createElement('div');
		dialog.classList.add('openlime-dialog');
		if (this.class)
			dialog.classList.add(this.class);

		(async () => {
			let close = await Skin.appendIcon(dialog, '.openlime-close');
			close.classList.add('openlime-close');
			close.addEventListener('click', () => this.hide());
			//content.appendChild(close);
		})();


		// let close = Skin.appendIcon(dialog, '.openlime-close');
		// close.classList.add('openlime-close');
		// close.addEventListener('click', () => this.hide());

		let content = document.createElement('div');
		content.classList.add('openlime-dialog-content');
		dialog.append(content);

		if (this.modal) { //FIXME backdrown => backdrop
			if (this.backdropEvents) background.addEventListener('click', (e) => { if (e.target == background) this.hide(); });
			background.appendChild(dialog);
			this.container.appendChild(background);
			this.element = background;
		} else {
			this.container.appendChild(dialog);
			this.element = dialog;
		}

		this.dialog = dialog;
		this.content = content;
		this.hide();
	}

	/**
	 * Sets dialog content
	 * @param {string|HTMLElement} html - Content to display
	 */
	setContent(html) {
		if (typeof (html) == 'string')
			this.content.innerHTML = html;
		else
			this.content.replaceChildren(html);
	}

	/**
	 * Shows the dialog.
	 */
	show() {
		this.element.classList.remove('hidden');
		this.visible = true;
	}

	/**
	 * Hides the dialog and emits closed event
	 * @fires UIDialog#closed
	 */
	hide() {
		/**
		 * The event is fired when the dialog is closed.
		 * @event UIDialog#closed
		 */
		this.element.classList.add('hidden');
		this.visible = false;
		this.emit('closed');
	}

	/**
	 * Toggles fade effect
	 * @param {boolean} on - Whether to enable fade effect
	 */
	fade(on) { //FIXME Does it work?
		this.element.classList.toggle('fading');
	}

	/**
	 * Toggles dialog visibility
	 * @param {boolean} [force] - Force specific state
	 */
	toggle(force) { //FIXME Why not remove force?
		this.element.classList.toggle('hidden', force);
		this.visible = !this.visible; //FIXME not in sync with 'force'
	}
}

/**
 * Event Definitions
 * 
 * Light Direction Change Event:
 * @event UIBasic#lightdirection
 * @type {Object}
 * @property {number[]} direction - [x, y, z] normalized light vector
 * 
 * Dialog Close Event:
 * @event UIDialog#closed
 * Emitted when dialog is closed through any means
 */

addSignals(UIDialog, 'closed');
addSignals(UIBasic, 'lightdirection');

export { UIBasic, UIDialog }