/**
 * SurfaceNetsMesher — построить гладкий mesh из DensityGrid через
 * **Naive Surface Nets** (Mikola, 2012).
 *
 * Главное отличие от vertex-per-cell:
 * Вершина ставится не в центре cell, а в **средней точке всех edges
 * с sign-change** через эту cell. Position вершины **интерполируется**
 * по density значениям (lerp). Это даёт **по-настоящему гладкие**
 * поверхности без ступенек (как в Roblox).
 *
 * Алгоритм:
 *   1. Для каждой "ячейки" (которая теперь = куб 1×1×1 с 8 corner-values из density):
 *      смотрим 12 рёбер куба. Если ребро пересекает threshold
 *      (один corner solid, другой пуст) — находим точку пересечения
 *      по Lerp.
 *   2. Position вершины = среднее всех intersection-точек.
 *   3. Material = доминирующий solid corner.
 *   4. Для каждого ребра воксельной решётки (между двумя cells) с sign-change
 *      — создаём quad из 4 vertices соседних ячеек.
 *
 * Babylon: эмпирически работает CW (inverted), индексы пишем в reverse order.
 *
 * UV: triplanar simulation — берём worldPos / cellSize по доминирующей оси.
 */

import { DENSITY_THRESHOLD, CELL_SIZE } from './DensityGrid';

// 12 рёбер куба: каждое — [cornerIdx0, cornerIdx1, edgeAxis (0=X,1=Y,2=Z)]
// Corners индексируются: ((dx) | (dy<<1) | (dz<<2)) для dx,dy,dz ∈ {0,1}.
//   0 = (0,0,0), 1=(1,0,0), 2=(0,1,0), 3=(1,1,0)
//   4 = (0,0,1), 5=(1,0,1), 6=(0,1,1), 7=(1,1,1)
const EDGES = [
    // Edges along X (axis 0)
    [0, 1, 0], [2, 3, 0], [4, 5, 0], [6, 7, 0],
    // Edges along Y (axis 1)
    [0, 2, 1], [1, 3, 1], [4, 6, 1], [5, 7, 1],
    // Edges along Z (axis 2)
    [0, 4, 2], [1, 5, 2], [2, 6, 2], [3, 7, 2],
];

// Координаты corner'ов внутри куба (0..1)
const CORNER_OFFSETS = [
    [0, 0, 0], [1, 0, 0], [0, 1, 0], [1, 1, 0],
    [0, 0, 1], [1, 0, 1], [0, 1, 1], [1, 1, 1],
];

/**
 * @param {DensityGrid} grid
 * @param {number} cx0
 * @param {number} cy0
 * @param {number} cz0
 * @param {number} sizeX
 * @param {number} sizeY
 * @param {number} sizeZ
 * @returns {Map<matKey, {positions, normals, uvs, indices, bbox}>}
 */
export function buildSurfaceNetsMesh(grid, cx0, cy0, cz0, sizeX, sizeY, sizeZ) {
    const palette = grid.palette;
    const groups = new Map();
    const getGroup = (key) => {
        let g = groups.get(key);
        if (!g) {
            g = { positions: [], normals: [], uvs: [], uvs2: [], uvs3: [],
                  indices: [], vIdx: 0, colors: [] };
            groups.set(key, g);
        }
        return g;
    };

    const T = DENSITY_THRESHOLD;
    // ВАЖНО: cell-куб — между corners (i, j, k) и (i+1, j+1, k+1). У него
    // 8 corner'ей со значениями density. Cells в чанке: lx ∈ [0..sizeX], то есть
    // одна больше чем sizeX чтобы захватить край.
    const lsX = sizeX + 1, lsY = sizeY + 1, lsZ = sizeZ + 1;
    const lsxy = lsX * lsY;

    // Для каждой граничной ячейки храним:
    //   vertexX/Y/Z — мировая позиция (interpolated avg)
    //   vertexMatId — доминирующий material
    //   vertexGlobalIdx — индекс в группе после регистрации
    //   cellMask — маска 8 corners (для определения видимости quad по граням)
    const vertexX = new Float32Array(lsX * lsY * lsZ);
    const vertexY = new Float32Array(lsX * lsY * lsZ);
    const vertexZ = new Float32Array(lsX * lsY * lsZ);
    const vertexMatId = new Uint8Array(lsX * lsY * lsZ);
    // AO per vertex (0..1). 1.0 = открыто (полностью освещено), 0.0 = закрыто
    // (глубокая впадина). Считаем как доля воздушных cells в кубе 3×3×3
    // вокруг вершины, в OFFSET 1 от центра. Чем больше solid вокруг → меньше AO.
    const vertexAO = new Float32Array(lsX * lsY * lsZ);
    const cellMask = new Uint8Array(lsX * lsY * lsZ);
    const vertexLocalIdx = new Int32Array(lsX * lsY * lsZ).fill(-1);

    let minX = Infinity, minY = Infinity, minZ = Infinity;
    let maxX = -Infinity, maxY = -Infinity, maxZ = -Infinity;

    const localIdx = (lx, ly, lz) => lz * lsxy + ly * lsX + lx;

    // === Шаг 1: для каждой ячейки чанка считаем 8 corner-density,
    // находим intersection-точки на рёбрах, position vertex = их average.
    for (let lz = 0; lz < lsZ; lz++) {
        for (let ly = 0; ly < lsY; ly++) {
            for (let lx = 0; lx < lsX; lx++) {
                const gx = cx0 + lx;
                const gy = cy0 + ly;
                const gz = cz0 + lz;
                // 8 corners — density в "до-cell" точках (то есть corner
                // (0,0,0) = density ячейки (gx-1, gy-1, gz-1))
                const corners = [
                    grid.getDensity(gx - 1, gy - 1, gz - 1),
                    grid.getDensity(gx,     gy - 1, gz - 1),
                    grid.getDensity(gx - 1, gy,     gz - 1),
                    grid.getDensity(gx,     gy,     gz - 1),
                    grid.getDensity(gx - 1, gy - 1, gz),
                    grid.getDensity(gx,     gy - 1, gz),
                    grid.getDensity(gx - 1, gy,     gz),
                    grid.getDensity(gx,     gy,     gz),
                ];
                // Mask: bit k = corner k solid
                let mask = 0;
                for (let k = 0; k < 8; k++) if (corners[k] >= T) mask |= (1 << k);
                if (mask === 0 || mask === 255) continue;

                // Перебираем 12 рёбер. Для каждого с sign-change → находим
                // intersection point (lerp по density).
                // Edge endpoints в local-coord (внутри 1×1×1 куба): 0..1.
                // Convert в world: cellOriginWorld + edgePoint * CELL_SIZE.
                let sumX = 0, sumY = 0, sumZ = 0, count = 0;
                for (let e = 0; e < 12; e++) {
                    const c0 = EDGES[e][0], c1 = EDGES[e][1];
                    const d0 = corners[c0], d1 = corners[c1];
                    const solid0 = d0 >= T, solid1 = d1 >= T;
                    if (solid0 === solid1) continue;
                    // Lerp t = (T - d0) / (d1 - d0)
                    let t = (T - d0) / (d1 - d0);
                    if (!isFinite(t)) t = 0.5;
                    t = Math.max(0.0, Math.min(1.0, t));
                    const o0 = CORNER_OFFSETS[c0];
                    const o1 = CORNER_OFFSETS[c1];
                    sumX += o0[0] + (o1[0] - o0[0]) * t;
                    sumY += o0[1] + (o1[1] - o0[1]) * t;
                    sumZ += o0[2] + (o1[2] - o0[2]) * t;
                    count++;
                }
                if (count === 0) continue;
                // Average локальная позиция (0..1 within cube)
                const lpX = sumX / count;
                const lpY = sumY / count;
                const lpZ = sumZ / count;
                // Мировая координата: cell-cube начало = (gx-1, gy-1, gz-1) cell-units
                // Worldorigin: grid.origin × CELL_SIZE.
                const wx = (grid.origin.x + cx0 + lx - 1 + lpX) * CELL_SIZE;
                const wy = (grid.origin.y + cy0 + ly - 1 + lpY) * CELL_SIZE;
                const wz = (grid.origin.z + cz0 + lz - 1 + lpZ) * CELL_SIZE;

                const lIdx = localIdx(lx, ly, lz);
                vertexX[lIdx] = wx;
                vertexY[lIdx] = wy;
                vertexZ[lIdx] = wz;
                cellMask[lIdx] = mask;

                // Material: доминирующий из solid corner с наибольшим density.
                let bestMat = 0, bestDen = 0;
                const cornerMats = [
                    grid.getMatId(gx - 1, gy - 1, gz - 1),
                    grid.getMatId(gx,     gy - 1, gz - 1),
                    grid.getMatId(gx - 1, gy,     gz - 1),
                    grid.getMatId(gx,     gy,     gz - 1),
                    grid.getMatId(gx - 1, gy - 1, gz),
                    grid.getMatId(gx,     gy - 1, gz),
                    grid.getMatId(gx - 1, gy,     gz),
                    grid.getMatId(gx,     gy,     gz),
                ];
                for (let k = 0; k < 8; k++) {
                    if (corners[k] >= T && corners[k] > bestDen) {
                        bestDen = corners[k];
                        bestMat = cornerMats[k];
                    }
                }
                vertexMatId[lIdx] = bestMat;
                vertexLocalIdx[lIdx] = 1;   // pre-mark

                // === Vertex AO ===
                // Считаем долю solid-cells в окружении ВЫШЕ вершины (gy+0..gy+3).
                // ВЫШЕ — это важно: AO имитирует свет сверху. Вершина в открытой
                // зоне (над ней воздух) = яркая. В пещере/углублении (над ней
                // solid-материал) = тёмная.
                let solidAbove = 0;
                let totalAbove = 0;
                for (let dz = -1; dz <= 1; dz++) {
                    for (let dy = 0; dy <= 3; dy++) {
                        for (let dx = -1; dx <= 1; dx++) {
                            const d = grid.getDensity(gx + dx, gy + dy, gz + dz);
                            if (d >= T) solidAbove++;
                            totalAbove++;
                        }
                    }
                }
                const aboveRatio = solidAbove / totalAbove;
                // Плоская поверхность: над ней воздух → aboveRatio ≈ 0 → AO ≈ 1.0
                // Глубокая впадина: над ней может быть solid → aboveRatio высоко → AO низко
                // На обычной открытой плоскости должно быть AO=1.
                let ao = 1.0 - aboveRatio * 0.6;
                if (ao > 1.0) ao = 1.0;
                if (ao < 0.55) ao = 0.55;
                vertexAO[lIdx] = ao;

                if (wx < minX) minX = wx; if (wx > maxX) maxX = wx;
                if (wy < minY) minY = wy; if (wy > maxY) maxY = wy;
                if (wz < minZ) minZ = wz; if (wz > maxZ) maxZ = wz;
            }
        }
    }

    // === Шаг 2: register vertices в ЕДИНУЮ группу '_blend'.
    //
    // ВАЖНО: чтобы избежать ДЫР на границах материалов (vertex принадлежит
    // только одной группе → quad другой группы не имеет shared vertex),
    // делаем ОДИН mesh для всей карты. Material каждого vertex кодируется
    // в vertex color RGBA: R=grass, G=rock, B=sand, A=snow (или 1.0 если
    // нет других — пока binary).
    //
    // На стыке материалов соседние vertex имеют разный RGB → shader будет
    // делать smooth blend (когда внедрим custom material). Сейчас же
    // используем diffuseColor.fromVertexColor=true со standard material —
    // получим грубый mix цветов (без текстур разных материалов).
    const SINGLE_GROUP_KEY = '_blend';
    for (let lz = 0; lz < lsZ; lz++) {
        for (let ly = 0; ly < lsY; ly++) {
            for (let lx = 0; lx < lsX; lx++) {
                const li = localIdx(lx, ly, lz);
                if (vertexLocalIdx[li] < 0) continue;
                const g = getGroup(SINGLE_GROUP_KEY);
                g.positions.push(vertexX[li], vertexY[li], vertexZ[li]);
                g.normals.push(0, 1, 0);
                g.uvs.push(vertexX[li] / CELL_SIZE, vertexZ[li] / CELL_SIZE);
                // Vertex 8-material encoding:
                //   color (vec4) = grass / rock / sand / snow
                //   uv2 (vec2)   = dirt / water
                //   uv3 (vec2)   = wood / glacier
                // Доминирующий материал → его канал = 1, остальные = 0.
                const matKey = palette[vertexMatId[li]] || 'grass';
                let r = 0, gC = 0, b = 0, a = 0;
                let u2x = 0, u2y = 0, u3x = 0, u3y = 0;
                switch (matKey) {
                    case 'rock':    gC = 1; break;
                    case 'sand':    b = 1; break;
                    case 'snow':    a = 1; break;
                    case 'dirt':    u2x = 1; break;
                    case 'water':   u2y = 1; break;
                    case 'wood':    u3x = 1; break;
                    case 'glacier': u3y = 1; break;
                    // Маппинг "близких" материалов:
                    case 'asphalt':
                    case 'concrete':
                    case 'rock_moss':
                    case 'salt':
                        gC = 1; break;
                    case 'mud':
                        u2x = 1; break;
                    case 'trunk':
                    case 'trunk_white':
                        u3x = 1; break;
                    case 'leaves':
                    case 'leaves_orange':
                    case 'tall_grass':
                    case 'flower_red':
                    case 'flower_blue':
                    case 'flower_yellow':
                    case 'mushroom_red':
                    default:        r = 1; break;
                }
                g.colors.push(r, gC, b, a);
                g.uvs2.push(u2x, u2y);
                g.uvs3.push(u3x, u3y);
                const newIdx = g.vIdx++;
                vertexLocalIdx[li] = newIdx;
            }
        }
    }

    // Helper: получить вершину
    const lookupVtx = (lx, ly, lz) => {
        if (lx < 0 || ly < 0 || lz < 0 || lx >= lsX || ly >= lsY || lz >= lsZ) return null;
        const li = localIdx(lx, ly, lz);
        if (vertexLocalIdx[li] < 0) return null;
        return li;
    };

    // === Шаг 3: edges. Для каждой пары соседних cells (по оси),
    // если на их общей грани есть sign-change — создаём quad.
    // Sign-change по оси X между (gx-1) и (gx) на line (gy-1, gz-1) → cells:
    //   (lx, ly-1, lz-1), (lx, ly, lz-1), (lx, ly-1, lz), (lx, ly, lz)
    // Это аналог из vertex-per-cell версии.
    for (let lz = 0; lz < lsZ; lz++) {
        for (let ly = 0; ly < lsY; ly++) {
            for (let lx = 0; lx < lsX; lx++) {
                const gx = cx0 + lx;
                const gy = cy0 + ly;
                const gz = cz0 + lz;
                const a = grid.getDensity(gx - 1, gy - 1, gz - 1);

                // X-edge: corner (gx-1,gy-1,gz-1) vs (gx,gy-1,gz-1)
                const bX = grid.getDensity(gx, gy - 1, gz - 1);
                if ((a >= T) !== (bX >= T)) {
                    const v0 = lookupVtx(lx, ly - 1, lz - 1);
                    const v1 = lookupVtx(lx, ly,     lz - 1);
                    const v2 = lookupVtx(lx, ly - 1, lz);
                    const v3 = lookupVtx(lx, ly,     lz);
                    if (v0 !== null && v1 !== null && v2 !== null && v3 !== null) {
                        emitQuad(getGroup(SINGLE_GROUP_KEY),
                            vertexLocalIdx[v0], vertexLocalIdx[v1],
                            vertexLocalIdx[v2], vertexLocalIdx[v3], a >= T);
                    }
                }
                // Y-edge: corner (gx-1,gy-1,gz-1) vs (gx-1,gy,gz-1)
                const bY = grid.getDensity(gx - 1, gy, gz - 1);
                if ((a >= T) !== (bY >= T)) {
                    const v0 = lookupVtx(lx - 1, ly, lz - 1);
                    const v1 = lookupVtx(lx,     ly, lz - 1);
                    const v2 = lookupVtx(lx - 1, ly, lz);
                    const v3 = lookupVtx(lx,     ly, lz);
                    if (v0 !== null && v1 !== null && v2 !== null && v3 !== null) {
                        emitQuad(getGroup(SINGLE_GROUP_KEY),
                            vertexLocalIdx[v0], vertexLocalIdx[v1],
                            vertexLocalIdx[v2], vertexLocalIdx[v3], a >= T);
                    }
                }
                // Z-edge: corner (gx-1,gy-1,gz-1) vs (gx-1,gy-1,gz)
                const bZ = grid.getDensity(gx - 1, gy - 1, gz);
                if ((a >= T) !== (bZ >= T)) {
                    const v0 = lookupVtx(lx - 1, ly - 1, lz);
                    const v1 = lookupVtx(lx,     ly - 1, lz);
                    const v2 = lookupVtx(lx - 1, ly,     lz);
                    const v3 = lookupVtx(lx,     ly,     lz);
                    if (v0 !== null && v1 !== null && v2 !== null && v3 !== null) {
                        emitQuad(getGroup(SINGLE_GROUP_KEY),
                            vertexLocalIdx[v0], vertexLocalIdx[v1],
                            vertexLocalIdx[v2], vertexLocalIdx[v3], a >= T);
                    }
                }
            }
        }
    }

    // === Шаг 4: вычислить smooth normals через DENSITY GRADIENT.
    //
    // Для каждой surface-vertex нормаль = -∇density (направление от
    // высокой плотности к низкой). Считаем градиент через центральные
    // конечные разности по 3 осям.
    //
    // Это даёт ФИЗИЧЕСКИ ПРАВИЛЬНЫЕ плавные нормали — намного лучше
    // face-normal averaging (который дёргался на гладких поверхностях).
    //
    // Обрабатываем все группы одновременно: проходим по cells второй раз
    // и записываем normals в правильное место в каждой группе.
    for (let lz = 0; lz < lsZ; lz++) {
        for (let ly = 0; ly < lsY; ly++) {
            for (let lx = 0; lx < lsX; lx++) {
                const li = localIdx(lx, ly, lz);
                const vIdx = vertexLocalIdx[li];
                if (vIdx < 0) continue;
                // gx,gy,gz — координата текущей cell в global grid
                // Surface vertex placed in pos = (lx-1+lpX..., ly-1+lpY..., lz-1+lpZ...)
                // Sample density в окрестности: берем gradient = density(+1) - density(-1).
                const gx = cx0 + lx - 1;   // -1 потому что vertex в "до-cell"
                const gy = cy0 + ly - 1;
                const gz = cz0 + lz - 1;
                // Сэмплируем 8 corner-density вокруг точки и считаем градиент.
                // Используем те же 8 углов (как в Шаге 1) для consistent.
                const cx_p = grid.getDensity(gx + 1, gy, gz) + grid.getDensity(gx + 1, gy + 1, gz)
                           + grid.getDensity(gx + 1, gy, gz + 1) + grid.getDensity(gx + 1, gy + 1, gz + 1);
                const cx_n = grid.getDensity(gx, gy, gz) + grid.getDensity(gx, gy + 1, gz)
                           + grid.getDensity(gx, gy, gz + 1) + grid.getDensity(gx, gy + 1, gz + 1);
                const cy_p = grid.getDensity(gx, gy + 1, gz) + grid.getDensity(gx + 1, gy + 1, gz)
                           + grid.getDensity(gx, gy + 1, gz + 1) + grid.getDensity(gx + 1, gy + 1, gz + 1);
                const cy_n = grid.getDensity(gx, gy, gz) + grid.getDensity(gx + 1, gy, gz)
                           + grid.getDensity(gx, gy, gz + 1) + grid.getDensity(gx + 1, gy, gz + 1);
                const cz_p = grid.getDensity(gx, gy, gz + 1) + grid.getDensity(gx + 1, gy, gz + 1)
                           + grid.getDensity(gx, gy + 1, gz + 1) + grid.getDensity(gx + 1, gy + 1, gz + 1);
                const cz_n = grid.getDensity(gx, gy, gz) + grid.getDensity(gx + 1, gy, gz)
                           + grid.getDensity(gx, gy + 1, gz) + grid.getDensity(gx + 1, gy + 1, gz);
                // Gradient = (D_positive - D_negative). Нормаль = -gradient (поверхность смотрит от высокой к низкой плотности... wait, наоборот — нормаль смотрит ОТ solid К empty, то есть В сторону низкой density). gradient указывает на повышение, поэтому normal = -gradient.
                // НО: в нашем тесте density УБЫВАЕТ с высотой y → gradient_y < 0 на поверхности → normal_y = -grad_y > 0 (правильно, нормаль смотрит вверх).
                let nx = -(cx_p - cx_n);
                let ny = -(cy_p - cy_n);
                let nz = -(cz_p - cz_n);
                const len = Math.sqrt(nx * nx + ny * ny + nz * nz);
                if (len > 1e-6) {
                    nx /= len; ny /= len; nz /= len;
                } else {
                    nx = 0; ny = 1; nz = 0;
                }
                // Записываем в группу: нужна group по matKey
                const matKey = palette[vertexMatId[li]] || 'grass';
                const g = groups.get(matKey);
                if (g) {
                    const off = vIdx * 3;
                    g.normals[off] = nx;
                    g.normals[off + 1] = ny;
                    g.normals[off + 2] = nz;
                }
            }
        }
    }

    // === Шаг 5: упаковка
    const out = new Map();
    for (const [key, g] of groups) {
        if (g.indices.length === 0) continue;
        out.set(key, {
            positions: new Float32Array(g.positions),
            normals: g.normals instanceof Float32Array ? g.normals : new Float32Array(g.normals),
            uvs: new Float32Array(g.uvs),
            uvs2: g.uvs2 && g.uvs2.length > 0 ? new Float32Array(g.uvs2) : null,
            uvs3: g.uvs3 && g.uvs3.length > 0 ? new Float32Array(g.uvs3) : null,
            colors: g.colors && g.colors.length > 0 ? new Float32Array(g.colors) : null,
            indices: new Uint32Array(g.indices),
            bbox: { minX, minY, minZ, maxX, maxY, maxZ },
        });
    }
    return out;
}

/**
 * Emit quad из 4 vIdx. Cell vertices layout:
 *   v0 — корень (-,-)
 *   v1 — (+,-)
 *   v2 — (-,+)
 *   v3 — (+,+)
 * flip: меняет winding для правильной нормали.
 */
function emitQuad(g, v0, v1, v2, v3, flip) {
    // Эмпирически в нашем Babylon: CW = front. Поэтому используем CW порядок.
    if (flip) {
        g.indices.push(v0, v3, v1, v0, v2, v3);
    } else {
        g.indices.push(v0, v1, v3, v0, v3, v2);
    }
}
