import {createModuleLogger} from "./utils/LoggerUtils.js";

const log = createModuleLogger('CanvasRenderer');

export class CanvasRenderer {
    canvas: any;
    isDirty: any;
    lastRenderTime: any;
    renderAnimationFrame: any;
    renderInterval: any;
    // Overlay used to preview in-progress mask strokes (separate from cursor overlay)
    strokeOverlayCanvas!: HTMLCanvasElement;
    strokeOverlayCtx!: CanvasRenderingContext2D;
    constructor(canvas: any) {
        this.canvas = canvas;
        this.renderAnimationFrame = null;
        this.lastRenderTime = 0;
        this.renderInterval = 1000 / 60;
        this.isDirty = false;
        
        // Initialize overlay canvases
        this.initOverlay();
        this.initStrokeOverlay();
    }

    /**
     * Helper function to draw text with background at world coordinates
     * @param ctx Canvas context
     * @param text Text to display
     * @param worldX World X coordinate
     * @param worldY World Y coordinate
     * @param options Optional styling options
     */
    drawTextWithBackground(ctx: any, text: string, worldX: number, worldY: number, options: any = {}) {
        const {
            font = "14px sans-serif",
            textColor = "white",
            backgroundColor = "rgba(0, 0, 0, 0.7)",
            padding = 10,
            lineHeight = 18
        } = options;

        ctx.save();
        ctx.setTransform(1, 0, 0, 1, 0, 0);
        const screenX = (worldX - this.canvas.viewport.x) * this.canvas.viewport.zoom;
        const screenY = (worldY - this.canvas.viewport.y) * this.canvas.viewport.zoom;
        
        ctx.font = font;
        ctx.textAlign = "center";
        ctx.textBaseline = "middle";
        
        const lines = text.split('\n');
        const textMetrics = lines.map(line => ctx.measureText(line));
        const bgWidth = Math.max(...textMetrics.map(m => m.width)) + padding;
        const bgHeight = lines.length * lineHeight + 4;
        
        ctx.fillStyle = backgroundColor;
        ctx.fillRect(screenX - bgWidth / 2, screenY - bgHeight / 2, bgWidth, bgHeight);
        
        ctx.fillStyle = textColor;
        lines.forEach((line, index) => {
            const yPos = screenY - (bgHeight / 2) + (lineHeight / 2) + (index * lineHeight) + 2;
            ctx.fillText(line, screenX, yPos);
        });
        
        ctx.restore();
    }

    /**
     * Helper function to draw rectangle with stroke style
     * @param ctx Canvas context
     * @param rect Rectangle bounds {x, y, width, height}
     * @param options Styling options
     */
    drawStyledRect(ctx: any, rect: any, options: any = {}) {
        const {
            strokeStyle = "rgba(255, 255, 255, 0.8)",
            lineWidth = 2,
            dashPattern = null
        } = options;

        ctx.save();
        ctx.strokeStyle = strokeStyle;
        ctx.lineWidth = lineWidth / this.canvas.viewport.zoom;
        
        if (dashPattern) {
            const scaledDash = dashPattern.map((d: number) => d / this.canvas.viewport.zoom);
            ctx.setLineDash(scaledDash);
        }
        
        ctx.strokeRect(rect.x, rect.y, rect.width, rect.height);
        
        if (dashPattern) {
            ctx.setLineDash([]);
        }
        
        ctx.restore();
    }

    render() {
        if (this.renderAnimationFrame) {
            this.isDirty = true;
            return;
        }
        this.renderAnimationFrame = requestAnimationFrame(() => {
            const now = performance.now();
            if (now - this.lastRenderTime >= this.renderInterval) {
                this.lastRenderTime = now;
                this.actualRender();
                this.isDirty = false;
            }

            if (this.isDirty) {
                this.renderAnimationFrame = null;
                this.render();
            } else {
                this.renderAnimationFrame = null;
            }
        });
    }

    actualRender() {
        if (this.canvas.offscreenCanvas.width !== this.canvas.canvas.clientWidth ||
            this.canvas.offscreenCanvas.height !== this.canvas.canvas.clientHeight) {
            const newWidth = Math.max(1, this.canvas.canvas.clientWidth);
            const newHeight = Math.max(1, this.canvas.canvas.clientHeight);
            this.canvas.offscreenCanvas.width = newWidth;
            this.canvas.offscreenCanvas.height = newHeight;
        }

        const ctx = this.canvas.offscreenCtx;

        ctx.fillStyle = '#606060';
        ctx.fillRect(0, 0, this.canvas.offscreenCanvas.width, this.canvas.offscreenCanvas.height);

        ctx.save();
        ctx.scale(this.canvas.viewport.zoom, this.canvas.viewport.zoom);
        ctx.translate(-this.canvas.viewport.x, -this.canvas.viewport.y);

        this.drawGrid(ctx);

        // Use CanvasLayers to draw layers with proper blend area support
        this.canvas.canvasLayers.drawLayersToContext(ctx, this.canvas.layers);
        
        // Draw mask AFTER layers but BEFORE all preview outlines
        const maskImage = this.canvas.maskTool.getMask();
        if (maskImage && this.canvas.maskTool.isOverlayVisible) {
            ctx.save();

            if (this.canvas.maskTool.isActive) {
                // In draw mask mode, use the previewOpacity value from the slider
                ctx.globalCompositeOperation = 'source-over';
                ctx.globalAlpha = this.canvas.maskTool.previewOpacity;
            } else {
                // When not in draw mask mode, show mask at full opacity
                ctx.globalCompositeOperation = 'source-over';
                ctx.globalAlpha = 1.0;
            }

            // Renderuj maskę w jej pozycji światowej (bez przesunięcia względem bounds)
            const maskWorldX = this.canvas.maskTool.x;
            const maskWorldY = this.canvas.maskTool.y;
            ctx.drawImage(maskImage, maskWorldX, maskWorldY);

            ctx.globalAlpha = 1.0;
            ctx.restore();
        }
        
        // Draw selection frames for selected layers
        const sortedLayers = [...this.canvas.layers].sort((a, b) => a.zIndex - b.zIndex);
        sortedLayers.forEach(layer => {
            if (!layer.image || !layer.visible) return;
            if (this.canvas.canvasSelection.selectedLayers.includes(layer)) {
                ctx.save();
                const centerX = layer.x + layer.width / 2;
                const centerY = layer.y + layer.height / 2;
                ctx.translate(centerX, centerY);
                ctx.rotate(layer.rotation * Math.PI / 180);

                const scaleH = layer.flipH ? -1 : 1;
                const scaleV = layer.flipV ? -1 : 1;
                if (layer.flipH || layer.flipV) {
                    ctx.scale(scaleH, scaleV);
                }
                
                this.drawSelectionFrame(ctx, layer);
                ctx.restore();
            }
        });

        this.drawCanvasOutline(ctx);
        this.drawOutputAreaExtensionPreview(ctx); // Draw extension preview
        this.drawPendingGenerationAreas(ctx); // Draw snapshot outlines

        this.renderInteractionElements(ctx);
        this.canvas.shapeTool.render(ctx);
        this.drawMaskAreaBounds(ctx); // Draw mask area bounds when mask tool is active
        this.renderLayerInfo(ctx);
        
        // Update custom shape menu position and visibility
        if (this.canvas.outputAreaShape) {
            this.canvas.customShapeMenu.show();
            this.canvas.customShapeMenu.updateScreenPosition();
        } else {
            this.canvas.customShapeMenu.hide();
        }

        ctx.restore();

        if (this.canvas.canvas.width !== this.canvas.offscreenCanvas.width ||
            this.canvas.canvas.height !== this.canvas.offscreenCanvas.height) {
            this.canvas.canvas.width = this.canvas.offscreenCanvas.width;
            this.canvas.canvas.height = this.canvas.offscreenCanvas.height;
        }
        this.canvas.ctx.drawImage(this.canvas.offscreenCanvas, 0, 0);

        // Ensure overlay canvases are in DOM and properly sized
        this.addOverlayToDOM();
        this.updateOverlaySize();
        this.addStrokeOverlayToDOM();
        this.updateStrokeOverlaySize();

        // Update Batch Preview UI positions
        if (this.canvas.batchPreviewManagers && this.canvas.batchPreviewManagers.length > 0) {
            this.canvas.batchPreviewManagers.forEach((manager: any) => {
                manager.updateScreenPosition(this.canvas.viewport);
            });
        }
    }

    renderInteractionElements(ctx: any) {
        const interaction = this.canvas.interaction;

        if (interaction.mode === 'resizingCanvas' && interaction.canvasResizeRect) {
            const rect = interaction.canvasResizeRect;
            this.drawStyledRect(ctx, rect, {
                strokeStyle: 'rgba(0, 255, 0, 0.8)',
                lineWidth: 2,
                dashPattern: [8, 4]
            });
            if (rect.width > 0 && rect.height > 0) {
                const text = `${Math.round(rect.width)}x${Math.round(rect.height)}`;
                const textWorldX = rect.x + rect.width / 2;
                const textWorldY = rect.y + rect.height + (20 / this.canvas.viewport.zoom);

                this.drawTextWithBackground(ctx, text, textWorldX, textWorldY, {
                    backgroundColor: "rgba(0, 128, 0, 0.7)"
                });
            }
        }

        if (interaction.mode === 'movingCanvas' && interaction.canvasMoveRect) {
            const rect = interaction.canvasMoveRect;
            this.drawStyledRect(ctx, rect, {
                strokeStyle: 'rgba(0, 150, 255, 0.8)',
                lineWidth: 2,
                dashPattern: [10, 5]
            });

            const text = `(${Math.round(rect.x)}, ${Math.round(rect.y)})`;
            const textWorldX = rect.x + rect.width / 2;
            const textWorldY = rect.y - (20 / this.canvas.viewport.zoom);

            this.drawTextWithBackground(ctx, text, textWorldX, textWorldY, {
                backgroundColor: "rgba(0, 100, 170, 0.7)"
            });
        }
    }

    renderLayerInfo(ctx: any) {
        if (this.canvas.canvasSelection.selectedLayer) {
            this.canvas.canvasSelection.selectedLayers.forEach((layer: any) => {
                if (!layer.image || !layer.visible) return;

                const layerIndex = this.canvas.layers.indexOf(layer);
                const currentWidth = Math.round(layer.width);
                const currentHeight = Math.round(layer.height);
                const rotation = Math.round(layer.rotation % 360);
                let text = `${currentWidth}x${currentHeight} | ${rotation}° | Layer #${layerIndex + 1}`;
                if (layer.originalWidth && layer.originalHeight) {
                    text += `\nOriginal: ${layer.originalWidth}x${layer.originalHeight}`;
                }
                const centerX = layer.x + layer.width / 2;
                const centerY = layer.y + layer.height / 2;
                const rad = layer.rotation * Math.PI / 180;
                const cos = Math.cos(rad);
                const sin = Math.sin(rad);

                const halfW = layer.width / 2;
                const halfH = layer.height / 2;

                const localCorners = [
                    {x: -halfW, y: -halfH},
                    {x: halfW, y: -halfH},
                    {x: halfW, y: halfH},
                    {x: -halfW, y: halfH}
                ];
                const worldCorners = localCorners.map(p => ({
                    x: centerX + p.x * cos - p.y * sin,
                    y: centerY + p.x * sin + p.y * cos
                }));
                let minX = Infinity, maxX = -Infinity, maxY = -Infinity;
                worldCorners.forEach(p => {
                    minX = Math.min(minX, p.x);
                    maxX = Math.max(maxX, p.x);
                    maxY = Math.max(maxY, p.y);
                });
                const padding = 20 / this.canvas.viewport.zoom;
                const textWorldX = (minX + maxX) / 2;
                const textWorldY = maxY + padding;
                
                this.drawTextWithBackground(ctx, text, textWorldX, textWorldY);
            });
        }
    }

    drawGrid(ctx: any) {
        const gridSize = 64;
        const lineWidth = 0.5 / this.canvas.viewport.zoom;

        const viewLeft = this.canvas.viewport.x;
        const viewTop = this.canvas.viewport.y;
        const viewRight = this.canvas.viewport.x + this.canvas.offscreenCanvas.width / this.canvas.viewport.zoom;
        const viewBottom = this.canvas.viewport.y + this.canvas.offscreenCanvas.height / this.canvas.viewport.zoom;

        ctx.beginPath();
        ctx.strokeStyle = '#707070';
        ctx.lineWidth = lineWidth;

        for (let x = Math.floor(viewLeft / gridSize) * gridSize; x < viewRight; x += gridSize) {
            ctx.moveTo(x, viewTop);
            ctx.lineTo(x, viewBottom);
        }

        for (let y = Math.floor(viewTop / gridSize) * gridSize; y < viewBottom; y += gridSize) {
            ctx.moveTo(viewLeft, y);
            ctx.lineTo(viewRight, y);
        }

        ctx.stroke();
    }

    /**
     * Check if custom shape overlaps with any active batch preview areas
     */
    isCustomShapeOverlappingWithBatchAreas(): boolean {
        if (!this.canvas.outputAreaShape || !this.canvas.batchPreviewManagers || this.canvas.batchPreviewManagers.length === 0) {
            return false;
        }

        // Get custom shape bounds
        const bounds = this.canvas.outputAreaBounds;
        const ext = this.canvas.outputAreaExtensionEnabled ? this.canvas.outputAreaExtensions : { top: 0, bottom: 0, left: 0, right: 0 };
        const shapeOffsetX = bounds.x + ext.left;
        const shapeOffsetY = bounds.y + ext.top;
        
        const shape = this.canvas.outputAreaShape;
        let minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity;
        
        // Calculate shape bounding box
        shape.points.forEach((point: { x: number; y: number }) => {
            const worldX = shapeOffsetX + point.x;
            const worldY = shapeOffsetY + point.y;
            minX = Math.min(minX, worldX);
            maxX = Math.max(maxX, worldX);
            minY = Math.min(minY, worldY);
            maxY = Math.max(maxY, worldY);
        });

        const shapeBounds = { x: minX, y: minY, width: maxX - minX, height: maxY - minY };

        // Check overlap with each active batch preview area
        for (const manager of this.canvas.batchPreviewManagers) {
            if (manager.generationArea) {
                const area = manager.generationArea;
                // Check if rectangles overlap
                if (!(shapeBounds.x + shapeBounds.width < area.x || 
                      area.x + area.width < shapeBounds.x || 
                      shapeBounds.y + shapeBounds.height < area.y || 
                      area.y + area.height < shapeBounds.y)) {
                    return true; // Overlap detected
                }
            }
        }

        return false;
    }

    drawCanvasOutline(ctx: any) {
        ctx.beginPath();
        ctx.strokeStyle = 'rgba(255, 255, 255, 0.8)';
        ctx.lineWidth = 2 / this.canvas.viewport.zoom;
        ctx.setLineDash([10 / this.canvas.viewport.zoom, 5 / this.canvas.viewport.zoom]);

        // Rysuj outline w pozycji outputAreaBounds
        const bounds = this.canvas.outputAreaBounds;
        ctx.rect(bounds.x, bounds.y, bounds.width, bounds.height);

        ctx.stroke();
        ctx.setLineDash([]);

        // Display dimensions under outputAreaBounds
        const dimensionsText = `${Math.round(bounds.width)}x${Math.round(bounds.height)}`;
        const textWorldX = bounds.x + bounds.width / 2;
        const textWorldY = bounds.y + bounds.height + (20 / this.canvas.viewport.zoom);
        
        this.drawTextWithBackground(ctx, dimensionsText, textWorldX, textWorldY);

        // Only draw custom shape if it doesn't overlap with batch preview areas
        if (this.canvas.outputAreaShape && !this.isCustomShapeOverlappingWithBatchAreas()) {
            ctx.save();
            ctx.strokeStyle = 'rgba(0, 255, 255, 0.9)';
            ctx.lineWidth = 2 / this.canvas.viewport.zoom;
            ctx.setLineDash([]);
            const shape = this.canvas.outputAreaShape;
            const bounds = this.canvas.outputAreaBounds;
            
            // Calculate custom shape position accounting for extensions
            // Custom shape should maintain its relative position within the original canvas area
            const ext = this.canvas.outputAreaExtensionEnabled ? this.canvas.outputAreaExtensions : { top: 0, bottom: 0, left: 0, right: 0 };
            const shapeOffsetX = bounds.x + ext.left;  // Add left extension to maintain relative position
            const shapeOffsetY = bounds.y + ext.top;   // Add top extension to maintain relative position
            
            ctx.beginPath();
            // Render custom shape with extension offset to maintain relative position
            ctx.moveTo(shapeOffsetX + shape.points[0].x, shapeOffsetY + shape.points[0].y);
            for (let i = 1; i < shape.points.length; i++) {
                ctx.lineTo(shapeOffsetX + shape.points[i].x, shapeOffsetY + shape.points[i].y);
            }
            ctx.closePath();
            ctx.stroke();
            ctx.restore();
        }
    }

    /**
     * Sprawdza czy punkt w świecie jest przykryty przez warstwy o wyższym zIndex
     */
    isPointCoveredByHigherLayers(worldX: number, worldY: number, currentLayer: any): boolean {
        // Znajdź warstwy o wyższym zIndex niż aktualny layer
        const higherLayers = this.canvas.layers.filter((l: any) => 
            l.zIndex > currentLayer.zIndex && l.visible && l !== currentLayer
        );

        for (const higherLayer of higherLayers) {
            // Sprawdź czy punkt jest wewnątrz tego layera
            const centerX = higherLayer.x + higherLayer.width / 2;
            const centerY = higherLayer.y + higherLayer.height / 2;

            // Przekształć punkt do lokalnego układu współrzędnych layera
            const dx = worldX - centerX;
            const dy = worldY - centerY;

            const rad = -higherLayer.rotation * Math.PI / 180;
            const rotatedX = dx * Math.cos(rad) - dy * Math.sin(rad);
            const rotatedY = dx * Math.sin(rad) + dy * Math.cos(rad);

            // Sprawdź czy punkt jest wewnątrz prostokąta layera
            if (Math.abs(rotatedX) <= higherLayer.width / 2 && 
                Math.abs(rotatedY) <= higherLayer.height / 2) {
                
                // Sprawdź przezroczystość layera - jeśli ma znaczącą nieprzezroczystość, uznaj za przykryty
                if (higherLayer.opacity > 0.1) {
                    return true;
                }
            }
        }
        return false;
    }

    /**
     * Rysuje linię z automatycznym przełączaniem między ciągłą a przerywaną w zależności od przykrycia
     */
    drawAdaptiveLine(ctx: any, startX: number, startY: number, endX: number, endY: number, layer: any) {
        const segmentLength = 8 / this.canvas.viewport.zoom; // Długość segmentu do sprawdzania
        const dashLength = 6 / this.canvas.viewport.zoom;
        const gapLength = 4 / this.canvas.viewport.zoom;
        
        const totalLength = Math.sqrt((endX - startX) ** 2 + (endY - startY) ** 2);
        const segments = Math.max(1, Math.floor(totalLength / segmentLength));
        
        let currentX = startX;
        let currentY = startY;
        let lastCovered = null;
        let segmentStart = { x: startX, y: startY };
        
        for (let i = 0; i <= segments; i++) {
            const t = i / segments;
            const x = startX + (endX - startX) * t;
            const y = startY + (endY - startY) * t;
            
            // Przekształć współrzędne lokalne na światowe
            const centerX = layer.x + layer.width / 2;
            const centerY = layer.y + layer.height / 2;
            const rad = layer.rotation * Math.PI / 180;
            const cos = Math.cos(rad);
            const sin = Math.sin(rad);
            
            const worldX = centerX + (x * cos - y * sin);
            const worldY = centerY + (x * sin + y * cos);
            
            const isCovered = this.isPointCoveredByHigherLayers(worldX, worldY, layer);
            
            // Jeśli stan się zmienił lub to ostatni segment, narysuj poprzedni odcinek
            if (lastCovered !== null && (lastCovered !== isCovered || i === segments)) {
                ctx.beginPath();
                ctx.moveTo(segmentStart.x, segmentStart.y);
                ctx.lineTo(currentX, currentY);
                
                if (lastCovered) {
                    // Przykryty - linia przerywana
                    ctx.setLineDash([dashLength, gapLength]);
                } else {
                    // Nie przykryty - linia ciągła
                    ctx.setLineDash([]);
                }
                
                ctx.stroke();
                
                segmentStart = { x: currentX, y: currentY };
            }
            
            lastCovered = isCovered;
            currentX = x;
            currentY = y;
        }
        
        // Narysuj ostatni segment jeśli potrzeba
        if (lastCovered !== null) {
            ctx.beginPath();
            ctx.moveTo(segmentStart.x, segmentStart.y);
            ctx.lineTo(endX, endY);
            
            if (lastCovered) {
                ctx.setLineDash([dashLength, gapLength]);
            } else {
                ctx.setLineDash([]);
            }
            
            ctx.stroke();
        }
        
        // Resetuj dash pattern
        ctx.setLineDash([]);
    }

    drawSelectionFrame(ctx: any, layer: any) {
        const lineWidth = 2 / this.canvas.viewport.zoom;
        const handleRadius = 5 / this.canvas.viewport.zoom;

        if (layer.cropMode && layer.cropBounds && layer.originalWidth) {
            // --- CROP MODE ---
            ctx.lineWidth = lineWidth;

            // 1. Draw dashed blue line for the full transform frame (the "original size" container)
            ctx.strokeStyle = '#007bff';
            ctx.setLineDash([8 / this.canvas.viewport.zoom, 8 / this.canvas.viewport.zoom]);
            ctx.strokeRect(-layer.width / 2, -layer.height / 2, layer.width, layer.height);
            ctx.setLineDash([]);

            // 2. Draw solid blue line for the crop bounds
            const layerScaleX = layer.width / layer.originalWidth;
            const layerScaleY = layer.height / layer.originalHeight;
            const s = layer.cropBounds;
            
            const cropRectX = (-layer.width / 2) + (s.x * layerScaleX);
            const cropRectY = (-layer.height / 2) + (s.y * layerScaleY);
            const cropRectW = s.width * layerScaleX;
            const cropRectH = s.height * layerScaleY;

            ctx.strokeStyle = '#007bff'; // Solid blue
            this.drawAdaptiveLine(ctx, cropRectX, cropRectY, cropRectX + cropRectW, cropRectY, layer); // Top
            this.drawAdaptiveLine(ctx, cropRectX + cropRectW, cropRectY, cropRectX + cropRectW, cropRectY + cropRectH, layer); // Right
            this.drawAdaptiveLine(ctx, cropRectX + cropRectW, cropRectY + cropRectH, cropRectX, cropRectY + cropRectH, layer); // Bottom
            this.drawAdaptiveLine(ctx, cropRectX, cropRectY + cropRectH, cropRectX, cropRectY, layer); // Left

        } else {
            // --- TRANSFORM MODE ---
            ctx.strokeStyle = '#00ff00'; // Green
            ctx.lineWidth = lineWidth;
            const halfW = layer.width / 2;
            const halfH = layer.height / 2;

            // Draw adaptive solid green line for transform frame
            this.drawAdaptiveLine(ctx, -halfW, -halfH, halfW, -halfH, layer);
            this.drawAdaptiveLine(ctx, halfW, -halfH, halfW, halfH, layer);
            this.drawAdaptiveLine(ctx, halfW, halfH, -halfW, halfH, layer);
            this.drawAdaptiveLine(ctx, -halfW, halfH, -halfW, -halfH, layer);

            // Draw line to rotation handle
            ctx.setLineDash([]);
            ctx.beginPath();
            const startY = layer.flipV ? halfH : -halfH;
            const endY = startY + (layer.flipV ? 1 : -1) * (20 / this.canvas.viewport.zoom);
            ctx.moveTo(0, startY);
            ctx.lineTo(0, endY);
            ctx.stroke();
        }
        
        // --- DRAW HANDLES (Unified Logic) ---
        const handles = this.canvas.canvasLayers.getHandles(layer);
        ctx.fillStyle = '#ffffff';
        ctx.strokeStyle = '#000000';
        ctx.lineWidth = 1 / this.canvas.viewport.zoom;

        const centerX = layer.x + layer.width / 2;
        const centerY = layer.y + layer.height / 2;

        for (const key in handles) {
            // Skip rotation handle in crop mode
            if (layer.cropMode && key === 'rot') continue;
            
            const point = handles[key];
            // The handle position is already in world space.
            // We need to convert it to the layer's local, un-rotated space.
            const dx = point.x - centerX;
            const dy = point.y - centerY;

            // "Un-rotate" the position to get it in the layer's local, un-rotated space
            const rad = -layer.rotation * Math.PI / 180;
            const cos = Math.cos(rad);
            const sin = Math.sin(rad);
            const localX = dx * cos - dy * sin;
            const localY = dx * sin + dy * cos;

            // The context is already flipped. We need to flip the coordinates
            // to match the visual transformation, so the arc is drawn in the correct place.
            const finalX = localX * (layer.flipH ? -1 : 1);
            const finalY = localY * (layer.flipV ? -1 : 1);

            ctx.beginPath();
            ctx.arc(finalX, finalY, handleRadius, 0, Math.PI * 2);
            ctx.fill();
            ctx.stroke();
        }
    }

    drawOutputAreaExtensionPreview(ctx: any) {
        if (!this.canvas.outputAreaExtensionPreview) {
            return;
        }

        // Calculate preview bounds based on original canvas size + preview extensions
        const baseWidth = this.canvas.originalCanvasSize ? this.canvas.originalCanvasSize.width : this.canvas.width;
        const baseHeight = this.canvas.originalCanvasSize ? this.canvas.originalCanvasSize.height : this.canvas.height;
        
        const ext = this.canvas.outputAreaExtensionPreview;
        
        // Calculate preview bounds relative to original custom shape position, not (0,0)
        const originalPos = this.canvas.originalOutputAreaPosition;
        const previewBounds = {
            x: originalPos.x - ext.left,  // ✅ Względem oryginalnej pozycji custom shape
            y: originalPos.y - ext.top,   // ✅ Względem oryginalnej pozycji custom shape
            width: baseWidth + ext.left + ext.right,
            height: baseHeight + ext.top + ext.bottom
        };

        this.drawStyledRect(ctx, previewBounds, {
            strokeStyle: 'rgba(255, 255, 0, 0.8)',
            lineWidth: 3,
            dashPattern: [8, 4]
        });
    }

    drawPendingGenerationAreas(ctx: any) {
        const pendingAreas = [];

        // 1. Get all pending generation areas (from pendingBatchContext)
        if (this.canvas.pendingBatchContext && this.canvas.pendingBatchContext.outputArea) {
            pendingAreas.push(this.canvas.pendingBatchContext.outputArea);
        }

        // 2. Draw only those pending areas, które NIE mają aktywnego batch preview managera dla tego samego obszaru
        const isAreaCoveredByBatch = (area: any) => {
            if (!this.canvas.batchPreviewManagers) return false;
            return this.canvas.batchPreviewManagers.some((manager: any) => {
                if (!manager.generationArea) return false;
                // Sprawdź czy obszary się pokrywają (prosty overlap AABB)
                const a = area;
                const b = manager.generationArea;
                return !(a.x + a.width < b.x || b.x + b.width < a.x || a.y + a.height < b.y || b.y + b.height < a.y);
            });
        };

        pendingAreas.forEach(area => {
            if (!isAreaCoveredByBatch(area)) {
                this.drawStyledRect(ctx, area, {
                    strokeStyle: 'rgba(0, 150, 255, 0.9)',
                    lineWidth: 3,
                    dashPattern: [12, 6]
                });
            }
        });
    }

    drawMaskAreaBounds(ctx: any) {
        // Only show mask area bounds when mask tool is active
        if (!this.canvas.maskTool.isActive) {
            return;
        }

        const maskTool = this.canvas.maskTool;
        
        // Get mask canvas bounds in world coordinates
        const maskBounds = {
            x: maskTool.x,
            y: maskTool.y,
            width: maskTool.getMask().width,
            height: maskTool.getMask().height
        };

        this.drawStyledRect(ctx, maskBounds, {
            strokeStyle: 'rgba(255, 100, 100, 0.7)',
            lineWidth: 2,
            dashPattern: [6, 6]
        });
        
        // Add text label to show this is the mask drawing area
        const textWorldX = maskBounds.x + maskBounds.width / 2;
        const textWorldY = maskBounds.y - (10 / this.canvas.viewport.zoom);
        
        this.drawTextWithBackground(ctx, "Mask Drawing Area", textWorldX, textWorldY, {
            font: "12px sans-serif",
            backgroundColor: "rgba(255, 100, 100, 0.8)",
            padding: 8
        });
    }

    /**
     * Initialize overlay canvas for lightweight overlays like brush cursor
     */
    initOverlay(): void {
        // Setup overlay canvas to match main canvas
        this.updateOverlaySize();
        
        // Position overlay canvas on top of main canvas
        this.canvas.overlayCanvas.style.position = 'absolute';
        this.canvas.overlayCanvas.style.left = '0px';
        this.canvas.overlayCanvas.style.top = '0px';
        this.canvas.overlayCanvas.style.pointerEvents = 'none';
        this.canvas.overlayCanvas.style.zIndex = '20'; // Above other overlays
        
        // Add overlay to DOM when main canvas is added
        this.addOverlayToDOM();
        
        log.debug('Overlay canvas initialized');
    }

    /**
     * Add overlay canvas to DOM if main canvas has a parent
     */
    addOverlayToDOM(): void {
        if (this.canvas.canvas.parentElement && !this.canvas.overlayCanvas.parentElement) {
            this.canvas.canvas.parentElement.appendChild(this.canvas.overlayCanvas);
            log.debug('Overlay canvas added to DOM');
        }
    }

    /**
     * Update overlay canvas size to match main canvas
     */
    updateOverlaySize(): void {
        if (this.canvas.overlayCanvas.width !== this.canvas.canvas.clientWidth ||
            this.canvas.overlayCanvas.height !== this.canvas.canvas.clientHeight) {
            
            this.canvas.overlayCanvas.width = Math.max(1, this.canvas.canvas.clientWidth);
            this.canvas.overlayCanvas.height = Math.max(1, this.canvas.canvas.clientHeight);
            
            log.debug(`Overlay canvas resized to ${this.canvas.overlayCanvas.width}x${this.canvas.overlayCanvas.height}`);
        }
    }

    /**
     * Clear overlay canvas
     */
    clearOverlay(): void {
        this.canvas.overlayCtx.clearRect(0, 0, this.canvas.overlayCanvas.width, this.canvas.overlayCanvas.height);
    }

    /**
     * Initialize a dedicated overlay for real-time mask stroke preview
     */
    initStrokeOverlay(): void {
        // Create canvas if not created yet
        if (!this.strokeOverlayCanvas) {
            this.strokeOverlayCanvas = document.createElement('canvas');
            const ctx = this.strokeOverlayCanvas.getContext('2d');
            if (!ctx) {
                throw new Error('Failed to get 2D context for stroke overlay canvas');
            }
            this.strokeOverlayCtx = ctx;
        }

        // Size match main canvas
        this.updateStrokeOverlaySize();

        // Position above main canvas but below cursor overlay
        this.strokeOverlayCanvas.style.position = 'absolute';
        this.strokeOverlayCanvas.style.left = '1px';
        this.strokeOverlayCanvas.style.top = '1px';
        this.strokeOverlayCanvas.style.pointerEvents = 'none';
        this.strokeOverlayCanvas.style.zIndex = '19'; // Below cursor overlay (20)
        // Opacity is now controlled by MaskTool.previewOpacity
        this.strokeOverlayCanvas.style.opacity = String(this.canvas.maskTool.previewOpacity || 0.5);

        // Add to DOM
        this.addStrokeOverlayToDOM();
        log.debug('Stroke overlay canvas initialized');
    }

    /**
     * Add stroke overlay canvas to DOM if needed
     */
    addStrokeOverlayToDOM(): void {
        if (this.canvas.canvas.parentElement && !this.strokeOverlayCanvas.parentElement) {
            this.canvas.canvas.parentElement.appendChild(this.strokeOverlayCanvas);
            log.debug('Stroke overlay canvas added to DOM');
        }
    }

    /**
     * Ensure stroke overlay size matches main canvas
     */
    updateStrokeOverlaySize(): void {
        const w = Math.max(1, this.canvas.canvas.clientWidth);
        const h = Math.max(1, this.canvas.canvas.clientHeight);
        if (this.strokeOverlayCanvas.width !== w || this.strokeOverlayCanvas.height !== h) {
            this.strokeOverlayCanvas.width = w;
            this.strokeOverlayCanvas.height = h;
            log.debug(`Stroke overlay resized to ${w}x${h}`);
        }
    }

    /**
     * Clear the stroke overlay
     */
    clearMaskStrokeOverlay(): void {
        if (!this.strokeOverlayCtx) return;
        this.strokeOverlayCtx.clearRect(0, 0, this.strokeOverlayCanvas.width, this.strokeOverlayCanvas.height);
    }

    /**
     * Draw a preview stroke segment onto the stroke overlay in screen space
     * Uses line drawing with gradient to match MaskTool's drawLineOnChunk exactly
     */
    drawMaskStrokeSegment(startWorld: { x: number; y: number }, endWorld: { x: number; y: number }): void {
        // Ensure overlay is present and sized
        this.updateStrokeOverlaySize();

        const zoom = this.canvas.viewport.zoom;
        const toScreen = (p: { x: number; y: number }) => ({
            x: (p.x - this.canvas.viewport.x) * zoom,
            y: (p.y - this.canvas.viewport.y) * zoom
        });

        const startScreen = toScreen(startWorld);
        const endScreen = toScreen(endWorld);

        const brushRadius = (this.canvas.maskTool.brushSize / 2) * zoom;
        const hardness = this.canvas.maskTool.brushHardness;
        const strength = this.canvas.maskTool.brushStrength;

        // If strength is 0, don't draw anything
        if (strength <= 0) {
            return;
        }

        this.strokeOverlayCtx.save();
        
        // Draw line segment exactly as MaskTool does
        this.strokeOverlayCtx.beginPath();
        this.strokeOverlayCtx.moveTo(startScreen.x, startScreen.y);
        this.strokeOverlayCtx.lineTo(endScreen.x, endScreen.y);
        
        // Match the gradient setup from MaskTool's drawLineOnChunk
        if (hardness === 1) {
            this.strokeOverlayCtx.strokeStyle = `rgba(255, 255, 255, ${strength})`;
        } else {
            const innerRadius = brushRadius * hardness;
            const gradient = this.strokeOverlayCtx.createRadialGradient(
                endScreen.x, endScreen.y, innerRadius,
                endScreen.x, endScreen.y, brushRadius
            );
            gradient.addColorStop(0, `rgba(255, 255, 255, ${strength})`);
            gradient.addColorStop(1, `rgba(255, 255, 255, 0)`);
            this.strokeOverlayCtx.strokeStyle = gradient;
        }
        
        // Match line properties from MaskTool
        this.strokeOverlayCtx.lineWidth = this.canvas.maskTool.brushSize * zoom;
        this.strokeOverlayCtx.lineCap = 'round';
        this.strokeOverlayCtx.lineJoin = 'round';
        this.strokeOverlayCtx.globalCompositeOperation = 'source-over';
        this.strokeOverlayCtx.stroke();
        
        this.strokeOverlayCtx.restore();
    }

    /**
     * Redraws the entire stroke overlay from world coordinates
     * Used when viewport changes during drawing to maintain visual consistency
     */
    redrawMaskStrokeOverlay(strokePoints: { x: number; y: number }[]): void {
        if (strokePoints.length < 2) return;

        // Clear the overlay first
        this.clearMaskStrokeOverlay();

        // Redraw all segments with current viewport
        for (let i = 1; i < strokePoints.length; i++) {
            this.drawMaskStrokeSegment(strokePoints[i - 1], strokePoints[i]);
        }
    }

    /**
     * Draw mask brush cursor on overlay canvas with visual feedback for size, strength and hardness
     * @param worldPoint World coordinates of cursor
     */
    drawMaskBrushCursor(worldPoint: { x: number, y: number }): void {
        if (!this.canvas.maskTool.isActive || !this.canvas.isMouseOver) {
            this.clearOverlay();
            return;
        }

        // Update overlay size if needed
        this.updateOverlaySize();
        
        // Clear previous cursor
        this.clearOverlay();
        
        // Convert world coordinates to screen coordinates
        const screenX = (worldPoint.x - this.canvas.viewport.x) * this.canvas.viewport.zoom;
        const screenY = (worldPoint.y - this.canvas.viewport.y) * this.canvas.viewport.zoom;
        
        // Get brush properties
        const brushRadius = (this.canvas.maskTool.brushSize / 2) * this.canvas.viewport.zoom;
        const brushStrength = this.canvas.maskTool.brushStrength;
        const brushHardness = this.canvas.maskTool.brushHardness;
        
        // Save context state
        this.canvas.overlayCtx.save();
        
        // If strength is 0, just draw outline
        if (brushStrength > 0) {
            // Draw inner fill to visualize brush effect - matches actual brush rendering
            const gradient = this.canvas.overlayCtx.createRadialGradient(
                screenX, screenY, 0,
                screenX, screenY, brushRadius
            );
            
            // Preview alpha - subtle to not obscure content
            const previewAlpha = brushStrength * 0.15; // Very subtle preview (max 15% opacity)
            
            if (brushHardness === 1) {
                // Hard brush - uniform fill within radius
                gradient.addColorStop(0, `rgba(255, 255, 255, ${previewAlpha})`);
                gradient.addColorStop(1, `rgba(255, 255, 255, ${previewAlpha})`);
            } else {
                // Soft brush - gradient fade matching actual brush
                gradient.addColorStop(0, `rgba(255, 255, 255, ${previewAlpha})`);
                if (brushHardness > 0) {
                    gradient.addColorStop(brushHardness, `rgba(255, 255, 255, ${previewAlpha})`);
                }
                gradient.addColorStop(1, `rgba(255, 255, 255, 0)`);
            }
            
            this.canvas.overlayCtx.beginPath();
            this.canvas.overlayCtx.arc(screenX, screenY, brushRadius, 0, 2 * Math.PI);
            this.canvas.overlayCtx.fillStyle = gradient;
            this.canvas.overlayCtx.fill();
        }
        
        // Draw outer circle (SIZE indicator)
        this.canvas.overlayCtx.beginPath();
        this.canvas.overlayCtx.arc(screenX, screenY, brushRadius, 0, 2 * Math.PI);
        
        // Stroke opacity based on strength (dimmer when strength is 0)
        const strokeOpacity = brushStrength > 0 ? (0.4 + brushStrength * 0.4) : 0.3;
        this.canvas.overlayCtx.strokeStyle = `rgba(255, 255, 255, ${strokeOpacity})`;
        this.canvas.overlayCtx.lineWidth = 1.5;
        
        // Visual feedback for hardness
        if (brushHardness > 0.8) {
            // Hard brush - solid line
            this.canvas.overlayCtx.setLineDash([]);
        } else {
            // Soft brush - dashed line
            const dashLength = 2 + (1 - brushHardness) * 4;
            this.canvas.overlayCtx.setLineDash([dashLength, dashLength]);
        }
        
        this.canvas.overlayCtx.stroke();
        
        // Center dot for small brushes
        if (brushRadius < 5) {
            this.canvas.overlayCtx.beginPath();
            this.canvas.overlayCtx.arc(screenX, screenY, 1, 0, 2 * Math.PI);
            this.canvas.overlayCtx.fillStyle = `rgba(255, 255, 255, ${strokeOpacity})`;
            this.canvas.overlayCtx.fill();
        }
        
        // Restore context state
        this.canvas.overlayCtx.restore();
    }

    /**
     * Update overlay position when viewport changes
     */
    updateOverlayPosition(): void {
        // Overlay canvas is positioned absolutely, so it doesn't need repositioning
        // Just ensure it's the right size
        this.updateOverlaySize();
    }
}
