/**
 * VoxelLayer — слой воксельного мира с фиксированным размером ячейки.
 *
 * Один слой = одна сетка чанков с одним voxelSize. Между слоями нет связи —
 * это позволяет иметь несколько разрешений в одном мире:
 *   terrain  — 0.25м, твёрдый, рендерится с MultiMaterial
 *   deco     — 0.05м (мини-voxel'ы для травы/цветов), не коллидит, без текстур
 *   build    — 0.5м (пользовательские блоки) — опционально, не первая версия
 *
 * Палитра — общий список строковых id материалов слоя. Внутри чанков
 * хранится индекс в этой палитре (1 байт), что экономит 99% размера vs
 * хранение строки на каждый voxel.
 *
 * Public API:
 *   getVoxel(gx, gy, gz)             — id материала или null
 *   setVoxel(gx, gy, gz, matId)      — устанавливает voxel
 *   removeVoxel(gx, gy, gz)          — то же что setVoxel(..., null)
 *   getMatIdx(gx, gy, gz)            — числовой индекс (для меширования)
 *   matIdxToId(idx) / matIdToIdx(id) — конвертация
 *   forEachChunk(fn)                 — обход всех чанков
 *   stats()                          — статистика для отладки
 *
 * Координаты voxel'ов (gx, gy, gz) — целочисленные grid-индексы.
 * Перевод в мировые метры: worldX = gx * voxelSize.
 */

import { VoxelChunk, voxelToChunk, chunkKey, CHUNK_SIZE } from './VoxelChunk';

export class VoxelLayer {
    /**
     * @param {string} name - 'terrain' | 'deco' | 'build' | ...
     * @param {number} voxelSize - метры на одну ячейку
     */
    constructor(name, voxelSize) {
        this.name = name;
        this.voxelSize = voxelSize;
        /** Map<"cx,cy,cz", VoxelChunk>. Пустые чанки удаляются. */
        this.chunks = new Map();
        /**
         * Палитра материалов. Индекс 0 зарезервирован под "пусто" (нельзя
         * использовать). 1..255 — реальные материалы.
         * Например: [null, 'grass', 'rock', 'sand', ...]
         */
        this.palette = [null];
        /** Обратный индекс: matId → matIdx. */
        this.idToIdx = new Map();
    }

    // ========================================================================
    // Палитра
    // ========================================================================

    /**
     * Получить или зарезервировать индекс материала. Палитра растёт по мере
     * появления новых материалов. Возвращает 0 если попытка зарегать null.
     */
    matIdToIdx(matId) {
        if (matId == null) return 0;
        let idx = this.idToIdx.get(matId);
        if (idx !== undefined) return idx;
        if (this.palette.length >= 256) {
            console.warn(`[VoxelLayer ${this.name}] palette overflow (>255 materials), cannot register '${matId}'`);
            return 0;
        }
        idx = this.palette.length;
        this.palette.push(matId);
        this.idToIdx.set(matId, idx);
        return idx;
    }

    /** Получить matId по индексу. null если пусто или неизвестен. */
    matIdxToId(idx) {
        if (idx <= 0 || idx >= this.palette.length) return null;
        return this.palette[idx];
    }

    // ========================================================================
    // CRUD voxel'ов
    // ========================================================================

    /** Получить чанк по chunk-координатам. Возвращает null если не существует. */
    getChunk(cx, cy, cz) {
        return this.chunks.get(chunkKey(cx, cy, cz)) ?? null;
    }

    /** Получить или создать чанк. */
    getOrCreateChunk(cx, cy, cz) {
        const k = chunkKey(cx, cy, cz);
        let ch = this.chunks.get(k);
        if (!ch) {
            ch = new VoxelChunk(cx, cy, cz);
            this.chunks.set(k, ch);
        }
        return ch;
    }

    /** material id ('grass'/'rock'/...) или null если пусто. */
    getVoxel(gx, gy, gz) {
        const { cx, cy, cz, lx, ly, lz } = voxelToChunk(gx, gy, gz);
        const ch = this.getChunk(cx, cy, cz);
        if (!ch) return null;
        return this.matIdxToId(ch.getLocal(lx, ly, lz));
    }

    /** Числовой индекс материала (для меширования — быстрее чем строки). */
    getMatIdx(gx, gy, gz) {
        const { cx, cy, cz, lx, ly, lz } = voxelToChunk(gx, gy, gz);
        const ch = this.getChunk(cx, cy, cz);
        if (!ch) return 0;
        return ch.getLocal(lx, ly, lz);
    }

    /**
     * Установить voxel. matId=null или undefined → удаление.
     * Возвращает true если значение изменилось.
     */
    setVoxel(gx, gy, gz, matId) {
        const { cx, cy, cz, lx, ly, lz } = voxelToChunk(gx, gy, gz);
        const matIdx = this.matIdToIdx(matId);
        // Если ячейка очищается и чанка нет — нечего делать.
        if (matIdx === 0) {
            const ch = this.getChunk(cx, cy, cz);
            if (!ch) return false;
            const changed = ch.setLocal(lx, ly, lz, 0);
            // Если чанк опустел — удалим его (но не сейчас, чтобы не
            // дёрнуть renderer; пометим dirty и пусть mesher решит)
            return changed;
        }
        const ch = this.getOrCreateChunk(cx, cy, cz);
        return ch.setLocal(lx, ly, lz, matIdx);
    }

    /** Удалить voxel. */
    removeVoxel(gx, gy, gz) {
        return this.setVoxel(gx, gy, gz, null);
    }

    /** Существует ли voxel. */
    hasVoxel(gx, gy, gz) {
        return this.getMatIdx(gx, gy, gz) !== 0;
    }

    // ========================================================================
    // Bulk операции
    // ========================================================================

    /**
     * Bulk-загрузка из плоского массива {x, y, z, m} — используется при
     * миграции legacy формата terrain[] в новый формат.
     */
    loadFromArray(voxels) {
        const t0 = performance.now();
        let added = 0;
        for (const v of voxels) {
            if (this.setVoxel(v.x, v.y, v.z, v.m)) added++;
        }
        const dt = performance.now() - t0;
        console.log(`[VoxelLayer ${this.name}] loadFromArray: ${added}/${voxels.length} voxels, ${this.chunks.size} chunks, ${dt.toFixed(0)}ms`);
    }

    /** Полная очистка слоя. */
    clear() {
        for (const ch of this.chunks.values()) {
            ch.clear();
        }
        this.chunks.clear();
    }

    /** Сколько занятых ячеек суммарно. */
    voxelCount() {
        let n = 0;
        for (const ch of this.chunks.values()) n += ch.nonEmptyCount;
        return n;
    }

    /** Обход всех чанков (включая пустые с dirty=true — для очистки рендера). */
    forEachChunk(fn) {
        for (const ch of this.chunks.values()) fn(ch);
    }

    /** Удалить пустые чанки. Безопасно вызывать после batch-операций. */
    purgeEmptyChunks() {
        const toDelete = [];
        for (const [k, ch] of this.chunks) {
            if (ch.isEmpty()) toDelete.push(k);
        }
        for (const k of toDelete) this.chunks.delete(k);
        return toDelete.length;
    }

    /** Отладочная статистика. */
    stats() {
        let totalVoxels = 0;
        let dirtyChunks = 0;
        for (const ch of this.chunks.values()) {
            totalVoxels += ch.nonEmptyCount;
            if (ch.dirty) dirtyChunks++;
        }
        return {
            name: this.name,
            voxelSize: this.voxelSize,
            chunks: this.chunks.size,
            voxels: totalVoxels,
            dirtyChunks,
            paletteSize: this.palette.length - 1,
        };
    }
}
