/**
 * Формат воксельной модели (JSON): размер, плотные данные base64,
 * слоты палитры. Модель — данные: генераторы/импорт .vox/ручная правка
 * дают один и тот же формат; валидация — числовая, без рендера.
 *
 * Слой Y растёт вверх; порядок вокселей в данных: x, затем y, затем z
 * (idx = x + sx*(y + sy*z) — как в VoxelGrid).
 */
import { VoxelGrid } from '../voxel/grid';

export interface VoxelModel {
    /** Размер по осям [sx, sy, sz], каждая > 0. */
    size: [number, number, number];
    /** Слоты палитры: слот-индекс сцены → hex; значения 1..255 — воксели ссылаются на ключи. */
    palette: Record<string, string>;
    /** base64 плотных данных: 1 байт на воксель, 0 = пусто. */
    data: string;
}

const MAX_DIM = 1024;      // предел по оси — защита от мусора и OOM
const MAX_VOXELS = 32 * 1024 * 1024; // ~32 МБ данных

/** Кодирует сетку в модель: палитра берётся из карты слот→hex. */
export function encodeModel(grid: VoxelGrid, palette: Record<string, string>): VoxelModel {
    const bytes = new Uint8Array(grid.sx * grid.sy * grid.sz);
    for (let i = 0; i < bytes.length; i++) bytes[i] = grid.data[i];
    return {
        size: [grid.sx, grid.sy, grid.sz],
        palette: { ...palette },
        data: bytesToBase64(bytes),
    };
}

/** Декодирует модель в сетку. Бросает на невалидных данных (см. validateModel). */
export function decodeModel(model: VoxelModel): VoxelGrid {
    assertModelValid(model);
    const [sx, sy, sz] = model.size;
    const bytes = base64ToBytes(model.data);
    if (bytes.length !== sx * sy * sz) {
        throw new Error(`decodeModel: данных ${bytes.length} байт, а нужно ${sx * sy * sz}`);
    }
    const grid = new VoxelGrid(sx, sy, sz);
    for (let i = 0; i < bytes.length; i++) grid.data[i] = bytes[i];
    return grid;
}

/**
 * Числовая валидация модели без декодирования. Возвращает список проблем;
 * пустой список = модель валидна. Проверяет: размер, объём, hex-слоты,
 * что данные — валидный base64 нужной длины, что байты ссылаются на слоты.
 */
export function validateModel(model: unknown): string[] {
    const problems: string[] = [];
    const m = model as Partial<VoxelModel> | null;
    if (!m || typeof m !== 'object') return ['модель — не объект'];
    if (!Array.isArray(m.size) || m.size.length !== 3 ||
        m.size.some((n) => !Number.isInteger(n) || n <= 0 || n > MAX_DIM)) {
        problems.push(`size должен быть [sx,sy,sz] с целыми 1..${MAX_DIM}`);
        return problems; // без размера дальше проверять нечего
    }
    const [sx, sy, sz] = m.size;
    const volume = sx * sy * sz;
    if (volume > MAX_VOXELS) problems.push(`объём ${volume} > ${MAX_VOXELS}`);

    if (!m.palette || typeof m.palette !== 'object') {
        problems.push('palette должен быть объектом слот→hex');
    } else {
        for (const [slot, hex] of Object.entries(m.palette)) {
            const idx = Number(slot);
            if (!Number.isInteger(idx) || idx < 1 || idx > 255) {
                problems.push(`слот «${slot}» вне 1..255`);
            }
            if (typeof hex !== 'string' || !/^#[0-9a-fA-F]{3}([0-9a-fA-F]{3})?$/.test(hex)) {
                problems.push(`слот ${slot}: hex «${hex}» не вида #rgb/#rrggbb`);
            }
        }
    }

    const bytes = safeBase64(m.data);
    if (!bytes) {
        problems.push('data — не валидный base64');
        return problems;
    }
    if (bytes.length !== volume) {
        problems.push(`данных ${bytes.length} байт, а size требует ${volume}`);
        return problems;
    }
    // Ссылки на слоты проверяем только при целостной палитре-объекте
    const palette = m.palette;
    if (palette && typeof palette === 'object' && !Array.isArray(palette)) {
        for (const b of bytes) {
            if (b !== 0 && !(b in palette)) {
                problems.push(`воксель со слотом ${b} отсутствует в палитре`);
                break;
            }
        }
    }
    return problems;
}

/** Кидает первую проблему валидации (удобно в decode). */
export function assertModelValid(model: unknown): void {
    const problems = validateModel(model);
    if (problems.length > 0) throw new Error(`модель невалидна: ${problems[0]}`);
}

function bytesToBase64(bytes: Uint8Array): string {
    let bin = '';
    for (let i = 0; i < bytes.length; i += 0x8000) {
        bin += String.fromCharCode(...bytes.subarray(i, i + 0x8000));
    }
    return btoa(bin);
}

function base64ToBytes(b64: string): Uint8Array {
    const bin = atob(b64);
    const bytes = new Uint8Array(bin.length);
    for (let i = 0; i < bin.length; i++) bytes[i] = bin.charCodeAt(i);
    return bytes;
}

function safeBase64(s: unknown): Uint8Array | null {
    if (typeof s !== 'string') return null;
    try { return base64ToBytes(s); } catch { return null; }
}