/**
 * Небо (подсистема небесных светил): чистая математика дуг/фаз/палитры
 * (тестируется в Vitest) и SkyLayer — шейдерная сфера BackSide вокруг
 * активной камеры: градиент зенит→горизонт, звёзды по хэшу cube-face
 * ячеек, диски+ореолы небесных тел (до SKY_BODY_MAX, у луны — фаза).
 *
 * Конвенция дуги: k — 0 восход → 0.5 зенит → 1 закат; та же формула, что
 * у DirectionalLight солнца в VoxelRenderer.setSun (диск обязан совпадать
 * со светом — обе стороны зовут одну функцию).
 */
import * as THREE from 'three';
import { cloudDim, DUSK_PEAK, lerpColor } from './daynight';

/** Максимум небесных тел в кадре (юниформ-массивы шейдера). */
export const SKY_BODY_MAX = 4;

/**
 * Высота светила на дуге k (рад): не ниже ~7° в дневной дуге. Отдельная
 * функция — фрустуму теней нужен сам угол, а не вектор.
 */
export function sunElevation(k: number): number {
    return Math.sin(k * Math.PI) * (Math.PI / 3) + 0.12;
}

/**
 * Направление на светило (unit, мировые оси) на дуге k: 0 восход → 0.5
 * зенит → 1 закат. БЕЗ клампа: k вне [0,1] уводит светило под горизонт
 * (луна ночью ходит по своей дуге через mod).
 */
export function celestialDir(k: number): readonly [number, number, number] {
    const az = Math.PI * (1 - k); // 180°→0°: восток → юг → запад
    const elev = sunElevation(k);
    return [Math.cos(az) * Math.cos(elev), Math.sin(elev), Math.sin(az) * Math.cos(elev)];
}

/** Направление солнца для СВЕТА: кламп k в [0,1] — бит-в-бит прежний setSun. */
export function sunDirection(k: number): readonly [number, number, number] {
    return celestialDir(Math.min(1, Math.max(0, k)));
}

/** k луны по часам: встаёт в 18:00, зенит в 0:00, садится в 6:00. */
export function moonK(hours: number): number {
    return (((hours - 18) / 12) % 1 + 1) % 1;
}

/**
 * Фаза луны 0..1 (0 новолуние → 0.5 четверть → 1 полнолуние): косинусный
 * цикл по суткам игры — светлая доля растёт и убывает плавно, без
 * «перещёлкивания» полная→тёмная. Дефолтный цикл — 8 суток.
 */
export function moonPhase(hours: number, cycleDays = 8): number {
    if (cycleDays <= 0) return 1;
    const days = hours / 24;
    return (1 - Math.cos(2 * Math.PI * (days % cycleDays) / cycleDays)) / 2;
}

/** Тёплый тон света у горизонта (warm = 1 − sin(kπ)): [1, 1−0.25w, 1−0.45w]. */
export function sunTint(k: number): readonly [number, number, number] {
    const warm = 1 - Math.sin(Math.min(1, Math.max(0, k)) * Math.PI);
    return [1, 1 - warm * 0.25, 1 - warm * 0.45];
}

/** Пара цветов неба: зенит + горизонт. */
export interface SkyPalette {
    zenith: number;
    horizon: number;
}

const SKY_DAY: SkyPalette = { zenith: 0x4a7ec2, horizon: 0xa8c4de };
const SKY_DUSK: SkyPalette = { zenith: 0x35507a, horizon: 0xe8a060 };
const SKY_NIGHT: SkyPalette = { zenith: 0x060a18, horizon: 0x101828 };

/**
 * Палитра неба: трёхопорный микс по фактору ночи (день → золотой час →
 * ночь, как dayNightAmbient), затем облачность — обесцвечивание к серому
 * и затемнение (та же арифметика cloudDim, что у фона сцены).
 */
export function skyColors(nightK: number, cloudK: number): SkyPalette {
    const k = Math.min(1, Math.max(0, nightK));
    const mixPair = (a: SkyPalette, b: SkyPalette, t: number): SkyPalette => ({
        zenith: lerpColor(a.zenith, b.zenith, t),
        horizon: lerpColor(a.horizon, b.horizon, t),
    });
    const pair = k <= 0 ? SKY_DAY : k >= 1 ? SKY_NIGHT
        : k < DUSK_PEAK ? mixPair(SKY_DAY, SKY_DUSK, k / DUSK_PEAK)
        : mixPair(SKY_DUSK, SKY_NIGHT, (k - DUSK_PEAK) / (1 - DUSK_PEAK));
    return {
        zenith: cloudDim(pair.zenith, cloudK, 0.7, 0.3),
        horizon: cloudDim(pair.horizon, cloudK, 0.7, 0.3),
    };
}

/** Яркость звёзд 0..1: ночь включает, облачность гасит (засветка). */
export function starBrightness(nightK: number, cloudK: number): number {
    const n = Math.min(1, Math.max(0, nightK));
    const c = Math.min(1, Math.max(0, cloudK));
    return n * (1 - 0.85 * c);
}

/** Небесное тело: диск+ореол на сфере (движок ставит dir, игра — всё). */
export interface SkyBody {
    /** Unit-направление на тело в мировых осях (диск совпадает со светом;
     *  ненормализованное нормализуется в packSkyBodies). */
    dir: readonly [number, number, number];
    /** Угловой радиус диска (рад), ~0.045. */
    angRadius: number;
    /** Цвет диска 0xRRGGBB (sRGB; в юниформы — линейный RGB). */
    color: number;
    /** Масштаб ореола (рад), 0 — нет. */
    glow?: number;
    /** Яркость 0..1+, дефолт 1 (игра гасит ночью). */
    intensity?: number;
    /** Терминатор 0..1: 0 новолуние, 1 полный диск (солнце — дефолт 1). */
    phase?: number;
}

/** Упакованные тела — плоские массивы для юниформ шейдера. */
export interface PackedSkyBodies {
    dirs: number[][];
    colors: number[][];
    params: number[][];
    count: number;
}

/** Опции неба (VoxelRenderer.setSky): null — прежний фон бит-в-бит. */
export interface SkyOptions {
    /** Небесные тела (до SKY_BODY_MAX): солнце, луна, вторая луна… */
    bodies?: readonly SkyBody[];
    /** Часы игры 0..24 — снапшот skyInfo/фаз (диски задаёт игра телами). */
    hours?: number;
    /** Звёзды (дефолт true; яркость сама гаснет днём/в облачность). */
    stars?: boolean;
}

/** sRGB-канал 0..1 → линейный (ColorManagement three — та же формула). */
export function srgbToLinear(c: number): number {
    return c <= 0.04045 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
}

/** Цвет 0xRRGGBB → линейный RGB-тройка. */
export function linearRgb(color: number): [number, number, number] {
    return [
        srgbToLinear(((color >> 16) & 0xff) / 255),
        srgbToLinear(((color >> 8) & 0xff) / 255),
        srgbToLinear((color & 0xff) / 255),
    ];
}

/**
 * Упаковка тел в юниформ-блоки: клампы (angRadius/glow > 0, интенсивность
 * ≥ 0, фаза 0..1), направление — как дали (шейдер ожидает unit; ненормали-
 * зованное клампится через acos — потому нормализуем здесь), лишние тела
 * обрезаются до SKY_BODY_MAX. Чистая — тестируется без WebGL.
 */
export function packSkyBodies(bodies: readonly SkyBody[]): PackedSkyBodies {
    const dirs: number[][] = [];
    const colors: number[][] = [];
    const params: number[][] = [];
    for (const b of bodies.slice(0, SKY_BODY_MAX)) {
        const len = Math.hypot(b.dir[0], b.dir[1], b.dir[2]) || 1;
        dirs.push([b.dir[0] / len, b.dir[1] / len, b.dir[2] / len]);
        colors.push(linearRgb(b.color));
        params.push([
            Math.max(1e-4, b.angRadius),
            Math.max(0, b.glow ?? 0),
            Math.max(0, b.intensity ?? 1),
            Math.min(1, Math.max(0, b.phase ?? 1)),
        ]);
    }
    return { dirs, colors, params, count: dirs.length };
}

/** Радиус небесной сферы в долях viewSize: край 9·viewSize < far 10·viewSize. */
export const SKY_RADIUS_K = 9;

const SKY_VERT = /* glsl */ `
varying vec3 vDir;
void main() {
    // направление в ЛОКАЛЬНЫХ осях (сфера центрируется на камере — локальное
    // направление = направление взгляда). Нормализуем здесь, на highp: fp16
    // varying больших vWorld (~R=720) даёт ступени по меридианам на крутом
    // smoothstep у горизонта
    vDir = normalize(position);
    gl_Position = projectionMatrix * viewMatrix * modelMatrix * vec4(position, 1.0);
}`;

const SKY_FRAG = /* glsl */ `
uniform vec3 uZenith;   // линейный RGB (юниформы — линейные, конверсия в конце)
uniform vec3 uHorizon;
uniform float uStars;   // 0..1 — starBrightness (ночь × облачность)
uniform float uTime;
uniform vec3 uBodyDir[${SKY_BODY_MAX}];
uniform vec3 uBodyColor[${SKY_BODY_MAX}];
uniform vec4 uBodyParams[${SKY_BODY_MAX}]; // angRadius, glow, intensity, phase
uniform int uBodyCount;
varying vec3 vDir;

float hash21(vec2 p) {
    p = fract(p * vec2(123.34, 456.21));
    p += dot(p, p + 45.32);
    return fract(p.x * p.y);
}

/** Дизер градиента: хэш экранных координат. Вход сперва сжимается в [0,1)
 *  (точный fract — целые до 2^24 не теряют биты), иначе hash21 на больших
 *  целых даёт вертикальные полосы на крутых градиентах. */
float dither(vec2 fc) {
    vec3 p3 = fract(vec3(fc.x, fc.y, fc.x * 0.618 + fc.y) / 1024.0);
    p3 += dot(p3, p3.yzx + 33.33);
    return fract((p3.x + p3.y) * p3.z);
}

/** Звёзды: хэш по cube-face ячейкам направления (без атласа) — работает
 *  для любого v, знак v.y не важен (в изо небо под горизонтом). */
float starField(vec3 v, float t) {
    vec3 a = abs(v);
    vec2 uv; float face;
    if (a.x >= a.y && a.x >= a.z) { uv = v.zy / a.x; face = v.x > 0.0 ? 0.0 : 1.0; }
    else if (a.y >= a.z)          { uv = v.xz / a.y; face = v.y > 0.0 ? 2.0 : 3.0; }
    else                          { uv = v.xy / a.z; face = v.z > 0.0 ? 4.0 : 5.0; }
    vec2 g = (uv * 0.5 + 0.5) * 96.0; // ~96 ячеек на грань
    vec2 cell = floor(g);
    float h = hash21(cell + face * 371.0);
    if (h > 0.1) return 0.0; // звезда ~в каждой 10-й ячейке
    vec2 sp = vec2(hash21(cell + 17.0), hash21(cell + 91.0)) * 0.6 + 0.2;
    float d = length(fract(g) - sp);
    float tw = 0.75 + 0.25 * sin(t * (1.5 + h * 20.0) + h * 40.0); // мерцание
    return smoothstep(0.12, 0.0, d) * tw;
}

void main() {
    // направление взгляда: сфера центрирована на камере, локальная позиция
    // и есть направление (для орто и перспективы одинаково)
    vec3 v = normalize(vDir);
    // градиент зенит → горизонт; под горизонтом — дымка чуть темнее
    vec3 c = mix(uHorizon, uZenith, smoothstep(0.0, 0.55, v.y));
    c = mix(uHorizon * 0.82, c, smoothstep(-0.25, 0.0, v.y));
    // звёзды поверх градиента (гасятся uStars днём и в облачность)
    c += vec3(1.0) * starField(v, uTime) * uStars;
    // небесные тела: диск (мягкий край) + фаза (терминатор) + ореол
    for (int i = 0; i < ${SKY_BODY_MAX}; i++) {
        if (i >= uBodyCount) break;
        vec3 bd = uBodyDir[i];
        float ang = acos(clamp(dot(v, bd), -1.0, 1.0));
        vec4 prm = uBodyParams[i];
        // локальный базис диска для фазы: e1 — горизонталь тела, e2 = bd×e1
        vec3 e1 = normalize(abs(bd.y) < 0.9
            ? cross(bd, vec3(0.0, 1.0, 0.0)) : cross(bd, vec3(1.0, 0.0, 0.0)));
        vec3 e2 = cross(bd, e1);
        vec2 p = vec2(dot(v - bd, e1), dot(v - bd, e2)) / prm.x;
        float r2 = dot(p, p);
        // терминатор: нормаль псевдо-сферы против фазовой плоскости;
        // тёмная часть — пепельный свет ×0.08
        vec3 n = vec3(p, sqrt(max(0.0, 1.0 - r2)));
        float th = 3.14159265 * prm.w;
        float lit = mix(0.08, 1.0,
            smoothstep(-0.08, 0.15, dot(n, vec3(sin(th), 0.0, cos(th)))));
        float disk = (1.0 - smoothstep(prm.x * 0.85, prm.x, ang)) * lit * prm.z;
        vec3 col = uBodyColor[i] * (disk + exp(-ang / max(prm.y, 1e-4)) * 0.55 * prm.z * step(1e-4, prm.y));
        c += col;
    }
    // статичный дизер против бандинга градиента (детерминизм кадра)
    c += (dither(gl_FragCoord.xy) - 0.5) / 255.0;
    gl_FragColor = vec4(c, 1.0);
    // конверсия следует за текущим RT (fog-RT линейно, прямой рендер sRGB)
    #include <colorspace_fragment>
}`;

/** Ручка слоя неба: меш в сцене + юниформы; движок владеет позицией (кадр),
 *  игра — телами/часами, цвета синхронизирует рендер из skyColors. */
export interface SkyLayer {
    readonly mesh: THREE.Mesh;
    readonly uniforms: Record<string, THREE.IUniform>;
    /** Палитра (sRGB 0xRRGGBB) → линейные юниформы. */
    setColors(zenith: number, horizon: number): void;
    /** Тела (упаковка packSkyBodies: клампы, линейный RGB, обрезка до 4). */
    setBodies(bodies: readonly SkyBody[]): void;
    dispose(): void;
}

/** Небесная сфера BackSide с шейдером: depthWrite off (мир поверх по
 *  depth), renderOrder −1 (рисуется первой), frustumCulled off (центр
 *  прыгает за камерой). Позицию меша ставит рендер каждый кадр. */
export function createSkyLayer(scene: THREE.Scene, radius: number): SkyLayer {
    const uniforms: Record<string, THREE.IUniform> = {
        uZenith: { value: new THREE.Vector3() },
        uHorizon: { value: new THREE.Vector3() },
        uStars: { value: 0 },
        uTime: { value: 0 },
        uBodyDir: { value: Array.from({ length: SKY_BODY_MAX }, () => new THREE.Vector3(0, 1, 0)) },
        uBodyColor: { value: Array.from({ length: SKY_BODY_MAX }, () => new THREE.Vector3()) },
        uBodyParams: { value: Array.from({ length: SKY_BODY_MAX }, () => new THREE.Vector4()) },
        uBodyCount: { value: 0 },
    };
    const mat = new THREE.ShaderMaterial({
        vertexShader: SKY_VERT,
        fragmentShader: SKY_FRAG,
        uniforms,
        side: THREE.BackSide,
        depthWrite: false,
    });
    const geo = new THREE.SphereGeometry(radius, 32, 16);
    const mesh = new THREE.Mesh(geo, mat);
    mesh.renderOrder = -1;
    mesh.frustumCulled = false;
    mesh.castShadow = false;
    scene.add(mesh);
    return {
        mesh,
        uniforms,
        setColors(zenith: number, horizon: number): void {
            const [zr, zg, zb] = linearRgb(zenith);
            (uniforms.uZenith.value as THREE.Vector3).set(zr, zg, zb);
            const [hr, hg, hb] = linearRgb(horizon);
            (uniforms.uHorizon.value as THREE.Vector3).set(hr, hg, hb);
        },
        setBodies(bodies: readonly SkyBody[]): void {
            const packed = packSkyBodies(bodies);
            for (let i = 0; i < SKY_BODY_MAX; i++) {
                if (i >= packed.count) continue;
                const d = packed.dirs[i]!;
                const c = packed.colors[i]!;
                const p = packed.params[i]!;
                (uniforms.uBodyDir.value as THREE.Vector3[])[i].set(d[0]!, d[1]!, d[2]!);
                (uniforms.uBodyColor.value as THREE.Vector3[])[i].set(c[0]!, c[1]!, c[2]!);
                (uniforms.uBodyParams.value as THREE.Vector4[])[i].set(p[0]!, p[1]!, p[2]!, p[3]!);
            }
            uniforms.uBodyCount.value = packed.count;
        },
        dispose(): void {
            scene.remove(mesh);
            geo.dispose();
            mat.dispose();
        },
    };
}