/**
 * Smart selection tools for PhotoEditor
 * Implements magic wand, lasso selection, and healing brush
 */
/* ────────────────────────────────────────────────────────────────
   SELECTION MASK UTILITIES
──────────────────────────────────────────────────────────────── */
function createEmptyMask(width, height) {
    return {
        width,
        height,
        data: new Uint8Array(width * height),
    };
}
function invertMask(mask) {
    const result = createEmptyMask(mask.width, mask.height);
    for (let i = 0; i < mask.data.length; i++) {
        result.data[i] = 255 - mask.data[i];
    }
    return result;
}
function combineMasks(maskA, maskB, mode) {
    if (maskA.width !== maskB.width || maskA.height !== maskB.height) {
        throw new Error("Mask dimensions must match");
    }
    const result = createEmptyMask(maskA.width, maskA.height);
    for (let i = 0; i < maskA.data.length; i++) {
        const a = maskA.data[i];
        const b = maskB.data[i];
        switch (mode) {
            case 'replace':
                result.data[i] = b;
                break;
            case 'add':
                result.data[i] = Math.min(255, a + b);
                break;
            case 'subtract':
                result.data[i] = Math.max(0, a - b);
                break;
            case 'intersect':
                result.data[i] = Math.min(a, b);
                break;
        }
    }
    return result;
}
function featherMask(mask, radius) {
    if (radius <= 0)
        return mask;
    const result = createEmptyMask(mask.width, mask.height);
    const kernel = createGaussianKernel(radius);
    const kernelSize = kernel.length;
    const halfKernel = Math.floor(kernelSize / 2);
    for (let y = 0; y < mask.height; y++) {
        for (let x = 0; x < mask.width; x++) {
            let sum = 0;
            let weightSum = 0;
            for (let ky = 0; ky < kernelSize; ky++) {
                for (let kx = 0; kx < kernelSize; kx++) {
                    const sx = x + kx - halfKernel;
                    const sy = y + ky - halfKernel;
                    if (sx >= 0 && sx < mask.width && sy >= 0 && sy < mask.height) {
                        const weight = kernel[ky][kx];
                        sum += mask.data[sy * mask.width + sx] * weight;
                        weightSum += weight;
                    }
                }
            }
            result.data[y * mask.width + x] = Math.round(sum / weightSum);
        }
    }
    return result;
}
function createGaussianKernel(radius) {
    const size = radius * 2 + 1;
    const kernel = [];
    const sigma = radius / 3;
    const twoSigmaSq = 2 * sigma * sigma;
    for (let y = 0; y < size; y++) {
        kernel[y] = [];
        for (let x = 0; x < size; x++) {
            const dx = x - radius;
            const dy = y - radius;
            kernel[y][x] = Math.exp(-(dx * dx + dy * dy) / twoSigmaSq);
        }
    }
    return kernel;
}
const DEFAULT_WAND_OPTIONS = {
    tolerance: 32,
    contiguous: true,
    antiAlias: true,
    feather: 0,
};
function magicWandSelect(imageData, startX, startY, options = {}) {
    const opts = { ...DEFAULT_WAND_OPTIONS, ...options };
    const { width, height, data } = imageData;
    const mask = createEmptyMask(width, height);
    // Get target color
    const startIdx = (startY * width + startX) * 4;
    const targetR = data[startIdx];
    const targetG = data[startIdx + 1];
    const targetB = data[startIdx + 2];
    // Color similarity function
    const isColorSimilar = (idx) => {
        const r = data[idx];
        const g = data[idx + 1];
        const b = data[idx + 2];
        const diff = Math.abs(r - targetR) + Math.abs(g - targetG) + Math.abs(b - targetB);
        return diff <= opts.tolerance * 3;
    };
    // For anti-aliasing, calculate how similar a pixel is (0-255)
    const getSimilarity = (idx) => {
        const r = data[idx];
        const g = data[idx + 1];
        const b = data[idx + 2];
        const diff = Math.abs(r - targetR) + Math.abs(g - targetG) + Math.abs(b - targetB);
        const maxDiff = opts.tolerance * 3;
        if (diff >= maxDiff)
            return 0;
        return Math.round(255 * (1 - diff / maxDiff));
    };
    if (opts.contiguous) {
        // Flood fill algorithm
        const visited = new Uint8Array(width * height);
        const stack = [startX, startY];
        while (stack.length > 0) {
            const y = stack.pop();
            const x = stack.pop();
            if (x < 0 || x >= width || y < 0 || y >= height)
                continue;
            const pixelIdx = y * width + x;
            if (visited[pixelIdx])
                continue;
            visited[pixelIdx] = 1;
            const colorIdx = pixelIdx * 4;
            if (!isColorSimilar(colorIdx))
                continue;
            mask.data[pixelIdx] = opts.antiAlias ? getSimilarity(colorIdx) : 255;
            // Add neighbors
            stack.push(x - 1, y);
            stack.push(x + 1, y);
            stack.push(x, y - 1);
            stack.push(x, y + 1);
        }
    }
    else {
        // Select all similar colors regardless of position
        for (let i = 0; i < width * height; i++) {
            const colorIdx = i * 4;
            if (isColorSimilar(colorIdx)) {
                mask.data[i] = opts.antiAlias ? getSimilarity(colorIdx) : 255;
            }
        }
    }
    // Apply feather if requested
    if (opts.feather > 0) {
        return featherMask(mask, opts.feather);
    }
    return mask;
}
const DEFAULT_LASSO_OPTIONS = {
    feather: 0,
    antiAlias: true,
};
function lassoSelect(width, height, points, options = {}) {
    const opts = { ...DEFAULT_LASSO_OPTIONS, ...options };
    const mask = createEmptyMask(width, height);
    if (points.length < 3)
        return mask;
    // Use scanline fill algorithm
    const minY = Math.max(0, Math.floor(Math.min(...points.map(p => p.y))));
    const maxY = Math.min(height - 1, Math.ceil(Math.max(...points.map(p => p.y))));
    for (let y = minY; y <= maxY; y++) {
        // Find intersections with polygon edges
        const intersections = [];
        for (let i = 0; i < points.length; i++) {
            const p1 = points[i];
            const p2 = points[(i + 1) % points.length];
            // Check if edge crosses this scanline
            if ((p1.y <= y && p2.y > y) || (p2.y <= y && p1.y > y)) {
                // Calculate x intersection
                const x = p1.x + (y - p1.y) / (p2.y - p1.y) * (p2.x - p1.x);
                intersections.push(x);
            }
        }
        // Sort intersections
        intersections.sort((a, b) => a - b);
        // Fill between pairs of intersections
        for (let i = 0; i < intersections.length - 1; i += 2) {
            const x1 = Math.max(0, Math.floor(intersections[i]));
            const x2 = Math.min(width - 1, Math.ceil(intersections[i + 1]));
            for (let x = x1; x <= x2; x++) {
                mask.data[y * width + x] = 255;
            }
        }
    }
    // Apply feather if requested
    if (opts.feather > 0) {
        return featherMask(mask, opts.feather);
    }
    return mask;
}
/** Magnetic lasso - snaps to edges in image */
function magneticLassoPoint(imageData, x, y, searchRadius = 10) {
    const { width, height, data } = imageData;
    // Find the point with highest edge strength within radius
    let bestX = x;
    let bestY = y;
    let bestEdge = 0;
    for (let dy = -searchRadius; dy <= searchRadius; dy++) {
        for (let dx = -searchRadius; dx <= searchRadius; dx++) {
            const nx = Math.round(x + dx);
            const ny = Math.round(y + dy);
            if (nx < 1 || nx >= width - 1 || ny < 1 || ny >= height - 1)
                continue;
            // Calculate edge strength using Sobel operator
            const edge = getEdgeStrength(data, width, nx, ny);
            // Weight by distance from cursor
            const dist = Math.sqrt(dx * dx + dy * dy);
            const weightedEdge = edge / (1 + dist * 0.2);
            if (weightedEdge > bestEdge) {
                bestEdge = weightedEdge;
                bestX = nx;
                bestY = ny;
            }
        }
    }
    return { x: bestX, y: bestY };
}
function getEdgeStrength(data, width, x, y) {
    // Sobel operator
    const getGray = (px, py) => {
        const i = (py * width + px) * 4;
        return (data[i] + data[i + 1] + data[i + 2]) / 3;
    };
    const gx = -getGray(x - 1, y - 1) - 2 * getGray(x - 1, y) - getGray(x - 1, y + 1) +
        getGray(x + 1, y - 1) + 2 * getGray(x + 1, y) + getGray(x + 1, y + 1);
    const gy = -getGray(x - 1, y - 1) - 2 * getGray(x, y - 1) - getGray(x + 1, y - 1) +
        getGray(x - 1, y + 1) + 2 * getGray(x, y + 1) + getGray(x + 1, y + 1);
    return Math.sqrt(gx * gx + gy * gy);
}
function selectColorRange(imageData, targetColor, options = {}) {
    const opts = { fuzziness: 40, feather: 0, ...options };
    const { width, height, data } = imageData;
    const mask = createEmptyMask(width, height);
    for (let i = 0; i < width * height; i++) {
        const idx = i * 4;
        const r = data[idx];
        const g = data[idx + 1];
        const b = data[idx + 2];
        // Calculate color difference in LAB-like space for perceptual accuracy
        const diff = Math.sqrt(Math.pow(r - targetColor.r, 2) +
            Math.pow(g - targetColor.g, 2) +
            Math.pow(b - targetColor.b, 2));
        const maxDiff = opts.fuzziness * 2;
        if (diff < maxDiff) {
            mask.data[i] = Math.round(255 * (1 - diff / maxDiff));
        }
    }
    if (opts.feather > 0) {
        return featherMask(mask, opts.feather);
    }
    return mask;
}
const DEFAULT_HEALING_OPTIONS = {
    radius: 20,
    hardness: 0.5,
    opacity: 1,
    mode: 'heal',
};
/** Apply healing brush at destination using source area */
function applyHealingBrush(imageData, srcX, srcY, dstX, dstY, options = {}) {
    const opts = { ...DEFAULT_HEALING_OPTIONS, ...options };
    const { width, height, data } = imageData;
    const result = new ImageData(new Uint8ClampedArray(data), width, height);
    const r = opts.radius;
    const r2 = r * r;
    // Calculate average color at destination for healing mode
    let avgR = 0, avgG = 0, avgB = 0, count = 0;
    if (opts.mode === 'heal') {
        for (let dy = -r; dy <= r; dy++) {
            for (let dx = -r; dx <= r; dx++) {
                if (dx * dx + dy * dy > r2)
                    continue;
                const px = dstX + dx;
                const py = dstY + dy;
                if (px < 0 || px >= width || py < 0 || py >= height)
                    continue;
                const idx = (py * width + px) * 4;
                avgR += data[idx];
                avgG += data[idx + 1];
                avgB += data[idx + 2];
                count++;
            }
        }
        if (count > 0) {
            avgR /= count;
            avgG /= count;
            avgB /= count;
        }
    }
    // Copy/heal pixels
    for (let dy = -r; dy <= r; dy++) {
        for (let dx = -r; dx <= r; dx++) {
            const distSq = dx * dx + dy * dy;
            if (distSq > r2)
                continue;
            const spx = srcX + dx;
            const spy = srcY + dy;
            const dpx = dstX + dx;
            const dpy = dstY + dy;
            if (spx < 0 || spx >= width || spy < 0 || spy >= height)
                continue;
            if (dpx < 0 || dpx >= width || dpy < 0 || dpy >= height)
                continue;
            const srcIdx = (spy * width + spx) * 4;
            const dstIdx = (dpy * width + dpx) * 4;
            // Calculate brush falloff
            const dist = Math.sqrt(distSq);
            const falloff = Math.min(1, Math.max(0, (r - dist) / (r * (1 - opts.hardness + 0.01))));
            const alpha = opts.opacity * falloff;
            if (opts.mode === 'clone') {
                // Simple clone
                result.data[dstIdx] = lerp(data[dstIdx], data[srcIdx], alpha);
                result.data[dstIdx + 1] = lerp(data[dstIdx + 1], data[srcIdx + 1], alpha);
                result.data[dstIdx + 2] = lerp(data[dstIdx + 2], data[srcIdx + 2], alpha);
            }
            else {
                // Healing mode: match destination luminance
                const srcR = data[srcIdx];
                const srcG = data[srcIdx + 1];
                const srcB = data[srcIdx + 2];
                // Adjust source to match destination average
                const srcAvgR = avgR;
                const srcAvgG = avgG;
                const srcAvgB = avgB;
                // Calculate source average in the brush area
                let sAvgR = 0, sAvgG = 0, sAvgB = 0, sCount = 0;
                for (let sy = -r; sy <= r; sy++) {
                    for (let sx = -r; sx <= r; sx++) {
                        if (sx * sx + sy * sy > r2)
                            continue;
                        const px = srcX + sx;
                        const py = srcY + sy;
                        if (px < 0 || px >= width || py < 0 || py >= height)
                            continue;
                        const idx = (py * width + px) * 4;
                        sAvgR += data[idx];
                        sAvgG += data[idx + 1];
                        sAvgB += data[idx + 2];
                        sCount++;
                    }
                }
                if (sCount > 0) {
                    sAvgR /= sCount;
                    sAvgG /= sCount;
                    sAvgB /= sCount;
                }
                // Adjust source color to match destination
                const adjustedR = srcR + (srcAvgR - sAvgR);
                const adjustedG = srcG + (srcAvgG - sAvgG);
                const adjustedB = srcB + (srcAvgB - sAvgB);
                result.data[dstIdx] = lerp(data[dstIdx], clamp(adjustedR, 0, 255), alpha);
                result.data[dstIdx + 1] = lerp(data[dstIdx + 1], clamp(adjustedG, 0, 255), alpha);
                result.data[dstIdx + 2] = lerp(data[dstIdx + 2], clamp(adjustedB, 0, 255), alpha);
            }
        }
    }
    return result;
}
function lerp(a, b, t) {
    return a + (b - a) * t;
}
function clamp(val, min, max) {
    return Math.max(min, Math.min(max, val));
}
/* ────────────────────────────────────────────────────────────────
   CONTENT-AWARE FILL (simplified)
──────────────────────────────────────────────────────────────── */
/** Simple content-aware fill using surrounding pixels */
function contentAwareFill(imageData, mask) {
    const { width, height, data } = imageData;
    const result = new ImageData(new Uint8ClampedArray(data), width, height);
    // Find selected pixels
    const selected = [];
    for (let i = 0; i < mask.data.length; i++) {
        if (mask.data[i] > 128) {
            selected.push(i);
        }
    }
    // Sort by distance from edge (process edge pixels first)
    selected.sort((a, b) => {
        const ax = a % width, ay = Math.floor(a / width);
        const bx = b % width, by = Math.floor(b / width);
        return distFromEdge(ax, ay, mask) - distFromEdge(bx, by, mask);
    });
    // Fill each pixel using nearby unselected pixels
    for (const pixelIdx of selected) {
        const x = pixelIdx % width;
        const y = Math.floor(pixelIdx / width);
        // Find nearby unselected pixels
        const sample = findBestMatch(result, mask, x, y, 15);
        if (sample) {
            const dstIdx = pixelIdx * 4;
            const srcIdx = (sample.y * width + sample.x) * 4;
            result.data[dstIdx] = result.data[srcIdx];
            result.data[dstIdx + 1] = result.data[srcIdx + 1];
            result.data[dstIdx + 2] = result.data[srcIdx + 2];
        }
    }
    return result;
}
function distFromEdge(x, y, mask) {
    const { width, height } = mask;
    let minDist = Infinity;
    for (let r = 1; r < 50; r++) {
        for (let dy = -r; dy <= r; dy++) {
            for (let dx = -r; dx <= r; dx++) {
                if (Math.abs(dx) !== r && Math.abs(dy) !== r)
                    continue;
                const nx = x + dx, ny = y + dy;
                if (nx < 0 || nx >= width || ny < 0 || ny >= height)
                    continue;
                if (mask.data[ny * width + nx] < 128) {
                    return Math.sqrt(dx * dx + dy * dy);
                }
            }
        }
    }
    return minDist;
}
function findBestMatch(imageData, mask, x, y, radius) {
    const { width, height, data } = imageData;
    const candidates = [];
    // Collect unselected candidate pixels
    for (let dy = -radius; dy <= radius; dy++) {
        for (let dx = -radius; dx <= radius; dx++) {
            const nx = x + dx, ny = y + dy;
            if (nx < 0 || nx >= width || ny < 0 || ny >= height)
                continue;
            if (mask.data[ny * width + nx] < 128) {
                candidates.push({ x: nx, y: ny });
            }
        }
    }
    if (candidates.length === 0)
        return null;
    // Find candidate with most similar surroundings
    let best = null;
    let bestScore = Infinity;
    const patchSize = 3;
    for (const cand of candidates) {
        let score = 0;
        let count = 0;
        for (let py = -patchSize; py <= patchSize; py++) {
            for (let px = -patchSize; px <= patchSize; px++) {
                const srcX = x + px, srcY = y + py;
                const candX = cand.x + px, candY = cand.y + py;
                if (srcX < 0 || srcX >= width || srcY < 0 || srcY >= height)
                    continue;
                if (candX < 0 || candX >= width || candY < 0 || candY >= height)
                    continue;
                // Only compare unselected pixels
                if (mask.data[srcY * width + srcX] > 128)
                    continue;
                const srcIdx = (srcY * width + srcX) * 4;
                const candIdx = (candY * width + candX) * 4;
                const dr = data[srcIdx] - data[candIdx];
                const dg = data[srcIdx + 1] - data[candIdx + 1];
                const db = data[srcIdx + 2] - data[candIdx + 2];
                score += dr * dr + dg * dg + db * db;
                count++;
            }
        }
        if (count > 0 && score / count < bestScore) {
            bestScore = score / count;
            best = cand;
        }
    }
    return best;
}
/* ────────────────────────────────────────────────────────────────
   SELECTION RENDERING
──────────────────────────────────────────────────────────────── */
/** Render marching ants selection outline */
function renderSelectionOutline(ctx, mask, offset = 0) {
    const { width, height, data } = mask;
    // Find edge pixels
    ctx.strokeStyle = '#ffffff';
    ctx.lineWidth = 1;
    ctx.setLineDash([4, 4]);
    ctx.lineDashOffset = offset;
    ctx.beginPath();
    for (let y = 0; y < height; y++) {
        for (let x = 0; x < width; x++) {
            const idx = y * width + x;
            if (data[idx] < 128)
                continue;
            // Check if this is an edge pixel
            const isEdge = (x === 0 || data[idx - 1] < 128) ||
                (x === width - 1 || data[idx + 1] < 128) ||
                (y === 0 || data[idx - width] < 128) ||
                (y === height - 1 || data[idx + width] < 128);
            if (isEdge) {
                // Draw pixel outline
                if (x === 0 || data[idx - 1] < 128) {
                    ctx.moveTo(x, y);
                    ctx.lineTo(x, y + 1);
                }
                if (x === width - 1 || data[idx + 1] < 128) {
                    ctx.moveTo(x + 1, y);
                    ctx.lineTo(x + 1, y + 1);
                }
                if (y === 0 || data[idx - width] < 128) {
                    ctx.moveTo(x, y);
                    ctx.lineTo(x + 1, y);
                }
                if (y === height - 1 || data[idx + width] < 128) {
                    ctx.moveTo(x, y + 1);
                    ctx.lineTo(x + 1, y + 1);
                }
            }
        }
    }
    ctx.stroke();
    // Draw again with black for contrast
    ctx.strokeStyle = '#000000';
    ctx.lineDashOffset = offset + 4;
    ctx.stroke();
    ctx.setLineDash([]);
}

export { applyHealingBrush, combineMasks, contentAwareFill, createEmptyMask, featherMask, invertMask, lassoSelect, magicWandSelect, magneticLassoPoint, renderSelectionOutline, selectColorRange };
//# sourceMappingURL=photoSelection.js.map
