const ADJACENT_ALL = [
    [0, -1, 0],
    [0, 1, 0],
    [0, 0, -1],
    [0, 0, 1],
];
const ADJACENT_SINGLE_PLANE = [
    [0, -1, 0],
    [0, 1, 0],
];
const ADJACENT_IN = [
    [0, -1, 0],
    [0, 1, 0],
    [0, 0, -1],
];
const ADJACENT_OUT = [
    [0, -1, 0],
    [0, 1, 0],
    [0, 0, 1],
];
export default class RLEVoxelMap {
    static copyMap(destination, source) {
        for (const [index, row] of source.rows) {
            destination.rows.set(index, structuredClone(row));
        }
    }
    constructor(width, height, depth = 1) {
        this.rows = new Map();
        this.height = 1;
        this.width = 1;
        this.depth = 1;
        this.jMultiple = 1;
        this.kMultiple = 1;
        this.numComps = 1;
        this.pixelDataConstructor = Uint8Array;
        this.updateScalarData = function (scalarData) {
            scalarData.fill(0);
            const callback = (index, rle, row) => {
                const { start, end, value } = rle;
                for (let i = start; i < end; i++) {
                    scalarData[index + i] = value;
                }
            };
            this.forEach(callback);
        };
        this.get = (index) => {
            const i = index % this.jMultiple;
            const j = (index - i) / this.jMultiple;
            const rle = this.getRLE(i, j);
            return rle?.value ?? this.defaultValue;
        };
        this.getRun = (j, k) => {
            const runIndex = j + k * this.height;
            return this.rows.get(runIndex);
        };
        this.set = (index, value) => {
            if (value === undefined) {
                return;
            }
            const i = index % this.width;
            const j = (index - i) / this.width;
            const row = this.rows.get(j);
            if (!row) {
                this.rows.set(j, [{ start: i, end: i + 1, value }]);
                return;
            }
            const rleIndex = this.findIndex(row, i);
            const rle1 = row[rleIndex];
            const rle0 = row[rleIndex - 1];
            if (!rle1) {
                if (!rle0 || rle0.value !== value || rle0.end !== i) {
                    row[rleIndex] = { start: i, end: i + 1, value };
                    return;
                }
                rle0.end++;
                return;
            }
            const { start, end, value: oldValue } = rle1;
            if (value === oldValue && i >= start) {
                return;
            }
            const rleInsert = { start: i, end: i + 1, value };
            const isAfter = i > start;
            const insertIndex = isAfter ? rleIndex + 1 : rleIndex;
            const rlePrev = isAfter ? rle1 : rle0;
            let rleNext = isAfter ? row[rleIndex + 1] : rle1;
            if (rlePrev?.value === value && rlePrev?.end === i) {
                rlePrev.end++;
                if (rleNext?.value === value && rleNext.start === i + 1) {
                    rlePrev.end = rleNext.end;
                    row.splice(rleIndex, 1);
                }
                else if (rleNext?.start === i) {
                    rleNext.start++;
                    if (rleNext.start === rleNext.end) {
                        row.splice(rleIndex, 1);
                        rleNext = row[rleIndex];
                        if (rleNext?.start === i + 1 && rleNext.value === value) {
                            rlePrev.end = rleNext.end;
                            row.splice(rleIndex, 1);
                        }
                    }
                }
                return;
            }
            if (rleNext?.value === value && rleNext.start === i + 1) {
                rleNext.start--;
                if (rlePrev?.end > i) {
                    rlePrev.end = i;
                    if (rlePrev.end === rlePrev.start) {
                        row.splice(rleIndex, 1);
                    }
                }
                return;
            }
            if (rleNext?.start === i && rleNext.end === i + 1) {
                rleNext.value = value;
                const nextnext = row[rleIndex + 1];
                if (nextnext?.start == i + 1 && nextnext.value === value) {
                    row.splice(rleIndex + 1, 1);
                    rleNext.end = nextnext.end;
                }
                return;
            }
            if (i === rleNext?.start) {
                rleNext.start++;
            }
            if (isAfter && end > i + 1) {
                row.splice(insertIndex, 0, rleInsert, {
                    start: i + 1,
                    end: rlePrev.end,
                    value: rlePrev.value,
                });
            }
            else {
                row.splice(insertIndex, 0, rleInsert);
            }
            if (rlePrev?.end > i) {
                rlePrev.end = i;
            }
        };
        this.width = width;
        this.height = height;
        this.depth = depth;
        this.jMultiple = width;
        this.kMultiple = this.jMultiple * height;
    }
    static { this.getScalarData = function (ArrayType = Uint8ClampedArray) {
        const scalarData = new ArrayType(this.frameSize);
        this.map.updateScalarData(scalarData);
        return scalarData;
    }; }
    toIJK(index) {
        const i = index % this.jMultiple;
        const j = ((index - i) / this.jMultiple) % this.height;
        const k = Math.floor(index / this.kMultiple);
        return [i, j, k];
    }
    toIndex([i, j, k]) {
        return i + k * this.kMultiple + j * this.jMultiple;
    }
    getRLE(i, j, k = 0) {
        const row = this.rows.get(j + k * this.height);
        if (!row) {
            return;
        }
        const index = this.findIndex(row, i);
        const rle = row[index];
        return i >= rle?.start ? rle : undefined;
    }
    has(index) {
        const i = index % this.jMultiple;
        const j = (index - i) / this.jMultiple;
        const rle = this.getRLE(i, j);
        return rle?.value !== undefined;
    }
    delete(index) {
        const i = index % this.width;
        const j = (index - i) / this.width;
        const row = this.rows.get(j);
        if (!row) {
            return;
        }
        const rleIndex = this.findIndex(row, i);
        const rle = row[rleIndex];
        if (!rle || rle.start > i) {
            return;
        }
        if (rle.end === i + 1) {
            rle.end--;
            if (rle.start >= rle.end) {
                row.splice(rleIndex, 1);
                if (!row.length) {
                    this.rows.delete(j);
                }
            }
            return;
        }
        if (rle.start === i) {
            rle.start++;
            return;
        }
        const newRle = {
            value: rle.value,
            start: i + 1,
            end: rle.end,
        };
        rle.end = i;
        row.splice(rleIndex + 1, 0, newRle);
    }
    findIndex(row, i) {
        for (let index = 0; index < row.length; index++) {
            const { end: iEnd } = row[index];
            if (i < iEnd) {
                return index;
            }
        }
        return row.length;
    }
    forEach(callback, options) {
        const rowModified = options?.rowModified;
        for (const [baseIndex, row] of this.rows) {
            const rowToUse = rowModified ? [...row] : row;
            for (const rle of rowToUse) {
                callback(baseIndex * this.width, rle, row);
            }
        }
    }
    forEachRow(callback) {
        for (const [baseIndex, row] of this.rows) {
            callback(baseIndex * this.width, row);
        }
    }
    clear() {
        this.rows.clear();
    }
    keys() {
        return [...this.rows.keys()];
    }
    getPixelData(k = 0, pixelData) {
        if (!pixelData) {
            pixelData = new this.pixelDataConstructor(this.width * this.height * this.numComps);
        }
        else {
            pixelData.fill(0);
        }
        const { width, height, numComps } = this;
        for (let j = 0; j < height; j++) {
            const row = this.getRun(j, k);
            if (!row) {
                continue;
            }
            if (numComps === 1) {
                for (const rle of row) {
                    const rowOffset = j * width;
                    const { start, end, value } = rle;
                    for (let i = start; i < end; i++) {
                        pixelData[rowOffset + i] = value;
                    }
                }
            }
            else {
                for (const rle of row) {
                    const rowOffset = j * width * numComps;
                    const { start, end, value } = rle;
                    for (let i = start; i < end; i += numComps) {
                        for (let comp = 0; comp < numComps; comp++) {
                            pixelData[rowOffset + i + comp] = value[comp];
                        }
                    }
                }
            }
        }
        return pixelData;
    }
    floodFill(i, j, k, value, options) {
        const rle = this.getRLE(i, j, k);
        if (!rle) {
            throw new Error(`Initial point ${i},${j},${k} isn't in the RLE`);
        }
        const stack = [[rle, j, k]];
        const replaceValue = rle.value;
        if (replaceValue === value) {
            throw new Error(`source (${replaceValue}) and destination (${value}) are identical`);
        }
        return this.flood(stack, replaceValue, value, options);
    }
    flood(stack, sourceValue, value, options) {
        let sum = 0;
        const { planar = true, diagonals = true, singlePlane = false, } = options || {};
        const childOptions = { planar, diagonals, singlePlane };
        while (stack.length) {
            const top = stack.pop();
            const [current] = top;
            if (current.value !== sourceValue) {
                continue;
            }
            current.value = value;
            sum += current.end - current.start;
            const adjacents = this.findAdjacents(top, childOptions).filter((adjacent) => adjacent && adjacent[0].value === sourceValue);
            stack.push(...adjacents);
        }
        return sum;
    }
    fillFrom(getter, boundsIJK) {
        for (let k = boundsIJK[2][0]; k <= boundsIJK[2][1]; k++) {
            for (let j = boundsIJK[1][0]; j <= boundsIJK[1][1]; j++) {
                let rle;
                let row;
                for (let i = boundsIJK[0][0]; i <= boundsIJK[0][1]; i++) {
                    const value = getter(i, j, k);
                    if (value === undefined) {
                        rle = undefined;
                        continue;
                    }
                    if (!row) {
                        row = [];
                        this.rows.set(j + k * this.height, row);
                    }
                    if (rle && rle.value !== value) {
                        rle = undefined;
                    }
                    if (!rle) {
                        rle = { start: i, end: i, value };
                        row.push(rle);
                    }
                    rle.end++;
                }
            }
        }
    }
    findAdjacents(item, { diagonals = true, planar = true, singlePlane = false }) {
        const [rle, j, k, adjacentsDelta] = item;
        const { start, end } = rle;
        const leftRle = start > 0 && this.getRLE(start - 1, j, k);
        const rightRle = end < this.width && this.getRLE(end, j, k);
        const range = diagonals
            ? [start > 0 ? start - 1 : start, end < this.width ? end + 1 : end]
            : [start, end];
        const adjacents = [];
        if (leftRle) {
            adjacents.push([leftRle, j, k]);
        }
        if (rightRle) {
            adjacents.push([rightRle, j, k]);
        }
        for (const delta of adjacentsDelta ||
            (singlePlane ? ADJACENT_SINGLE_PLANE : ADJACENT_ALL)) {
            const [, delta1, delta2] = delta;
            const testJ = delta1 + j;
            const testK = delta2 + k;
            if (testJ < 0 || testJ >= this.height) {
                continue;
            }
            if (testK < 0 || testK >= this.depth) {
                continue;
            }
            const row = this.getRun(testJ, testK);
            if (!row) {
                continue;
            }
            for (const testRle of row) {
                const newAdjacentDelta = adjacentsDelta ||
                    (singlePlane && ADJACENT_SINGLE_PLANE) ||
                    (planar && delta2 > 0 && ADJACENT_OUT) ||
                    (planar && delta2 < 0 && ADJACENT_IN) ||
                    ADJACENT_ALL;
                if (!(testRle.end <= range[0] || testRle.start >= range[1])) {
                    adjacents.push([testRle, testJ, testK, newAdjacentDelta]);
                }
            }
        }
        return adjacents;
    }
}
