/**
 * Окно видимости частиц: 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],
    ]);
}

/** Параметры окна ПЕРСПЕКТИВНОЙ камеры (пирамида вместо сляба). */
export interface PerspWindowParams {
    /** Базис камеры: r — экранная горизонталь, u — вертикаль, f — взгляд. */
    r: V3; u: V3; f: V3;
    /** Позиция камеры и цель (система отсчёта δ = точка − target). */
    camPos: V3; target: V3;
    /** tan(fov/2) по вертикали и аспект кадра (полуширина = tanHalf·aspect). */
    tanHalf: number; aspect: number;
    /** Ближняя/дальняя плоскости (far — уже с cap'ом вызывающего). */
    near: number; far: number;
    /** Горизонтальный слой частиц относительно target.y. */
    dy0: number; dy1: number;
}

/** Окно частиц для перспективной камеры: та же схема «плоскости → polyAABB»,
 *  но боковые грани проходят через камеру (n·(x−cam) ≤ K·f·(x−cam) при
 *  n = ±r/±u, K = tanHalf·[aspect]) — в δ = x − target это n·δ ≤ n·d,
 *  d = camPos − target. Потребители (wrap/плотность осадков) остаются
 *  прежними — окно просто становится перспективно-корректным AABB.
 *  Контракт: окно ВСЕГДА конечное — если фрустум не пересекает слой частиц
 *  (1st person смотрит в небо, far cap'ом срезан в ноль), полиэдр пуст и
 *  polyAABB вернул бы ±Infinity, а wrapV у потребителей делит на размер
 *  окна. В этом случае возвращаем вырожденный бокс в точке цели: после
 *  маржи weatherWindow он превращается в крошечное окно вокруг цели —
 *  частицы заворачиваются туда и гасятся краем кадра, NaN невозможен.
 *  Чистая функция (тесты без WebGL). */
export function particleWindowPersp(p: PerspWindowParams): WindowBox {
    const K = p.tanHalf * p.aspect; // полуширина пирамиды на единицу глубины
    const T = p.tanHalf;
    const d: V3 = [p.camPos[0] - p.target[0], p.camPos[1] - p.target[1], p.camPos[2] - p.target[2]];
    // плоскость n·δ ≤ n·d: точка δ внутри боковой грани ⇔ её отклонение от
    // оси взгляда растёт не быстрее радиуса пирамиды
    const side = (n: [number, number, number]): Plane => {
        const b = n[0]! * d[0]! + n[1]! * d[1]! + n[2]! * d[2]!;
        return [n, b];
    };
    const fd = p.f[0]! * d[0]! + p.f[1]! * d[1]! + p.f[2]! * d[2]!;
    const box = polyAABB([
        side([p.r[0]! - K * p.f[0]!, p.r[1]! - K * p.f[1]!, p.r[2]! - K * p.f[2]!]),
        side([-p.r[0]! - K * p.f[0]!, -p.r[1]! - K * p.f[1]!, -p.r[2]! - K * p.f[2]!]),
        side([p.u[0]! - T * p.f[0]!, p.u[1]! - T * p.f[1]!, p.u[2]! - T * p.f[2]!]),
        side([-p.u[0]! - T * p.f[0]!, -p.u[1]! - T * p.f[1]!, -p.u[2]! - T * p.f[2]!]),
        [[-p.f[0]!, -p.f[1]!, -p.f[2]!], -fd - p.near], // near: f·δ ≥ near
        [[p.f[0]!, p.f[1]!, p.f[2]!], fd + p.far],      // far: f·δ ≤ far
        [[0, -1, 0], -p.dy0], [[0, 1, 0], p.dy1],
    ]);
    const finite = [box.minX, box.maxX, box.minZ, box.maxZ].every(Number.isFinite);
    return finite ? box : { minX: 0, maxX: 0, minZ: 0, maxZ: 0 }; // пустой фрустум → точка цели
}
