/** @internal NOTE: Internal APIs. Subject to change. Use of these APIs in production applications is not supported. */ /** */

import { Writeable } from '../Types';
import { createKnuthShuffledNumberArray } from './shuffle';

export enum RgbaComponent {
    Red = 0,
    Green = 1,
    Blue = 2,
    Alpha = 3
}

class Rgba {
    private static _toArrayLookup: {
        [key: number]: (color: number, dst: Writeable<ArrayLike<number>>, dstOffset: number) => void;
    } = {
        '1': (color: number, dst: Writeable<ArrayLike<number>>, dstOffset = 0) => {
            dst[dstOffset] = color & 0x000000ff;
        },
        '2': (color: number, dst: Writeable<ArrayLike<number>>, dstOffset = 0) => {
            dst[dstOffset] = color & 0x000000ff;
            dst[dstOffset + 1] = (color >> 8) & 0x000000ff;
        },
        '3': (color: number, dst: Writeable<ArrayLike<number>>, dstOffset = 0) => {
            dst[dstOffset] = color & 0x000000ff;
            dst[dstOffset + 1] = (color >> 8) & 0x000000ff;
            dst[dstOffset + 2] = (color >> 16) & 0x000000ff;
        },
        '4': (color: number, dst: Writeable<ArrayLike<number>>, dstOffset = 0) => {
            dst[dstOffset] = color & 0x000000ff;
            dst[dstOffset + 1] = (color >> 8) & 0x000000ff;
            dst[dstOffset + 2] = (color >> 16) & 0x000000ff;
            dst[dstOffset + 3] = (color >> 24) & 0x000000ff;
        }
    };

    private static _toInt32Lookup: { [key: number]: (color: ArrayLike<number>, offset: number) => number } = {
        '1': (color: ArrayLike<number>, offset = 0) => {
            return color[offset];
        },
        '2': (color: ArrayLike<number>, offset = 0) => {
            return color[offset] | (color[offset + 1] << 8);
        },
        '3': (color: ArrayLike<number>, offset = 0) => {
            return color[offset] | (color[offset + 1] << 8) | (color[offset + 2] << 16);
        },
        '4': (color: ArrayLike<number>, offset = 0) => {
            return color[offset] | (color[offset + 1] << 8) | (color[offset + 2] << 16) | (color[offset + 3] << 24);
        }
    };

    public toInt32(color: ArrayLike<number>, count: 1 | 2 | 3 | 4 = 4, offset = 0): number {
        return Rgba._toInt32Lookup[count](color, offset);
    }

    public toArray(color: number, dst: Writeable<ArrayLike<number>>, count: 1 | 2 | 3 | 4 = 4, dstOffset = 0): void {
        Rgba._toArrayLookup[count](color, dst, dstOffset);
    }

    /**
     * Useful for debug purposes when we have a array of RGBA pixels where we want to make sure that
     * pixels with similiar colors (and probably located close to each other in the image) have their colors
     * assigned to a random (and unique color). This will make it easier for a human to spot differences
     * in a image where pixels originally just differ a little in color.
     * @param srcRGBA Src array of RGBA pixels
     * @param dstRGBA  Dst array of RGBA pixels. Pixels from srcRGBA will be written to this array. Pixels of same color in {@link srcRGBA}
     * are written here (at same offset) but with a new random unique color assigned. If same reference is passed
     * as to {@link srcRGBA} then {@link srcRGBA} is overwritten (which is ok if you want to do a inplace randomization).
     * @param predicate If specified then it is called for every pixel in {@link srcRGBA}. If predicate returns true then that
     * pixel is recolorized
     * @returns Reference to {@link dstRGBA}
     */
    public randomizeRGBAColorsWithoutModifyingAlphaInPlace(
        srcRGBA: Uint8Array,
        dstRGBA: Uint8Array,
        predicate?: (srcRGBA: Uint8Array, rgbaOffset: number) => boolean
    ): Uint8Array {
        predicate = predicate ?? (() => true);

        const randomColorRgbaOffsetByRGBValue = new Map<number, number>();
        const srcRGBALen = srcRGBA.length;
        const dstRGBALen = dstRGBA.length;

        const rgbaByteSize = 4;

        // Map each unique original RGB color (ignore alpha) to a unique rgba offset. This offset
        // points to a rgba value in a array of unique colors (calculated further down)
        let randomColorRgbaOffset = 0;
        for (let srcRgbaOffset = 0; srcRgbaOffset < srcRGBALen; srcRgbaOffset += rgbaByteSize) {
            const includeRgbValue = predicate(srcRGBA, srcRgbaOffset);
            if (includeRgbValue) {
                const rgbValue = this.toInt32(srcRGBA, 3, srcRgbaOffset); // Skip alpha
                if (!randomColorRgbaOffsetByRGBValue.has(rgbValue)) {
                    randomColorRgbaOffsetByRGBValue.set(rgbValue, randomColorRgbaOffset);
                    randomColorRgbaOffset += rgbaByteSize;
                }
            }
        }

        if (randomColorRgbaOffsetByRGBValue.size === 0) {
            return dstRGBA;
        }

        const uniqueRGBCount = randomColorRgbaOffsetByRGBValue.size;
        const maxRgbValue = 16777216; // Max number of unique colors if we dont use alpha.
        if (randomColorRgbaOffsetByRGBValue.size >= maxRgbValue) {
            throw new Error(
                `Found ${uniqueRGBCount} unique RGB colors which is more that the supported 
                 number of unique ${maxRgbValue} RGB colors.`
            );
        }

        // Have a range of [0..uniqueColorPixelIndices.size) unique RGB values. Each such value
        // shall be mapped to a new random RGB value.
        const rgbaStep = Math.floor(maxRgbValue / uniqueRGBCount);
        // Create array of unique RGBA colors (where A is always 0) where we attempt to have as much
        // "difference" between each RGBA value as possible.

        // TODO Calculating large knutshuffled arrays are expensive. If it happens a lot we can use the
        // LazyResizableTypedArray to optimize allocation (only done when we need to grow the array).
        const randomUniqueRGBAColors = new Uint8Array(
            createKnuthShuffledNumberArray(() => new Uint32Array(uniqueRGBCount), rgbaStep).buffer
        );

        const rgbaLen = Math.min(srcRGBALen, dstRGBALen);
        for (let rgbaOffset = 0; rgbaOffset < rgbaLen; rgbaOffset += rgbaByteSize) {
            const includeRgbValue = predicate(srcRGBA, rgbaOffset);
            const originalRgbValue = this.toInt32(srcRGBA, 3, rgbaOffset); // Skip alpha;
            const randomColorRgbaOffset = randomColorRgbaOffsetByRGBValue.get(originalRgbValue);
            if (!includeRgbValue || randomColorRgbaOffset === undefined) {
                // If we have no mapping then simply copy the value from src array into dst array
                dstRGBA[rgbaOffset + RgbaComponent.Red] = srcRGBA[rgbaOffset + RgbaComponent.Red];
                dstRGBA[rgbaOffset + RgbaComponent.Green] = srcRGBA[rgbaOffset + RgbaComponent.Green];
                dstRGBA[rgbaOffset + RgbaComponent.Blue] = srcRGBA[rgbaOffset + RgbaComponent.Blue];
            } else {
                // Convert src RGB color to a random RGB color and store in dst array
                dstRGBA[rgbaOffset + RgbaComponent.Red] =
                    randomUniqueRGBAColors[randomColorRgbaOffset + RgbaComponent.Red];
                dstRGBA[rgbaOffset + RgbaComponent.Green] =
                    randomUniqueRGBAColors[randomColorRgbaOffset + RgbaComponent.Green];
                dstRGBA[rgbaOffset + RgbaComponent.Blue] =
                    randomUniqueRGBAColors[randomColorRgbaOffset + RgbaComponent.Blue];
            }
            dstRGBA[rgbaOffset + RgbaComponent.Alpha] = srcRGBA[rgbaOffset + RgbaComponent.Alpha];
        }
        return dstRGBA;
    }
}

export const rgba = new Rgba();
