/**
 * Окно видимости частиц: AABB полиэдра из полуплоскостей (сляб орто-камеры,
 * пересечённый с горизонтальным слоем частиц). Точный перебор вершин —
 * пересечений троек плоскостей: ограничений 8, комбинаций 56, дёшево.
 */

type Plane = readonly [readonly [number, number, number], number];

/** AABB допустимой области {δ: n·δ ≤ b для всех плоскостей} по осям x, z. */
export function polyAABB(planes: readonly Plane[]): {
    minX: number; maxX: number; minZ: number; maxZ: number;
} {
    let minX = Infinity, maxX = -Infinity, minZ = Infinity, maxZ = -Infinity;
    const n = planes.length;
    for (let i = 0; i < n; i++)
        for (let j = i + 1; j < n; j++)
            for (let k = j + 1; k < n; k++) {
                const p = solve3(planes[i]![0], planes[i]![1], planes[j]![0], planes[j]![1], planes[k]![0], planes[k]![1]);
                if (!p) continue; // вырожденный набор нормалей
                let ok = true;
                for (const [nv, b] of planes)
                    if (nv[0]! * p[0]! + nv[1]! * p[1]! + nv[2]! * p[2]! > b + 1e-6) { ok = false; break; }
                if (!ok) continue;
                minX = Math.min(minX, p[0]!); maxX = Math.max(maxX, p[0]!);
                minZ = Math.min(minZ, p[2]!); maxZ = Math.max(maxZ, p[2]!);
            }
    return { minX, maxX, minZ, maxZ };
}

/** Пересечение трёх плоскостей n_i·δ = b_i (Крамер); null — если нормали
 *  зависимы (определитель ~0). */
function solve3(
    n1: readonly [number, number, number], b1: number,
    n2: readonly [number, number, number], b2: number,
    n3: readonly [number, number, number], b3: number,
): [number, number, number] | null {
    const det =
        n1[0]! * (n2[1]! * n3[2]! - n2[2]! * n3[1]!) -
        n1[1]! * (n2[0]! * n3[2]! - n2[2]! * n3[0]!) +
        n1[2]! * (n2[0]! * n3[1]! - n2[1]! * n3[0]!);
    if (Math.abs(det) < 1e-9) return null;
    const dx =
        b1 * (n2[1]! * n3[2]! - n2[2]! * n3[1]!) -
        n1[1]! * (b2 * n3[2]! - n2[2]! * b3) +
        n1[2]! * (b2 * n3[1]! - n2[1]! * b3);
    const dy =
        n1[0]! * (b2 * n3[2]! - n2[2]! * b3) -
        b1 * (n2[0]! * n3[2]! - n2[2]! * n3[0]!) +
        n1[2]! * (n2[0]! * b3 - b2 * n3[0]!);
    const dz =
        n1[0]! * (n2[1]! * b3 - b2 * n3[1]!) -
        n1[1]! * (n2[0]! * b3 - b2 * n3[0]!) +
        b1 * (n2[0]! * n3[1]! - n2[1]! * n3[0]!);
    return [dx / det, dy / det, dz / det];
}

/** Полуразмеры и слой — параметры окна частиц. */
export interface WindowParams {
    /** Базис камеры: r — экранная горизонталь, u — вертикаль, f — взгляд. */
    r: V3; u: V3; f: V3;
    /** Полуразмеры фрустума: половина ширины и высоты. */
    a: number; h: number;
    /** Глубины ближней/дальней плоскостей относительно цели камеры. */
    k0: number; k1: number;
    /** Горизонтальный слой частиц относительно цели камеры. */
    dy0: number; dy1: number;
}
type V3 = readonly [number, number, number];
export interface WindowBox { minX: number; maxX: number; minZ: number; maxZ: number }

/** Окно видимости частиц: AABB сляба ортокамеры на слое частиц. Смещение δ
 *  от цели камеры видно на экране ⇔ |r·δ|≤a, |u·δ|≤h, k0 ≤ f·δ ≤ k1; слой
 *  добавляет δ.y ∈ [dy0,dy1]. Окно шире наземного следа кадра: высокую
 *  частицу видно дальше от цели, сляб уходит в глубину до дальней плоскости.
 *  Все ограничения — в форме n·δ ≤ b (нижние границы — с переворотом нормали). */
export function particleWindow(p: WindowParams): WindowBox {
    return polyAABB([
        [[-p.r[0]!, -p.r[1]!, -p.r[2]!], p.a], [p.r, p.a],
        [[-p.u[0]!, -p.u[1]!, -p.u[2]!], p.h], [p.u, p.h],
        [[-p.f[0]!, -p.f[1]!, -p.f[2]!], -p.k0], [p.f, p.k1],
        [[0, -1, 0], -p.dy0], [[0, 1, 0], p.dy1],
    ]);
}
