/**
 * ChunkMesher — строит геометрию одного чанка из его voxel-данных.
 *
 * На этом этапе (1) — простой surface culling без greedy merge:
 *   Для каждой занятой ячейки проверяем 6 соседей.
 *   Если сосед пустой → грань видна → добавляем в геометрию (квад из 2 треугольников).
 *
 * Greedy meshing появится в Этапе 2.
 *
 * Производит геометрию в формате «по материалам»:
 *   Map<matId, GeometryData>
 *     GeometryData = { positions, normals, uvs, indices }
 *
 * Это позволяет renderer'у создать ОДИН Mesh на чанк × материал,
 * а не один большой mesh с MultiMaterial (последнее сложнее с
 * корректным culling и shadow casting в Babylon).
 *
 * Pure function — не зависит от Babylon/Filament. Тестируется отдельно.
 * Renderer-адаптеры (VoxelRenderer.js для веба, VoxelRenderer.kt для
 * Android) принимают этот результат и создают meshes своей графикой.
 */

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

/**
 * 6 граней voxel-куба. Каждая запись:
 *   - normal:    направление нормали (для культинга соседа)
 *   - corners:   4 угла грани в относительных координатах (0/1 по 3 осям)
 *   - faceIdx:   индекс грани (0=-Z, 1=+X, 2=+Z, 3=-X, 4=-Y, 5=+Y) для
 *                совместимости с FACE_INDEX в существующем TerrainManager
 *
 * Порядок углов: counter-clockwise при взгляде снаружи воксел'а.
 * UV: первый угол — (0,1), второй — (0,0), третий — (1,0), четвёртый — (1,1).
 * Это совпадает с тем как Babylon BoxBuilder раскладывает UV на гранях.
 */
const FACES = [
    // 0: -Z (front к камере при стандартной ориентации)
    { faceIdx: 0, normal: [0, 0, -1], dx: 0, dy: 0, dz: -1, corners: [
        [0, 0, 0], [0, 1, 0], [1, 1, 0], [1, 0, 0],
    ]},
    // 1: +X (right)
    { faceIdx: 1, normal: [1, 0, 0], dx: 1, dy: 0, dz: 0, corners: [
        [1, 0, 0], [1, 1, 0], [1, 1, 1], [1, 0, 1],
    ]},
    // 2: +Z (back)
    { faceIdx: 2, normal: [0, 0, 1], dx: 0, dy: 0, dz: 1, corners: [
        [1, 0, 1], [1, 1, 1], [0, 1, 1], [0, 0, 1],
    ]},
    // 3: -X (left)
    { faceIdx: 3, normal: [-1, 0, 0], dx: -1, dy: 0, dz: 0, corners: [
        [0, 0, 1], [0, 1, 1], [0, 1, 0], [0, 0, 0],
    ]},
    // 4: -Y (bottom)
    { faceIdx: 4, normal: [0, -1, 0], dx: 0, dy: -1, dz: 0, corners: [
        [0, 0, 1], [0, 0, 0], [1, 0, 0], [1, 0, 1],
    ]},
    // 5: +Y (top)
    { faceIdx: 5, normal: [0, 1, 0], dx: 0, dy: 1, dz: 0, corners: [
        [0, 1, 0], [0, 1, 1], [1, 1, 1], [1, 1, 0],
    ]},
];

/**
 * Прозрачные материалы (через них видна геометрия за ними). Используются
 * для пропуска грани при surface culling — например водный voxel рядом с
 * каменной стеной должен показать стену.
 *
 * Список совпадает с веб-плеером и Android (NON_SOLID_TERRAIN).
 */
const TRANSPARENT_MATERIALS = new Set([
    'water', 'glacier',
    'leaves', 'leaves_orange',
    'flower_red', 'flower_blue', 'flower_yellow',
    'mushroom_red',
    'tall_grass',
]);

/**
 * Multi-cube материалы — top/side/bottom разные текстуры. Mesher
 * разделяет их в ключи материалов с суффиксом ":top" / ":side" /
 * ":bottom". VoxelRenderer применяет соответствующую текстуру.
 *
 * Без этого верх grass-voxel'а был бы землёй, а бока — травой.
 */
const MULTICUBE_MATERIALS = new Set([
    'grass', 'trunk', 'trunk_white',
]);

/**
 * Получить ключ материала с учётом грани (для MultiCube).
 *   grass + faceIdx=5 (top) → "grass:top"
 *   grass + faceIdx=4 (bottom) → "grass:bottom"
 *   grass + side (0..3)        → "grass:side"
 *   rock + любая грань         → "rock"
 */
function materialKey(matId, faceIdx) {
    if (!MULTICUBE_MATERIALS.has(matId)) return matId;
    if (faceIdx === 5) return `${matId}:top`;
    if (faceIdx === 4) return `${matId}:bottom`;
    return `${matId}:side`;
}

/**
 * Хелпер: грань между own (текущий voxel) и neighbor (сосед) видна?
 *   - neighbor пустой → грань видна
 *   - оба непрозрачные → грань НЕ видна (поверхности слиплись)
 *   - own непрозрачный, neighbor прозрачный → грань видна (стена за водой)
 *   - own прозрачный, neighbor непрозрачный → грань НЕ видна (внутри стены)
 *   - оба прозрачные, разные → грань видна (граница вода/листва)
 *   - оба прозрачные, одинаковые → грань НЕ видна (вода-вода)
 */
function isFaceVisible(ownMat, neighborMat) {
    if (!neighborMat) return true;
    const ownTransparent = TRANSPARENT_MATERIALS.has(ownMat);
    const nbTransparent = TRANSPARENT_MATERIALS.has(neighborMat);
    if (!ownTransparent && !nbTransparent) return false;
    if (!ownTransparent && nbTransparent) return true;
    if (ownTransparent && !nbTransparent) return false;
    return ownMat !== neighborMat;
}

/**
 * @typedef {Object} GeometryData
 * @property {Float32Array} positions — Vector3 на каждую вершину
 * @property {Float32Array} normals
 * @property {Float32Array} uvs
 * @property {Uint32Array} indices
 * @property {number} faceCount — для статистики
 */

/**
 * @typedef {Object} MeshResult
 * @property {Map<string, GeometryData>} byMaterial
 * @property {number} totalFaces
 * @property {number} timeMs
 */

/**
 * Построить геометрию чанка.
 *
 * @param {VoxelChunk} chunk
 * @param {VoxelLayer} layer
 * @param {(gx:number,gy:number,gz:number)=>number} neighborMatIdx
 *        Функция получения соседа по глобальным voxel-координатам — нужна
 *        для проверки соседних чанков на границе. Если не передана —
 *        соседи за пределами чанка считаются пустыми (всегда видимые
 *        грани на границах; визуально может быть швы между чанками,
 *        но безопасно как fallback).
 * @returns {MeshResult}
 */
export function buildChunkGeometry(chunk, layer, neighborMatIdx = null) {
    const t0 = (typeof performance !== 'undefined' ? performance.now() : Date.now());
    const result = new Map();   // matId → GeometryData accumulator (arrays)
    let totalFaces = 0;

    const voxelSize = layer.voxelSize;
    const ox = chunk.voxelOriginX();
    const oy = chunk.voxelOriginY();
    const oz = chunk.voxelOriginZ();

    // Pre-аллоцированные временные массивы; финальные Float32Array
    // соберём в конце.
    const accumulators = new Map(); // matId → { positions:[], normals:[], uvs:[], indices:[] }

    function getAccum(matId) {
        let a = accumulators.get(matId);
        if (!a) {
            a = { positions: [], normals: [], uvs: [], indices: [], faceCount: 0 };
            accumulators.set(matId, a);
        }
        return a;
    }

    // Хелпер: matIdx соседа по локальным координатам внутри чанка, либо
    // через callback для соседнего чанка.
    function getNeighborIdx(lx, ly, lz) {
        if (lx >= 0 && lx < CHUNK_SIZE &&
            ly >= 0 && ly < CHUNK_SIZE &&
            lz >= 0 && lz < CHUNK_SIZE) {
            return chunk.data[VoxelChunk.localIndex(lx, ly, lz)];
        }
        // За пределами чанка
        if (neighborMatIdx) {
            return neighborMatIdx(ox + lx, oy + ly, oz + lz);
        }
        return 0;
    }

    // Основной цикл: идём по всем ячейкам чанка
    for (let ly = 0; ly < CHUNK_SIZE; ly++) {
        for (let lz = 0; lz < CHUNK_SIZE; lz++) {
            for (let lx = 0; lx < CHUNK_SIZE; lx++) {
                const ownIdx = chunk.data[VoxelChunk.localIndex(lx, ly, lz)];
                if (ownIdx === 0) continue;
                const ownMat = layer.matIdxToId(ownIdx);
                if (!ownMat) continue;

                // Глобальные координаты в воксельных единицах
                const gx = ox + lx;
                const gy = oy + ly;
                const gz = oz + lz;
                // Мировые координаты левого-нижнего угла voxel'а
                const wx0 = gx * voxelSize;
                const wy0 = gy * voxelSize;
                const wz0 = gz * voxelSize;

                // Проверяем 6 граней
                for (let f = 0; f < 6; f++) {
                    const face = FACES[f];
                    const nbIdx = getNeighborIdx(lx + face.dx, ly + face.dy, lz + face.dz);
                    const nbMat = nbIdx !== 0 ? layer.matIdxToId(nbIdx) : null;
                    if (!isFaceVisible(ownMat, nbMat)) continue;

                    // Добавляем 4 вершины + 2 треугольника.
                    // Для MultiCube (grass/trunk/trunk_white) ключ материала
                    // содержит суффикс ":top"/":side"/":bottom" — Renderer
                    // применит правильную текстуру.
                    const matKey = materialKey(ownMat, face.faceIdx);
                    const accum = getAccum(matKey);
                    const baseV = accum.positions.length / 3;
                    const nx = face.normal[0], ny = face.normal[1], nz = face.normal[2];
                    const uvCorners = [[0, 1], [0, 0], [1, 0], [1, 1]];

                    for (let i = 0; i < 4; i++) {
                        const corner = face.corners[i];
                        accum.positions.push(
                            wx0 + corner[0] * voxelSize,
                            wy0 + corner[1] * voxelSize,
                            wz0 + corner[2] * voxelSize,
                        );
                        accum.normals.push(nx, ny, nz);
                        const uv = uvCorners[i];
                        accum.uvs.push(uv[0], uv[1]);
                    }
                    // Два треугольника: (0,1,2) и (0,2,3)
                    accum.indices.push(baseV, baseV + 1, baseV + 2);
                    accum.indices.push(baseV, baseV + 2, baseV + 3);
                    accum.faceCount++;
                    totalFaces++;
                }
            }
        }
    }

    // Преобразуем аккумуляторы в Typed Arrays
    for (const [matId, accum] of accumulators) {
        result.set(matId, {
            positions: new Float32Array(accum.positions),
            normals: new Float32Array(accum.normals),
            uvs: new Float32Array(accum.uvs),
            indices: new Uint32Array(accum.indices),
            faceCount: accum.faceCount,
        });
    }

    const dt = (typeof performance !== 'undefined' ? performance.now() : Date.now()) - t0;
    return { byMaterial: result, totalFaces, timeMs: dt };
}
