/**
 * GreedyMesher — оптимизированный mesher с жадным объединением граней.
 *
 * Алгоритм (классический Mikola greedy):
 *   1. Для каждой из 6 ориентаций граней (-Z/+X/+Z/-X/-Y/+Y):
 *      Идём слоями вдоль перпендикулярной оси (0..CHUNK_SIZE).
 *      На каждом слое строим 2D-маску CHUNK_SIZE×CHUNK_SIZE где
 *      mask[u][v] = matIdx если грань видна, иначе 0.
 *   2. Жадный merge маски в прямоугольники:
 *      Для каждой непустой клетки находим max-ширину справа того же
 *      matIdx, затем max-высоту вниз где все W ячеек того же matIdx,
 *      генерируем один большой квад W×H, помечаем эти ячейки как обработанные.
 *
 * Что даёт:
 *   - Плоская земля 32×32 одного материала = 1 квад вместо 1024.
 *   - Реальные карты обычно × 15-20 меньше треугольников.
 *
 * Tiled UV: для большого квада W×H UV-координаты идут от 0 до W (по
 * одной оси) и 0 до H (по другой). Текстура повторяется по поверхности.
 * Требует чтобы материал был с TextureSampler.REPEAT и текстура
 * power-of-two (64×64 в Kenney — OK).
 *
 * MultiCube (grass с разными top/side/bottom):
 *   На этапе 2 поддерживается через разделение faceIdx → ключ
 *   материала "grass:top" / "grass:side" / "grass:bottom". Renderer
 *   создаёт отдельный mesh на каждую вариацию.
 *
 * Pure function. Не зависит от Babylon — работает с typed arrays.
 */

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

/**
 * Прозрачные материалы — те же что и в ChunkMesher для совместимости.
 * Через них видна геометрия за ними, и они НЕ мерджатся с непрозрачными.
 */
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". Renderer должен распознать суффикс и применить соответст-
 * вующую текстуру из палитры.
 *
 * Это критично: на одной voxel-ячейке grass верх должен быть зелёный
 * grass_top, а бока — dirt. Без разделения greedy сольёт всё в одну
 * текстуру и trava будет на боках столбца земли.
 */
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 и neighbor — те же правила что в ChunkMesher.
 */
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;
}

/**
 * Геометрические данные для каждой ориентации грани.
 * Каждая ось:
 *   axis    = 0/1/2 (X/Y/Z) — перпендикулярная к грани, идём по этой оси слоями
 *   u       = ось 1 в плоскости грани (для маски — это u)
 *   v       = ось 2 в плоскости грани (для маски — это v)
 *   dir     = +1 (грань смотрит вправо по axis) или -1 (влево)
 *   faceIdx = 0..5 (для materialKey и normals)
 *
 * Порядок: -Z, +Z, -X, +X, -Y, +Y.
 *
 * normal — единичный вектор нормали грани (для shading).
 *
 * При генерации квада размером (w, h) в координатах маски:
 *   - 4 угла квада в 3D: opposite axis = layerIdx + (dir>0 ? 1 : 0)
 *     u-coords = [u0, u0, u0+w, u0+w]
 *     v-coords = [v0, v0+h, v0+h, v0]
 *   - UV-координаты: (0,0) (0,h) (w,h) (w,0) — tiled
 */
const FACE_DEFS = [
    // 0: -Z (normal=-Z, axis=Z, dir=-1)
    { faceIdx: 0, axis: 2, u: 0, v: 1, dir: -1, normal: [0, 0, -1] },
    // 1: +X (normal=+X, axis=X, dir=+1)
    { faceIdx: 1, axis: 0, u: 2, v: 1, dir: +1, normal: [1, 0, 0] },
    // 2: +Z (normal=+Z, axis=Z, dir=+1)
    { faceIdx: 2, axis: 2, u: 0, v: 1, dir: +1, normal: [0, 0, 1] },
    // 3: -X (normal=-X, axis=X, dir=-1)
    { faceIdx: 3, axis: 0, u: 2, v: 1, dir: -1, normal: [-1, 0, 0] },
    // 4: -Y (normal=-Y, axis=Y, dir=-1)
    { faceIdx: 4, axis: 1, u: 0, v: 2, dir: -1, normal: [0, -1, 0] },
    // 5: +Y (normal=+Y, axis=Y, dir=+1)
    { faceIdx: 5, axis: 1, u: 0, v: 2, dir: +1, normal: [0, 1, 0] },
];

// Переиспользуемый mask-буфер. Один на весь модуль — экономит ~24000
// аллокаций Int16Array(1024) на полном rebuildAll проекта 1. Безопасно
// потому что greedy meshing синхронный, не параллельный (Web Worker —
// отдельный модульный экземпляр будет иметь свою копию).
const _maskBuffer = new Int16Array(CHUNK_SIZE * CHUNK_SIZE);

/**
 * Построить геометрию чанка greedy-методом.
 *
 * @param {VoxelChunk} chunk
 * @param {VoxelLayer} layer
 * @param {(gx:number,gy:number,gz:number)=>number} neighborMatIdx
 * @returns {{ byMaterial: Map<string, GeometryData>, totalFaces: number, timeMs: number }}
 */
export function buildChunkGeometryGreedy(chunk, layer, neighborMatIdx = null) {
    const t0 = (typeof performance !== 'undefined' ? performance.now() : Date.now());
    const voxelSize = layer.voxelSize;
    const ox = chunk.voxelOriginX();
    const oy = chunk.voxelOriginY();
    const oz = chunk.voxelOriginZ();
    const accumulators = new Map();   // matKey → arrays
    let totalFaces = 0;

    // Прекомпиляция transparency-маски по индексам палитры. Это в ~10 раз
    // быстрее чем дёргать TRANSPARENT_MATERIALS.has(matId) в hot loop.
    // transparentByIdx[paletteIdx] = true если матерал прозрачный
    const paletteLen = layer.palette.length;
    const transparentByIdx = new Uint8Array(paletteLen);
    for (let i = 1; i < paletteLen; i++) {
        if (TRANSPARENT_MATERIALS.has(layer.palette[i])) transparentByIdx[i] = 1;
    }

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

    /**
     * Чтение matIdx по локальным координатам, с уходом в соседние чанки.
     * Inlined hot path: bounds check + прямой data[] access без вызова
     * VoxelChunk.localIndex (которое тоже inline-able, но мы дублируем
     * расчёт здесь для JIT).
     */
    const chData = chunk.data;
    function getMatIdx(lx, ly, lz) {
        if (lx >= 0 && lx < CHUNK_SIZE &&
            ly >= 0 && ly < CHUNK_SIZE &&
            lz >= 0 && lz < CHUNK_SIZE) {
            return chData[ly * CHUNK_SIZE * CHUNK_SIZE + lz * CHUNK_SIZE + lx];
        }
        if (neighborMatIdx) {
            return neighborMatIdx(ox + lx, oy + ly, oz + lz);
        }
        return 0;
    }

    // Для каждой ориентации грани — отдельный pass.
    const mask = _maskBuffer;
    const CS = CHUNK_SIZE;
    for (let fIdx = 0; fIdx < 6; fIdx++) {
        const fd = FACE_DEFS[fIdx];
        const faceIdx = fd.faceIdx;
        const axis = fd.axis;
        const u = fd.u;
        const v = fd.v;
        const dir = fd.dir;
        const normal = fd.normal;

        // Дельты по axis для соседа
        const naxis = axis;   // perpendicular ось — где сосед
        const ndir = dir;

        // Для каждого слоя layerIdx ∈ [0..CS-1]
        for (let layerIdx = 0; layerIdx < CS; layerIdx++) {
            // 1. Заполняем маску. mask переиспользуется — обнуляем.
            mask.fill(0);
            let maskHasData = false;

            for (let uu = 0; uu < CS; uu++) {
                for (let vv = 0; vv < CS; vv++) {
                    // Координаты own voxel'а по таблице FACE_DEFS:
                    //   axis=0 (X-perp, u=2 Z, v=1 Y): uu→lz, vv→ly
                    //   axis=1 (Y-perp, u=0 X, v=2 Z): uu→lx, vv→lz
                    //   axis=2 (Z-perp, u=0 X, v=1 Y): uu→lx, vv→ly
                    let lx, ly, lz;
                    if (axis === 0) {
                        lx = layerIdx; lz = uu; ly = vv;
                    } else if (axis === 1) {
                        ly = layerIdx; lx = uu; lz = vv;
                    } else {
                        lz = layerIdx; lx = uu; ly = vv;
                    }

                    const ownIdx = getMatIdx(lx, ly, lz);
                    if (ownIdx === 0) continue;

                    // Координаты соседа
                    let nx = lx, ny = ly, nz = lz;
                    if (naxis === 0) nx += ndir;
                    else if (naxis === 1) ny += ndir;
                    else nz += ndir;
                    const nbIdx = getMatIdx(nx, ny, nz);

                    // Быстрая проверка видимости грани через паленту
                    // transparency-флагов. Логика та же что isFaceVisible.
                    const ownTrans = ownIdx < paletteLen ? transparentByIdx[ownIdx] : 0;
                    let visible;
                    if (nbIdx === 0) {
                        visible = true;     // соседа нет
                    } else {
                        const nbTrans = nbIdx < paletteLen ? transparentByIdx[nbIdx] : 0;
                        if (!ownTrans && !nbTrans) visible = false;       // оба непрозрачные → не видна
                        else if (!ownTrans && nbTrans) visible = true;    // мы непрозрачные, сосед — нет
                        else if (ownTrans && !nbTrans) visible = false;   // мы прозрачные, сосед — нет
                        else visible = (ownIdx !== nbIdx);                // оба прозрачные, разные?
                    }
                    if (!visible) continue;

                    // Получаем matKey один раз на ownIdx + faceIdx — кэшируем
                    // через локальную таблицу keysByIdxFace[faceIdx*paletteLen+ownIdx].
                    // Но для простоты пока — прямой вызов.
                    const ownMat = layer.palette[ownIdx];
                    if (!ownMat) continue;
                    const mk = materialKey(ownMat, faceIdx);
                    mask[uu * CS + vv] = matKeyToIndex(mk);
                    maskHasData = true;
                }
            }

            // Пропускаем слой если в нём нет граней (типично — пустые слои в середине неба)
            if (!maskHasData) continue;

            // 2. Жадный merge маски.
            for (let uu0 = 0; uu0 < CS; uu0++) {
                const rowOffset = uu0 * CS;
                for (let vv0 = 0; vv0 < CS; vv0++) {
                    const mkIdx = mask[rowOffset + vv0];
                    if (mkIdx === 0) continue;

                    // Ширина (по vv)
                    let w = 1;
                    while (vv0 + w < CS && mask[rowOffset + vv0 + w] === mkIdx) w++;

                    // Высота (по uu) — пока все W ячеек на следующих uu того же matKey
                    let h = 1;
                    let canExpand = true;
                    while (canExpand && uu0 + h < CS) {
                        const nextRow = (uu0 + h) * CS;
                        for (let k = 0; k < w; k++) {
                            if (mask[nextRow + vv0 + k] !== mkIdx) {
                                canExpand = false;
                                break;
                            }
                        }
                        if (canExpand) h++;
                    }

                    // Помечаем обработанные клетки как пустые
                    for (let i = 0; i < h; i++) {
                        const rOff = (uu0 + i) * CS;
                        for (let j = 0; j < w; j++) {
                            mask[rOff + vv0 + j] = 0;
                        }
                    }

                    // Генерируем квад W×H
                    const matKey = indexToMatKey(mkIdx);
                    emitQuad(getAccum(matKey), {
                        axis, u, v, dir,
                        layerIdx, uu0, vv0, w, h,
                        normal,
                        voxelSize, ox, oy, oz,
                    });
                    totalFaces++;
                }
            }
        }
    }

    // Convert to typed arrays
    const result = new Map();
    for (const [matKey, accum] of accumulators) {
        result.set(matKey, {
            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 };
}

/**
 * Генерация одного квада W×H в указанной ориентации.
 * Записывает 4 позиции + 4 нормали + 4 UV + 6 индексов в accum.
 */
function emitQuad(accum, params) {
    const { axis, u, v, dir, layerIdx, uu0, vv0, w, h,
            normal, voxelSize, ox, oy, oz } = params;

    // Базовая точка квада в local-coords (voxel-grid units)
    // layer-axis = layerIdx + (1 если dir>0 else 0) — для +граней нужно
    // сдвинуться на 1, иначе квад внутри voxel'а.
    const lAxis = layerIdx + (dir > 0 ? 1 : 0);

    // 4 угла в (u, v) координатах: (uu0, vv0), (uu0+h, vv0),
    // (uu0+h, vv0+w), (uu0, vv0+w).
    // Winding-order должен быть counter-clockwise при взгляде ИЗВНЕ voxel'а:
    //   dir>0: face смотрит в +axis, наблюдатель из +axis смотрит на -axis
    //   dir<0: face смотрит в -axis, наблюдатель из -axis смотрит на +axis
    // В первом случае стандартный порядок (u, v) → CCW.
    // Во втором — нужно обратить порядок u/v чтобы face не оказался изнанкой.

    const corners = dir > 0
        ? [[uu0, vv0], [uu0 + h, vv0], [uu0 + h, vv0 + w], [uu0, vv0 + w]]
        : [[uu0 + h, vv0], [uu0, vv0], [uu0, vv0 + w], [uu0 + h, vv0 + w]];

    // UV-tiling: размер квада в воксельных ячейках = (h, w).
    // UV (0,0) → (0,w), (h,w), (h,0) — но мы хотим tiled повторение
    // текстуры по граням, поэтому UV.x = u-coord (0..h), UV.y = v-coord (0..w).
    const uvs = dir > 0
        ? [[0, 0], [h, 0], [h, w], [0, w]]
        : [[h, 0], [0, 0], [0, w], [h, w]];

    const baseV = accum.positions.length / 3;
    for (let i = 0; i < 4; i++) {
        const [cu, cv] = corners[i];
        // Конвертация (u, v) в (lx, ly, lz) в зависимости от axis
        let lx, ly, lz;
        if (axis === 0) { // X-perp face
            lx = lAxis; ly = (u === 1 ? cu : (v === 1 ? cv : 0)); lz = (u === 2 ? cu : (v === 2 ? cv : 0));
        } else if (axis === 1) { // Y-perp face
            ly = lAxis; lx = (u === 0 ? cu : (v === 0 ? cv : 0)); lz = (u === 2 ? cu : (v === 2 ? cv : 0));
        } else { // axis === 2, Z-perp face
            lz = lAxis; lx = (u === 0 ? cu : (v === 0 ? cv : 0)); ly = (u === 1 ? cu : (v === 1 ? cv : 0));
        }
        // World-coords (в метрах)
        accum.positions.push(
            (ox + lx) * voxelSize,
            (oy + ly) * voxelSize,
            (oz + lz) * voxelSize,
        );
        accum.normals.push(normal[0], normal[1], normal[2]);
        accum.uvs.push(uvs[i][0], uvs[i][1]);
    }
    // 2 треугольника: (0,1,2) и (0,2,3)
    accum.indices.push(baseV, baseV + 1, baseV + 2);
    accum.indices.push(baseV, baseV + 2, baseV + 3);
    accum.faceCount++;
}

// ============================================================================
// Глобальная палитра matKey → index (для упаковки в Int16 маску).
// Это per-mesher state — не делимся между чанками для thread-safety
// в будущем (Web Worker).
// ============================================================================

const _matKeyToIdx = new Map();
const _matKeyByIdx = [null]; // index 0 зарезервирован под "пусто"

function matKeyToIndex(matKey) {
    let idx = _matKeyToIdx.get(matKey);
    if (idx !== undefined) return idx;
    if (_matKeyByIdx.length >= 32767) {
        console.warn('[GreedyMesher] matKey palette overflow');
        return 1;
    }
    idx = _matKeyByIdx.length;
    _matKeyByIdx.push(matKey);
    _matKeyToIdx.set(matKey, idx);
    return idx;
}

function indexToMatKey(idx) {
    return _matKeyByIdx[idx];
}
