"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.analyzeEntityMaterials = exports.DEFAULT_SATURATION_FACTOR = void 0;
exports.collectMaterialUsage = collectMaterialUsage;
exports.rebalanceLuminance = rebalanceLuminance;
exports.applySaturation = applySaturation;
exports.mapLuminance = mapLuminance;
exports.mapSaturation = mapSaturation;
exports.rewriteEntityPalette = rewriteEntityPalette;
exports.clusterMaterials = clusterMaterials;
exports.analyzePalette = analyzePalette;
const textures_1 = require("./textures");
const FACE_KEYS = ['top', 'bottom', 'north', 'south', 'east', 'west'];
const EPSILON = 1e-8;
exports.DEFAULT_SATURATION_FACTOR = 2;
const DEFAULT_LUMINANCE_OPTIONS = {
    min: 0.04,
    max: 0.8,
    kneeLow: 0.02,
    kneeHigh: 0.08,
};
function collectMaterialUsage(entity) {
    const context = {
        primitives: new Map(entity.primitives.map(primitive => [primitive.id, primitive])),
        textures: new Map(entity.textures.map(texture => [texture.id, texture])),
        patterns: new Map(entity.patterns.map(pattern => [pattern.id, pattern])),
        materials: new Map(entity.materials.map(material => [material.id, material])),
        counts: new Map(),
        textureHistograms: new Map(),
    };
    if (entity.geometry?.root) {
        traverseNodes(context, entity.geometry.root, new Set());
    }
    const results = [];
    context.counts.forEach((count, materialId) => {
        const material = context.materials.get(materialId);
        if (!material) {
            return;
        }
        const baseColor = Array.isArray(material.baseColor) && material.baseColor.length === 3
            ? [...material.baseColor]
            : [0, 0, 0];
        results.push({
            materialId,
            baseColor,
            opacity: typeof material.opacity === 'number' ? material.opacity : undefined,
            usageCount: count,
        });
    });
    results.sort((a, b) => {
        if (b.usageCount !== a.usageCount) {
            return b.usageCount - a.usageCount;
        }
        return a.materialId.localeCompare(b.materialId);
    });
    return results;
}
function rebalanceLuminance(color, options) {
    const opts = { ...DEFAULT_LUMINANCE_OPTIONS, ...options };
    const [rLin, gLin, bLin] = color.map(component => srgbChannelToLinear(component));
    const currentLuminance = computeLuminance(rLin, gLin, bLin);
    const target = compressLuminance(currentLuminance, opts);
    let nextR = rLin;
    let nextG = gLin;
    let nextB = bLin;
    if (currentLuminance > EPSILON) {
        const scale = target / (currentLuminance + EPSILON);
        nextR = clamp01(rLin * scale);
        nextG = clamp01(gLin * scale);
        nextB = clamp01(bLin * scale);
    }
    else if (target > 0) {
        nextR = nextG = nextB = clamp01(target);
    }
    return {
        luminance: target,
        color: [linearToSrgbByte(nextR), linearToSrgbByte(nextG), linearToSrgbByte(nextB)],
    };
}
function applySaturation(color, options) {
    const scale = Number.isFinite(options.scale) && options.scale > 0 ? options.scale : 1;
    const targetLuminance = options.luminance ?? computeLuminanceVec(color);
    const lab = rgbToLab(color);
    const [, chroma, hue] = labToLch(lab);
    if (chroma <= EPSILON) {
        return { color, luminance: targetLuminance, clipped: false };
    }
    const evaluate = (s) => {
        const scaledLab = lchToLab([lab[0], chroma * s, hue]);
        const [rLinRaw, gLinRaw, bLinRaw] = labToLinearRgb(scaledLab);
        const clipped = rLinRaw < -1e-5 ||
            rLinRaw > 1 + 1e-5 ||
            gLinRaw < -1e-5 ||
            gLinRaw > 1 + 1e-5 ||
            bLinRaw < -1e-5 ||
            bLinRaw > 1 + 1e-5;
        const rLin = clamp01(rLinRaw);
        const gLin = clamp01(gLinRaw);
        const bLin = clamp01(bLinRaw);
        const luminance = computeLuminance(rLin, gLin, bLin);
        return { rLin, gLin, bLin, luminance, clipped };
    };
    let result = evaluate(1);
    if (scale > 1) {
        let lo = 1;
        let hi = scale;
        for (let i = 0; i < 12 && hi - lo > 1e-3; i += 1) {
            const mid = (lo + hi) / 2;
            const candidate = evaluate(mid);
            if (!candidate.clipped) {
                result = candidate;
                lo = mid;
            }
            else {
                hi = mid;
            }
        }
    }
    else if (scale < 1) {
        result = evaluate(scale);
    }
    // Rescale to target luminance while keeping hue/chroma.
    let { rLin, gLin, bLin } = result;
    if (targetLuminance > EPSILON) {
        const scaleFactor = targetLuminance / (result.luminance + EPSILON);
        rLin = clamp01(rLin * scaleFactor);
        gLin = clamp01(gLin * scaleFactor);
        bLin = clamp01(bLin * scaleFactor);
    }
    const finalLuminance = computeLuminance(rLin, gLin, bLin);
    return {
        color: [linearToSrgbByte(rLin), linearToSrgbByte(gLin), linearToSrgbByte(bLin)],
        luminance: finalLuminance,
        clipped: result.clipped,
    };
}
function mapLuminance(samples, options) {
    return samples.map(sample => {
        const result = rebalanceLuminance(sample.baseColor, options);
        return {
            ...sample,
            color: result.color,
            luminance: result.luminance,
        };
    });
}
function mapSaturation(entries, options) {
    return entries.map(entry => applySaturation(entry.color, { scale: options.scale, luminance: entry.luminance }));
}
function rewriteEntityPalette(entity, colors) {
    const clone = structuredClone(entity);
    clone.materials = clone.materials?.map(material => {
        const next = colors.get(material.id);
        if (!next)
            return material;
        return {
            ...material,
            baseColor: [...next],
        };
    }) ?? null;
    return clone;
}
function clusterMaterials(samples, options) {
    if (!Array.isArray(samples) || samples.length === 0) {
        return [];
    }
    const threshold = options?.deltaE && options.deltaE > 0 ? options.deltaE : 0;
    const entriesWithLab = samples.map(sample => ({
        sample,
        lab: rgbToLab(sample.baseColor),
    }));
    if (threshold === 0) {
        return entriesWithLab
            .sort((a, b) => b.sample.usageCount - a.sample.usageCount)
            .map(entry => ({
            id: entry.sample.materialId,
            centerRgb: entry.sample.baseColor,
            centerLab: entry.lab,
            totalCount: entry.sample.usageCount,
            samples: [entry.sample],
        }));
    }
    const sorted = entriesWithLab.sort((a, b) => b.sample.usageCount - a.sample.usageCount);
    const clusters = [];
    sorted.forEach(entry => {
        let best = null;
        let bestDistance = Infinity;
        for (const cluster of clusters) {
            const distance = deltaE(cluster.center, entry.lab);
            if (distance <= threshold && distance < bestDistance) {
                best = cluster;
                bestDistance = distance;
            }
        }
        if (best) {
            best.members.push(entry.sample);
            best.sumWeight += entry.sample.usageCount;
            best.sumL += entry.lab[0] * entry.sample.usageCount;
            best.sumA += entry.lab[1] * entry.sample.usageCount;
            best.sumB += entry.lab[2] * entry.sample.usageCount;
            best.center = [
                best.sumL / best.sumWeight,
                best.sumA / best.sumWeight,
                best.sumB / best.sumWeight,
            ];
        }
        else {
            clusters.push({
                members: [entry.sample],
                sumWeight: entry.sample.usageCount,
                sumL: entry.lab[0] * entry.sample.usageCount,
                sumA: entry.lab[1] * entry.sample.usageCount,
                sumB: entry.lab[2] * entry.sample.usageCount,
                center: entry.lab,
            });
        }
    });
    return clusters
        .map((cluster, index) => ({
        id: `cluster_${index + 1}`,
        centerLab: cluster.center,
        centerRgb: labToRgb(cluster.center),
        totalCount: cluster.sumWeight,
        samples: cluster.members.sort((a, b) => b.usageCount - a.usageCount),
    }))
        .sort((a, b) => b.totalCount - a.totalCount);
}
function analyzePalette(entity, options = {}) {
    const samples = collectMaterialUsage(entity);
    const luminanceOptions = {
        ...DEFAULT_LUMINANCE_OPTIONS,
        ...options.luminance,
    };
    const balanced = mapLuminance(samples, luminanceOptions);
    const saturated = mapSaturation(balanced.map(entry => ({ color: entry.color, luminance: entry.luminance })), {
        scale: options.saturationScale ?? exports.DEFAULT_SATURATION_FACTOR,
    });
    return balanced.map((entry, index) => ({
        materialId: entry.materialId,
        baseColor: entry.baseColor,
        opacity: entry.opacity,
        count: entry.usageCount,
        luminance: computeLuminanceVec(entry.baseColor),
        balancedColor: entry.color,
        balancedLuminance: entry.luminance,
        saturatedColor: saturated[index].color,
        saturatedLuminance: saturated[index].luminance,
        saturationClipped: saturated[index].clipped,
    }));
}
// Backwards compatibility wrapper
exports.analyzeEntityMaterials = analyzePalette;
function traverseNodes(context, node, stack) {
    if (node.mesh) {
        analyzePrimitiveInstance(context, node.mesh, 1, stack);
    }
    node.children?.forEach(child => traverseNodes(context, child, stack));
}
function analyzePrimitiveInstance(context, primitiveId, multiplier, stack) {
    const primitive = context.primitives.get(primitiveId);
    if (!primitive) {
        return;
    }
    switch (primitive.kind) {
        case 'plain_box':
            incrementUsage(context, primitive.material, multiplier);
            break;
        case 'textured_box':
            analyzeTexturedBox(context, primitive, multiplier);
            break;
        case 'voxel_box':
            analyzeVoxelBox(context, primitive, multiplier);
            break;
        case 'combo_box': {
            if (stack.has(primitiveId)) {
                return;
            }
            stack.add(primitiveId);
            analyzeComboBox(context, primitive, multiplier, stack);
            stack.delete(primitiveId);
            break;
        }
        default:
            break;
    }
}
function incrementUsage(context, materialId, amount) {
    if (!context.materials.has(materialId)) {
        return;
    }
    const existing = context.counts.get(materialId) ?? 0;
    context.counts.set(materialId, existing + amount);
}
function analyzeTexturedBox(context, primitive, multiplier) {
    FACE_KEYS.forEach(face => {
        const baseTextureId = (0, textures_1.resolveFaceTextureId)(primitive, face);
        if (baseTextureId) {
            countTextureMaterials(context, baseTextureId, multiplier);
        }
        const overlayTextureId = (0, textures_1.resolveOverlayFaceTextureId)(primitive, face);
        if (overlayTextureId) {
            countTextureMaterials(context, overlayTextureId, multiplier);
        }
    });
}
function analyzeVoxelBox(context, primitive, multiplier) {
    const palette = primitive.palette ?? [];
    if (palette.length === 0) {
        return;
    }
    primitive.voxels?.forEach(value => {
        if (typeof value !== 'number' || value <= 0) {
            return;
        }
        const paletteIndex = value - 1;
        const materialId = palette[paletteIndex];
        if (!materialId) {
            return;
        }
        incrementUsage(context, materialId, multiplier);
    });
}
function analyzeComboBox(context, primitive, multiplier, stack) {
    primitive.slots?.forEach(slot => {
        if (!slot?.primitive) {
            return;
        }
        analyzePrimitiveInstance(context, slot.primitive, multiplier, stack);
    });
}
function countTextureMaterials(context, textureId, multiplier) {
    const histogram = buildTextureHistogram(context, textureId);
    histogram.forEach((value, materialId) => {
        incrementUsage(context, materialId, value * multiplier);
    });
}
const buildTextureHistogram = (context, textureId) => {
    const cached = context.textureHistograms.get(textureId);
    if (cached) {
        return cached;
    }
    const histogram = new Map();
    const texture = context.textures.get(textureId);
    if (!texture) {
        context.textureHistograms.set(textureId, histogram);
        return histogram;
    }
    const pattern = context.patterns.get(texture.pattern);
    if (!pattern || !Array.isArray(pattern.data)) {
        context.textureHistograms.set(textureId, histogram);
        return histogram;
    }
    const materials = texture.materials ?? [];
    pattern.data.forEach(cell => {
        const materialIndex = Number(cell);
        if (!Number.isInteger(materialIndex) || materialIndex < 0 || materialIndex >= materials.length) {
            return;
        }
        const materialId = materials[materialIndex];
        if (!materialId) {
            return;
        }
        histogram.set(materialId, (histogram.get(materialId) ?? 0) + 1);
    });
    context.textureHistograms.set(textureId, histogram);
    return histogram;
};
function compressLuminance(value, options) {
    if (value < options.min) {
        const diff = options.min - value;
        const weight = smoothstep(0, options.kneeLow ?? DEFAULT_LUMINANCE_OPTIONS.kneeLow, diff);
        const lifted = value + diff * weight;
        return Math.min(options.min, lifted);
    }
    if (value > options.max) {
        const diff = value - options.max;
        const weight = smoothstep(0, options.kneeHigh ?? DEFAULT_LUMINANCE_OPTIONS.kneeHigh, diff);
        const lowered = value - diff * weight;
        return Math.max(options.max, lowered);
    }
    return value;
}
function smoothstep(edge0, edge1, x) {
    const t = clamp01((x - edge0) / (edge1 - edge0));
    return t * t * (3 - 2 * t);
}
function clamp01(value) {
    return Math.min(1, Math.max(0, value));
}
function srgbChannelToLinear(value) {
    const normalized = value / 255;
    if (normalized <= 0.04045) {
        return normalized / 12.92;
    }
    return Math.pow((normalized + 0.055) / 1.055, 2.4);
}
function linearChannelToSrgb(value) {
    const normalized = clamp01(value);
    if (normalized <= 0.0031308) {
        return normalized * 12.92;
    }
    return 1.055 * Math.pow(normalized, 1 / 2.4) - 0.055;
}
function linearToSrgbByte(value) {
    return Math.round(clamp01(linearChannelToSrgb(value)) * 255);
}
function computeLuminance(r, g, b) {
    return 0.2126 * r + 0.7152 * g + 0.0722 * b;
}
function computeLuminanceVec(color) {
    const [r, g, b] = color.map(component => srgbChannelToLinear(component));
    return computeLuminance(r, g, b);
}
const RGB_TO_XYZ = {
    x: [0.4124, 0.3576, 0.1805],
    y: [0.2126, 0.7152, 0.0722],
    z: [0.0193, 0.1192, 0.9505],
};
const REF_X = 95.047;
const REF_Y = 100;
const REF_Z = 108.883;
function xyzToLabHelper(value) {
    if (value > 0.008856) {
        return Math.cbrt(value);
    }
    return (7.787 * value) + (16 / 116);
}
function labToXyzHelper(value) {
    const cubed = value * value * value;
    if (cubed > 0.008856) {
        return cubed;
    }
    return (value - 16 / 116) / 7.787;
}
function rgbToLab(rgb) {
    const rLin = srgbChannelToLinear(rgb[0]);
    const gLin = srgbChannelToLinear(rgb[1]);
    const bLin = srgbChannelToLinear(rgb[2]);
    const x = (rLin * RGB_TO_XYZ.x[0] + gLin * RGB_TO_XYZ.x[1] + bLin * RGB_TO_XYZ.x[2]) * 100;
    const y = (rLin * RGB_TO_XYZ.y[0] + gLin * RGB_TO_XYZ.y[1] + bLin * RGB_TO_XYZ.y[2]) * 100;
    const z = (rLin * RGB_TO_XYZ.z[0] + gLin * RGB_TO_XYZ.z[1] + bLin * RGB_TO_XYZ.z[2]) * 100;
    const xr = xyzToLabHelper(x / REF_X);
    const yr = xyzToLabHelper(y / REF_Y);
    const zr = xyzToLabHelper(z / REF_Z);
    const L = 116 * yr - 16;
    const a = 500 * (xr - yr);
    const b = 200 * (yr - zr);
    return [L, a, b];
}
function labToLinearRgb(lab) {
    const [L, a, b] = lab;
    const fy = (L + 16) / 116;
    const fx = fy + (a / 500);
    const fz = fy - (b / 200);
    const xr = labToXyzHelper(fx);
    const yr = labToXyzHelper(fy);
    const zr = labToXyzHelper(fz);
    const x = (xr * REF_X) / 100;
    const y = (yr * REF_Y) / 100;
    const z = (zr * REF_Z) / 100;
    const rLin = x * 3.2406 + y * -1.5372 + z * -0.4986;
    const gLin = x * -0.9689 + y * 1.8758 + z * 0.0415;
    const bLin = x * 0.0557 + y * -0.204 + z * 1.057;
    return [rLin, gLin, bLin];
}
function labToRgb(lab) {
    const [rLin, gLin, bLin] = labToLinearRgb(lab);
    const r = Math.round(clamp01(linearChannelToSrgb(rLin)) * 255);
    const g = Math.round(clamp01(linearChannelToSrgb(gLin)) * 255);
    const b = Math.round(clamp01(linearChannelToSrgb(bLin)) * 255);
    return [r, g, b];
}
function labToLch(lab) {
    const [L, a, b] = lab;
    const C = Math.sqrt(a * a + b * b);
    const h = Math.atan2(b, a);
    return [L, C, h];
}
function lchToLab(lch) {
    const [L, C, h] = lch;
    const a = C * Math.cos(h);
    const b = C * Math.sin(h);
    return [L, a, b];
}
function deltaE(a, b) {
    const dL = a[0] - b[0];
    const dA = a[1] - b[1];
    const dB = a[2] - b[2];
    return Math.sqrt(dL * dL + dA * dA + dB * dB);
}
//# sourceMappingURL=palette_tools.js.map