/**
 * TerrainMesh — новый менеджер ландшафта в стиле Roblox/Minecraft.
 *
 * Заменяет thin-instance TerrainManager на полигональный mesh, построенный
 * из voxel-grid через Greedy Meshing. Это даёт ×100-1000 меньше треугольников
 * на сложных холмистых картах.
 *
 * Архитектура:
 *   - VoxelGrid хранит весь мир как Uint8Array (1 байт = matId, 1м/cell)
 *   - Сцена разбита на chunks 32×32×32 ячеек (32м)
 *   - Каждый chunk = Map<faceMatKey, Mesh> (MultiCube гранями top/side/bottom)
 *   - При изменении voxel'а помечаем затронутый chunk как dirty
 *   - Render-loop перестраивает dirty chunks дёшево
 *   - Streaming: chunks вне radius — disabled (как раньше для region-meshes)
 *
 * MultiCube (grass/trunk/trunk_white):
 *   - У этих материалов разные текстуры на гранях (top=trava, side=dirt, bottom=dirt)
 *   - GreedyMesher разделяет грани по типу: 'grass:top', 'grass:side', 'grass:bottom'
 *   - TerrainMesh создаёт отдельный StandardMaterial для каждого ключа
 *
 * Public API (совместимый с TerrainManager где возможно):
 *   constructor(scene)
 *   setVoxel(ix, iy, iz, matKey)
 *   removeVoxel(ix, iy, iz)
 *   clear()
 *   serialize() — даёт RLE-данные для БД
 *   loadFromGrid(grid) — берёт готовый VoxelGrid и строит chunks
 *   updateStreaming(camX, camZ, radius)
 *   getVoxelCount() / getChunkCount()
 */

import {
    Mesh, VertexData, StandardMaterial, Color3, Texture, Vector3, BoundingInfo,
} from '@babylonjs/core';
import { VoxelGrid, CELL_SIZE } from './VoxelGrid';
import { buildChunkMesh } from './GreedyMesher';
import { TERRAIN_MATERIALS } from '../TerrainManager';

const TEX_BASE = '/textures/voxel';   // тот же путь что и у TerrainManager

/** Размер chunk'а в ячейках. 32×32×32 = 32768 ячеек = ~2-5K трианглов после greedy. */
export const CHUNK_SIZE = 32;

export class TerrainMesh {
    /**
     * @param {Object} scene — Babylon scene
     */
    constructor(scene) {
        this.scene = scene;
        /** @type {VoxelGrid|null} */
        this.grid = null;
        /** Все chunks: Map<"cx,cy,cz", Map<faceMatKey, Mesh>>. */
        this.chunks = new Map();
        /** Materials по faceMatKey (см. faceMatKey в GreedyMesher). */
        this._materials = new Map();
        /** Pending chunks для lazy-build при streaming. */
        this._pendingChunks = new Set();
        /** Dirty chunks для перестройки после brush'а. */
        this._dirtyChunks = new Set();
        /** Колбэк изменений. */
        this._onChange = null;
    }

    setOnChange(cb) { this._onChange = cb; }
    _emit() { try { this._onChange?.(); } catch (e) {} }

    // =========================================================================
    // LOAD/CREATE
    // =========================================================================

    /**
     * Заменить grid и подготовить chunks для lazy-build.
     * Видимые chunks материализуются в updateStreaming.
     */
    loadFromGrid(grid) {
        this.disposeAll();
        this.grid = grid;
        // Заполняем pending — все chunks ждут материализации
        const sx = grid.size.x, sy = grid.size.y, sz = grid.size.z;
        const nx = Math.ceil(sx / CHUNK_SIZE);
        const ny = Math.ceil(sy / CHUNK_SIZE);
        const nz = Math.ceil(sz / CHUNK_SIZE);
        for (let cx = 0; cx < nx; cx++) {
            for (let cy = 0; cy < ny; cy++) {
                for (let cz = 0; cz < nz; cz++) {
                    this._pendingChunks.add(`${cx},${cy},${cz}`);
                }
            }
        }
        console.log(`[TerrainMesh] loaded grid ${sx}×${sy}×${sz}, ${nx}×${ny}×${nz}=${nx * ny * nz} pending chunks, ${grid.byteLength() / 1024 / 1024 | 0}MB heap`);
        this._emit();
    }

    /**
     * Создать пустой grid заданного размера (в ячейках).
     * Origin центрирует grid вокруг (0,0,0).
     */
    createEmpty(sizeX, sizeY, sizeZ) {
        this.disposeAll();
        this.grid = new VoxelGrid({
            origin: { x: -sizeX / 2 | 0, y: 0, z: -sizeZ / 2 | 0 },
            size: { x: sizeX, y: sizeY, z: sizeZ },
        });
        this._emit();
    }

    /** Очистить всё. */
    clear() {
        if (this.grid) this.grid.clear();
        for (const chunkKey of this.chunks.keys()) {
            this._disposeChunk(chunkKey);
        }
        this.chunks.clear();
        this._pendingChunks.clear();
        this._dirtyChunks.clear();
        this._emit();
    }

    /** Полностью удалить (вместе с grid). */
    disposeAll() {
        this.clear();
        this.grid = null;
        for (const mat of this._materials.values()) {
            try { mat.dispose(); } catch (e) {}
        }
        this._materials.clear();
    }

    // =========================================================================
    // EDIT
    // =========================================================================

    setVoxel(ix, iy, iz, matKey) {
        if (!this.grid) return;
        const changed = this.grid.set(ix, iy, iz, matKey);
        if (changed) this._markDirtyAt(ix, iy, iz);
    }

    removeVoxel(ix, iy, iz) {
        if (!this.grid) return;
        const changed = this.grid.erase(ix, iy, iz);
        if (changed) this._markDirtyAt(ix, iy, iz);
    }

    /**
     * Помечаем chunk + соседние (если ячейка на границе) как dirty.
     * Соседние нужны потому что грани в chunk зависят от соседей.
     */
    _markDirtyAt(ix, iy, iz) {
        const cx = Math.floor(ix / CHUNK_SIZE);
        const cy = Math.floor(iy / CHUNK_SIZE);
        const cz = Math.floor(iz / CHUNK_SIZE);
        this._dirtyChunks.add(`${cx},${cy},${cz}`);
        // На границах чанков — пометить соседа
        const localX = ix - cx * CHUNK_SIZE;
        const localY = iy - cy * CHUNK_SIZE;
        const localZ = iz - cz * CHUNK_SIZE;
        if (localX === 0) this._dirtyChunks.add(`${cx - 1},${cy},${cz}`);
        if (localX === CHUNK_SIZE - 1) this._dirtyChunks.add(`${cx + 1},${cy},${cz}`);
        if (localY === 0) this._dirtyChunks.add(`${cx},${cy - 1},${cz}`);
        if (localY === CHUNK_SIZE - 1) this._dirtyChunks.add(`${cx},${cy + 1},${cz}`);
        if (localZ === 0) this._dirtyChunks.add(`${cx},${cy},${cz - 1}`);
        if (localZ === CHUNK_SIZE - 1) this._dirtyChunks.add(`${cx},${cy},${cz + 1}`);
    }

    // =========================================================================
    // MESH BUILD
    // =========================================================================

    /**
     * Построить mesh для одного chunk'а. Если был — удаляет старый.
     */
    _buildChunk(chunkKey) {
        if (!this.grid) return false;
        const [cx, cy, cz] = chunkKey.split(',').map(Number);
        const cx0 = cx * CHUNK_SIZE;
        const cy0 = cy * CHUNK_SIZE;
        const cz0 = cz * CHUNK_SIZE;
        const sizeX = Math.min(CHUNK_SIZE, this.grid.size.x - cx0);
        const sizeY = Math.min(CHUNK_SIZE, this.grid.size.y - cy0);
        const sizeZ = Math.min(CHUNK_SIZE, this.grid.size.z - cz0);
        if (sizeX <= 0 || sizeY <= 0 || sizeZ <= 0) return false;

        // Удалить старые меши этого chunk'а
        this._disposeChunk(chunkKey);

        // Построить геометрию (с greedy)
        const groups = buildChunkMesh(this.grid, cx0, cy0, cz0, sizeX, sizeY, sizeZ);

        if (groups.size === 0) {
            // Пустой chunk (все ячейки пусты или вокруг закрыты)
            return false;
        }

        // Создать Mesh для каждой группы (faceMatKey)
        const chunkMeshes = new Map();
        for (const [faceMatKey, data] of groups) {
            const mesh = new Mesh(`__terrainMesh_${chunkKey}_${faceMatKey}`, this.scene);
            const vd = new VertexData();
            vd.positions = data.positions;
            vd.normals = data.normals;
            vd.uvs = data.uvs;
            vd.indices = data.indices;
            vd.applyToMesh(mesh, false);
            mesh.material = this._getMaterial(faceMatKey);
            mesh.isPickable = false;
            mesh.receiveShadows = true;
            mesh.alwaysSelectAsActiveMesh = false;
            mesh.metadata = {
                _isTerrainMesh: true,
                chunkKey, faceMatKey,
                // streaming metadata
                chunkCenterX: (data.bbox.minX + data.bbox.maxX) * 0.5,
                chunkCenterZ: (data.bbox.minZ + data.bbox.maxZ) * 0.5,
                chunkHalfDiag: Math.sqrt(
                    Math.pow((data.bbox.maxX - data.bbox.minX) * 0.5, 2)
                    + Math.pow((data.bbox.maxZ - data.bbox.minZ) * 0.5, 2),
                ),
            };
            const pad = CELL_SIZE * 0.5;
            try {
                mesh.setBoundingInfo(new BoundingInfo(
                    new Vector3(data.bbox.minX - pad, data.bbox.minY - pad, data.bbox.minZ - pad),
                    new Vector3(data.bbox.maxX + pad, data.bbox.maxY + pad, data.bbox.maxZ + pad),
                ));
            } catch (e) {}
            try { mesh.freezeWorldMatrix(); } catch (e) {}
            chunkMeshes.set(faceMatKey, mesh);
        }
        this.chunks.set(chunkKey, chunkMeshes);
        return true;
    }

    _disposeChunk(chunkKey) {
        const m = this.chunks.get(chunkKey);
        if (!m) return;
        for (const mesh of m.values()) {
            try { mesh.dispose(); } catch (e) {}
        }
        this.chunks.delete(chunkKey);
    }

    /**
     * Перестроить все dirty chunks. Вызывается из render-loop.
     * Лимит: rebuild не более N chunks за один вызов чтобы не блокировать.
     */
    flushDirty(maxRebuilds = 4) {
        if (this._dirtyChunks.size === 0) return 0;
        const keys = [...this._dirtyChunks];
        let count = 0;
        for (const key of keys) {
            if (count >= maxRebuilds) break;
            this._dirtyChunks.delete(key);
            this._buildChunk(key);
            count++;
        }
        return count;
    }

    // =========================================================================
    // STREAMING
    // =========================================================================

    /**
     * Материализовать pending chunks в радиусе и enable/disable существующие.
     * @returns {{built:number, enabled:number, disabled:number, pending:number}}
     */
    updateStreaming(camX, camZ, radius = 60, options) {
        // Дефолт 8 — компромисс между плавностью UI и быстрой подгрузкой
        // при движении камеры. Каждый chunk ~3-5мс build → 24-40мс блокировки.
        const maxBuild = options?.maxBuild ?? 8;
        const halfDiag = CHUNK_SIZE * 0.71;
        const cutoff = radius + halfDiag;
        const cutoff2 = cutoff * cutoff;

        // ШАГ 1: материализуем pending
        let built = 0;
        if (this._pendingChunks.size > 0) {
            const candidates = [];
            for (const key of this._pendingChunks) {
                const [cx, , cz] = key.split(',').map(Number);
                const ccx = (this.grid.origin.x + cx * CHUNK_SIZE + CHUNK_SIZE * 0.5);
                const ccz = (this.grid.origin.z + cz * CHUNK_SIZE + CHUNK_SIZE * 0.5);
                const dx = ccx - camX, dz = ccz - camZ;
                const d2 = dx * dx + dz * dz;
                if (d2 <= cutoff2) candidates.push({ key, d2 });
            }
            candidates.sort((a, b) => a.d2 - b.d2);
            const limit = Math.min(maxBuild, candidates.length);
            for (let i = 0; i < limit; i++) {
                this._pendingChunks.delete(candidates[i].key);
                this._buildChunk(candidates[i].key);
                built++;
            }
        }

        // ШАГ 2: enable/disable existing
        let enabled = 0, disabled = 0;
        for (const [chunkKey, meshes] of this.chunks) {
            // Берём первый mesh — у всех мешей одного chunk'а одинаковый центр
            const firstMesh = meshes.values().next().value;
            if (!firstMesh) continue;
            const md = firstMesh.metadata;
            const dx = md.chunkCenterX - camX;
            const dz = md.chunkCenterZ - camZ;
            const d2 = dx * dx + dz * dz;
            const shouldShow = d2 <= cutoff2;
            for (const mesh of meshes.values()) {
                if (shouldShow) {
                    if (!mesh.isEnabled()) mesh.setEnabled(true);
                } else {
                    if (mesh.isEnabled()) mesh.setEnabled(false);
                }
            }
            if (shouldShow) enabled++; else disabled++;
        }

        return {
            built, enabled, disabled,
            pending: this._pendingChunks.size,
            total: this.chunks.size + this._pendingChunks.size,
        };
    }

    // =========================================================================
    // MATERIALS
    // =========================================================================

    /**
     * Получить (создать) материал для faceMatKey. Кэшируется.
     *
     * Формат ключа:
     *   - 'rock' — обычный материал (одна текстура)
     *   - 'grass:top' / 'grass:side' / 'grass:bottom' — MultiCube для grass
     */
    _getMaterial(faceMatKey) {
        let mat = this._materials.get(faceMatKey);
        if (mat) return mat;

        // Парсим faceMatKey
        const colonIdx = faceMatKey.indexOf(':');
        const baseMat = colonIdx >= 0 ? faceMatKey.slice(0, colonIdx) : faceMatKey;
        const facePart = colonIdx >= 0 ? faceMatKey.slice(colonIdx + 1) : '';
        const def = TERRAIN_MATERIALS[baseMat];
        if (!def) {
            console.warn('[TerrainMesh] unknown material:', baseMat);
            const fallback = new StandardMaterial(`__terrainMat_${faceMatKey}`, this.scene);
            fallback.diffuseColor = new Color3(0.5, 0.3, 0.3);
            this._materials.set(faceMatKey, fallback);
            return fallback;
        }

        // Выбираем текстуру по face
        let texPath;
        if (facePart === 'top') texPath = def.top || def.texture;
        else if (facePart === 'side') texPath = def.side || def.texture;
        else if (facePart === 'bottom') texPath = def.bottom || def.texture;
        else texPath = def.texture || def.top || def.side;

        mat = new StandardMaterial(`__terrainMat_${faceMatKey}`, this.scene);
        mat.specularColor = new Color3(0, 0, 0);
        mat.ambientColor = new Color3(1, 1, 1);
        if (texPath) {
            const tex = new Texture(texPath, this.scene, /*noMipmap*/ true, /*invertY*/ false, Texture.NEAREST_SAMPLINGMODE);
            // wrap U/V — текстура повторяется на больших квадах (греды)
            tex.wrapU = Texture.WRAP_ADDRESSMODE;
            tex.wrapV = Texture.WRAP_ADDRESSMODE;
            mat.diffuseTexture = tex;
            if (def.alpha != null && def.alpha < 1) {
                mat.diffuseTexture.hasAlpha = true;
                mat.useAlphaFromDiffuseTexture = true;
                mat.alpha = def.alpha;
            }
            if (Array.isArray(def.emissive)) {
                mat.emissiveColor = new Color3(def.emissive[0], def.emissive[1], def.emissive[2]);
            }
        } else if (def.color) {
            mat.diffuseColor = Color3.FromHexString(def.color);
        }
        try { mat.freeze(); } catch (e) {}
        this._materials.set(faceMatKey, mat);
        return mat;
    }

    // =========================================================================
    // SERIALIZATION
    // =========================================================================

    serialize() {
        if (!this.grid) return { format: 'tmesh-v1', empty: true };
        return this.grid.serialize();
    }

    loadFromState(state) {
        if (!state || state.empty) {
            this.disposeAll();
            return;
        }
        const grid = VoxelGrid.deserialize(state);
        this.loadFromGrid(grid);
    }

    // =========================================================================
    // STATS / DEBUG
    // =========================================================================

    getVoxelCount() {
        return this.grid ? this.grid.countSolid() : 0;
    }

    getChunkCount() {
        return this.chunks.size + this._pendingChunks.size;
    }

    getActiveTriangles() {
        let n = 0;
        for (const meshes of this.chunks.values()) {
            for (const m of meshes.values()) {
                if (!m.isEnabled()) continue;
                n += (m.getTotalIndices?.() ?? 0) / 3;
            }
        }
        return n;
    }
}
