Source: LayerSvgAnnotation.js

import { Util } from './Util'
import { Layer } from './Layer'
import { Annotation } from './Annotation'
import { LayerAnnotation } from './LayerAnnotation'
import { CoordinateSystem } from './CoordinateSystem';

/**
* @typedef {Object} AnnoClass
* @property {string} stroke - CSS color for SVG elements (lines, text, outlines)
* @property {string} label - Display name for the class
*/

/**
* @typedef {Object.<string, AnnoClass>} AnnoClasses
* @description Map of class names to their visual properties
*/

/**
* @typedef {Object} LayerSvgAnnotationOptions
* @property {AnnoClasses} classes - Annotation class definitions with styles
* @property {Function} [onClick] - Callback for annotation click events (param: selected annotation)
* @property {boolean} [shadow=true] - Whether to use Shadow DOM for SVG elements
* @property {HTMLElement} [overlayElement] - Container for SVG overlay
* @property {string} [style] - Additional CSS styles for annotations
* @property {Function} [annotationUpdate] - Custom update function for annotations
* @extends LayerAnnotationOptions
*/

/**
* LayerSvgAnnotation provides SVG-based annotation capabilities in OpenLIME.
* It renders SVG elements directly on the canvas overlay, outside the WebGL context,
* enabling rich vector graphics annotations with interactive features.
* 
* Features:
* - SVG-based vector annotations
* - Custom styling per annotation class
* - Interactive selection
* - Shadow DOM isolation
* - Dynamic SVG transformation
* - Event handling
* - Custom update callbacks
* 
* Technical Details:
* - Uses SVG overlay for rendering
* - Handles coordinate system transformations
* - Manages DOM element lifecycle
* - Supports custom class styling
* - Implements visibility management
* - Provides selection mechanisms
* 
* @extends LayerAnnotation
* 
* @example
* ```javascript
* // Create SVG annotation layer with custom classes
* const annotationLayer = new OpenLIME.Layer({
*   type: 'svg_annotations',
*   classes: {
*     'highlight': { stroke: '#ff0', label: 'Highlight' },
*     'comment': { stroke: '#0f0', label: 'Comment' }
*   },
*   onClick: (annotation) => {
*     console.log('Clicked:', annotation.label);
*   },
*   shadow: true
* });
* 
* // Add to viewer
* viewer.addLayer('annotations', annotationLayer);
* ```
*/
class LayerSvgAnnotation extends LayerAnnotation {
	/**
	 * Creates a new LayerSvgAnnotation instance
	 * @param {LayerSvgAnnotationOptions} [options] - Configuration options
	 */
	constructor(options) {
		options = Object.assign({
			overlayElement: null,   //reference to canvas overlayElement. TODO: check if really needed.
			shadow: true,           //svg attached as shadow node (so style apply only the svg layer)
			svgElement: null, 		//the svg layer
			svgGroup: null,
			onClick: null,			//callback function
			classes: {
				'': { stroke: '#000', label: '' },
			},
			annotationUpdate: null
		}, options);
		super(options);
		for (const [key, value] of Object.entries(this.classes)) {
			this.style += `[data-class=${key}] { ` + Object.entries(value).map(g => `${g[0]}: ${g[1]};`).join('\n') + '}';
		}

		this.style += `.openlime-svgoverlay { position:absolute; top:0px; left:0px;}`;

		//this.createOverlaySVGElement();
		//this.setLayout(this.layout);
	}

	/**
	 * Creates the SVG overlay element and initializes the shadow DOM if enabled
	 * @private
	 */
	createOverlaySVGElement() {
		this.svgElement = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
		this.svgElement.classList.add('openlime-svgoverlay');
		this.svgGroup = document.createElementNS('http://www.w3.org/2000/svg', 'g');
		this.svgElement.append(this.svgGroup);

		// Check if the shadow root already exists before attaching
		let root = this.overlayElement;
		if (this.shadow) {
			if (!this.overlayElement.shadowRoot) {
				root = this.overlayElement.attachShadow({ mode: "open" });
			} else {
				root = this.overlayElement.shadowRoot; // Use existing shadow root
			}
		}

		if (this.style) {
			const style = document.createElement('style');
			style.textContent = this.style;
			root.append(style);
		}
		root.appendChild(this.svgElement);
	}
	/*  unused for the moment!!! 
		async loadSVG(url) {
			var response = await fetch(url);
			if (!response.ok) {
				this.status = "Failed loading " + this.url + ": " + response.statusText;
				return;
			}
			let text = await response.text();
			let parser = new DOMParser();
			this.svgXML = parser.parseFromString(text, "image/svg+xml").documentElement;
			throw "if viewbox is set in svgURL should it overwrite options.viewbox or viceversa?"
		}
	*/

	/**
	 * Sets visibility of the annotation layer
	 * Updates both SVG display and underlying layer visibility
	 * @param {boolean} visible - Whether layer should be visible
	 * @override
	 */
	setVisible(visible) {
		if (this.svgElement)
			this.svgElement.style.display = visible ? 'block' : 'none';
		super.setVisible(visible);
	}

	/**
	 * Clears all annotation selections
	 */
	clearSelected() {
		if (!this.svgElement) this.createOverlaySVGElement();
		//		return;
		this.svgGroup.querySelectorAll('[data-annotation]').forEach((e) => e.classList.remove('selected'));
		super.clearSelected();
	}

	/**
	 * Sets selection state of an annotation
	 * @param {Annotation} anno - The annotation to select/deselect
	 * @param {boolean} [on=true] - Whether to select (true) or deselect (false)
	 */
	setSelected(anno, on = true) {
		for (let a of this.svgElement.querySelectorAll(`[data-annotation="${anno.id}"]`))
			a.classList.toggle('selected', on);

		super.setSelected(anno, on);
	}

	/**
	 * Creates a new SVG annotation
	 * @param {Annotation} [annotation] - Optional existing annotation to use
	 * @returns {Annotation} The created annotation
	 * @private
	 */
	newAnnotation(annotation) {
		let svg = Util.createSVGElement('svg');
		if (!annotation)
			annotation = new Annotation({ element: svg, selector_type: 'SvgSelector' });
		return super.newAnnotation(annotation)
	}

	/**
	 * Renders the SVG annotations
	 * Updates SVG viewBox and transformation to match current view
	 * @param {Transform} transform - Current view transform
	 * @param {Object} viewport - Current viewport
	 * @returns {boolean} Whether render completed successfully
	 * @override
	 */
	draw(transform, viewport) {
		if (!this.svgElement)
			return true;
		this.svgElement.setAttribute('viewBox', `${-viewport.w / 2} ${-viewport.h / 2} ${viewport.w} ${viewport.h}`);

		const svgTransform = this.getSvgGroupTransform(transform);
		this.svgGroup.setAttribute("transform", svgTransform);
		return true;
	}

	/**
	 * Calculates SVG group transform string
	 * @param {Transform} transform - Current view transform
	 * @param {boolean} [inverse=false] - Whether to return inverse transform
	 * @returns {string} SVG transform attribute value
	 */
	getSvgGroupTransform(transform, inverse = false) {
		let t = this.transform.compose(transform);
		let c = this.boundingBox().corner(0);
		// FIXME CHECK IT: Convert from GL to SVG, but without any scaling. It just needs to reflect around 0,
		t = CoordinateSystem.reflectY(t);
		return inverse ?
			`translate(${-c.x} ${-c.y})  scale(${1 / t.z} ${1 / t.z}) rotate(${t.a} 0 0) translate(${-t.x} ${-t.y})` :
			`translate(${t.x} ${t.y}) rotate(${-t.a} 0 0) scale(${t.z} ${t.z}) translate(${c.x} ${c.y})`;
	}

	/**
	 * Prepares annotations for rendering
	 * Handles SVG element creation and updates
	 * @param {Transform} transform - Current view transform
	 * @private
	 */
	prefetch(transform) {
		if (!this.svgElement)
			this.createOverlaySVGElement();

		if (!this.visible) return;
		if (this.status != 'ready')
			return;

		if (typeof (this.annotations) == "string") return; //FIXME Is it right? Should we use this.status?

		const bBox = this.boundingBox();
		//this.svgElement.setAttribute('viewBox', `${bBox.xLow} ${bBox.yLow} ${bBox.xHigh - bBox.xLow} ${bBox.yHigh - bBox.yLow}`);

		//find which annotations needs to be added to the ccanvas, some 
		//indexing whould be used, for the moment we just iterate all of them.

		for (let anno of this.annotations) {

			//TODO check for class visibility and bbox culling (or maybe should go to prefetch?)
			if (!anno.ready && typeof anno.svg == 'string') {
				let parser = new DOMParser();
				let element = parser.parseFromString(anno.svg, "image/svg+xml").documentElement;
				anno.elements = [...element.children]
				anno.ready = true;

				/*				} else if(this.svgXML) {
									a.svgElement = this.svgXML.querySelector(`#${a.id}`);
									if(!a.svgElement)
										throw Error(`Could not find element with id: ${id} in svg`);
								} */
			}

			if (this.annotationUpdate)
				this.annotationUpdate(anno, transform);

			if (!anno.needsUpdate)
				continue;

			anno.needsUpdate = false;

			for (let e of this.svgGroup.querySelectorAll(`[data-annotation="${anno.id}"]`))
				e.remove();

			if (!anno.visible)
				continue;

			//second time will be 0 elements, but we need to 
			//store somewhere knowledge of which items in the scene and which still not.
			for (let child of anno.elements) {
				let c = child; //.cloneNode(true);
				c.setAttribute('data-annotation', anno.id);
				c.setAttribute('data-class', anno.class);

				//c.setAttribute('data-layer', this.id);
				c.classList.add('openlime-annotation');
				if (this.selected.has(anno.id))
					c.classList.add('selected');
				this.svgGroup.appendChild(c);
				c.onpointerdown = (e) => {
					if (e.button == 0) {
						e.preventDefault();
						e.stopPropagation();
						if (this.onClick && this.onClick(anno))
							return;
						if (this.selected.has(anno.id))
							return;
						this.clearSelected();
						this.setSelected(anno, true);
					}
				}
			}
		}
	}
}

/**
 * Register this layer type with the Layer factory
 * @type {Function}
 * @private
 */
Layer.prototype.types['svg_annotations'] = (options) => { return new LayerSvgAnnotation(options); }

export { LayerSvgAnnotation }