/**
 * VoxelGrid — компактное хранилище воксельной карты.
 *
 * Замена старого TerrainManager.voxels Map<"x,y,z", matId> на Uint8Array
 * с фиксированным размером. Это даёт:
 *   - ×100 быстрее get/set чем Map (O(1) индексация без хеша)
 *   - В 10-50× компактнее в памяти (5 МБ vs 200+ МБ на больших картах)
 *   - Линейный обход без аллокаций
 *
 * Размер ячейки 1м (как Roblox), не 0.25м как раньше. Это компромисс
 * между детализацией и производительностью: ×64 меньше ячеек на ту же
 * площадь, но грубее визуал.
 *
 * matId — 1 байт (0..255). Палитра материалов хранится отдельно — это
 * мапит matId на TERRAIN_MATERIALS-ключ ('grass'/'dirt'/'rock'/...).
 * matId=0 всегда = пусто (нет блока).
 *
 * Координаты:
 *   ix, iy, iz — целые индексы [0..size.x-1] × [0..size.y-1] × [0..size.z-1]
 *   worldX = origin.x + ix    (1 ячейка = 1 метр)
 *   centerWorldX = origin.x + ix + 0.5
 */

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

/** Размер 1 ячейки в метрах. В отличие от старого TerrainManager (0.25м). */
export const CELL_SIZE = 1.0;

/** Максимальные размеры карты (в ячейках). 512×128×512 = 32 МБ — приемлемо. */
export const MAX_DIM_XZ = 512;
export const MAX_DIM_Y = 128;

export class VoxelGrid {
    /**
     * @param {Object} opts
     * @param {{x:number,y:number,z:number}} opts.origin — мировые координаты ячейки (0,0,0)
     * @param {{x:number,y:number,z:number}} opts.size — размер в ячейках
     * @param {string[]} [opts.palette] — индекс → matId-ключ. palette[0] = ''
     */
    constructor(opts) {
        const { origin, size, palette } = opts;
        if (!origin || !size) throw new Error('VoxelGrid: origin and size required');
        if (size.x > MAX_DIM_XZ || size.z > MAX_DIM_XZ || size.y > MAX_DIM_Y) {
            throw new Error(`VoxelGrid: size exceeds max ${MAX_DIM_XZ}×${MAX_DIM_Y}×${MAX_DIM_XZ}`);
        }
        this.origin = { x: origin.x, y: origin.y, z: origin.z };
        this.size = { x: size.x, y: size.y, z: size.z };
        // Палитра по умолчанию: [пусто, и всё что есть в TERRAIN_MATERIALS]
        this.palette = palette ? palette.slice() : ['', ...Object.keys(TERRAIN_MATERIALS)];
        // matId-key → byte. matIdByKey['grass'] === 1, и т.д.
        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 для линейной индексации
        this._sxy = this.size.x * this.size.y;   // stride для Z
        this._sx = this.size.x;                  // stride для Y
        this.data = new Uint8Array(this.size.x * this.size.y * this.size.z);
    }

    /** Размер данных в байтах. */
    byteLength() { return this.data.length; }

    /** Проверка что (ix, iy, iz) внутри grid. */
    inBounds(ix, iy, iz) {
        return ix >= 0 && ix < this.size.x
            && iy >= 0 && iy < this.size.y
            && iz >= 0 && iz < this.size.z;
    }

    /** Линейный индекс. Layout: x-major, потом y, потом z (data[z*sxy + y*sx + x]). */
    _idx(ix, iy, iz) {
        return iz * this._sxy + iy * this._sx + ix;
    }

    /**
     * Получить matId-ключ ('grass' etc) или '' если пусто.
     * Для значений вне bounds возвращает ''.
     */
    get(ix, iy, iz) {
        if (!this.inBounds(ix, iy, iz)) return '';
        const b = this.data[this._idx(ix, iy, iz)];
        return this.palette[b] || '';
    }

    /** Получить byte напрямую (быстрее для алгоритмов). */
    getByte(ix, iy, iz) {
        if (!this.inBounds(ix, iy, iz)) return 0;
        return this.data[this._idx(ix, iy, iz)];
    }

    /** Установить matId-ключ. Если ключ '' или null — стираем (=0). */
    set(ix, iy, iz, matKey) {
        if (!this.inBounds(ix, iy, iz)) return false;
        let b = 0;
        if (matKey) {
            b = this._matIdByKey.get(matKey);
            if (b === undefined) {
                // Расширяем палитру
                if (this.palette.length >= 255) {
                    console.warn('[VoxelGrid] palette full, cannot add', matKey);
                    return false;
                }
                b = this.palette.length;
                this.palette.push(matKey);
                this._matIdByKey.set(matKey, b);
            }
        }
        const idx = this._idx(ix, iy, iz);
        if (this.data[idx] === b) return false;   // no change
        this.data[idx] = b;
        return true;
    }

    /** Удалить (= set 0). */
    erase(ix, iy, iz) {
        return this.set(ix, iy, iz, '');
    }

    /** True если ячейка занята (не пусто). */
    isSolid(ix, iy, iz) {
        if (!this.inBounds(ix, iy, iz)) return false;
        return this.data[this._idx(ix, iy, iz)] !== 0;
    }

    /** Очистить всё. */
    clear() {
        this.data.fill(0);
    }

    /** Подсчёт заполненных ячеек (для debug/stats). */
    countSolid() {
        let n = 0;
        for (let i = 0; i < this.data.length; i++) if (this.data[i] !== 0) n++;
        return n;
    }

    /**
     * Скопировать диапазон ячеек в плоский Uint8Array.
     * @returns {Uint8Array} length = (x1-x0)*(y1-y0)*(z1-z0)
     */
    extractRange(x0, y0, z0, x1, y1, z1) {
        // Клампинг к bounds
        x0 = Math.max(0, x0); y0 = Math.max(0, y0); z0 = Math.max(0, z0);
        x1 = Math.min(this.size.x, x1); y1 = Math.min(this.size.y, y1); z1 = Math.min(this.size.z, z1);
        const sx = x1 - x0, sy = y1 - y0, sz = z1 - z0;
        if (sx <= 0 || sy <= 0 || sz <= 0) return new Uint8Array(0);
        const out = new Uint8Array(sx * sy * sz);
        for (let z = 0; z < sz; z++) {
            for (let y = 0; y < sy; y++) {
                const srcRow = this._idx(x0, y0 + y, z0 + z);
                const dstRow = z * sx * sy + y * sx;
                out.set(this.data.subarray(srcRow, srcRow + sx), dstRow);
            }
        }
        return out;
    }

    // =========================================================================
    // СЕРИАЛИЗАЦИЯ — RLE формат для БД
    // =========================================================================

    /**
     * Сжать в RLE с base64. Эффективно для разреженных карт.
     * Формат:
     *   { format: 'tmesh-v1', origin, size, palette, data: base64 }
     * data — Uint8Array после RLE: повторение [count_u16, value_u8].
     */
    serialize() {
        const rle = this._encodeRLE();
        return {
            format: 'tmesh-v1',
            origin: this.origin,
            size: this.size,
            palette: this.palette,
            data: this._uint8ToBase64(rle),
        };
    }

    /** RLE encode: для длинных runs одного byte пишем [count_lo, count_hi, value]. */
    _encodeRLE() {
        const buf = [];
        const N = this.data.length;
        let i = 0;
        while (i < N) {
            const v = this.data[i];
            let run = 1;
            while (i + run < N && this.data[i + run] === v && run < 65535) run++;
            buf.push(run & 0xff, (run >> 8) & 0xff, v);
            i += run;
        }
        return new Uint8Array(buf);
    }

    /** RLE decode. */
    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;
    }

    /** Восстановить из serialize-результата. */
    static deserialize(obj) {
        if (!obj || obj.format !== 'tmesh-v1') {
            throw new Error('VoxelGrid: bad format ' + (obj?.format ?? 'undefined'));
        }
        const grid = new VoxelGrid({
            origin: obj.origin,
            size: obj.size,
            palette: obj.palette,
        });
        const rle = VoxelGrid._base64ToUint8(obj.data);
        grid.data = VoxelGrid._decodeRLE(rle, grid.data.length);
        return grid;
    }

    /** Uint8Array → base64. */
    _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);
    }

    /** base64 → Uint8Array. */
    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;
    }
}
