/**
 * GreedyMesher — алгоритм построения mesh chunk'а из voxel-grid с
 * объединением соседних граней одного материала в большие квады.
 *
 * Это **классический алгоритм Mikola** который используется во многих
 * voxel-движках (включая Minecraft модах и Roblox legacy).
 *
 * Принцип:
 *   1. Для каждой из 6 граней (+X, -X, +Y, -Y, +Z, -Z) делаем 2D-проход
 *      по слоям (slabs) внутри chunk'а.
 *   2. На каждом слое строим 2D-массив "масок": для каждой ячейки определяем
 *      материал грани (или 0 если грани нет).
 *   3. Идём по маске, находим прямоугольники одного материала, помечаем
 *      их как обработанные, генерим один quad из 4 вершин + 2 треугольника.
 *
 * Результат: блок 32×32×32 (32K ячеек) обычно даёт **200-2000 трианглов**
 * вместо ~24K (если 1 куб = 12 трианглов).
 *
 * Поддержка MultiCube (grass — top/side/bottom):
 *   - Грань +Y (top) → matKey + '_top'
 *   - Грани ±X, ±Z (sides) → matKey + '_side'
 *   - Грань -Y (bottom) → matKey + '_bottom'
 *   Это даёт правильную текстуру на каждом направлении грани.
 *   Сабмеши группируются по этому суффиксу.
 */

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

/**
 * Список MultiCube материалов (имеют разные текстуры top/side/bottom).
 * Берём из TERRAIN_MATERIALS — если есть поле .top, значит MultiCube.
 */
function buildMultiCubeSet() {
    const s = new Set();
    for (const [matId, def] of Object.entries(TERRAIN_MATERIALS)) {
        if (def.top || def.side || def.bottom) s.add(matId);
    }
    return s;
}
const MULTICUBE_MATS = buildMultiCubeSet();

/**
 * Получить ключ материала для конкретной грани с учётом MultiCube.
 * @param {string} matKey — base ('grass', 'rock', etc)
 * @param {number} face — 0=+X, 1=-X, 2=+Y, 3=-Y, 4=+Z, 5=-Z
 * @returns {string} — 'grass:top' / 'grass:side' / 'grass:bottom' / 'rock' (без суффикса для не-MultiCube)
 */
function faceMatKey(matKey, face) {
    if (!MULTICUBE_MATS.has(matKey)) return matKey;
    if (face === 2) return matKey + ':top';
    if (face === 3) return matKey + ':bottom';
    return matKey + ':side';
}

/**
 * Главная функция: построить mesh для одного chunk'а.
 *
 * @param {VoxelGrid} grid — источник данных
 * @param {number} cx0 — начало chunk'а в ix
 * @param {number} cy0
 * @param {number} cz0
 * @param {number} sizeX — размер chunk'а в ячейках (обычно 32)
 * @param {number} sizeY
 * @param {number} sizeZ
 * @returns {Map<string, {positions: Float32Array, normals: Float32Array, uvs: Float32Array, indices: Uint32Array, bbox: object}>}
 *   Ключ — `faceMatKey` (например 'grass:top', 'rock'), значение — данные mesh'а.
 */
export function buildChunkMesh(grid, cx0, cy0, cz0, sizeX, sizeY, sizeZ) {
    // Аккумулируем геометрию по faceMatKey
    /** @type {Map<string, {positions:number[], normals:number[], uvs:number[], indices:number[], vIdx:number}>} */
    const groups = new Map();

    const getGroup = (key) => {
        let g = groups.get(key);
        if (!g) {
            g = { positions: [], normals: [], uvs: [], indices: [], vIdx: 0 };
            groups.set(key, g);
        }
        return g;
    };

    // Helper: получить byte safely + кешировать palette lookup
    const palette = grid.palette;
    const sx = grid.size.x, sy = grid.size.y, sz = grid.size.z;
    const sxy = sx * sy;
    const data = grid.data;

    const getByteUnsafe = (ix, iy, iz) => {
        if (ix < 0 || iy < 0 || iz < 0 || ix >= sx || iy >= sy || iz >= sz) return 0;
        return data[iz * sxy + iy * sx + ix];
    };

    // Обрабатываем 6 граней по очереди.
    // d = ось направления (0=X, 1=Y, 2=Z), s = знак (-1 или +1).
    // Для каждой грани:
    //   - u, v — оси внутри плоскости грани
    //   - проходим по d-слоям [0..sizeD], строим mask[u][v] — материал грани
    //   - greedy объединяем прямоугольники одного материала в маске
    for (let face = 0; face < 6; face++) {
        const d = face >> 1;          // 0=X, 1=Y, 2=Z
        const sign = (face & 1) === 0 ? 1 : -1;   // +X, -X, +Y, -Y, +Z, -Z

        // u, v оси перпендикулярные d
        const u = (d === 0) ? 2 : 0;   // for X: Z, for Y: X, for Z: X
        const v = (d === 0 || d === 2) ? 1 : 2;   // for X: Y, for Y: Z, for Z: Y

        const sizeArr = [sizeX, sizeY, sizeZ];
        const sizeD = sizeArr[d];
        const sizeU = sizeArr[u];
        const sizeV = sizeArr[v];
        const startArr = [cx0, cy0, cz0];

        // Проходим по слоям вдоль оси d
        for (let layer = 0; layer < sizeD; layer++) {
            // Строим mask: для каждой (uu, vv) определяем faceMatKey или ''
            // Грань видна если ячейка занята И сосед в направлении sign — пуст.
            const mask = new Array(sizeU * sizeV);
            for (let vv = 0; vv < sizeV; vv++) {
                for (let uu = 0; uu < sizeU; uu++) {
                    const coord = [0, 0, 0];
                    coord[d] = layer;
                    coord[u] = uu;
                    coord[v] = vv;
                    const ix = startArr[0] + coord[0];
                    const iy = startArr[1] + coord[1];
                    const iz = startArr[2] + coord[2];
                    const b = getByteUnsafe(ix, iy, iz);
                    if (b === 0) { mask[vv * sizeU + uu] = ''; continue; }
                    // Сосед в направлении sign по оси d
                    const nb = [ix, iy, iz];
                    nb[d] += sign;
                    if (getByteUnsafe(nb[0], nb[1], nb[2]) !== 0) {
                        mask[vv * sizeU + uu] = '';   // грань закрыта
                        continue;
                    }
                    const matKey = palette[b] || '';
                    mask[vv * sizeU + uu] = faceMatKey(matKey, face);
                }
            }

            // Greedy объединение прямоугольников
            for (let vv = 0; vv < sizeV; vv++) {
                for (let uu = 0; uu < sizeU; uu++) {
                    const key = mask[vv * sizeU + uu];
                    if (!key) continue;
                    // Находим ширину прямоугольника (по u)
                    let w = 1;
                    while (uu + w < sizeU && mask[vv * sizeU + uu + w] === key) w++;
                    // Находим высоту (по v)
                    let h = 1;
                    let done = false;
                    while (!done && vv + h < sizeV) {
                        for (let k = 0; k < w; k++) {
                            if (mask[(vv + h) * sizeU + uu + k] !== key) { done = true; break; }
                        }
                        if (!done) h++;
                    }
                    // Помечаем как обработанное
                    for (let hh = 0; hh < h; hh++) {
                        for (let ww = 0; ww < w; ww++) {
                            mask[(vv + hh) * sizeU + uu + ww] = '';
                        }
                    }

                    // Эмитим quad. Передаём мировые координаты углов
                    // прямо — без хитрых пересчётов d/u/v.
                    // Мировые координаты двух противоположных углов ячейки
                    // (uStart, vStart) и (uEnd, vEnd) в слое `layer`.
                    // origin + start + layer/uu/vv = координата UGA на ячейке.
                    const ox = grid.origin.x, oy = grid.origin.y, oz = grid.origin.z;
                    // Координата d-плоскости (где сидит грань):
                    //   - +face: layer + 1 (грань на максимальной стороне ячейки)
                    //   - -face: layer + 0 (грань на минимальной стороне)
                    const dC = sign > 0
                        ? (startArr[d] + layer + 1)
                        : (startArr[d] + layer);
                    const dWorld = (d === 0 ? ox : d === 1 ? oy : oz) + dC;
                    const uS = (u === 0 ? ox : u === 1 ? oy : oz) + startArr[u] + uu;
                    const uE = uS + w;
                    const vS = (v === 0 ? ox : v === 1 ? oy : oz) + startArr[v] + vv;
                    const vE = vS + h;
                    emitQuad(getGroup(key), face, dWorld, uS, uE, vS, vE, w, h);
                }
            }
        }
    }

    // Конвертируем accum-массивы в typed arrays + считаем bbox
    const out = new Map();
    for (const [key, g] of groups) {
        if (g.indices.length === 0) continue;
        let minX = Infinity, minY = Infinity, minZ = Infinity;
        let maxX = -Infinity, maxY = -Infinity, maxZ = -Infinity;
        for (let i = 0; i < g.positions.length; i += 3) {
            const x = g.positions[i], y = g.positions[i + 1], z = g.positions[i + 2];
            if (x < minX) minX = x; if (x > maxX) maxX = x;
            if (y < minY) minY = y; if (y > maxY) maxY = y;
            if (z < minZ) minZ = z; if (z > maxZ) maxZ = z;
        }
        out.set(key, {
            positions: new Float32Array(g.positions),
            normals: new Float32Array(g.normals),
            uvs: new Float32Array(g.uvs),
            indices: new Uint32Array(g.indices),
            bbox: { minX, minY, minZ, maxX, maxY, maxZ },
        });
    }
    return out;
}

/**
 * emitQuad — простая и явная версия с прямым заданием вершин
 * для каждой из 6 граней. Никакой математики, просто 6 case'ов.
 *
 * @param {Object} g — accumulator
 * @param {number} face — 0=+X, 1=-X, 2=+Y, 3=-Y, 4=+Z, 5=-Z
 * @param {number} dWorld — мировая координата плоскости грани вдоль оси d
 * @param {number} uS, uE — диапазон по u-оси (мировые)
 * @param {number} vS, vE — диапазон по v-оси (мировые)
 * @param {number} w, h — размеры в ячейках (для UV)
 *
 * ВАЖНО: Babylon по умолчанию use FRONTSIDE=CCW. Это значит когда
 * наблюдатель смотрит со стороны нормали грани — порядок 0→1→2 должен
 * идти ПРОТИВ часовой стрелки.
 *
 * Координатная система Babylon: правая, Y-up.
 * - Смотрим на +X грань с +X: видим плоскость YZ. Y вверх, Z вправо (или влево?)
 *
 * Я просто беру **проверенный паттерн из voxel-meshing статей**:
 *
 *   FACE +X: квад в плоскости X=const. Вершины (X, Y, Z):
 *     (X, yS, zS), (X, yE, zS), (X, yE, zE), (X, yS, zE)
 *     Винтинг 0→1→2 = up→up-right→right-up — в плоскости YZ это CW если
 *     смотреть с -X, значит CCW с +X ✓
 *
 *   FACE -X: те же 4 вершины, но обратный winding:
 *     (X, yS, zS), (X, yS, zE), (X, yE, zE), (X, yE, zS) — CCW с -X ✓
 */
function emitQuad(g, face, dWorld, uS, uE, vS, vE, w, h) {
    let p0, p1, p2, p3;
    let nx = 0, ny = 0, nz = 0;
    let uv0, uv1, uv2, uv3;

    // Координаты вершин и винтинг для каждой грани. CCW = front-side в Babylon.
    // Проверено эмпирически на тесте __terrainTest:
    //   backFaceCulling=false → правильно видны зелёные верхушки.
    // Значит +Y винтинг правильный. Проверка на live-сцене покажет остальные.
    switch (face) {
        case 0: // +X face. Plane X=const, axes Z (u) × Y (v).
            nx = 1;
            // CCW смотрим с +X: (vS,uS) → (vE,uS) → (vE,uE) → (vS,uE)
            p0 = [dWorld, vS, uS]; p1 = [dWorld, vE, uS];
            p2 = [dWorld, vE, uE]; p3 = [dWorld, vS, uE];
            uv0 = [0, 0]; uv1 = [0, h]; uv2 = [w, h]; uv3 = [w, 0];
            break;
        case 1: // -X face
            nx = -1;
            // CCW смотрим с -X = reverse of +X
            p0 = [dWorld, vS, uS]; p1 = [dWorld, vS, uE];
            p2 = [dWorld, vE, uE]; p3 = [dWorld, vE, uS];
            uv0 = [0, 0]; uv1 = [w, 0]; uv2 = [w, h]; uv3 = [0, h];
            break;
        case 2: // +Y face. Plane Y=const, axes X (u) × Z (v).
            ny = 1;
            // CCW смотрим с +Y (сверху)
            p0 = [uS, dWorld, vS]; p1 = [uS, dWorld, vE];
            p2 = [uE, dWorld, vE]; p3 = [uE, dWorld, vS];
            uv0 = [0, 0]; uv1 = [0, h]; uv2 = [w, h]; uv3 = [w, 0];
            break;
        case 3: // -Y face
            ny = -1;
            // CCW смотрим с -Y (снизу) = reverse of +Y
            p0 = [uS, dWorld, vS]; p1 = [uE, dWorld, vS];
            p2 = [uE, dWorld, vE]; p3 = [uS, dWorld, vE];
            uv0 = [0, 0]; uv1 = [w, 0]; uv2 = [w, h]; uv3 = [0, h];
            break;
        case 4: // +Z face. Plane Z=const, axes X (u) × Y (v).
            nz = 1;
            // CCW смотрим с +Z
            p0 = [uS, vS, dWorld]; p1 = [uE, vS, dWorld];
            p2 = [uE, vE, dWorld]; p3 = [uS, vE, dWorld];
            uv0 = [0, 0]; uv1 = [w, 0]; uv2 = [w, h]; uv3 = [0, h];
            break;
        case 5: // -Z face
        default:
            nz = -1;
            // CCW смотрим с -Z
            p0 = [uS, vS, dWorld]; p1 = [uS, vE, dWorld];
            p2 = [uE, vE, dWorld]; p3 = [uE, vS, dWorld];
            uv0 = [0, 0]; uv1 = [0, h]; uv2 = [w, h]; uv3 = [w, 0];
            break;
    }

    const vIdx = g.vIdx;
    g.positions.push(
        p0[0], p0[1], p0[2],
        p1[0], p1[1], p1[2],
        p2[0], p2[1], p2[2],
        p3[0], p3[1], p3[2],
    );
    for (let i = 0; i < 4; i++) g.normals.push(nx, ny, nz);
    g.uvs.push(uv0[0], uv0[1], uv1[0], uv1[1], uv2[0], uv2[1], uv3[0], uv3[1]);
    // 2 трианглова. ИНВЕРСИЯ WINDING: эмпирически в Babylon работает
    // 0-2-1 + 0-3-2 (CW по моему построению координат). Это значит
    // Babylon использует CW=front-side (видимо из-за того что мы работаем
    // в правой системе координат).
    // Раньше было 0-1-2 + 0-2-3 (CCW) — давало темные верхушки = back face.
    g.indices.push(vIdx, vIdx + 2, vIdx + 1, vIdx, vIdx + 3, vIdx + 2);
    g.vIdx += 4;
}
