/**
 * DensityGrid — хранилище для Roblox-style smooth terrain.
 *
 * Содержит два параллельных массива одного размера:
 *   - matData[idx]: Uint8 — id материала (0=пусто, 1+=palette[i])
 *   - densityData[idx]: Uint8 — плотность 0..255 (0=пусто, 255=solid)
 *     Промежуточные значения позволяют **плавные** поверхности
 *     (Surface Nets интерполирует позицию вершины).
 *
 * Размер ячейки — 4м (как Roblox). Карта 500×128×500 м = 125×32×125 = 500K
 * ячеек = 500 КБ matData + 500 КБ density = 1 МБ. Это в 200 раз компактнее
 * чем voxel-terrain старой системы на той же площади.
 *
 * Палитра — общая с TERRAIN_MATERIALS (`grass`, `rock`, `sand`, ...).
 *
 * Сериализация — компактный RLE (matData отдельно от density).
 */

import { TERRAIN_MATERIALS } from '../TerrainManager';

/** Размер одной ячейки в метрах. Roblox использует 4м. */
export const CELL_SIZE = 4;

/** Threshold: ячейка считается solid если density >= 128 (0..255). */
export const DENSITY_THRESHOLD = 128;

export class DensityGrid {
    /**
     * @param {Object} opts
     * @param {{x,y,z}} opts.origin — мировые координаты ячейки (0,0,0). Размер целочисленный (в cell-units, потом × CELL_SIZE = м).
     * @param {{x,y,z}} opts.size — размер в ячейках
     * @param {string[]} [opts.palette]
     */
    constructor(opts) {
        const { origin, size, palette } = opts;
        if (!origin || !size) throw new Error('DensityGrid: origin and size required');
        this.origin = { x: origin.x|0, y: origin.y|0, z: origin.z|0 };
        this.size = { x: size.x|0, y: size.y|0, z: size.z|0 };
        // Палитра по умолчанию: [пусто, и всё что есть в TERRAIN_MATERIALS]
        this.palette = palette ? palette.slice() : ['', ...Object.keys(TERRAIN_MATERIALS)];
        this._matIdByKey = new Map();
        for (let i = 0; i < this.palette.length; i++) {
            if (this.palette[i]) this._matIdByKey.set(this.palette[i], i);
        }
        // Strides для линейной индексации: data[z*sxy + y*sx + x]
        this._sx = this.size.x;
        this._sxy = this.size.x * this.size.y;
        const total = this.size.x * this.size.y * this.size.z;
        this.matData = new Uint8Array(total);
        this.densityData = new Uint8Array(total);
    }

    /** Линейный индекс. */
    _idx(ix, iy, iz) {
        return iz * this._sxy + iy * this._sx + ix;
    }

    /** В пределах ли grid. */
    inBounds(ix, iy, iz) {
        return ix >= 0 && ix < this.size.x
            && iy >= 0 && iy < this.size.y
            && iz >= 0 && iz < this.size.z;
    }

    /** Получить density (0..255). Вне bounds → 0. */
    getDensity(ix, iy, iz) {
        if (!this.inBounds(ix, iy, iz)) return 0;
        return this.densityData[this._idx(ix, iy, iz)];
    }

    /** Получить material-id (0=пусто). Вне bounds → 0. */
    getMatId(ix, iy, iz) {
        if (!this.inBounds(ix, iy, iz)) return 0;
        return this.matData[this._idx(ix, iy, iz)];
    }

    /** Получить material-key ('grass' и т.п.) или ''. */
    getMatKey(ix, iy, iz) {
        const m = this.getMatId(ix, iy, iz);
        return this.palette[m] || '';
    }

    /** Решение: считается ли ячейка "solid". */
    isSolid(ix, iy, iz) {
        return this.getDensity(ix, iy, iz) >= DENSITY_THRESHOLD;
    }

    /**
     * Установить density + material для ячейки.
     * @param {number} density 0..255
     * @param {string|null} matKey
     */
    set(ix, iy, iz, density, matKey) {
        if (!this.inBounds(ix, iy, iz)) return;
        const idx = this._idx(ix, iy, iz);
        this.densityData[idx] = Math.max(0, Math.min(255, density|0));
        if (matKey != null) {
            let m = this._matIdByKey.get(matKey);
            if (m === undefined) {
                if (this.palette.length >= 255) return;
                m = this.palette.length;
                this.palette.push(matKey);
                this._matIdByKey.set(matKey, m);
            }
            this.matData[idx] = m;
        }
        // Если density упал до 0 — обнуляем mat (логически empty).
        if (this.densityData[idx] === 0) this.matData[idx] = 0;
    }

    /** Очистить полностью. */
    clear() {
        this.matData.fill(0);
        this.densityData.fill(0);
    }

    /** Подсчёт solid-ячеек. */
    countSolid() {
        let n = 0;
        for (let i = 0; i < this.densityData.length; i++) {
            if (this.densityData[i] >= DENSITY_THRESHOLD) n++;
        }
        return n;
    }

    /** Преобразование cell-coord → world position center (метры). */
    cellToWorld(ix, iy, iz) {
        return {
            x: (this.origin.x + ix + 0.5) * CELL_SIZE,
            y: (this.origin.y + iy + 0.5) * CELL_SIZE,
            z: (this.origin.z + iz + 0.5) * CELL_SIZE,
        };
    }

    // =========================================================================
    // СЕРИАЛИЗАЦИЯ — RLE
    // =========================================================================

    serialize() {
        const matRLE = this._encodeRLE(this.matData);
        const densityRLE = this._encodeRLE(this.densityData);
        return {
            format: 'robloxterrain-v1',
            origin: this.origin,
            size: this.size,
            palette: this.palette,
            mat: this._uint8ToBase64(matRLE),
            density: this._uint8ToBase64(densityRLE),
        };
    }

    _encodeRLE(arr) {
        const buf = [];
        const N = arr.length;
        let i = 0;
        while (i < N) {
            const v = arr[i];
            let run = 1;
            while (i + run < N && arr[i + run] === v && run < 65535) run++;
            buf.push(run & 0xff, (run >> 8) & 0xff, v);
            i += run;
        }
        return new Uint8Array(buf);
    }

    static _decodeRLE(bytes, totalLength) {
        const out = new Uint8Array(totalLength);
        let outIdx = 0;
        let i = 0;
        while (i < bytes.length && outIdx < totalLength) {
            const run = bytes[i] | (bytes[i + 1] << 8);
            const v = bytes[i + 2];
            i += 3;
            for (let k = 0; k < run && outIdx < totalLength; k++) {
                out[outIdx++] = v;
            }
        }
        return out;
    }

    static deserialize(obj) {
        if (!obj || obj.format !== 'robloxterrain-v1') {
            throw new Error('DensityGrid: bad format ' + (obj?.format ?? 'undefined'));
        }
        const grid = new DensityGrid({
            origin: obj.origin,
            size: obj.size,
            palette: obj.palette,
        });
        const matRLE = DensityGrid._base64ToUint8(obj.mat);
        const densityRLE = DensityGrid._base64ToUint8(obj.density);
        const total = grid.matData.length;
        grid.matData = DensityGrid._decodeRLE(matRLE, total);
        grid.densityData = DensityGrid._decodeRLE(densityRLE, total);
        return grid;
    }

    _uint8ToBase64(arr) {
        let binary = '';
        const chunkSize = 8192;
        for (let i = 0; i < arr.length; i += chunkSize) {
            const chunk = arr.subarray(i, Math.min(i + chunkSize, arr.length));
            binary += String.fromCharCode.apply(null, chunk);
        }
        return btoa(binary);
    }

    static _base64ToUint8(b64) {
        const binary = atob(b64);
        const out = new Uint8Array(binary.length);
        for (let i = 0; i < binary.length; i++) out[i] = binary.charCodeAt(i);
        return out;
    }
}
