import { RawTexture, Texture } from '../babylonjs-import';
export var DitheringTextureMode;
(function (DitheringTextureMode) {
    DitheringTextureMode[DitheringTextureMode["blueNoise"] = 0] = "blueNoise";
    DitheringTextureMode[DitheringTextureMode["bayer"] = 1] = "bayer";
})(DitheringTextureMode || (DitheringTextureMode = {}));
/*
 * @internal NOTE: Internal APIs. Subject to change. Use of these APIs in production applications is not supported.
 */
export class DitherTexture {
    currentMode;
    animatedNoiseTextures;
    // See https://blog.demofox.org/2017/10/31/animating-noise-for-integration-over-time/
    // For more on animated noise
    static _ditherSize = 32; // Hardcoded to 32 for now
    static _goldenRatio = 1.61803398875;
    _createdTextures = new Map();
    static _rgbTextureArray = new Uint8Array(DitherTexture._ditherSize * DitherTexture._ditherSize * 4);
    _currentNoiseFrame = 0;
    _lastModeCalculatedFor = undefined;
    constructor(currentMode, animatedNoiseTextures) {
        this.currentMode = currentMode;
        this.animatedNoiseTextures = animatedNoiseTextures;
    }
    packRG(val) {
        const r = Math.floor(val / 256);
        const g = val - r * 256;
        return [r, g, 0];
    }
    setRGBDataToBayerMatrix() {
        // The bayer matrix texture need to be created at this point because otherwise the context for the texture is missing
        const order = 31 - Math.clz32(DitherTexture._ditherSize);
        const size = 1 << order;
        const dm = (m, x, y) => {
            let v = 0;
            const xc = x ^ y;
            const yc = y;
            let mask = m - 1;
            for (let bit = 0; bit < 2 * m;) {
                v |= ((yc >> mask) & 1) << bit++;
                v |= ((xc >> mask) & 1) << bit++;
                --mask;
            }
            return v;
        };
        for (let row = 0; row < size; row++) {
            for (let col = 0; col < size; col++) {
                const d = dm(order, size - (row + 1), size - (col + 1));
                const rgb = this.packRG(d);
                const rgbaIndex = (row * size + (size - col - 1)) * 4;
                DitherTexture._rgbTextureArray[rgbaIndex + 0] = rgb[0];
                DitherTexture._rgbTextureArray[rgbaIndex + 1] = rgb[1];
                DitherTexture._rgbTextureArray[rgbaIndex + 2] = rgb[2];
                DitherTexture._rgbTextureArray[rgbaIndex + 3] = 255;
            }
        }
    }
    // This method is slow, but 32 * 32 is so small that it is fast enough
    setRGBDataToBlueNoiseUsingvoidAndCluster() {
        const size = DitherTexture._ditherSize;
        const lineSpace = (startValue, stopValue, cardinality) => {
            const space = [];
            const step = (stopValue - startValue) / (cardinality - 1);
            for (let i = 0; i < cardinality; i++) {
                space.push(startValue + step * i);
            }
            return space;
        };
        const outerProduct = (a, b) => {
            return a.map((v) => b.map((w) => v * w));
        };
        const randomNumberInInterval = (min, max) => {
            return Math.floor(Math.random() * (max - min + 1) + min);
        };
        let wrappedPattern = [
            ...lineSpace(0, size / 2 - 1, Math.floor(size / 2)),
            ...lineSpace(size / 2, 1, Math.floor(size / 2))
        ];
        const SIGMA = 1.9;
        wrappedPattern = wrappedPattern.map((v) => Math.exp((-0.5 * v * v) / (SIGMA * SIGMA)));
        const precomputedEnergyMask = outerProduct(wrappedPattern, wrappedPattern);
        precomputedEnergyMask[0][0] = Number.MAX_VALUE;
        const shuffle = (array) => {
            for (let i = array.length - 1; i > 0; i--) {
                const j = Math.floor(Math.random() * (i + 1));
                [array[i], array[j]] = [array[j], array[i]];
            }
            return array;
        };
        const rollArray = [];
        for (let i = 0; i < size; i++) {
            const initialArray = [];
            for (let j = 0; j < size; j++) {
                initialArray.push(0);
            }
            rollArray.push(initialArray);
        }
        const roll = (array, rollArray, shift) => {
            for (let i = 0; i < array.length; i++) {
                const subArray = array[i];
                for (let j = 0; j < subArray.length; j++) {
                    rollArray[(i + shift[0]) % array.length][(j + shift[1]) % subArray.length] = subArray[j];
                }
            }
            return rollArray;
        };
        const energy = (position) => {
            return roll(precomputedEnergyMask, rollArray, position);
        };
        const pointsSet = [];
        const seedPointsPerDim = Math.max(Math.floor(size / 8.0), 1);
        const bucketSize = Math.floor(size / seedPointsPerDim);
        for (let x = 0; x < seedPointsPerDim; x++) {
            for (let y = 0; y < seedPointsPerDim; y++) {
                const pointX = randomNumberInInterval(x * bucketSize, (x + 1) * bucketSize - 1);
                const pointY = randomNumberInInterval(y * bucketSize, (y + 1) * bucketSize - 1);
                pointsSet.push([pointX, pointY]);
            }
        }
        shuffle(pointsSet);
        const currentEnergy = [];
        for (let i = 0; i < size; i++) {
            currentEnergy[i] = [];
            for (let j = 0; j < size; j++) {
                currentEnergy[i][j] = 0;
            }
        }
        for (let i = 0; i < pointsSet.length; i++) {
            const point = pointsSet[i];
            const pointEnergy = energy(point);
            for (let j = 0; j < pointEnergy.length; j++) {
                for (let k = 0; k < pointEnergy[j].length; k++) {
                    currentEnergy[j][k] = currentEnergy[j][k] + pointEnergy[j][k];
                }
            }
        }
        let lowestEnergy = Number.MAX_VALUE;
        let lowestEnergyPosition = [-1, -1];
        const updateStep = (currentEnergy) => {
            lowestEnergy = Number.MAX_VALUE;
            lowestEnergyPosition[0] = -1;
            lowestEnergyPosition[1] = -1;
            // Find lowest value index, which is the the void to fill
            for (let i = 0; i < currentEnergy.length; i++) {
                for (let j = 0; j < currentEnergy[i].length; j++) {
                    const energy = currentEnergy[i][j];
                    if (energy < lowestEnergy) {
                        lowestEnergy = energy;
                        lowestEnergyPosition = [i, j];
                    }
                }
            }
            // Take the void position and add the energy mask to it
            const energyMask = energy(lowestEnergyPosition);
            // Add the energy mask to the current energy
            for (let i = 0; i < currentEnergy.length; i++) {
                for (let j = 0; j < currentEnergy[i].length; j++) {
                    currentEnergy[i][j] = currentEnergy[i][j] + energyMask[i][j];
                }
            }
            return lowestEnergyPosition;
        };
        const finalResult = [];
        for (let i = 0; i < size; i++) {
            const initialArray = [];
            for (let j = 0; j < size; j++) {
                initialArray.push(0);
            }
            finalResult.push(initialArray);
        }
        const initialSize = seedPointsPerDim * seedPointsPerDim;
        for (let i = 0; i < size * size - initialSize; i++) {
            const lowestEnergyPosition = updateStep(currentEnergy);
            finalResult[lowestEnergyPosition[0]][lowestEnergyPosition[1]] += i + initialSize;
        }
        for (let i = 0; i < size; i++) {
            for (let j = 0; j < size; j++) {
                finalResult[i][j] /= size * size;
            }
        }
        // Write the result of the blue noise to the data used to create the texture when getTexture is called
        for (let i = 0; i < size; i++) {
            for (let j = 0; j < size; j++) {
                const rgb = this.packRG(Math.floor(finalResult[i][j] * 255));
                DitherTexture._rgbTextureArray[(i * size + j) * 4 + 0] = rgb[0];
                DitherTexture._rgbTextureArray[(i * size + j) * 4 + 1] = rgb[1];
                DitherTexture._rgbTextureArray[(i * size + j) * 4 + 2] = rgb[2];
                DitherTexture._rgbTextureArray[(i * size + j) * 4 + 3] = 255;
            }
        }
    }
    calculateDithering() {
        switch (this.currentMode) {
            case DitheringTextureMode.blueNoise:
                this.setRGBDataToBlueNoiseUsingvoidAndCluster();
                break;
            case DitheringTextureMode.bayer:
                this.setRGBDataToBayerMatrix();
                break;
        }
    }
    /**
     * @hidden
     */
    updateAnimatedFrame() {
        this._currentNoiseFrame = (this._currentNoiseFrame + 1) % this.animatedNoiseTextures;
    }
    dispose() {
        for (const texture of this._createdTextures.values()) {
            texture.dispose();
        }
        this._createdTextures.clear();
    }
    /**
     * @hidden
     */
    getTexture(scene) {
        return this._createdTextures.getOrAdd(`DitherTexture_${this.currentMode}-${this._currentNoiseFrame}`, () => {
            if (this._lastModeCalculatedFor !== this.currentMode) {
                this.calculateDithering();
                this._lastModeCalculatedFor = this.currentMode;
            }
            const addGoldenRatioToTextureValue = (sourceArray, targetArray, index, frameIndex) => {
                const arrayValue = sourceArray[index];
                targetArray[index] =
                    arrayValue > 0
                        ? (arrayValue + frameIndex * Math.floor(DitherTexture._goldenRatio * 255.0)) % 255.0
                        : 0.0;
            };
            // Just add the golden ratio for every frame of noise
            const frameAnimatedNoise = new Uint8Array(DitherTexture._ditherSize * DitherTexture._ditherSize * 4);
            for (let j = 0; j < DitherTexture._rgbTextureArray.length; j += 4) {
                addGoldenRatioToTextureValue(DitherTexture._rgbTextureArray, frameAnimatedNoise, j + 0, this._currentNoiseFrame); // R
                addGoldenRatioToTextureValue(DitherTexture._rgbTextureArray, frameAnimatedNoise, j + 1, this._currentNoiseFrame); // G
                addGoldenRatioToTextureValue(DitherTexture._rgbTextureArray, frameAnimatedNoise, j + 2, this._currentNoiseFrame); // B
                frameAnimatedNoise[j + 3] = DitherTexture._rgbTextureArray[j + 3];
                // Don't add the golden ratio to the alpha channel
            }
            const rawTexture = RawTexture.CreateRGBATexture(frameAnimatedNoise, DitherTexture._ditherSize, DitherTexture._ditherSize, scene, undefined, false, Texture.NEAREST_SAMPLINGMODE);
            rawTexture.wrapU = Texture.WRAP_ADDRESSMODE;
            rawTexture.wrapV = Texture.WRAP_ADDRESSMODE;
            return rawTexture;
        });
    }
}
//# sourceMappingURL=DitherTexture.js.map