/**
 * VoxelRenderer — Babylon-адаптер для рендера VoxelWorld.
 *
 * Идея: один Mesh на чанк × материал. Чанк перестраивается только когда
 * dirty=true. Меш кешируется в chunk.meshRef = Map<matId, Mesh>.
 *
 * Материалы переиспользуются из существующего TERRAIN_MATERIALS — тот
 * же набор текстур (Kenney Voxel Pack), что и в legacy TerrainManager.
 * Это даёт визуальную совместимость и единую палитру.
 *
 * MultiCube материалы (grass: top/side/bottom):
 *   В этой версии (Этап 1) — НЕ поддерживаются. Все грани куба красятся
 *   ОДНОЙ текстурой материала. Для grass — берём top (зелёная травка),
 *   что выглядит OK на верхушке, но на боках будет тоже травка вместо
 *   земли. Это компромисс: greedy meshing (Этап 2) сделает корректный
 *   MultiCube через разделение mesh'ей по граням.
 */

import {
    Mesh,
    VertexData,
    StandardMaterial,
    Texture,
    Color3,
} from '@babylonjs/core';
// ВАЖНО: для terrain-слоя (0.25м, текстуры Kenney) используем surface
// culling (ChunkMesher), а НЕ greedy. Voxlands-стиль требует чтобы
// каждый voxel был виден как отдельный куб с тёмными гранями между
// ячейками. Greedy сливает соседние грани в один большой квад → теряется
// объём (см. RUBLOX_VOXEL_ENGINE_PLAN.md, Этап 2 примечание).
//
// GreedyMesher оставлен в коде — будет использоваться для будущего
// build-слоя (пользовательские постройки из 1×1м блоков), где как раз
// нужно мерджить большие плоские стены.
import { buildChunkGeometry } from './ChunkMesher';

/**
 * Описание материалов слоя. Структура совпадает с TERRAIN_MATERIALS
 * в TerrainManager.js, но дублируется чтобы не было кругового импорта.
 *
 * matId → { color, texture, top, side, bottom, alpha, emissive }
 */
const TEX = '/kubikon-assets/textures';

export const VOXEL_MATERIALS = {
    grass: { color: '#52b15a', top: `${TEX}/grass_top.png`, side: `${TEX}/dirt.png`, bottom: `${TEX}/dirt.png` },
    rock: { color: '#7e7e7e', texture: `${TEX}/greystone.png` },
    sand: { color: '#e6d27a', texture: `${TEX}/sand.png` },
    snow: { color: '#f5f7fb', texture: `${TEX}/snow.png` },
    dirt: { color: '#7c5430', texture: `${TEX}/dirt.png` },
    water: { color: '#3a8fd6', texture: `${TEX}/water.png`, alpha: 0.72 },
    asphalt: { color: '#3b3b3b', texture: `${TEX}/stone.png` },
    concrete: { color: '#b8b8b8', texture: `${TEX}/greystone.png` },
    wood: { color: '#a06a3a', texture: `${TEX}/wood.png` },
    glacier: { color: '#c8e6f5', texture: `${TEX}/ice.png`, alpha: 0.92 },
    salt: { color: '#ecedef', texture: `${TEX}/snow.png` },
    mud: { color: '#553a25', texture: `${TEX}/gravel_dirt.png` },
    // Voxlands-декорации
    leaves: { color: '#3f7a3a', texture: `${TEX}/leaves.png` },
    leaves_orange: { color: '#c2741e', texture: `${TEX}/leaves_orange.png` },
    trunk: { color: '#5a3b1f', top: `${TEX}/trunk_top.png`, side: `${TEX}/trunk_side.png`, bottom: `${TEX}/trunk_bottom.png` },
    trunk_white: { color: '#e0dfd6', top: `${TEX}/trunk_top.png`, side: `${TEX}/trunk_white_side.png`, bottom: `${TEX}/trunk_top.png` },
    rock_moss: { color: '#5d6f43', texture: `${TEX}/rock_moss.png` },
    flower_red: { color: '#b84141', texture: `${TEX}/cotton_red.png` },
    flower_blue: { color: '#4673b8', texture: `${TEX}/cotton_blue.png` },
    flower_yellow: { color: '#d4c84a', texture: `${TEX}/cotton_tan.png` },
    mushroom_red: { color: '#a02525', texture: `${TEX}/mushroom_red.png` },
    tall_grass: { color: '#5fa84e', texture: `${TEX}/wheat_stage4.png` },
};

/** Конвертация hex '#aabbcc' → Color3. */
function hexToColor3(hex) {
    const m = /^#?([a-f0-9]{6})$/i.exec(hex || '');
    if (!m) return new Color3(0.7, 0.7, 0.7);
    const n = parseInt(m[1], 16);
    return new Color3(((n >> 16) & 0xff) / 255, ((n >> 8) & 0xff) / 255, (n & 0xff) / 255);
}

export class VoxelRenderer {
    /**
     * @param {VoxelWorld} world
     * @param {BABYLON.Scene} scene
     */
    constructor(world, scene) {
        this.world = world;
        this.scene = scene;
        /** Map<matId, StandardMaterial> — кеш материалов. */
        this._materials = new Map();
        /** Колбэк создания меша — для теневой регистрации (shadowMap.renderList). */
        this._onMeshCreated = null;
    }

    setOnMeshCreated(cb) { this._onMeshCreated = cb; }

    /**
     * Получить или создать Babylon-материал по ключу.
     * Ключ может быть простым ('rock', 'sand', ...) или MultiCube
     * вариантом ('grass:top', 'grass:side', 'grass:bottom', 'trunk:top', ...).
     *
     * Для MultiCube ключей берём соответствующую текстуру из def.top/side/
     * bottom; для простых — def.texture.
     *
     * Wrap-режим: REPEAT (Texture.WRAP_ADDRESSMODE) — критично для greedy
     * meshing. Большой квад W×H требует чтобы текстура повторялась W раз
     * по одной оси и H раз по другой. UV-coords тогда идут (0..W, 0..H).
     */
    _getMaterial(matKey) {
        let mat = this._materials.get(matKey);
        if (mat) return mat;
        // Разбираем MultiCube ключ
        const [matId, face] = matKey.includes(':') ? matKey.split(':') : [matKey, null];
        const def = VOXEL_MATERIALS[matId];
        if (!def) {
            mat = new StandardMaterial(`__voxel_mat_${matKey}_missing`, this.scene);
            mat.diffuseColor = new Color3(1, 0, 1);
            this._materials.set(matKey, mat);
            return mat;
        }
        mat = new StandardMaterial(`__voxel_mat_${matKey}`, this.scene);
        // Specular выключен — на pixel-art-текстуре любой блик ломает стиль.
        mat.specularColor = new Color3(0, 0, 0);
        // КРИТИЧНО: ambient=(1,1,1) чтобы hemisphere-свет освещал материал
        // со всех сторон. Без этого тыловые/нижние грани выходят почти
        // чёрными (как было на скрине — потемневший террейн).
        mat.ambientColor = new Color3(1, 1, 1);

        // Выбираем текстуру в зависимости от грани (для MultiCube)
        let texPath;
        if (face === 'top') {
            texPath = def.top ?? def.texture;
        } else if (face === 'bottom') {
            texPath = def.bottom ?? def.texture;
        } else if (face === 'side') {
            texPath = def.side ?? def.texture;
        } else {
            texPath = def.texture ?? def.top;
        }
        if (texPath) {
            // noMipmap=true — критично для pixel-art (mipmap усредняет пиксели).
            // invertY=false — Kenney pack уже в нормальной ориентации.
            const tex = new Texture(
                texPath, this.scene,
                /*noMipmap*/ true,
                /*invertY*/ false,
                Texture.NEAREST_SAMPLINGMODE,
            );
            tex.wrapU = Texture.CLAMP_ADDRESSMODE;
            tex.wrapV = Texture.CLAMP_ADDRESSMODE;
            mat.diffuseTexture = tex;
            if (def.alpha != null && def.alpha < 1) {
                mat.diffuseTexture.hasAlpha = true;
                mat.useAlphaFromDiffuseTexture = true;
                mat.alpha = def.alpha;
            }
            // НЕ ставим diffuseColor — иначе текстура × цвет → потемнение.
            // Текстура показывается в полной интенсивности.
        } else {
            // Fallback на цвет если нет текстуры
            mat.diffuseColor = hexToColor3(def.color);
            if (def.alpha != null && def.alpha < 1) mat.alpha = def.alpha;
        }
        this._materials.set(matKey, mat);
        return mat;
    }

    /**
     * Перестроить mesh'и одного чанка. Если чанк не dirty — no-op.
     * Вызывается из update() для каждого dirty-чанка.
     */
    rebuildChunk(chunk, layer) {
        if (!chunk.dirty) return;

        // Хелпер для проверки соседних чанков по глобальной voxel-координате
        const neighborMatIdx = (gx, gy, gz) => layer.getMatIdx(gx, gy, gz);

        // Surface culling без greedy — сохраняем Voxlands-look (каждый voxel
        // виден как отдельный куб с тёмными гранями между ячейками).
        // Greedy убирал эти грани и портил визуальный стиль.
        const mesh = buildChunkGeometry(chunk, layer, neighborMatIdx);

        // Удаляем старые меши (если были)
        if (chunk.meshRef) {
            for (const m of chunk.meshRef.values()) {
                m.dispose();
            }
        }

        if (mesh.byMaterial.size === 0) {
            chunk.meshRef = null;
            chunk.dirty = false;
            return;
        }

        // Создаём по одному Mesh на каждый материал
        const meshMap = new Map();
        for (const [matId, geo] of mesh.byMaterial) {
            const meshName = `__voxel_${layer.name}_${chunk.cx}_${chunk.cy}_${chunk.cz}_${matId}`;
            const m = new Mesh(meshName, this.scene);
            const vd = new VertexData();
            vd.positions = geo.positions;
            vd.normals = geo.normals;
            vd.uvs = geo.uvs;
            vd.indices = geo.indices;
            vd.applyToMesh(m, false);   // not updateable — статичные данные
            m.material = this._getMaterial(matId);
            // isPickable=false: терреин не пикается через scene.pick, мы
            // используем свой raycast по voxel-grid.
            m.isPickable = false;
            m.receiveShadows = true;
            // Метаданные для дебага/идентификации
            m.metadata = {
                _isVoxelChunkMesh: true,
                layerName: layer.name,
                chunkKey: `${chunk.cx},${chunk.cy},${chunk.cz}`,
                matId,
            };
            meshMap.set(matId, m);
            try { this._onMeshCreated?.(m); } catch (e) {}
        }
        chunk.meshRef = meshMap;
        chunk.dirty = false;
    }

    /**
     * Перестроить все dirty-чанки во всех слоях. Вызывается из game loop
     * или после загрузки мира. Возвращает количество перестроенных чанков.
     */
    rebuildDirty() {
        let rebuilt = 0;
        for (const layer of this.world.layers.values()) {
            for (const chunk of layer.chunks.values()) {
                if (chunk.dirty) {
                    this.rebuildChunk(chunk, layer);
                    rebuilt++;
                }
            }
        }
        return rebuilt;
    }

    /** Полная перестройка всего мира (для миграции после loadFromArray). */
    rebuildAll() {
        const t0 = performance.now();
        let chunks = 0;
        let totalQuads = 0;
        let totalTriangles = 0;
        let totalMeshes = 0;
        for (const layer of this.world.layers.values()) {
            for (const chunk of layer.chunks.values()) {
                chunk.dirty = true;
                this.rebuildChunk(chunk, layer);
                chunks++;
                if (chunk.meshRef) {
                    totalMeshes += chunk.meshRef.size;
                    for (const m of chunk.meshRef.values()) {
                        if (m.getIndices) {
                            const idx = m.getIndices();
                            if (idx) {
                                const tris = idx.length / 3;
                                totalTriangles += tris;
                                totalQuads += tris / 2;
                            }
                        }
                    }
                }
            }
        }
        const dt = performance.now() - t0;
        console.log(`[VoxelRenderer] rebuildAll: ${chunks} chunks, ${totalMeshes} meshes, ${Math.round(totalQuads)} quads, ${Math.round(totalTriangles)} triangles in ${dt.toFixed(0)}ms`);
        return chunks;
    }

    // ========================================================================
    // Этап 4 — Streaming (рендер только чанков в радиусе от камеры)
    // ========================================================================

    /**
     * Уничтожить mesh'и одного чанка, оставив данные voxel'ов в памяти.
     * Используется при выходе чанка из render distance.
     */
    unloadChunkMesh(chunk) {
        if (!chunk.meshRef) return;
        for (const m of chunk.meshRef.values()) m.dispose();
        chunk.meshRef = null;
        // dirty НЕ ставим — данные не изменились, при следующем входе в
        // radius re-build пересоздаст mesh.
    }

    /**
     * Обновить streaming: для каждого слоя — определить какие чанки
     * должны быть видимы (в радиусе renderRadius от точки center).
     * Чанки внутри — meshed (lazy build). Чанки снаружи — unloaded.
     *
     * @param {Vector3-like} center — мировые координаты центра ({x,z} or Vector3)
     * @param {number} renderRadius — радиус в метрах (default 64м = 8 чанков × 0.25 × 32)
     * @returns {{loaded:number, unloaded:number, total:number}}
     */
    updateStreaming(center, renderRadius = 64) {
        let loaded = 0;
        let unloaded = 0;
        let total = 0;
        const cx = center.x ?? 0;
        const cz = center.z ?? 0;

        for (const layer of this.world.layers.values()) {
            // Размер чанка в метрах = CHUNK_SIZE × voxelSize
            const chunkWorldSize = 32 * layer.voxelSize;   // 8м для terrain
            // Квадратичный radius для сравнения с distance² (быстрее sqrt)
            const r2 = renderRadius * renderRadius;

            for (const chunk of layer.chunks.values()) {
                total++;
                // Центр чанка в мире
                const chunkCenterX = (chunk.cx + 0.5) * chunkWorldSize;
                const chunkCenterZ = (chunk.cz + 0.5) * chunkWorldSize;
                const dx = chunkCenterX - cx;
                const dz = chunkCenterZ - cz;
                const dist2 = dx * dx + dz * dz;

                // Учитываем «полу-диагональ» чанка чтобы не cull-ить почти
                // видимые. Если центр чанка дальше radius+halfDiagonal —
                // точно невидим.
                const halfDiag = chunkWorldSize * 0.707;   // sqrt(2)/2
                const cutoff = renderRadius + halfDiag;
                const shouldRender = dist2 <= cutoff * cutoff;

                if (shouldRender) {
                    if (!chunk.meshRef && !chunk.isEmpty()) {
                        // Lazy mesh build
                        chunk.dirty = true;
                        this.rebuildChunk(chunk, layer);
                        loaded++;
                    } else if (chunk.dirty) {
                        // Mesh уже есть, но данные изменились — пересборка
                        this.rebuildChunk(chunk, layer);
                    }
                } else {
                    if (chunk.meshRef) {
                        this.unloadChunkMesh(chunk);
                        unloaded++;
                    }
                }
            }
        }
        return { loaded, unloaded, total };
    }

    /** Текущее число активных (отрендеренных) чанков. */
    getActiveChunkCount() {
        let n = 0;
        for (const layer of this.world.layers.values()) {
            for (const ch of layer.chunks.values()) {
                if (ch.meshRef) n++;
            }
        }
        return n;
    }

    /** Освободить все mesh'и (при unload или dispose редактора). */
    dispose() {
        for (const layer of this.world.layers.values()) {
            for (const chunk of layer.chunks.values()) {
                if (chunk.meshRef) {
                    for (const m of chunk.meshRef.values()) m.dispose();
                    chunk.meshRef = null;
                }
            }
        }
        for (const m of this._materials.values()) m.dispose();
        this._materials.clear();
    }
}
