/**
 * ChunkSerializer — компактная сериализация чанков через RLE + base64.
 *
 * Чанк хранится как 32768 байт (по 1 байту на ячейку, индекс материала
 * в палитре слоя). Большинство ячеек обычно пустые → высокая степень
 * сжатия через Run-Length Encoding.
 *
 * Binary формат одного чанка:
 *   uint16 numRuns
 *   foreach run:
 *     uint16 startOffset   — где в data[] начинается run
 *     uint16 length        — сколько подряд ячеек
 *     uint8  matIdx        — материал run'а
 *
 * Сохраняем ТОЛЬКО non-empty runs. Пустые промежутки между ними
 * подразумеваются (расшифровщик заполняет нулями всё что не покрыто).
 *
 * Пример (типовая земля с холмами): 32 768 байт → ~200-1000 байт →
 * base64 → ~300-1400 символов. На карте 80×80м (126 чанков) суммарно
 * ~50-200 КБ vs 5.6 МБ JSON. Уменьшение в 30-100 раз.
 *
 * Не используем zlib/gzip — браузер всё равно gzip'ит HTTP-ответы,
 * и второй слой compression не даст много (RLE уже устраняет повторения).
 */

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

// ============================================================================
// Encode: VoxelChunk → base64 RLE
// ============================================================================

/**
 * Закодировать data[] чанка в RLE байты. Возвращает Uint8Array.
 * @param {Uint8Array} data — длина CHUNK_VOLUME (32768)
 * @returns {Uint8Array}
 */
export function encodeChunkRLE(data) {
    if (data.length !== CHUNK_VOLUME) {
        throw new Error(`encodeChunkRLE: expected ${CHUNK_VOLUME} bytes, got ${data.length}`);
    }
    // Сначала собираем runs в массив
    const runs = [];   // [{start, length, matIdx}, ...]
    let i = 0;
    while (i < CHUNK_VOLUME) {
        const mat = data[i];
        if (mat === 0) { i++; continue; }   // пустую ячейку не записываем
        // Сколько подряд ячеек того же материала?
        let j = i + 1;
        // RLE длина run'а — max uint16 = 65535, но 32768 < 65535, поэтому
        // одно ограничение — длина CHUNK_VOLUME.
        while (j < CHUNK_VOLUME && data[j] === mat) j++;
        const length = j - i;
        runs.push({ start: i, length, matIdx: mat });
        i = j;
    }

    // Аллоцируем буфер: 2 байта header + 5 байт на каждый run
    const bufSize = 2 + runs.length * 5;
    const buf = new Uint8Array(bufSize);
    const view = new DataView(buf.buffer);
    view.setUint16(0, runs.length, true);   // little-endian
    let offset = 2;
    for (const r of runs) {
        view.setUint16(offset, r.start, true);
        view.setUint16(offset + 2, r.length, true);
        view.setUint8(offset + 4, r.matIdx);
        offset += 5;
    }
    return buf;
}

/**
 * Декодировать RLE байты обратно в data[] чанка.
 * @param {Uint8Array} bytes
 * @returns {Uint8Array} — длина CHUNK_VOLUME (32768)
 */
export function decodeChunkRLE(bytes) {
    const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
    const numRuns = view.getUint16(0, true);
    const data = new Uint8Array(CHUNK_VOLUME);
    let offset = 2;
    for (let i = 0; i < numRuns; i++) {
        const start = view.getUint16(offset, true);
        const length = view.getUint16(offset + 2, true);
        const matIdx = view.getUint8(offset + 4);
        for (let j = 0; j < length; j++) {
            data[start + j] = matIdx;
        }
        offset += 5;
    }
    return data;
}

// ============================================================================
// Base64 helpers — встроенный browser btoa/atob с binary-safe wrapper.
// ============================================================================

/** Uint8Array → base64 строка. */
export function bytesToBase64(bytes) {
    // Chunked encoding чтобы не упереться в stack-limit при больших массивах
    let binary = '';
    const len = bytes.length;
    const chunkSize = 8192;
    for (let i = 0; i < len; i += chunkSize) {
        const chunk = bytes.subarray(i, Math.min(i + chunkSize, len));
        binary += String.fromCharCode.apply(null, chunk);
    }
    return btoa(binary);
}

/** base64 строка → Uint8Array. */
export function base64ToBytes(b64) {
    const binary = atob(b64);
    const len = binary.length;
    const bytes = new Uint8Array(len);
    for (let i = 0; i < len; i++) bytes[i] = binary.charCodeAt(i);
    return bytes;
}

// ============================================================================
// High-level: VoxelChunk ↔ base64
// ============================================================================

/** VoxelChunk → base64 строка. */
export function chunkToBase64(chunk) {
    if (!(chunk instanceof VoxelChunk)) {
        throw new Error('chunkToBase64: expected VoxelChunk');
    }
    const rle = encodeChunkRLE(chunk.data);
    return bytesToBase64(rle);
}

/**
 * base64 строка → новый VoxelChunk с указанными (cx,cy,cz).
 * data[] заполняется десериализованным RLE.
 */
export function chunkFromBase64(b64, cx, cy, cz) {
    const rle = base64ToBytes(b64);
    const data = decodeChunkRLE(rle);
    const chunk = new VoxelChunk(cx, cy, cz);
    // Прямая подмена data + пересчёт nonEmptyCount
    chunk.data = data;
    let count = 0;
    for (let i = 0; i < CHUNK_VOLUME; i++) {
        if (data[i] !== 0) count++;
    }
    chunk.nonEmptyCount = count;
    chunk.dirty = true;   // нужен ремеш после загрузки
    return chunk;
}

// ============================================================================
// Layer-level: VoxelLayer ↔ JSON serializable
// ============================================================================

/**
 * Сериализовать VoxelLayer в plain-object для JSON.
 * Не сохраняем пустые чанки (там 0 voxel'ов).
 */
export function serializeLayer(layer) {
    const chunks = {};
    for (const [key, chunk] of layer.chunks) {
        if (chunk.isEmpty()) continue;
        chunks[key] = chunkToBase64(chunk);
    }
    return {
        voxelSize: layer.voxelSize,
        palette: layer.palette.slice(),   // copy
        chunks,
    };
}

/**
 * Десериализовать слой из plain-object. Возвращает массив { key, chunk,
 * palette } — caller должен загрузить в VoxelLayer (через прямую запись
 * .chunks и .palette).
 */
export function deserializeLayerData(layerData) {
    const palette = layerData.palette ?? [null];
    const chunks = new Map();
    for (const [key, b64] of Object.entries(layerData.chunks ?? {})) {
        const [cx, cy, cz] = key.split(',').map(n => parseInt(n, 10));
        const ch = chunkFromBase64(b64, cx, cy, cz);
        chunks.set(key, ch);
    }
    return {
        voxelSize: layerData.voxelSize ?? 0.25,
        palette,
        chunks,
    };
}
