/**
 * Image Annotation Manager
 *
 * Provides canvas-based image annotation capabilities using Fabric.js.
 * Supports bounding boxes, polygons, freeform drawing, and landmark points.
 */

class ImageAnnotationManager {
    /**
     * Create an ImageAnnotationManager.
     * @param {string} canvasId - ID of the canvas element
     * @param {string} inputId - ID of the hidden input for storing annotation data
     * @param {Object} config - Configuration object
     */
    constructor(canvasId, inputId, config) {
        this.canvasId = canvasId;
        this.inputId = inputId;
        this.config = config;

        this.canvas = null;
        this.image = null;
        this.currentTool = null;
        this.currentLabel = null;
        this.currentColor = '#FF6B6B';

        // Drawing state
        this.isDrawing = false;
        this.drawingObject = null;
        this.polygonPoints = [];
        this.isPanning = false;
        this.lastPosX = 0;
        this.lastPosY = 0;

        // History for undo/redo
        this.history = [];
        this.historyIndex = -1;
        this.maxHistory = 50;

        // Annotations storage
        this.annotations = [];

        // Callback for annotation count changes
        this.onAnnotationChange = null;

        // Segmentation mask state
        this.maskCanvas = null;
        this.maskCtx = null;
        this.masks = {};  // label -> ImageData
        this.brushSize = config.brushSize || 20;
        this.eraserSize = config.eraserSize || 20;
        this.maskOpacity = config.maskOpacity || 0.5;
        this.isMaskDrawing = false;

        // Initialize canvas
        this._initCanvas();
        this._initMaskCanvas();
        this._setupEventListeners();
        this._setupKeyboardShortcuts();
    }

    /**
     * Initialize the mask canvas for segmentation.
     */
    _initMaskCanvas() {
        const canvasEl = document.getElementById(this.canvasId);
        if (!canvasEl) return;

        const maskCanvasId = this.canvasId.replace('canvas-', 'mask-canvas-');
        this.maskCanvas = document.getElementById(maskCanvasId);

        if (!this.maskCanvas) {
            // Create mask canvas if it doesn't exist
            this.maskCanvas = document.createElement('canvas');
            this.maskCanvas.id = maskCanvasId;
            this.maskCanvas.className = 'mask-canvas';
            canvasEl.parentElement.appendChild(this.maskCanvas);
        }

        // Position mask canvas over the main canvas
        this.maskCanvas.style.position = 'absolute';
        this.maskCanvas.style.top = '0';
        this.maskCanvas.style.left = '0';
        this.maskCanvas.style.pointerEvents = 'none';  // Let events pass through to Fabric canvas

        this.maskCtx = this.maskCanvas.getContext('2d');
    }

    /**
     * Set up mask canvas event listeners.
     */
    _setupMaskEventListeners() {
        if (!this.maskCanvas) return;

        this.maskCanvas.addEventListener('mousedown', (e) => {
            if (this.currentTool === 'fill') {
                this._floodFill(e);
            } else if (this.currentTool === 'brush' || this.currentTool === 'eraser') {
                this._startMaskDraw(e);
            }
        });

        this.maskCanvas.addEventListener('mousemove', (e) => {
            this._continueMaskDraw(e);
        });

        this.maskCanvas.addEventListener('mouseup', () => {
            this._finishMaskDraw();
        });

        this.maskCanvas.addEventListener('mouseleave', () => {
            this._finishMaskDraw();
        });
    }

    /**
     * Initialize the Fabric.js canvas.
     */
    _initCanvas() {
        const canvasEl = document.getElementById(this.canvasId);
        if (!canvasEl) {
            console.error('Canvas element not found:', this.canvasId);
            return;
        }

        // Get parent container dimensions
        const container = canvasEl.parentElement;
        const width = container.clientWidth || 800;
        const height = 600;

        this.canvas = new fabric.Canvas(this.canvasId, {
            width: width,
            height: height,
            selection: true,
            preserveObjectStacking: true,
            backgroundColor: '#f8f9fa',  // Light gray background
        });

        // Set initial viewport
        this.canvas.setViewportTransform([1, 0, 0, 1, 0, 0]);
    }

    /**
     * Set up canvas event listeners for drawing.
     */
    _setupEventListeners() {
        if (!this.canvas) return;

        // Set up mask canvas event listeners
        this._setupMaskEventListeners();

        // Mouse down - start drawing
        this.canvas.on('mouse:down', (opt) => {
            const evt = opt.e;
            const pointer = this.canvas.getPointer(evt);

            // Handle pan with space key
            if (evt.altKey || this._spaceKeyDown) {
                this.isPanning = true;
                this.canvas.selection = false;
                this.lastPosX = evt.clientX;
                this.lastPosY = evt.clientY;
                return;
            }

            // If clicking on existing object, don't start new drawing
            if (opt.target && opt.target !== this.image) {
                return;
            }

            this._startDrawing(pointer);
        });

        // Mouse move - continue drawing or pan
        this.canvas.on('mouse:move', (opt) => {
            const evt = opt.e;

            if (this.isPanning) {
                const vpt = this.canvas.viewportTransform;
                vpt[4] += evt.clientX - this.lastPosX;
                vpt[5] += evt.clientY - this.lastPosY;
                this.canvas.requestRenderAll();
                this.lastPosX = evt.clientX;
                this.lastPosY = evt.clientY;
                // Re-render masks to follow pan
                this._renderAllMasks();
                return;
            }

            if (this.isDrawing) {
                const pointer = this.canvas.getPointer(evt);
                this._continueDrawing(pointer);
            }
        });

        // Mouse up - finish drawing
        this.canvas.on('mouse:up', (opt) => {
            if (this.isPanning) {
                this.isPanning = false;
                this.canvas.selection = true;
                return;
            }

            if (this.isDrawing) {
                this._finishDrawing();
            }
        });

        // Double click - complete polygon
        this.canvas.on('mouse:dblclick', (opt) => {
            if (this.currentTool === 'polygon' && this.polygonPoints.length > 2) {
                this._completePolygon();
            }
        });

        // Object modified - save state
        this.canvas.on('object:modified', () => {
            this._saveState();
            this._updateAnnotationData();
        });

        // Object removed
        this.canvas.on('object:removed', (opt) => {
            if (opt.target && opt.target.annotationData) {
                this._updateAnnotationData();
            }
        });

        // Selection events
        this.canvas.on('selection:created', () => {
            this._updateDeleteButtonState();
        });

        this.canvas.on('selection:cleared', () => {
            this._updateDeleteButtonState();
        });
    }

    /**
     * Set up keyboard shortcuts.
     */
    _setupKeyboardShortcuts() {
        this._spaceKeyDown = false;

        document.addEventListener('keydown', (e) => {
            // Only handle if canvas container is focused or visible
            const container = document.querySelector(`.image-annotation-container[data-schema="${this.config.schemaName}"]`);
            if (!container || !this._isElementVisible(container)) return;

            // Space for pan
            if (e.code === 'Space' && !this._spaceKeyDown) {
                this._spaceKeyDown = true;
                this.canvas.defaultCursor = 'grab';
                e.preventDefault();
            }

            // Tool shortcuts
            if (!e.ctrlKey && !e.metaKey) {
                switch (e.key.toLowerCase()) {
                    case 'b':
                        if (this.config.tools.includes('bbox')) {
                            this._selectTool('bbox');
                        }
                        break;
                    case 'p':
                        if (this.config.tools.includes('polygon')) {
                            this._selectTool('polygon');
                        }
                        break;
                    case 'f':
                        if (this.config.tools.includes('freeform')) {
                            this._selectTool('freeform');
                        }
                        break;
                    case 'l':
                        if (this.config.tools.includes('landmark')) {
                            this._selectTool('landmark');
                        }
                        break;
                    case 'delete':
                    case 'backspace':
                        this.deleteSelected();
                        e.preventDefault();
                        break;
                    case '+':
                    case '=':
                        this.zoom(1.2);
                        e.preventDefault();
                        break;
                    case '-':
                        this.zoom(0.8);
                        e.preventDefault();
                        break;
                    case '0':
                        this.zoomFit();
                        e.preventDefault();
                        break;
                }

                // Label shortcuts
                for (const label of this.config.labels) {
                    if (label.key_value && e.key === label.key_value) {
                        this.setLabel(label.name, label.color);
                        this._updateLabelButtonState(label.name);
                    }
                }
            }

            // Undo/Redo
            if ((e.ctrlKey || e.metaKey) && e.key === 'z') {
                if (e.shiftKey) {
                    this.redo();
                } else {
                    this.undo();
                }
                e.preventDefault();
            }
        });

        document.addEventListener('keyup', (e) => {
            if (e.code === 'Space') {
                this._spaceKeyDown = false;
                this.canvas.defaultCursor = 'default';
            }
        });
    }

    /**
     * Check if element is visible.
     */
    _isElementVisible(el) {
        return el.offsetParent !== null;
    }

    /**
     * Select a tool programmatically.
     */
    _selectTool(tool) {
        this.setTool(tool);
        const container = document.querySelector(`.image-annotation-container[data-schema="${this.config.schemaName}"]`);
        if (container) {
            container.querySelectorAll('.tool-btn').forEach(btn => {
                btn.classList.remove('active');
                if (btn.dataset.tool === tool) {
                    btn.classList.add('active');
                }
            });
        }
    }

    /**
     * Update label button state.
     */
    _updateLabelButtonState(labelName) {
        const container = document.querySelector(`.image-annotation-container[data-schema="${this.config.schemaName}"]`);
        if (container) {
            container.querySelectorAll('.label-btn').forEach(btn => {
                btn.classList.remove('active');
                if (btn.dataset.label === labelName) {
                    btn.classList.add('active');
                }
            });
        }
    }

    /**
     * Update delete button enabled state.
     */
    _updateDeleteButtonState() {
        const container = document.querySelector(`.image-annotation-container[data-schema="${this.config.schemaName}"]`);
        if (container) {
            const deleteBtn = container.querySelector('.delete-btn');
            if (deleteBtn) {
                const hasSelection = this.canvas.getActiveObject() !== null;
                deleteBtn.disabled = !hasSelection;
            }
        }
    }

    /**
     * Load an image onto the canvas.
     * @param {string} imageUrl - URL of the image to load
     */
    loadImage(imageUrl) {
        if (!this.canvas) return;

        // Get the container element for status updates
        const container = document.querySelector(`.image-annotation-container[data-schema="${this.config.schemaName}"]`);

        // Show loading state
        if (container) {
            container.classList.add('loading');
            container.classList.remove('error');
        }

        console.log('Loading image:', imageUrl);
        fabric.Image.fromURL(imageUrl, (img) => {
            // Remove loading state
            if (container) {
                container.classList.remove('loading');
            }

            if (!img || !img.width || !img.height) {
                console.error('Failed to load image:', imageUrl);
                if (container) {
                    container.classList.add('error');
                }
                // Show error message on canvas
                this._showCanvasMessage('Failed to load image. Check the URL or CORS settings.');
                return;
            }
            console.log('Image loaded successfully:', img.width, 'x', img.height);

            this.image = img;

            // Scale image to fit canvas while maintaining aspect ratio
            const canvasWidth = this.canvas.getWidth();
            const canvasHeight = this.canvas.getHeight();
            const scale = Math.min(
                canvasWidth / img.width,
                canvasHeight / img.height,
                1  // Don't scale up
            );

            img.set({
                scaleX: scale,
                scaleY: scale,
                left: (canvasWidth - img.width * scale) / 2,
                top: (canvasHeight - img.height * scale) / 2,
                selectable: false,
                evented: false,
                hoverCursor: 'default',
            });

            // Store original dimensions for coordinate normalization
            this.imageOriginalWidth = img.width;
            this.imageOriginalHeight = img.height;
            this.imageScale = scale;
            this.imageLeft = img.left;
            this.imageTop = img.top;

            this.canvas.add(img);
            this.canvas.sendToBack(img);
            this.canvas.renderAll();

            // Initialize mask canvas dimensions
            this._resizeMaskCanvas();

            // Load any existing annotations
            this._loadExistingAnnotations();

            // Load any existing masks
            this._loadExistingMasks();
        }, { crossOrigin: 'anonymous' });
    }

    /**
     * Set the current drawing tool.
     * @param {string} tool - Tool name (bbox, polygon, freeform, landmark, brush, eraser, fill)
     */
    setTool(tool) {
        this.currentTool = tool;
        this.isDrawing = false;
        this.drawingObject = null;
        this.polygonPoints = [];

        // Update canvas mode
        if (tool === 'freeform') {
            this.canvas.isDrawingMode = true;
            this.canvas.freeDrawingBrush.color = this.currentColor;
            this.canvas.freeDrawingBrush.width = this.config.freeformBrushSize || 5;
            this._showMaskCanvas(false);
        } else {
            this.canvas.isDrawingMode = false;
        }

        // Show/hide mask canvas for mask tools
        if (tool === 'brush' || tool === 'eraser' || tool === 'fill') {
            this._showMaskCanvas(true);
            this.maskCanvas.style.pointerEvents = 'auto';
        } else {
            this._showMaskCanvas(this._hasMasks());
            if (this.maskCanvas) {
                this.maskCanvas.style.pointerEvents = 'none';
            }
        }

        // Update cursor
        switch (tool) {
            case 'bbox':
            case 'polygon':
            case 'landmark':
                this.canvas.defaultCursor = 'crosshair';
                break;
            case 'brush':
            case 'eraser':
                this.canvas.defaultCursor = 'crosshair';
                break;
            case 'fill':
                this.canvas.defaultCursor = 'crosshair';
                break;
            default:
                this.canvas.defaultCursor = 'default';
        }
    }

    /**
     * Set the brush/eraser size.
     * @param {number} size - Brush size in pixels
     */
    setBrushSize(size) {
        this.brushSize = size;
        this.eraserSize = size;
    }

    /**
     * Show or hide the mask canvas.
     * @param {boolean} show - Whether to show the mask canvas
     */
    _showMaskCanvas(show) {
        if (this.maskCanvas) {
            this.maskCanvas.style.display = show ? 'block' : 'none';
        }
    }

    /**
     * Check if there are any masks.
     */
    _hasMasks() {
        return Object.keys(this.masks).length > 0;
    }

    /**
     * Resize mask canvas to match image dimensions.
     */
    _resizeMaskCanvas() {
        if (!this.maskCanvas || !this.image) return;

        const imgWidth = this.image.width * this.image.scaleX;
        const imgHeight = this.image.height * this.image.scaleY;

        this.maskCanvas.width = this.canvas.getWidth();
        this.maskCanvas.height = this.canvas.getHeight();

        // Store mask dimensions relative to image
        this.maskImgWidth = this.image.width;
        this.maskImgHeight = this.image.height;

        // Re-render masks
        this._renderAllMasks();
    }

    /**
     * Start mask drawing (brush/eraser).
     */
    _startMaskDraw(e) {
        if (this.currentTool !== 'brush' && this.currentTool !== 'eraser') return;
        if (!this.currentLabel) return;

        this.isMaskDrawing = true;
        this._drawMaskPoint(e);
    }

    /**
     * Continue mask drawing.
     */
    _continueMaskDraw(e) {
        if (!this.isMaskDrawing) return;
        this._drawMaskPoint(e);
    }

    /**
     * Finish mask drawing.
     */
    _finishMaskDraw() {
        if (this.isMaskDrawing) {
            this.isMaskDrawing = false;
            this._saveState();
            this._updateMaskData();
        }
    }

    /**
     * Draw a point on the mask canvas.
     */
    _drawMaskPoint(e) {
        if (!this.image || !this.currentLabel) return;

        const rect = this.maskCanvas.getBoundingClientRect();
        const x = e.clientX - rect.left;
        const y = e.clientY - rect.top;

        // Get or create mask for current label
        if (!this.masks[this.currentLabel]) {
            this.masks[this.currentLabel] = {
                color: this.currentColor,
                data: new Uint8ClampedArray(this.maskImgWidth * this.maskImgHeight * 4)
            };
        }

        const mask = this.masks[this.currentLabel];
        const size = this.currentTool === 'eraser' ? this.eraserSize : this.brushSize;

        // Convert screen coordinates to image coordinates
        const imgCoords = this._screenToImageCoords(x, y);
        if (!imgCoords) return;

        // Draw circle on mask data
        this._drawCircleOnMask(mask, imgCoords.x, imgCoords.y, size / 2, this.currentTool === 'eraser');

        // Re-render the visible mask
        this._renderAllMasks();
    }

    /**
     * Convert screen coordinates to image coordinates.
     */
    _screenToImageCoords(screenX, screenY) {
        if (!this.image) return null;

        const vpt = this.canvas.viewportTransform;
        const zoom = this.canvas.getZoom();

        // Account for viewport transform
        const canvasX = (screenX - vpt[4]) / zoom;
        const canvasY = (screenY - vpt[5]) / zoom;

        // Convert to image coordinates
        const imgX = (canvasX - this.image.left) / this.image.scaleX;
        const imgY = (canvasY - this.image.top) / this.image.scaleY;

        // Check bounds
        if (imgX < 0 || imgX >= this.image.width || imgY < 0 || imgY >= this.image.height) {
            return null;
        }

        return { x: Math.floor(imgX), y: Math.floor(imgY) };
    }

    /**
     * Draw a circle on the mask data.
     */
    _drawCircleOnMask(mask, cx, cy, radius, erase) {
        const width = this.maskImgWidth;
        const height = this.maskImgHeight;
        const r = Math.floor(radius);

        for (let dy = -r; dy <= r; dy++) {
            for (let dx = -r; dx <= r; dx++) {
                if (dx * dx + dy * dy <= r * r) {
                    const x = cx + dx;
                    const y = cy + dy;
                    if (x >= 0 && x < width && y >= 0 && y < height) {
                        const idx = (y * width + x) * 4;
                        if (erase) {
                            mask.data[idx + 3] = 0;  // Set alpha to 0
                        } else {
                            // Parse color
                            const color = this._hexToRgb(mask.color);
                            mask.data[idx] = color.r;
                            mask.data[idx + 1] = color.g;
                            mask.data[idx + 2] = color.b;
                            mask.data[idx + 3] = 255;  // Full alpha in data
                        }
                    }
                }
            }
        }
    }

    /**
     * Flood fill on mask.
     */
    _floodFill(e) {
        if (!this.image || !this.currentLabel) return;

        const rect = this.maskCanvas.getBoundingClientRect();
        const x = e.clientX - rect.left;
        const y = e.clientY - rect.top;

        const imgCoords = this._screenToImageCoords(x, y);
        if (!imgCoords) return;

        // Get or create mask for current label
        if (!this.masks[this.currentLabel]) {
            this.masks[this.currentLabel] = {
                color: this.currentColor,
                data: new Uint8ClampedArray(this.maskImgWidth * this.maskImgHeight * 4)
            };
        }

        const mask = this.masks[this.currentLabel];
        const color = this._hexToRgb(mask.color);

        // Simple flood fill using a queue
        const width = this.maskImgWidth;
        const height = this.maskImgHeight;
        const visited = new Set();
        const queue = [[imgCoords.x, imgCoords.y]];

        // Get the target color (what we're filling over)
        const startIdx = (imgCoords.y * width + imgCoords.x) * 4;
        const targetAlpha = mask.data[startIdx + 3];

        // Only fill if starting on an empty area
        if (targetAlpha > 128) return;

        while (queue.length > 0) {
            const [px, py] = queue.shift();
            const key = `${px},${py}`;

            if (visited.has(key)) continue;
            if (px < 0 || px >= width || py < 0 || py >= height) continue;

            const idx = (py * width + px) * 4;
            if (mask.data[idx + 3] > 128) continue;  // Already filled

            visited.add(key);

            // Fill this pixel
            mask.data[idx] = color.r;
            mask.data[idx + 1] = color.g;
            mask.data[idx + 2] = color.b;
            mask.data[idx + 3] = 255;

            // Add neighbors (4-connected)
            queue.push([px + 1, py]);
            queue.push([px - 1, py]);
            queue.push([px, py + 1]);
            queue.push([px, py - 1]);

            // Limit fill size to prevent browser hang
            if (visited.size > 1000000) break;
        }

        this._renderAllMasks();
        this._saveState();
        this._updateMaskData();
    }

    /**
     * Render all masks to the mask canvas.
     */
    _renderAllMasks() {
        if (!this.maskCtx || !this.image) return;

        // Clear canvas
        this.maskCtx.clearRect(0, 0, this.maskCanvas.width, this.maskCanvas.height);

        const vpt = this.canvas.viewportTransform;
        const zoom = this.canvas.getZoom();

        // Calculate image position on screen
        const imgLeft = this.image.left * zoom + vpt[4];
        const imgTop = this.image.top * zoom + vpt[5];
        const imgWidth = this.image.width * this.image.scaleX * zoom;
        const imgHeight = this.image.height * this.image.scaleY * zoom;

        // Render each mask
        for (const label in this.masks) {
            const mask = this.masks[label];

            // Create ImageData from mask data
            const imageData = new ImageData(
                new Uint8ClampedArray(mask.data),
                this.maskImgWidth,
                this.maskImgHeight
            );

            // Create temporary canvas for scaling
            const tempCanvas = document.createElement('canvas');
            tempCanvas.width = this.maskImgWidth;
            tempCanvas.height = this.maskImgHeight;
            const tempCtx = tempCanvas.getContext('2d');
            tempCtx.putImageData(imageData, 0, 0);

            // Draw scaled mask with opacity
            this.maskCtx.globalAlpha = this.maskOpacity;
            this.maskCtx.drawImage(tempCanvas, imgLeft, imgTop, imgWidth, imgHeight);
        }

        this.maskCtx.globalAlpha = 1.0;
    }

    /**
     * Convert hex color to RGB.
     */
    _hexToRgb(hex) {
        const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
        return result ? {
            r: parseInt(result[1], 16),
            g: parseInt(result[2], 16),
            b: parseInt(result[3], 16)
        } : { r: 255, g: 0, b: 0 };
    }

    /**
     * Encode mask data as RLE (Run-Length Encoding).
     */
    _encodeMaskRLE(maskData) {
        const rle = [];
        let count = 0;
        let currentVal = 0;

        for (let i = 3; i < maskData.length; i += 4) {
            const val = maskData[i] > 128 ? 1 : 0;
            if (val === currentVal) {
                count++;
            } else {
                if (count > 0) rle.push(count);
                currentVal = val;
                count = 1;
            }
        }
        if (count > 0) rle.push(count);

        return rle;
    }

    /**
     * Decode RLE to mask data.
     */
    _decodeMaskRLE(rle, width, height, color) {
        const data = new Uint8ClampedArray(width * height * 4);
        let idx = 0;
        let val = 0;

        const rgb = this._hexToRgb(color);

        for (const count of rle) {
            for (let i = 0; i < count && idx < width * height; i++) {
                const dataIdx = idx * 4;
                if (val === 1) {
                    data[dataIdx] = rgb.r;
                    data[dataIdx + 1] = rgb.g;
                    data[dataIdx + 2] = rgb.b;
                    data[dataIdx + 3] = 255;
                }
                idx++;
            }
            val = 1 - val;
        }

        return data;
    }

    /**
     * Update the hidden input with mask data.
     */
    _updateMaskData() {
        const maskInputId = this.inputId.replace('input-', 'mask-input-');
        const input = document.getElementById(maskInputId);
        if (!input) return;

        const masksData = {};
        for (const label in this.masks) {
            const mask = this.masks[label];
            masksData[label] = {
                color: mask.color,
                rle: this._encodeMaskRLE(mask.data),
                width: this.maskImgWidth,
                height: this.maskImgHeight
            };
        }

        input.value = JSON.stringify(masksData);
    }

    /**
     * Load existing mask data.
     */
    _loadExistingMasks() {
        const maskInputId = this.inputId.replace('input-', 'mask-input-');
        const input = document.getElementById(maskInputId);
        if (!input || !input.value) return;

        try {
            const masksData = JSON.parse(input.value);
            for (const label in masksData) {
                const maskInfo = masksData[label];
                this.masks[label] = {
                    color: maskInfo.color,
                    data: this._decodeMaskRLE(maskInfo.rle, maskInfo.width, maskInfo.height, maskInfo.color)
                };
            }
            this._renderAllMasks();
        } catch (e) {
            console.warn('Failed to load existing masks:', e);
        }
    }

    /**
     * Set the current label and color.
     * @param {string} label - Label name
     * @param {string} color - Color hex code
     */
    setLabel(label, color) {
        this.currentLabel = label;
        this.currentColor = color || '#FF6B6B';

        if (this.canvas.isDrawingMode) {
            this.canvas.freeDrawingBrush.color = this.currentColor;
        }
    }

    /**
     * Start drawing based on current tool.
     */
    _startDrawing(pointer) {
        if (!this.currentTool || !this.currentLabel) return;

        switch (this.currentTool) {
            case 'bbox':
                this._startBbox(pointer);
                break;
            case 'polygon':
                this._addPolygonPoint(pointer);
                break;
            case 'landmark':
                this._addLandmark(pointer);
                break;
            // Freeform handled by Fabric's drawing mode
        }
    }

    /**
     * Continue drawing based on current tool.
     */
    _continueDrawing(pointer) {
        switch (this.currentTool) {
            case 'bbox':
                this._updateBbox(pointer);
                break;
        }
    }

    /**
     * Finish drawing based on current tool.
     */
    _finishDrawing() {
        switch (this.currentTool) {
            case 'bbox':
                this._finishBbox();
                break;
        }
    }

    /**
     * Start drawing a bounding box.
     */
    _startBbox(pointer) {
        this.isDrawing = true;
        this.startX = pointer.x;
        this.startY = pointer.y;

        this.drawingObject = new fabric.Rect({
            left: pointer.x,
            top: pointer.y,
            width: 0,
            height: 0,
            fill: this._colorWithAlpha(this.currentColor, 0.2),
            stroke: this.currentColor,
            strokeWidth: 2,
            selectable: true,
            hasControls: true,
            hasBorders: true,
        });

        this.canvas.add(this.drawingObject);
    }

    /**
     * Update bounding box while drawing.
     */
    _updateBbox(pointer) {
        if (!this.drawingObject) return;

        const left = Math.min(this.startX, pointer.x);
        const top = Math.min(this.startY, pointer.y);
        const width = Math.abs(pointer.x - this.startX);
        const height = Math.abs(pointer.y - this.startY);

        this.drawingObject.set({
            left: left,
            top: top,
            width: width,
            height: height,
        });

        this.canvas.renderAll();
    }

    /**
     * Finish drawing bounding box.
     */
    _finishBbox() {
        if (!this.drawingObject) return;

        this.isDrawing = false;

        // Only keep if it has reasonable size
        if (this.drawingObject.width > 5 && this.drawingObject.height > 5) {
            this.drawingObject.annotationData = {
                type: 'bbox',
                label: this.currentLabel,
                color: this.currentColor,
            };
            this._saveState();
            this._updateAnnotationData();
        } else {
            this.canvas.remove(this.drawingObject);
        }

        this.drawingObject = null;
    }

    /**
     * Add a point to the current polygon.
     */
    _addPolygonPoint(pointer) {
        this.polygonPoints.push({ x: pointer.x, y: pointer.y });

        // Draw point marker
        const point = new fabric.Circle({
            left: pointer.x - 4,
            top: pointer.y - 4,
            radius: 4,
            fill: this.currentColor,
            stroke: '#fff',
            strokeWidth: 1,
            selectable: false,
            evented: false,
            polygonMarker: true,
        });
        this.canvas.add(point);

        // Draw line to previous point
        if (this.polygonPoints.length > 1) {
            const prev = this.polygonPoints[this.polygonPoints.length - 2];
            const line = new fabric.Line(
                [prev.x, prev.y, pointer.x, pointer.y],
                {
                    stroke: this.currentColor,
                    strokeWidth: 2,
                    selectable: false,
                    evented: false,
                    polygonLine: true,
                }
            );
            this.canvas.add(line);
        }

        this.canvas.renderAll();
    }

    /**
     * Complete the polygon shape.
     */
    _completePolygon() {
        if (this.polygonPoints.length < 3) return;

        // Remove temporary markers and lines
        const toRemove = this.canvas.getObjects().filter(
            obj => obj.polygonMarker || obj.polygonLine
        );
        toRemove.forEach(obj => this.canvas.remove(obj));

        // Create polygon
        const polygon = new fabric.Polygon(this.polygonPoints, {
            fill: this._colorWithAlpha(this.currentColor, 0.2),
            stroke: this.currentColor,
            strokeWidth: 2,
            selectable: true,
            hasControls: true,
            hasBorders: true,
        });

        polygon.annotationData = {
            type: 'polygon',
            label: this.currentLabel,
            color: this.currentColor,
        };

        this.canvas.add(polygon);
        this.polygonPoints = [];
        this._saveState();
        this._updateAnnotationData();
    }

    /**
     * Add a landmark point.
     */
    _addLandmark(pointer) {
        const landmark = new fabric.Circle({
            left: pointer.x - 8,
            top: pointer.y - 8,
            radius: 8,
            fill: this.currentColor,
            stroke: '#fff',
            strokeWidth: 2,
            selectable: true,
            hasControls: false,
            hasBorders: true,
            originX: 'center',
            originY: 'center',
        });

        landmark.annotationData = {
            type: 'landmark',
            label: this.currentLabel,
            color: this.currentColor,
        };

        // Add label text
        const text = new fabric.Text(this.currentLabel, {
            left: pointer.x + 12,
            top: pointer.y - 6,
            fontSize: 12,
            fill: this.currentColor,
            selectable: false,
            evented: false,
        });

        // Group landmark and label
        const group = new fabric.Group([landmark, text], {
            left: pointer.x - 8,
            top: pointer.y - 8,
            selectable: true,
            hasControls: false,
        });

        group.annotationData = landmark.annotationData;

        this.canvas.add(group);
        this._saveState();
        this._updateAnnotationData();
    }

    /**
     * Handle freeform path completion.
     */
    _handleFreeformPath() {
        const path = this.canvas.getObjects().find(
            obj => obj.type === 'path' && !obj.annotationData
        );

        if (path) {
            path.annotationData = {
                type: 'freeform',
                label: this.currentLabel,
                color: this.currentColor,
            };

            path.set({
                stroke: this.currentColor,
                fill: this._colorWithAlpha(this.currentColor, 0.1),
            });

            this._saveState();
            this._updateAnnotationData();
        }
    }

    /**
     * Delete the currently selected annotation.
     */
    deleteSelected() {
        const active = this.canvas.getActiveObject();
        if (active && active !== this.image) {
            this.canvas.remove(active);
            this._saveState();
            this._updateAnnotationData();
        }
    }

    /**
     * Zoom the canvas.
     * @param {number} factor - Zoom factor (>1 to zoom in, <1 to zoom out)
     */
    zoom(factor) {
        if (!this.canvas) return;

        const center = this.canvas.getCenter();
        let zoom = this.canvas.getZoom() * factor;

        // Clamp zoom
        zoom = Math.max(0.1, Math.min(10, zoom));

        this.canvas.zoomToPoint(
            new fabric.Point(center.left, center.top),
            zoom
        );

        // Re-render masks to match new viewport
        this._renderAllMasks();
    }

    /**
     * Zoom to fit the image.
     */
    zoomFit() {
        if (!this.canvas || !this.image) return;

        const canvasWidth = this.canvas.getWidth();
        const canvasHeight = this.canvas.getHeight();

        const scale = Math.min(
            canvasWidth / (this.image.width * this.image.scaleX),
            canvasHeight / (this.image.height * this.image.scaleY),
            1
        );

        this.canvas.setViewportTransform([1, 0, 0, 1, 0, 0]);
        this.canvas.zoomToPoint(
            new fabric.Point(canvasWidth / 2, canvasHeight / 2),
            scale
        );

        // Re-render masks to match new viewport
        this._renderAllMasks();
    }

    /**
     * Reset zoom to 100%.
     */
    zoomReset() {
        if (!this.canvas) return;
        this.canvas.setViewportTransform([1, 0, 0, 1, 0, 0]);

        // Re-render masks to match new viewport
        this._renderAllMasks();
    }

    /**
     * Undo the last action.
     */
    undo() {
        if (this.historyIndex > 0) {
            this.historyIndex--;
            this._restoreState(this.history[this.historyIndex]);
        }
    }

    /**
     * Redo the last undone action.
     */
    redo() {
        if (this.historyIndex < this.history.length - 1) {
            this.historyIndex++;
            this._restoreState(this.history[this.historyIndex]);
        }
    }

    /**
     * Save current state to history.
     */
    _saveState() {
        // Remove future history if we're not at the end
        this.history = this.history.slice(0, this.historyIndex + 1);

        // Save current state
        const state = this._serializeAnnotations();
        this.history.push(state);

        // Trim history if too long
        if (this.history.length > this.maxHistory) {
            this.history.shift();
        }

        this.historyIndex = this.history.length - 1;
    }

    /**
     * Restore a saved state.
     */
    _restoreState(state) {
        // Remove all annotation objects (keep image)
        const toRemove = this.canvas.getObjects().filter(
            obj => obj !== this.image && obj.annotationData
        );
        toRemove.forEach(obj => this.canvas.remove(obj));

        // Restore annotations
        this._deserializeAnnotations(state);
        this._updateAnnotationData();
    }

    /**
     * Serialize all annotations to JSON.
     */
    _serializeAnnotations() {
        const annotations = [];

        this.canvas.getObjects().forEach(obj => {
            if (obj.annotationData) {
                const ann = {
                    type: obj.annotationData.type,
                    label: obj.annotationData.label,
                    color: obj.annotationData.color,
                    coordinates: this._getObjectCoordinates(obj),
                };
                annotations.push(ann);
            }
        });

        return JSON.stringify(annotations);
    }

    /**
     * Deserialize annotations from JSON and add to canvas.
     */
    _deserializeAnnotations(json) {
        const annotations = JSON.parse(json);

        annotations.forEach(ann => {
            this._createAnnotationObject(ann);
        });

        this.canvas.renderAll();
    }

    /**
     * Get normalized coordinates for an object.
     */
    _getObjectCoordinates(obj) {
        if (!this.image) return null;

        const imgWidth = this.image.width * this.image.scaleX;
        const imgHeight = this.image.height * this.image.scaleY;
        const imgLeft = this.image.left;
        const imgTop = this.image.top;

        const normalize = (x, y) => ({
            x: (x - imgLeft) / imgWidth,
            y: (y - imgTop) / imgHeight,
        });

        switch (obj.annotationData.type) {
            case 'bbox':
                const tl = normalize(obj.left, obj.top);
                return {
                    x: tl.x,
                    y: tl.y,
                    width: (obj.width * obj.scaleX) / imgWidth,
                    height: (obj.height * obj.scaleY) / imgHeight,
                };

            case 'polygon':
                return obj.points.map(p => {
                    const absX = obj.left + p.x - obj.pathOffset.x;
                    const absY = obj.top + p.y - obj.pathOffset.y;
                    return normalize(absX, absY);
                });

            case 'landmark':
                if (obj.type === 'group') {
                    const centerX = obj.left + obj.width / 2;
                    const centerY = obj.top + obj.height / 2;
                    return normalize(centerX, centerY);
                }
                return normalize(obj.left + 8, obj.top + 8);

            case 'freeform':
                // Serialize path data
                return {
                    path: obj.path,
                    left: (obj.left - imgLeft) / imgWidth,
                    top: (obj.top - imgTop) / imgHeight,
                    scaleX: obj.scaleX / (imgWidth / this.image.width),
                    scaleY: obj.scaleY / (imgHeight / this.image.height),
                };

            default:
                return null;
        }
    }

    /**
     * Create annotation object from serialized data.
     */
    _createAnnotationObject(ann) {
        if (!this.image) return;

        const imgWidth = this.image.width * this.image.scaleX;
        const imgHeight = this.image.height * this.image.scaleY;
        const imgLeft = this.image.left;
        const imgTop = this.image.top;

        const denormalize = (nx, ny) => ({
            x: nx * imgWidth + imgLeft,
            y: ny * imgHeight + imgTop,
        });

        let obj;

        switch (ann.type) {
            case 'bbox':
                const pos = denormalize(ann.coordinates.x, ann.coordinates.y);
                obj = new fabric.Rect({
                    left: pos.x,
                    top: pos.y,
                    width: ann.coordinates.width * imgWidth,
                    height: ann.coordinates.height * imgHeight,
                    fill: this._colorWithAlpha(ann.color, 0.2),
                    stroke: ann.color,
                    strokeWidth: 2,
                    selectable: true,
                    hasControls: true,
                });
                break;

            case 'polygon':
                const points = ann.coordinates.map(c => {
                    const p = denormalize(c.x, c.y);
                    return { x: p.x, y: p.y };
                });
                obj = new fabric.Polygon(points, {
                    fill: this._colorWithAlpha(ann.color, 0.2),
                    stroke: ann.color,
                    strokeWidth: 2,
                    selectable: true,
                    hasControls: true,
                });
                break;

            case 'landmark':
                const lpos = denormalize(ann.coordinates.x, ann.coordinates.y);
                const circle = new fabric.Circle({
                    left: 0,
                    top: 0,
                    radius: 8,
                    fill: ann.color,
                    stroke: '#fff',
                    strokeWidth: 2,
                    originX: 'center',
                    originY: 'center',
                });
                const text = new fabric.Text(ann.label, {
                    left: 12,
                    top: -6,
                    fontSize: 12,
                    fill: ann.color,
                });
                obj = new fabric.Group([circle, text], {
                    left: lpos.x - 8,
                    top: lpos.y - 8,
                    selectable: true,
                    hasControls: false,
                });
                break;

            case 'freeform':
                const coords = ann.coordinates;
                obj = new fabric.Path(coords.path, {
                    left: coords.left * imgWidth + imgLeft,
                    top: coords.top * imgHeight + imgTop,
                    scaleX: coords.scaleX * (imgWidth / this.image.width),
                    scaleY: coords.scaleY * (imgHeight / this.image.height),
                    stroke: ann.color,
                    fill: this._colorWithAlpha(ann.color, 0.1),
                    strokeWidth: 2,
                    selectable: true,
                });
                break;
        }

        if (obj) {
            obj.annotationData = {
                type: ann.type,
                label: ann.label,
                color: ann.color,
            };
            this.canvas.add(obj);
        }
    }

    /**
     * Update the hidden input with current annotation data.
     */
    _updateAnnotationData() {
        const input = document.getElementById(this.inputId);
        if (input) {
            input.value = this._serializeAnnotations();
        }

        // Update annotation count
        const count = this.canvas.getObjects().filter(
            obj => obj !== this.image && obj.annotationData
        ).length;

        if (this.onAnnotationChange) {
            this.onAnnotationChange(count);
        }
    }

    /**
     * Load existing annotations from the hidden input.
     */
    _loadExistingAnnotations() {
        const input = document.getElementById(this.inputId);
        if (input && input.value) {
            try {
                this._deserializeAnnotations(input.value);
                this._saveState();
                this._updateAnnotationData();  // Update count display after loading
            } catch (e) {
                console.warn('Failed to load existing annotations:', e);
            }
        } else {
            // No existing annotations - still update count to show 0
            this._updateAnnotationData();
        }
    }

    /**
     * Convert hex color to rgba with alpha.
     */
    _colorWithAlpha(hex, alpha) {
        const r = parseInt(hex.slice(1, 3), 16);
        const g = parseInt(hex.slice(3, 5), 16);
        const b = parseInt(hex.slice(5, 7), 16);
        return `rgba(${r}, ${g}, ${b}, ${alpha})`;
    }

    /**
     * Show a message on the canvas (for errors/loading states).
     * @param {string} message - Message to display
     */
    _showCanvasMessage(message) {
        if (!this.canvas) return;

        // Clear canvas and show message
        this.canvas.clear();
        this.canvas.setBackgroundColor('#f8f9fa', this.canvas.renderAll.bind(this.canvas));

        const text = new fabric.Text(message, {
            left: this.canvas.getWidth() / 2,
            top: this.canvas.getHeight() / 2,
            fontSize: 16,
            fill: '#dc3545',
            fontFamily: 'system-ui, -apple-system, sans-serif',
            originX: 'center',
            originY: 'center',
            textAlign: 'center',
            selectable: false,
            evented: false,
        });

        this.canvas.add(text);
        this.canvas.renderAll();
    }

    /**
     * Get current annotation count.
     */
    getAnnotationCount() {
        return this.canvas.getObjects().filter(
            obj => obj !== this.image && obj.annotationData
        ).length;
    }

    /**
     * Clear all annotations from the canvas.
     * Used when switching to a new instance.
     */
    clearAnnotations() {
        const objects = this.canvas.getObjects();
        objects.forEach(obj => {
            if (obj !== this.image && obj.annotationData) {
                this.canvas.remove(obj);
            }
        });
        this.canvas.renderAll();

        // Reset history
        this.history = [];
        this.historyIndex = -1;

        // Update the hidden input and count display
        this._updateAnnotationData();
    }

    /**
     * Serialize annotations for form submission.
     */
    serialize() {
        return this._serializeAnnotations();
    }

    /**
     * Load annotations from JSON.
     */
    deserialize(json) {
        this._deserializeAnnotations(json);
        this._saveState();
        this._updateAnnotationData();
    }
}

// Export for use in modules if needed
if (typeof module !== 'undefined' && module.exports) {
    module.exports = ImageAnnotationManager;
}

// Make available in browser environments
if (typeof window !== 'undefined') {
    window.ImageAnnotationManager = ImageAnnotationManager;
}
