/**
 * SkyboxManager — кастомное небо для сцены (задача 16).
 *
 * Реализует процедурный gradient-skybox без внешних текстур (работает offline):
 *   - купол-сфера (BACKSIDE) с ShaderMaterial: плавный градиент верх→низ,
 *     солнечный диск, лёгкая дымка у горизонта;
 *   - low-poly горы на горизонте (как в Roblox-эталоне);
 *   - billboard-облака (плоскости, медленный дрейф);
 *   - атмосферный туман (scene.fog).
 *
 * Пресеты: clear-summer-day / cloudy / sunset / starry-night / space /
 *          lowpoly-roblox (+ gradient вручную). Плавный fadeTo между ними.
 *
 * API (через game.scene.*):
 *   setSkybox({ preset } | { mode:'gradient', topColor, bottomColor, ... })
 *   setClouds({ enabled, cover, density, speed, color })
 *   setFog({ color, density, near, far } | enabled:false)
 *   skybox.fadeTo(opts, durationSec)
 *   skybox.setSunDirection({x,y,z})
 *
 * Фича-парность: при портировании в плеер — тот же модуль в rublox-player/src/engine/.
 */
import {
    Color3, Color4, Vector3,
    MeshBuilder, ShaderMaterial, Effect, StandardMaterial, Texture,
    DynamicTexture, VertexData, Mesh,
} from '@babylonjs/core';

// ── Шейдер градиентного неба ──────────────────────────────────────────────
// Купол изнутри: цвет интерполируется по высоте (y от -1 низ до +1 верх),
// плюс солнечный диск и осветление у горизонта (дымка).
const SKY_VERT = `
precision highp float;
attribute vec3 position;
uniform mat4 worldViewProjection;
varying vec3 vDir;
void main(void){
    vDir = normalize(position);
    gl_Position = worldViewProjection * vec4(position, 1.0);
}`;

const SKY_FRAG = `
precision highp float;
varying vec3 vDir;
uniform vec3 topColor;
uniform vec3 bottomColor;
uniform vec3 horizonColor;
uniform vec3 sunDir;
uniform vec3 sunColor;
uniform float sunSize;     // 0..1 угловой радиус
uniform float horizonHaze; // 0..1 сила дымки у горизонта
void main(void){
    float h = clamp(vDir.y * 0.5 + 0.5, 0.0, 1.0);   // 0 низ .. 1 верх
    // Градиент: низ→горизонт→верх (двойная интерполяция через горизонт у h~0.5)
    vec3 col;
    if (h < 0.5) {
        col = mix(bottomColor, horizonColor, h * 2.0);
    } else {
        col = mix(horizonColor, topColor, (h - 0.5) * 2.0);
    }
    // Дымка у горизонта — осветление узкой полосы около h=0.5
    float haze = smoothstep(0.5, 0.0, abs(h - 0.5)) * horizonHaze;
    col = mix(col, horizonColor + vec3(0.08), haze * 0.5);
    // Солнечный диск + гало
    float d = distance(normalize(vDir), normalize(sunDir));
    float disk = smoothstep(sunSize, sunSize * 0.4, d);
    float glow = smoothstep(sunSize * 6.0, 0.0, d) * 0.35;
    col += sunColor * disk;
    col += sunColor * glow;
    gl_FragColor = vec4(col, 1.0);
}`;

let _shaderRegistered = false;
function registerSkyShader() {
    if (_shaderRegistered) return;
    Effect.ShadersStore['kubikonSkyVertexShader'] = SKY_VERT;
    Effect.ShadersStore['kubikonSkyFragmentShader'] = SKY_FRAG;
    _shaderRegistered = true;
}

const hexToRgb = (hex) => {
    if (Array.isArray(hex)) return hex;
    let h = String(hex || '#ffffff').replace('#', '').trim();
    // Короткая форма #fff → #ffffff.
    if (h.length === 3) h = h[0] + h[0] + h[1] + h[1] + h[2] + h[2];
    if (h.length < 6) h = (h + 'ffffff').slice(0, 6);
    const r = parseInt(h.substring(0, 2), 16);
    const g = parseInt(h.substring(2, 4), 16);
    const b = parseInt(h.substring(4, 6), 16);
    return [
        (Number.isFinite(r) ? r : 255) / 255,
        (Number.isFinite(g) ? g : 255) / 255,
        (Number.isFinite(b) ? b : 255) / 255,
    ];
};

// ── Пресеты неба ──────────────────────────────────────────────────────────
// top/bottom/horizon — цвета купола; sun — направление/цвет/размер солнца;
// mountains — рисовать ли горы; clouds — параметры облаков; fog — туман;
// stars — звёздное небо (для ночи/космоса).
const PRESETS = {
    'clear-summer-day': {
        top: '#3d7fe0', horizon: '#bcd9f2', bottom: '#dcebf7',
        sunDir: [0.3, 0.85, 0.4], sunColor: '#fff6d8', sunSize: 0.035, haze: 0.6,
        mountains: false,
        clouds: { enabled: true, cover: 0.25, color: '#ffffff', speed: 0.015 },
        fog: { color: '#cfe2f2', density: 0.0035 },
        light: { sunIntensity: 1.0, sunColor: '#fff6e0', hemiIntensity: 0.7, ambient: '#b9d4ee' },
    },
    'lowpoly-roblox': {
        top: '#4a93e6', horizon: '#cfe6f5', bottom: '#e6f1fa',
        sunDir: [0.4, 0.8, 0.45], sunColor: '#fffbe6', sunSize: 0.03, haze: 0.85,
        mountains: true,
        clouds: { enabled: true, cover: 0.45, color: '#ffffff', speed: 0.012 },
        fog: { color: '#e2eef7', density: 0.005 },
        light: { sunIntensity: 0.95, sunColor: '#fff7e0', hemiIntensity: 0.75, ambient: '#cfe6f5' },
    },
    'cloudy': {
        top: '#8fa6bd', horizon: '#c2ccd6', bottom: '#d8dde2',
        sunDir: [0.2, 0.7, 0.3], sunColor: '#e8e8e8', sunSize: 0.0, haze: 0.4,
        mountains: false,
        clouds: { enabled: true, cover: 0.8, color: '#e8ebef', speed: 0.02 },
        fog: { color: '#cfd6dd', density: 0.008 },
        light: { sunIntensity: 0.5, sunColor: '#dfe4ea', hemiIntensity: 0.8, ambient: '#c2ccd6' },
    },
    'sunset': {
        top: '#2a3a6b', horizon: '#f5915a', bottom: '#f7c98a',
        sunDir: [0.7, 0.12, -0.2], sunColor: '#ffd28a', sunSize: 0.05, haze: 1.0,
        mountains: true,
        clouds: { enabled: true, cover: 0.35, color: '#ffd9b3', speed: 0.01 },
        fog: { color: '#f0b483', density: 0.006 },
        light: { sunIntensity: 0.7, sunColor: '#ff9a52', hemiIntensity: 0.5, ambient: '#9a6a78' },
    },
    'starry-night': {
        top: '#070b1f', horizon: '#1b2547', bottom: '#243056',
        sunDir: [-0.4, 0.75, 0.3], sunColor: '#cdd6ff', sunSize: 0.02, haze: 0.3,
        mountains: true, stars: true,
        clouds: { enabled: false },
        fog: { color: '#141c38', density: 0.004 },
        light: { sunIntensity: 0.18, sunColor: '#9aa8d8', hemiIntensity: 0.25, ambient: '#1b2547' },
    },
    'space': {
        top: '#02030a', horizon: '#06070f', bottom: '#0a0c18',
        sunDir: [0.5, 0.6, 0.4], sunColor: '#ffffff', sunSize: 0.015, haze: 0.0,
        mountains: false, stars: true,
        clouds: { enabled: false },
        fog: { enabled: false },
        light: { sunIntensity: 0.6, sunColor: '#ffffff', hemiIntensity: 0.2, ambient: '#10121c' },
    },
};

export class SkyboxManager {
    constructor(scene, hemiLight, sunLight) {
        this.scene = scene;
        this.hemiLight = hemiLight || null;   // ambient
        this.sunLight = sunLight || null;     // directional (тени)
        this._dome = null;
        this._mat = null;
        this._mountains = null;
        this._clouds = [];     // [{mesh, baseX, speed}]
        this._cloudRoot = null;
        this._stars = null;
        this._fade = null;     // активный fadeTo {from,to,t,dur}
        this._state = this._defaultState();
        registerSkyShader();
        this._buildDome();
    }

    _defaultState() {
        return {
            mode: 'gradient',
            top: '#4a93e6', horizon: '#cfe6f5', bottom: '#e6f1fa',
            sunDir: [0.4, 0.8, 0.45], sunColor: '#fffbe6', sunSize: 0.03, haze: 0.8,
            mountains: false, stars: false,
            clouds: { enabled: false, cover: 0.4, color: '#ffffff', speed: 0.012 },
            fog: { enabled: false, color: '#dde8f2', density: 0.005 },
            light: { sunIntensity: 0.95, sunColor: '#fff7e0', hemiIntensity: 0.75, ambient: '#cfe6f5' },
        };
    }

    // ── Купол ──────────────────────────────────────────────────────────────
    _buildDome() {
        const dome = MeshBuilder.CreateSphere('kubikonSkyDome', {
            diameter: 1000, segments: 24, sideOrientation: Mesh.BACKSIDE,
        }, this.scene);
        dome.isPickable = false;
        dome.infiniteDistance = true;      // не двигается с камерой
        dome.renderingGroupId = 0;
        dome.applyFog = false;

        const mat = new ShaderMaterial('kubikonSkyMat', this.scene, {
            vertex: 'kubikonSky', fragment: 'kubikonSky',
        }, {
            attributes: ['position'],
            uniforms: ['worldViewProjection', 'topColor', 'bottomColor',
                'horizonColor', 'sunDir', 'sunColor', 'sunSize', 'horizonHaze'],
        });
        mat.backFaceCulling = false;
        mat.disableDepthWrite = true;       // небо всегда позади
        dome.material = mat;
        this._dome = dome;
        this._mat = mat;
        this._applyShaderUniforms();
    }

    _applyShaderUniforms() {
        const s = this._state;
        const m = this._mat;
        if (!m) return;
        m.setColor3('topColor', Color3.FromArray(hexToRgb(s.top)));
        m.setColor3('bottomColor', Color3.FromArray(hexToRgb(s.bottom)));
        m.setColor3('horizonColor', Color3.FromArray(hexToRgb(s.horizon)));
        const sd = Array.isArray(s.sunDir) ? s.sunDir : [0.4, 0.8, 0.45];
        m.setVector3('sunDir', new Vector3(sd[0], sd[1], sd[2]).normalize());
        m.setColor3('sunColor', Color3.FromArray(hexToRgb(s.sunColor)));
        m.setFloat('sunSize', s.sunSize || 0.03);
        m.setFloat('horizonHaze', s.haze != null ? s.haze : 0.7);
    }

    // ── Горы (low-poly на горизонте) ────────────────────────────────────────
    _buildMountains(colorHex) {
        this._disposeMountains();
        const positions = [], indices = [];
        const ringR = 420, baseY = -10, segs = 64;
        // Кольцо из треугольных пиков переменной высоты — стилизованный силуэт.
        let vi = 0;
        for (let i = 0; i < segs; i++) {
            const a0 = (i / segs) * Math.PI * 2;
            const a1 = ((i + 1) / segs) * Math.PI * 2;
            const am = (a0 + a1) / 2;
            // Псевдослучайная высота пика (детерминированно от индекса).
            const hh = 40 + Math.abs(Math.sin(i * 1.7) * Math.cos(i * 0.6)) * 130;
            const x0 = Math.cos(a0) * ringR, z0 = Math.sin(a0) * ringR;
            const x1 = Math.cos(a1) * ringR, z1 = Math.sin(a1) * ringR;
            const xm = Math.cos(am) * ringR, zm = Math.sin(am) * ringR;
            positions.push(x0, baseY, z0, x1, baseY, z1, xm, baseY + hh, zm);
            indices.push(vi, vi + 1, vi + 2);
            vi += 3;
        }
        const vd = new VertexData();
        vd.positions = positions; vd.indices = indices;
        const normals = [];
        VertexData.ComputeNormals(positions, indices, normals);
        vd.normals = normals;
        const mesh = new Mesh('kubikonSkyMountains', this.scene);
        vd.applyToMesh(mesh);
        mesh.isPickable = false;
        mesh.applyFog = true;               // горы выцветают в туман (атмосфера)
        mesh.renderingGroupId = 0;
        const mat = new StandardMaterial('kubikonSkyMountainsMat', this.scene);
        const c = hexToRgb(colorHex || '#8fa98a');
        mat.diffuseColor = new Color3(c[0], c[1], c[2]);
        mat.specularColor = new Color3(0, 0, 0);
        mat.emissiveColor = new Color3(c[0] * 0.25, c[1] * 0.25, c[2] * 0.25);
        mesh.material = mat;
        this._mountains = mesh;
    }

    _disposeMountains() {
        if (this._mountains) { this._mountains.material?.dispose(); this._mountains.dispose(); this._mountains = null; }
    }

    // ── Облака (billboard-плоскости) ────────────────────────────────────────
    _buildClouds(opts) {
        this._disposeClouds();
        const o = opts || {};
        if (!o.enabled) return;
        const cover = o.cover != null ? o.cover : 0.4;
        const count = Math.round(4 + cover * 16);   // 4..20 облаков
        const tex = this._makeCloudTexture(o.color || '#ffffff');
        for (let i = 0; i < count; i++) {
            const w = 60 + Math.random() * 90;
            const plane = MeshBuilder.CreatePlane(`kubikonCloud_${i}`, { width: w, height: w * 0.55 }, this.scene);
            plane.billboardMode = Mesh.BILLBOARDMODE_ALL;
            plane.isPickable = false;
            plane.applyFog = false;
            plane.renderingGroupId = 0;
            const mat = new StandardMaterial(`kubikonCloudMat_${i}`, this.scene);
            mat.diffuseTexture = tex;
            mat.opacityTexture = tex;
            mat.emissiveColor = new Color3(1, 1, 1);
            mat.disableLighting = true;
            mat.backFaceCulling = false;
            plane.material = mat;
            const ang = Math.random() * Math.PI * 2;
            const rad = 150 + Math.random() * 200;
            const x = Math.cos(ang) * rad;
            const z = Math.sin(ang) * rad;
            const y = 90 + Math.random() * 70;
            plane.position.set(x, y, z);
            this._clouds.push({ mesh: plane, speed: (o.speed || 0.012) * (0.6 + Math.random() * 0.8) });
        }
    }

    /** Процедурная мягкая текстура-облако (radial-gradient на DynamicTexture). */
    _makeCloudTexture(colorHex) {
        const size = 256;
        const dt = new DynamicTexture('kubikonCloudTex', { width: size, height: size }, this.scene, false);
        const ctx = dt.getContext();
        ctx.clearRect(0, 0, size, size);
        const c = hexToRgb(colorHex);
        const rgb = `${Math.round(c[0] * 255)},${Math.round(c[1] * 255)},${Math.round(c[2] * 255)}`;
        // Несколько перекрывающихся мягких кругов → пухлое облако.
        const blobs = [[128, 140, 70], [90, 150, 50], [170, 150, 55], [128, 110, 55], [110, 130, 45], [150, 130, 45]];
        for (const [bx, by, br] of blobs) {
            const g = ctx.createRadialGradient(bx, by, 0, bx, by, br);
            g.addColorStop(0, `rgba(${rgb},0.9)`);
            g.addColorStop(0.6, `rgba(${rgb},0.5)`);
            g.addColorStop(1, `rgba(${rgb},0)`);
            ctx.fillStyle = g;
            ctx.beginPath(); ctx.arc(bx, by, br, 0, Math.PI * 2); ctx.fill();
        }
        dt.hasAlpha = true;
        dt.update();
        return dt;
    }

    _disposeClouds() {
        for (const c of this._clouds) { c.mesh.material?.diffuseTexture?.dispose?.(); c.mesh.material?.dispose(); c.mesh.dispose(); }
        this._clouds = [];
    }

    // ── Звёзды (точки на куполе) ─────────────────────────────────────────────
    _buildStars(enabled) {
        this._disposeStars();
        if (!enabled) return;
        const size = 1024;
        const dt = new DynamicTexture('kubikonStarsTex', { width: size, height: size }, this.scene, false);
        const ctx = dt.getContext();
        ctx.fillStyle = 'rgba(0,0,0,0)'; ctx.clearRect(0, 0, size, size);
        for (let i = 0; i < 600; i++) {
            const x = Math.random() * size, y = Math.random() * size;
            const r = Math.random() * 1.4 + 0.3;
            const a = 0.4 + Math.random() * 0.6;
            ctx.fillStyle = `rgba(255,255,255,${a})`;
            ctx.beginPath(); ctx.arc(x, y, r, 0, Math.PI * 2); ctx.fill();
        }
        dt.hasAlpha = true; dt.update();
        const dome = MeshBuilder.CreateSphere('kubikonStarsDome', {
            diameter: 980, segments: 16, sideOrientation: Mesh.BACKSIDE,
        }, this.scene);
        dome.isPickable = false; dome.infiniteDistance = true;
        dome.applyFog = false; dome.renderingGroupId = 0;
        const mat = new StandardMaterial('kubikonStarsMat', this.scene);
        mat.diffuseTexture = dt; mat.opacityTexture = dt;
        mat.emissiveColor = new Color3(1, 1, 1);
        mat.disableLighting = true; mat.backFaceCulling = false;
        mat.disableDepthWrite = true;
        dome.material = mat;
        this._stars = dome;
    }

    _disposeStars() {
        if (this._stars) { this._stars.material?.diffuseTexture?.dispose?.(); this._stars.material?.dispose(); this._stars.dispose(); this._stars = null; }
    }

    // ── Туман ────────────────────────────────────────────────────────────────
    _applyFog(fog) {
        if (!this.scene) return;
        if (fog && fog.enabled !== false && (fog.density != null || fog.color)) {
            this.scene.fogMode = 2; // EXP
            const c = hexToRgb(fog.color || '#dde8f2');
            this.scene.fogColor = new Color3(c[0], c[1], c[2]);
            this.scene.fogDensity = fog.density != null ? fog.density : 0.005;
        } else if (fog && fog.enabled === false) {
            this.scene.fogMode = 0;
        }
    }

    // ── Освещение (единый источник: небо управляет светом сцены) ─────────────
    /** Выставить направление/яркость солнца и ambient под текущее небо. */
    _applyLighting(light, sunDir) {
        if (this.sunLight && sunDir) {
            // DirectionalLight.direction указывает КУДА падает свет → от солнца вниз.
            const d = new Vector3(-sunDir[0], -sunDir[1], -sunDir[2]);
            if (d.lengthSquared() > 0) { d.normalize(); this.sunLight.direction = d; }
        }
        if (!light) return;
        if (this.sunLight) {
            if (typeof light.sunIntensity === 'number') this.sunLight.intensity = light.sunIntensity;
            if (light.sunColor) this.sunLight.diffuse = Color3.FromArray(hexToRgb(light.sunColor));
        }
        if (this.hemiLight) {
            if (typeof light.hemiIntensity === 'number') this.hemiLight.intensity = light.hemiIntensity;
            if (light.ambient) this.hemiLight.groundColor = Color3.FromArray(hexToRgb(light.ambient));
        }
    }

    // ── Public API ───────────────────────────────────────────────────────────

    /** Применить пресет или ручные опции gradient. */
    setSkybox(opts) {
        if (!opts) return;
        const preset = opts.preset && PRESETS[opts.preset] ? { ...PRESETS[opts.preset] } : null;
        const s = this._state;
        if (preset) {
            s.top = preset.top; s.horizon = preset.horizon; s.bottom = preset.bottom;
            s.sunDir = preset.sunDir; s.sunColor = preset.sunColor; s.sunSize = preset.sunSize;
            s.haze = preset.haze; s.mountains = !!preset.mountains; s.stars = !!preset.stars;
            s.clouds = { ...(preset.clouds || { enabled: false }) };
            s.fog = { enabled: preset.fog?.enabled !== false, ...(preset.fog || {}) };
            s.light = preset.light || null;
            this._applyLighting(preset.light, preset.sunDir);
        } else {
            // Ручной gradient: { topColor, bottomColor, horizonColor, sunDirection, sunColor, sunSize }
            if (opts.topColor) s.top = opts.topColor;
            if (opts.bottomColor) s.bottom = opts.bottomColor;
            if (opts.horizonColor) s.horizon = opts.horizonColor;
            if (opts.sunDirection) s.sunDir = [opts.sunDirection.x, opts.sunDirection.y, opts.sunDirection.z];
            if (opts.sunColor) s.sunColor = opts.sunColor;
            if (typeof opts.sunSize === 'number') s.sunSize = opts.sunSize;
            if (typeof opts.haze === 'number') s.haze = opts.haze;
            if (typeof opts.mountains === 'boolean') s.mountains = opts.mountains;
            if (typeof opts.stars === 'boolean') s.stars = opts.stars;
        }
        this._rebuildAll();
    }

    /** Облака поверх любого режима. */
    setClouds(opts) {
        if (!opts) return;
        this._state.clouds = { ...this._state.clouds, ...opts };
        if (this._state.clouds.enabled == null) this._state.clouds.enabled = true;
        this._buildClouds(this._state.clouds);
    }

    /** Атмосферный туман. */
    setFog(opts) {
        if (!opts) { return; }
        this._state.fog = { ...this._state.fog, ...opts };
        if (opts.enabled == null) this._state.fog.enabled = true;
        this._applyFog(this._state.fog);
    }

    /** Установить направление солнца (для программной анимации). */
    setSunDirection(dir) {
        if (!dir) return;
        this._state.sunDir = [dir.x, dir.y, dir.z];
        this._applyShaderUniforms();
    }

    /** Плавный переход к пресету/опциям за durationSec секунд (цвета купола + туман). */
    fadeTo(opts, durationSec = 2) {
        const target = opts?.preset && PRESETS[opts.preset] ? { ...PRESETS[opts.preset] } : null;
        if (!target) { this.setSkybox(opts); return; }
        // Запоминаем стартовые цвета и целевые — анимируем в tick().
        this._fade = {
            t: 0, dur: Math.max(0.1, durationSec),
            from: {
                top: hexToRgb(this._state.top), horizon: hexToRgb(this._state.horizon),
                bottom: hexToRgb(this._state.bottom), sunColor: hexToRgb(this._state.sunColor),
                sunDir: this._state.sunDir.slice(), sunSize: this._state.sunSize, haze: this._state.haze,
            },
            to: {
                top: hexToRgb(target.top), horizon: hexToRgb(target.horizon),
                bottom: hexToRgb(target.bottom), sunColor: hexToRgb(target.sunColor),
                sunDir: target.sunDir.slice(), sunSize: target.sunSize, haze: target.haze,
            },
            target,
        };
        // Немедленно перестраиваем «дискретные» части (горы/звёзды/облака/туман
        // целевого пресета появляются сразу, цвета купола — плавно).
        const s = this._state;
        s.mountains = !!target.mountains; s.stars = !!target.stars;
        s.clouds = { ...(target.clouds || { enabled: false }) };
        s.fog = { enabled: target.fog?.enabled !== false, ...(target.fog || {}) };
        s.light = target.light || null;
        this._rebuildExtras();
        // Запоминаем стартовые/целевые значения света для плавной анимации.
        if (target.light) {
            this._fade.lightFrom = {
                sunInt: this.sunLight?.intensity ?? 1,
                hemiInt: this.hemiLight?.intensity ?? 0.7,
            };
            this._fade.lightTo = {
                sunInt: target.light.sunIntensity ?? 1,
                hemiInt: target.light.hemiIntensity ?? 0.7,
                sunColor: target.light.sunColor, ambient: target.light.ambient,
            };
        }
    }

    /** Пересобрать всё (купол-uniforms + горы + звёзды + облака + туман + свет). */
    _rebuildAll() {
        this._applyShaderUniforms();
        this._rebuildExtras();
        this._applyLighting(this._state.light, this._state.sunDir);
    }

    _rebuildExtras() {
        const s = this._state;
        if (s.mountains) {
            // Цвет гор подбираем под пресет (днём зеленовато-серый, ночью тёмный).
            const mc = s.stars ? '#2a3550' : '#8fa98a';
            this._buildMountains(mc);
        } else this._disposeMountains();
        this._buildStars(!!s.stars);
        this._buildClouds(s.clouds);
        this._applyFog(s.fog);
    }

    /** Вызывать каждый кадр: дрейф облаков + анимация fadeTo. */
    tick(dt) {
        // Дрейф облаков по кругу.
        for (const c of this._clouds) {
            c.mesh.position.x += c.speed * dt * 60;
            if (c.mesh.position.x > 380) c.mesh.position.x = -380;
        }
        // Анимация перехода неба.
        if (this._fade) {
            this._fade.t += dt;
            const k = Math.min(1, this._fade.t / this._fade.dur);
            const f = this._fade.from, t = this._fade.to, m = this._mat;
            const mix = (a, b) => [a[0] + (b[0] - a[0]) * k, a[1] + (b[1] - a[1]) * k, a[2] + (b[2] - a[2]) * k];
            if (m) {
                m.setColor3('topColor', Color3.FromArray(mix(f.top, t.top)));
                m.setColor3('bottomColor', Color3.FromArray(mix(f.bottom, t.bottom)));
                m.setColor3('horizonColor', Color3.FromArray(mix(f.horizon, t.horizon)));
                m.setColor3('sunColor', Color3.FromArray(mix(f.sunColor, t.sunColor)));
                const sd = mix(f.sunDir, t.sunDir);
                m.setVector3('sunDir', new Vector3(sd[0], sd[1], sd[2]).normalize());
                m.setFloat('sunSize', f.sunSize + (t.sunSize - f.sunSize) * k);
                m.setFloat('horizonHaze', f.haze + (t.haze - f.haze) * k);
                // Плавно ведём направление солнца (свет) к целевому (используем sd выше).
                if (this.sunLight) {
                    const d = new Vector3(-sd[0], -sd[1], -sd[2]);
                    if (d.lengthSquared() > 0) { d.normalize(); this.sunLight.direction = d; }
                }
            }
            // Плавно ведём яркость/ambient света.
            if (this._fade.lightFrom && this._fade.lightTo) {
                const lf = this._fade.lightFrom, lt = this._fade.lightTo;
                if (this.sunLight) {
                    this.sunLight.intensity = lf.sunInt + (lt.sunInt - lf.sunInt) * k;
                    if (lt.sunColor) this.sunLight.diffuse = Color3.FromArray(hexToRgb(lt.sunColor));
                }
                if (this.hemiLight) {
                    this.hemiLight.intensity = lf.hemiInt + (lt.hemiInt - lf.hemiInt) * k;
                    if (lt.ambient) this.hemiLight.groundColor = Color3.FromArray(hexToRgb(lt.ambient));
                }
            }
            if (k >= 1) {
                // Зафиксировать целевое состояние в _state (как hex).
                const tp = this._fade.target;
                Object.assign(this._state, {
                    top: tp.top, horizon: tp.horizon, bottom: tp.bottom,
                    sunColor: tp.sunColor, sunDir: tp.sunDir.slice(),
                    sunSize: tp.sunSize, haze: tp.haze,
                });
                this._fade = null;
            }
        }
    }

    serialize() {
        return { ...this._state, _active: true };
    }

    load(data) {
        if (!data) return;
        this._state = { ...this._defaultState(), ...data };
        this._rebuildAll();
    }

    dispose() {
        this._disposeMountains();
        this._disposeClouds();
        this._disposeStars();
        if (this._dome) { this._mat?.dispose(); this._dome.dispose(); this._dome = null; }
    }
}
