import {
  BackSide,
  Camera,
  Color,
  Group,
  Mesh,
  MeshBasicMaterial,
  PlaneGeometry,
  Scene,
  ShaderMaterial,
  SphereGeometry,
  Texture,
  Vector2,
  Vector3
} from "three";

import type { Vec3 } from "../core/vector";
import type {
  WorldBackgroundSettings,
  WorldSunLightSettings
} from "../document/world-settings";
import type { WorldShaderSkyRenderState } from "./world-shader-sky";

const BACKGROUND_SPHERE_RADIUS = 320;
const BACKGROUND_SPHERE_WIDTH_SEGMENTS = 48;
const BACKGROUND_SPHERE_HEIGHT_SEGMENTS = 24;
const DEFAULT_IMAGE_BACKGROUND_FALLBACK_COLOR = "#0d1116";
const NIGHT_BACKGROUND_EPSILON = 1e-4;
const MIN_CELESTIAL_BODY_INTENSITY = 1e-4;
const MIN_CELESTIAL_BODY_ALTITUDE = -0.02;
const CELESTIAL_BODY_DISTANCE = BACKGROUND_SPHERE_RADIUS - 6;
const SUN_CELESTIAL_BODY_SIZE = 28;
const MOON_CELESTIAL_BODY_SIZE = 20;

function clamp(value: number, min: number, max: number) {
  return Math.min(Math.max(value, min), max);
}

function lerp(left: number, right: number, amount: number) {
  return left + (right - left) * amount;
}

function resolveGradientColors(background: WorldBackgroundSettings) {
  if (background.mode === "solid") {
    return {
      topColorHex: background.colorHex,
      bottomColorHex: background.colorHex
    };
  }

  if (background.mode === "verticalGradient") {
    return {
      topColorHex: background.topColorHex,
      bottomColorHex: background.bottomColorHex
    };
  }

  return {
    topColorHex: DEFAULT_IMAGE_BACKGROUND_FALLBACK_COLOR,
    bottomColorHex: DEFAULT_IMAGE_BACKGROUND_FALLBACK_COLOR
  };
}

export interface WorldBackgroundOverlayState {
  texture: Texture | null;
  opacity: number;
  environmentIntensity: number;
}

export interface WorldEnvironmentBlendTextureResolver {
  resolveBlendTexture(
    baseTexture: Texture,
    overlayTexture: Texture,
    blendAmount: number
  ): Texture | null;
}

export interface WorldShaderSkyEnvironmentTextureResolver {
  resolveEnvironmentTexture(state: WorldShaderSkyRenderState): Texture | null;
}

export interface WorldEnvironmentState {
  texture: Texture | null;
  intensity: number;
}

export interface WorldCelestialBodyState {
  colorHex: string;
  direction: Vec3;
  intensity: number;
  size: number;
}

export interface WorldCelestialBodiesState {
  sun: WorldCelestialBodyState | null;
  moon: WorldCelestialBodyState | null;
}

export function resolveWorldEnvironmentState(
  background: WorldBackgroundSettings,
  backgroundTexture: Texture | null,
  overlay: WorldBackgroundOverlayState | null,
  environmentBlendTextureResolver: WorldEnvironmentBlendTextureResolver | null = null,
  shaderSkyState: WorldShaderSkyRenderState | null = null,
  shaderSkyEnvironmentTextureResolver: WorldShaderSkyEnvironmentTextureResolver | null = null
): WorldEnvironmentState {
  if (background.mode === "shader") {
    return {
      texture:
        shaderSkyState === null
          ? null
          : (shaderSkyEnvironmentTextureResolver?.resolveEnvironmentTexture(
              shaderSkyState
            ) ?? null),
      intensity: 1
    };
  }

  const baseTexture = background.mode === "image" ? backgroundTexture : null;
  const baseIntensity =
    background.mode === "image" ? background.environmentIntensity : 0;
  const overlayTexture = overlay?.texture ?? null;
  const overlayOpacity = clamp(overlay?.opacity ?? 0, 0, 1);
  const overlayIntensity = overlay?.environmentIntensity ?? 0;

  if (
    baseTexture !== null &&
    overlayTexture !== null &&
    overlayOpacity > NIGHT_BACKGROUND_EPSILON &&
    overlayOpacity < 1 - NIGHT_BACKGROUND_EPSILON
  ) {
    const blendedTexture =
      environmentBlendTextureResolver?.resolveBlendTexture(
        baseTexture,
        overlayTexture,
        overlayOpacity
      ) ?? baseTexture;

    return {
      texture: blendedTexture,
      intensity: lerp(baseIntensity, overlayIntensity, overlayOpacity)
    };
  }

  if (overlayTexture !== null && overlayOpacity > NIGHT_BACKGROUND_EPSILON) {
    if (baseTexture === null) {
      return {
        texture: overlayTexture,
        intensity: overlayIntensity * overlayOpacity
      };
    }

    if (overlayOpacity >= 1 - NIGHT_BACKGROUND_EPSILON) {
      return {
        texture: overlayTexture,
        intensity: overlayIntensity
      };
    }
  }

  if (baseTexture !== null) {
    return {
      texture: baseTexture,
      intensity: baseIntensity
    };
  }

  if (overlayTexture !== null && overlayOpacity > NIGHT_BACKGROUND_EPSILON) {
    return {
      texture: overlayTexture,
      intensity: overlayIntensity * overlayOpacity
    };
  }

  return {
    texture: null,
    intensity: 1
  };
}

function normalizeDirection(direction: Vec3): Vec3 | null {
  const length = Math.hypot(direction.x, direction.y, direction.z);

  if (length <= 1e-6) {
    return null;
  }

  return {
    x: direction.x / length,
    y: direction.y / length,
    z: direction.z / length
  };
}

function resolveWorldCelestialBodyState(
  light: WorldSunLightSettings | null,
  size: number
): WorldCelestialBodyState | null {
  if (light === null || light.intensity <= MIN_CELESTIAL_BODY_INTENSITY) {
    return null;
  }

  const direction = normalizeDirection(light.direction);

  if (direction === null || direction.y < MIN_CELESTIAL_BODY_ALTITUDE) {
    return null;
  }

  return {
    colorHex: light.colorHex,
    direction,
    intensity: light.intensity,
    size
  };
}

export function resolveWorldCelestialBodiesState(
  showCelestialBodies: boolean,
  sunLight: WorldSunLightSettings,
  moonLight: WorldSunLightSettings | null
): WorldCelestialBodiesState {
  if (!showCelestialBodies) {
    return {
      sun: null,
      moon: null
    };
  }

  return {
    sun: resolveWorldCelestialBodyState(sunLight, SUN_CELESTIAL_BODY_SIZE),
    moon: resolveWorldCelestialBodyState(moonLight, MOON_CELESTIAL_BODY_SIZE)
  };
}

const GRADIENT_VERTEX_SHADER = `
varying vec3 vWorldPosition;

void main() {
  vec4 worldPosition = modelMatrix * vec4(position, 1.0);
  vWorldPosition = worldPosition.xyz;
  gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`;

const GRADIENT_FRAGMENT_SHADER = `
uniform vec3 uTopColor;
uniform vec3 uBottomColor;
varying vec3 vWorldPosition;

void main() {
  vec3 direction = normalize(vWorldPosition - cameraPosition);
  float gradientAmount = clamp(direction.y * 0.5 + 0.5, 0.0, 1.0);
  vec3 color = mix(uBottomColor, uTopColor, gradientAmount);
gl_FragColor = vec4(color, 1.0);
}
`;

const DEFAULT_SKY_FRAGMENT_SHADER = `
uniform vec3 uSkyTopColor;
uniform vec3 uSkyBottomColor;
uniform float uHorizonHeight;
uniform vec3 uSunDirection;
uniform vec3 uSunColor;
uniform float uSunIntensity;
uniform float uSunDiscSizeDegrees;
uniform float uSunVisible;
uniform vec3 uMoonDirection;
uniform vec3 uMoonColor;
uniform float uMoonIntensity;
uniform float uMoonDiscSizeDegrees;
uniform float uMoonVisible;
uniform float uDaylightFactor;
uniform float uTwilightFactor;
uniform float uStarDensity;
uniform float uStarBrightness;
uniform float uStarVisibility;
uniform float uStarHorizonFadeOffset;
uniform vec3 uStarRotationAxis;
uniform float uStarRotationRadians;
uniform float uCloudCoverage;
uniform float uCloudDensity;
uniform float uCloudSoftness;
uniform float uCloudScale;
uniform float uCloudHeight;
uniform float uCloudHeightVariation;
uniform vec3 uCloudTint;
uniform float uCloudOpacity;
uniform float uCloudOpacityRandomness;
uniform vec2 uCloudDriftOffset;
uniform float uAuroraVisibility;
uniform float uAuroraIntensity;
uniform float uAuroraHeight;
uniform float uAuroraThickness;
uniform float uAuroraSpeed;
uniform vec3 uAuroraPrimaryColor;
uniform vec3 uAuroraSecondaryColor;
uniform float uAuroraRotationRadians;
uniform float uAuroraTimeHours;
varying vec3 vWorldPosition;

float hash12(vec2 point) {
  return fract(sin(dot(point, vec2(127.1, 311.7))) * 43758.5453123);
}

float hash13(vec3 point) {
  return fract(sin(dot(point, vec3(127.1, 311.7, 74.7))) * 43758.5453123);
}

float noise2(vec2 point) {
  vec2 cell = floor(point);
  vec2 local = fract(point);
  vec2 blend = local * local * (3.0 - 2.0 * local);
  float a = hash12(cell);
  float b = hash12(cell + vec2(1.0, 0.0));
  float c = hash12(cell + vec2(0.0, 1.0));
  float d = hash12(cell + vec2(1.0, 1.0));
  float x1 = mix(a, b, blend.x);
  float x2 = mix(c, d, blend.x);

  return mix(x1, x2, blend.y);
}

float noise3(vec3 point) {
  vec3 cell = floor(point);
  vec3 local = fract(point);
  vec3 blend = local * local * (3.0 - 2.0 * local);
  float a = hash13(cell);
  float b = hash13(cell + vec3(1.0, 0.0, 0.0));
  float c = hash13(cell + vec3(0.0, 1.0, 0.0));
  float d = hash13(cell + vec3(1.0, 1.0, 0.0));
  float e = hash13(cell + vec3(0.0, 0.0, 1.0));
  float f = hash13(cell + vec3(1.0, 0.0, 1.0));
  float g = hash13(cell + vec3(0.0, 1.0, 1.0));
  float h = hash13(cell + vec3(1.0, 1.0, 1.0));
  float x1 = mix(a, b, blend.x);
  float x2 = mix(c, d, blend.x);
  float y1 = mix(x1, x2, blend.y);
  float x3 = mix(e, f, blend.x);
  float x4 = mix(g, h, blend.x);
  float y2 = mix(x3, x4, blend.y);

  return mix(y1, y2, blend.z);
}

float fbm2(vec2 point) {
  float value = 0.0;
  float amplitude = 0.5;

  for (int octave = 0; octave < 5; octave++) {
    value += noise2(point) * amplitude;
    point = point * 2.03 + vec2(17.3, 9.1);
    amplitude *= 0.5;
  }

  return value;
}

float fbm3(vec3 point) {
  float value = 0.0;
  float amplitude = 0.5;

  for (int octave = 0; octave < 4; octave++) {
    value += noise3(point) * amplitude;
    point = point * 2.11 + vec3(13.7, 8.3, 19.1);
    amplitude *= 0.5;
  }

  return value;
}

float auroraRayPattern(float x, float timeShift, float phase) {
  float field = fbm2(
    vec2(
      x * 0.55 + phase + timeShift * 0.03,
      x * 0.12 - timeShift * 0.012
    )
  );
  float slowFold =
    0.5 +
    0.5 *
    sin(x * 12.0 + field * 5.2 + phase + timeShift * 0.42);
  float ribbonFold =
    0.5 +
    0.5 *
    sin(x * 21.0 + field * 7.2 + phase * 1.4 + timeShift * 0.42);
  float filament =
    0.5 +
    0.5 *
    sin(x * 34.0 + field * 7.0 + phase * 1.9 + timeShift * 0.58);
  float gaps = smoothstep(
    0.18,
    0.92,
    fbm2(vec2(x * 1.2 - timeShift * 0.025 + phase, x * 0.08 + 6.0))
  );

  return gaps *
    (pow(slowFold, 1.9) * 0.72 +
      pow(ribbonFold, 3.1) * 0.48 +
      pow(filament, 7.0) * 0.36);
}

mat3 rotationY(float radians) {
  float sine = sin(radians);
  float cosine = cos(radians);

  return mat3(
    cosine, 0.0, -sine,
    0.0, 1.0, 0.0,
    sine, 0.0, cosine
  );
}

vec3 rotateAroundAxis(vec3 value, vec3 axis, float radians) {
  vec3 normalizedAxis = normalize(axis);
  float sine = sin(radians);
  float cosine = cos(radians);

  return value * cosine +
    cross(normalizedAxis, value) * sine +
    normalizedAxis * dot(normalizedAxis, value) * (1.0 - cosine);
}

float discMask(vec3 direction, vec3 lightDirection, float sizeDegrees, float featherScale) {
  float sizeRadians = radians(max(sizeDegrees, 0.01));
  float alignment = dot(direction, normalize(lightDirection));
  float outerCos = cos(sizeRadians * 1.6);
  float innerCos = cos(sizeRadians * max(featherScale, 0.18));

  return smoothstep(outerCos, innerCos, alignment);
}

float glowMask(vec3 direction, vec3 lightDirection, float sizeDegrees, float radiusScale) {
  float sizeRadians = radians(max(sizeDegrees, 0.01) * max(radiusScale, 1.0));
  float alignment = dot(direction, normalize(lightDirection));
  float outerCos = cos(sizeRadians * 1.8);
  float innerCos = cos(sizeRadians * 0.55);

  return smoothstep(outerCos, innerCos, alignment);
}

vec3 starTint(float seed) {
  vec3 cool = vec3(0.58, 0.7, 1.0);
  vec3 warm = vec3(1.0, 0.9, 0.64);
  vec3 violet = vec3(0.92, 0.68, 1.0);
  vec3 base = mix(cool, warm, smoothstep(0.18, 0.82, seed));

  return mix(base, violet, smoothstep(0.84, 1.0, seed) * 0.46);
}

vec3 starLayer(vec3 direction, float scale, float densityThreshold, float radius) {
  vec3 scaledDirection = direction * scale;
  vec3 cell = floor(scaledDirection);
  vec3 local = fract(scaledDirection) - 0.5;
  float seed = hash13(cell);
  float distanceToCenter = length(local);
  float star = smoothstep(radius, 0.0, distanceToCenter);
  float core = smoothstep(radius * 0.34, 0.0, distanceToCenter);
  float visible = step(densityThreshold, seed);
  vec3 tint = starTint(hash13(cell + vec3(19.0, 7.0, 41.0)));
  float sparkle = mix(0.48, 1.28, hash13(cell + 17.0));

  return visible * tint * (star * sparkle + core * 0.68);
}

vec2 projectCloudUv(
  vec3 direction,
  float horizonHeight,
  float cloudHeight,
  float layerOffset,
  vec2 driftOffset
) {
  float projectedHeight =
    max(
      direction.y -
        horizonHeight +
        mix(0.24, 0.86, clamp(cloudHeight, 0.0, 1.0)) +
        layerOffset,
      0.08
    );

  return direction.xz / projectedHeight + driftOffset;
}

void main() {
  vec3 direction = normalize(vWorldPosition - cameraPosition);
  float shiftedY = clamp(direction.y - uHorizonHeight, -1.0, 1.0);
  float skyMix = clamp(shiftedY * 0.5 + 0.5, 0.0, 1.0);
  skyMix = pow(skyMix, 0.72);

  vec3 skyColor = mix(uSkyBottomColor, uSkyTopColor, skyMix);
  float horizonMask = pow(clamp(1.0 - abs(shiftedY), 0.0, 1.0), 2.6);
  skyColor += mix(uSkyBottomColor, vec3(1.0), 0.1 + uTwilightFactor * 0.18) * horizonMask * 0.04;

  float sunHorizonFade = smoothstep(uHorizonHeight - 0.14, uHorizonHeight + 0.03, uSunDirection.y);
  float moonHorizonFade = smoothstep(uHorizonHeight - 0.14, uHorizonHeight + 0.03, uMoonDirection.y);
  float sunDisc = uSunVisible * sunHorizonFade * discMask(direction, uSunDirection, uSunDiscSizeDegrees, 0.42);
  float sunGlow = uSunVisible * sunHorizonFade * glowMask(direction, uSunDirection, uSunDiscSizeDegrees, 4.8);
  float moonDisc = uMoonVisible * moonHorizonFade * discMask(direction, uMoonDirection, uMoonDiscSizeDegrees, 0.5);
  float moonGlow = uMoonVisible * moonHorizonFade * glowMask(direction, uMoonDirection, uMoonDiscSizeDegrees, 5.6);
  float sunAtmosphere = uSunVisible * sunHorizonFade * glowMask(direction, uSunDirection, uSunDiscSizeDegrees, 12.0);
  float moonAtmosphere = uMoonVisible * moonHorizonFade * glowMask(direction, uMoonDirection, uMoonDiscSizeDegrees, 10.0);

  vec3 rotatedStarDirection =
    normalize(rotateAroundAxis(direction, uStarRotationAxis, uStarRotationRadians));
  float starDensity = clamp(uStarDensity, 0.0, 2.0);
  vec3 starLayerA = starLayer(
    rotatedStarDirection,
    mix(120.0, 380.0, clamp(starDensity * 0.64, 0.0, 1.0)),
    mix(0.995, 0.91, clamp(starDensity, 0.0, 1.0)),
    0.145
  );
  vec3 starLayerB = starLayer(
    normalize(rotateAroundAxis(direction, uStarRotationAxis, uStarRotationRadians + 1.618)),
    mix(230.0, 700.0, clamp(starDensity * 0.56, 0.0, 1.0)),
    mix(0.999, 0.955, clamp(starDensity * 0.85, 0.0, 1.0)),
    0.12
  );
  vec3 heroStars = starLayer(
    normalize(rotateAroundAxis(direction, uStarRotationAxis, uStarRotationRadians - 0.93)),
    mix(52.0, 130.0, clamp(starDensity * 0.7, 0.0, 1.0)),
    mix(0.9992, 0.985, clamp(starDensity, 0.0, 1.0)),
    0.19
  );
  float starTwinkle = noise3(rotatedStarDirection * 24.0 + vec3(uTwilightFactor * 17.0, uStarRotationRadians * 0.5, 9.0));
  vec3 stars = (starLayerA * 0.74 + starLayerB * 1.08 + heroStars * 1.65) *
    mix(0.82, 1.2, starTwinkle);
  float starHorizonLine = uHorizonHeight + uStarHorizonFadeOffset;
  float starHorizonFade = smoothstep(starHorizonLine - 0.08, starHorizonLine + 0.12, direction.y);
  vec3 galacticNormal = normalize(vec3(0.08, 0.46, 0.89));
  float galacticLatitude = abs(dot(rotatedStarDirection, galacticNormal));
  float galaxyWide = 1.0 - smoothstep(0.02, 0.38, galacticLatitude);
  float galaxyCore = 1.0 - smoothstep(0.0, 0.13, galacticLatitude);
  float galaxyNoise = fbm3(rotatedStarDirection * 5.2 + vec3(1.2, 6.1, 2.4));
  float galaxyClumps = smoothstep(0.38, 0.82, galaxyNoise) * galaxyWide;
  float dustLane =
    smoothstep(
      0.48,
      0.78,
      fbm3(rotatedStarDirection * 18.0 + vec3(11.0, 3.0, 7.0))
    ) *
    (1.0 - smoothstep(0.04, 0.2, galacticLatitude));
  float galaxyBulge =
    smoothstep(
      0.74,
      1.0,
      dot(rotatedStarDirection, normalize(vec3(-0.42, 0.14, 0.9)))
    ) *
    galaxyWide;
  float galaxy = max(galaxyCore * 0.38, galaxyClumps) + galaxyBulge * 0.62;
  galaxy *= 1.0 - dustLane * 0.64;
  vec3 galaxyColor = mix(
    vec3(0.15, 0.34, 0.95),
    vec3(1.0, 0.68, 0.95),
    smoothstep(0.34, 0.86, galaxyNoise)
  );
  vec3 nebulaColor = mix(
    vec3(0.08, 0.72, 1.0),
    vec3(1.0, 0.28, 0.82),
    fbm3(rotatedStarDirection * 3.0 + vec3(4.0, 9.0, 1.0))
  );
  float nebula =
    galaxyWide *
    pow(
      smoothstep(
        0.52,
        0.92,
        fbm3(rotatedStarDirection * 3.7 + vec3(8.0, 2.0, 13.0))
      ),
      2.2
    );
  vec3 nightLift = mix(vec3(0.01, 0.025, 0.08), vec3(0.04, 0.08, 0.2), skyMix);
  skyColor += nightLift * uStarVisibility * starHorizonFade * 0.2;
  skyColor += stars * uStarBrightness * uStarVisibility * starHorizonFade;
  skyColor += galaxyColor * galaxy * uStarBrightness * uStarVisibility * starHorizonFade * 0.42;
  skyColor += nebulaColor * nebula * uStarBrightness * uStarVisibility * starHorizonFade * 0.16;

  float cloudScale = max(uCloudScale, 0.01);
  float cloudDensity = clamp(uCloudDensity, 0.0, 2.0);
  vec2 driftA = uCloudDriftOffset * 0.55;
  vec2 driftB =
    vec2(-uCloudDriftOffset.y, uCloudDriftOffset.x) * 0.32 +
    uCloudDriftOffset * 0.35;
  vec2 cloudUvA =
    projectCloudUv(direction, uHorizonHeight, uCloudHeight, 0.12, driftA) *
    (0.28 + cloudScale * 0.48);
  vec2 cloudUvB =
    projectCloudUv(direction, uHorizonHeight, uCloudHeight, 0.28, driftB) *
    (0.54 + cloudScale * 0.82);
  float layerA = fbm2(cloudUvA);
  float layerB = fbm2(cloudUvB * 1.37 + vec2(17.0, 11.0));
  float layerC = noise2(cloudUvA * 2.9 + vec2(5.0, 3.0));
  float cloudShape =
    mix(
      layerA,
      layerA * 0.6 + layerB * 0.4,
      clamp(cloudDensity / 1.35, 0.0, 1.0)
    );
  cloudShape = mix(cloudShape, cloudShape * 0.76 + layerC * 0.24, 0.42);

  float bandCenter = mix(-0.15, 0.85, clamp(uCloudHeight, 0.0, 1.0)) + uHorizonHeight;
  float bandNoise =
    (noise2(cloudUvB * 0.2 + vec2(23.0, 7.0)) - 0.5) *
    2.0 *
    clamp(uCloudHeightVariation, 0.0, 1.0);
  float bandDistance = abs(direction.y - (bandCenter + bandNoise * 0.45));
  float bandWidth = mix(0.7, 0.34, clamp(cloudDensity / 1.4, 0.0, 1.0));
  float bandMask = 1.0 - smoothstep(bandWidth, bandWidth + 0.18, bandDistance);
  float coverageThreshold = mix(0.94, 0.12, clamp(uCloudCoverage, 0.0, 1.0));
  float softness = mix(0.01, 0.22, clamp(uCloudSoftness, 0.0, 1.0));
  float opacityNoise =
    mix(
      1.0,
      noise2(cloudUvA * 1.8 + vec2(31.0, 13.0)),
      clamp(uCloudOpacityRandomness, 0.0, 1.0)
    );
  float cloudHorizonFade = smoothstep(
    uHorizonHeight - 0.16,
    uHorizonHeight + 0.08,
    direction.y
  );
  float clouds = smoothstep(coverageThreshold - softness, coverageThreshold + softness, cloudShape + bandMask * 0.22);
  clouds *= bandMask * cloudHorizonFade;
  clouds *= clamp(uCloudOpacity, 0.0, 1.0) * mix(0.82, 1.0, opacityNoise);

  vec3 cloudLight = mix(mix(uSkyBottomColor, uSkyTopColor, 0.62), vec3(1.0), 0.12 + uDaylightFactor * 0.18 + uTwilightFactor * 0.08);
  cloudLight += uSunColor * sunGlow * (0.16 + uTwilightFactor * 0.12);
  cloudLight += uMoonColor * moonGlow * 0.04 * (1.0 - uDaylightFactor);
  vec3 cloudColor = mix(cloudLight, uCloudTint, 0.56);
  skyColor = mix(skyColor, cloudColor, clamp(clouds, 0.0, 1.0));

  if (uAuroraVisibility > 0.001) {
    vec3 auroraDirection = normalize(rotationY(uAuroraRotationRadians) * direction);
    float auroraTime = uAuroraTimeHours * max(uAuroraSpeed, 0.0);
    float planeDepth = max(auroraDirection.z, 0.12);
    float planeX = auroraDirection.x / planeDepth;
    float planeY = auroraDirection.y / planeDepth;
    float horizonFade = smoothstep(
      uHorizonHeight - 0.03,
      uHorizonHeight + 0.2,
      direction.y
    );
    float zenithFade = 1.0 - smoothstep(0.92, 1.0, direction.y);
    float forwardMask = smoothstep(0.04, 0.3, auroraDirection.z);
    float sideFalloff = 1.0 - smoothstep(1.22, 2.18, abs(planeX));
    float authoredHeight = clamp(uAuroraHeight, 0.0, 1.0);
    float authoredThickness = clamp(uAuroraThickness, 0.0, 1.0);
    float lowerEdge = uHorizonHeight * 0.18 + 0.04;
    float upperEdge = lowerEdge + mix(0.62, 1.58, authoredHeight);
    float thicknessWidth = mix(0.18, 0.52, authoredThickness);
    float verticalStart = smoothstep(lowerEdge, lowerEdge + thicknessWidth, planeY);
    float verticalEnd =
      1.0 -
      smoothstep(upperEdge, upperEdge + thicknessWidth * 1.45, planeY);
    float verticalVeil = verticalStart * verticalEnd;
    float topDrape =
      1.0 -
      smoothstep(
        thicknessWidth * 0.25,
        thicknessWidth * 1.65,
        abs(planeY - upperEdge)
      );
    float bottomGlow =
      1.0 -
      smoothstep(
        lowerEdge + thicknessWidth * 0.4,
        lowerEdge + thicknessWidth * 3.4,
        planeY
      );
    float layerA = auroraRayPattern(planeX * 1.2, auroraTime, 0.0);
    float layerB = auroraRayPattern(planeX * 1.42 + 1.7, auroraTime * 0.72 + 9.0, 2.6);
    float layerC = auroraRayPattern(planeX * 0.88 - 2.4, auroraTime * 0.48 + 15.0, 5.4);
    float rayField = layerA * 0.72 + layerB * 0.46 + layerC * 0.28;
    float veilTurbulence =
      0.76 +
      0.24 *
      fbm2(vec2(planeX * 0.7 + auroraTime * 0.018, planeY * 0.52 + 4.0));
    float softSkyBloom =
      verticalVeil *
      fbm2(vec2(planeX * 0.3 + auroraTime * 0.012, planeY * 0.34 + 4.0));
    float auroraStrength =
      (rayField * verticalVeil * veilTurbulence + topDrape * rayField * 0.18 + bottomGlow * layerA * 0.08 + softSkyBloom * 0.18) *
      forwardMask *
      sideFalloff *
      horizonFade *
      zenithFade;
    float colorNoise = fbm2(
      vec2(
        planeX * 0.82 - auroraTime * 0.018,
        planeY * 1.2 + 0.45
      )
    );
    float auroraColorMix =
      clamp(
        smoothstep(lowerEdge + thicknessWidth * 0.7, upperEdge, planeY) *
          0.72 +
          colorNoise * 0.34,
        0.0,
        1.0
      );
    vec3 auroraColor = mix(
      uAuroraPrimaryColor,
      uAuroraSecondaryColor,
      auroraColorMix
    );
    vec3 innerFire = mix(vec3(0.78, 1.0, 0.82), uAuroraPrimaryColor, 0.5);
    float cloudOcclusion = 1.0 - clamp(clouds * 0.58, 0.0, 0.72);
    float auroraEnergy = uAuroraVisibility * uAuroraIntensity * cloudOcclusion;
    skyColor += auroraColor * auroraStrength * auroraEnergy * 0.72;
    skyColor += innerFire * rayField * verticalVeil * auroraEnergy * 0.16;
    skyColor += auroraColor * softSkyBloom * auroraEnergy * 0.18;
  }

  skyColor +=
    uSunColor *
    (sunAtmosphere * (0.03 + uTwilightFactor * 0.18) +
      sunGlow * (0.16 + uTwilightFactor * 0.32) +
      sunDisc * (0.75 + min(uSunIntensity, 3.5) * 0.25));
  skyColor +=
    uMoonColor *
    (moonAtmosphere * 0.015 * (1.0 - uDaylightFactor) +
      moonGlow * 0.12 +
      moonDisc * 0.35 * (0.35 + min(uMoonIntensity, 2.0) * 0.65));
  skyColor = clamp(skyColor, 0.0, 1.0);

  gl_FragColor = vec4(skyColor, 1.0);
}
`;

const CELESTIAL_BODY_VERTEX_SHADER = `
varying vec2 vUv;

void main() {
  vUv = uv;
  gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`;

const SUN_FRAGMENT_SHADER = `
uniform vec3 uColor;
uniform float uIntensity;
varying vec2 vUv;

void main() {
  vec2 centeredUv = vUv * 2.0 - 1.0;
  float distanceFromCenter = length(centeredUv);
  float disc = smoothstep(0.52, 0.18, distanceFromCenter);
  float core = smoothstep(0.26, 0.0, distanceFromCenter);
  float halo = smoothstep(1.0, 0.28, distanceFromCenter);
  float glow = clamp(0.18 + min(uIntensity, 4.0) * 0.18, 0.18, 0.95);
  vec3 warmCore = mix(uColor, vec3(1.0, 0.97, 0.88), 0.45);
  vec3 color =
    warmCore * (disc * 1.15 + core * 0.8) +
    uColor * halo * glow * 0.55;
  float alpha = disc * 0.82 + core * 0.28 + halo * glow * 0.22;

  if (alpha <= 0.001) {
    discard;
  }

  gl_FragColor = vec4(color, alpha);
}
`;

const MOON_FRAGMENT_SHADER = `
uniform vec3 uColor;
uniform float uIntensity;
varying vec2 vUv;

float hash(vec2 point) {
  return fract(sin(dot(point, vec2(127.1, 311.7))) * 43758.5453123);
}

float noise(vec2 point) {
  vec2 cell = floor(point);
  vec2 local = fract(point);
  vec2 blend = local * local * (3.0 - 2.0 * local);

  float a = hash(cell);
  float b = hash(cell + vec2(1.0, 0.0));
  float c = hash(cell + vec2(0.0, 1.0));
  float d = hash(cell + vec2(1.0, 1.0));

  return mix(mix(a, b, blend.x), mix(c, d, blend.x), blend.y);
}

void main() {
  vec2 centeredUv = vUv * 2.0 - 1.0;
  float distanceFromCenter = length(centeredUv);
  float disc = smoothstep(0.52, 0.44, distanceFromCenter);
  float body = smoothstep(0.48, 0.0, distanceFromCenter);
  float halo = smoothstep(0.92, 0.32, distanceFromCenter);
  float craterNoise = noise(centeredUv * 6.0 + vec2(17.0, 9.0));
  float mariaNoise = noise(centeredUv * 3.0 + vec2(3.0, 5.0));
  float surfaceVariation = mix(0.86, 1.04, craterNoise * 0.65 + mariaNoise * 0.35);
  float glow = clamp(0.12 + min(uIntensity, 2.0) * 0.16, 0.12, 0.42);
  vec3 moonColor = mix(uColor, vec3(1.0, 1.0, 1.0), 0.35) * surfaceVariation;
  vec3 color = moonColor * (body * 1.08 + disc * 0.18) + uColor * halo * glow * 0.18;
  float alpha = disc * 0.82 + halo * glow * 0.12;

  if (alpha <= 0.001) {
    discard;
  }

  gl_FragColor = vec4(color, alpha);
}
`;

function createCelestialBodyMaterial(fragmentShader: string) {
  return new ShaderMaterial({
    uniforms: {
      uColor: {
        value: new Color("#ffffff")
      },
      uIntensity: {
        value: 0
      }
    },
    vertexShader: CELESTIAL_BODY_VERTEX_SHADER,
    fragmentShader,
    depthTest: false,
    depthWrite: false,
    fog: false,
    transparent: true
  });
}

function createShaderSkyMaterial() {
  return new ShaderMaterial({
    uniforms: {
      uSkyTopColor: {
        value: new Color(DEFAULT_IMAGE_BACKGROUND_FALLBACK_COLOR)
      },
      uSkyBottomColor: {
        value: new Color(DEFAULT_IMAGE_BACKGROUND_FALLBACK_COLOR)
      },
      uHorizonHeight: {
        value: 0
      },
      uSunDirection: {
        value: new Vector3(0, 1, 0)
      },
      uSunColor: {
        value: new Color("#ffffff")
      },
      uSunIntensity: {
        value: 0
      },
      uSunDiscSizeDegrees: {
        value: 2.6
      },
      uSunVisible: {
        value: 0
      },
      uMoonDirection: {
        value: new Vector3(0, 1, 0)
      },
      uMoonColor: {
        value: new Color("#ffffff")
      },
      uMoonIntensity: {
        value: 0
      },
      uMoonDiscSizeDegrees: {
        value: 1.8
      },
      uMoonVisible: {
        value: 0
      },
      uDaylightFactor: {
        value: 1
      },
      uTwilightFactor: {
        value: 0
      },
      uStarDensity: {
        value: 0.5
      },
      uStarBrightness: {
        value: 0.75
      },
      uStarVisibility: {
        value: 0
      },
      uStarHorizonFadeOffset: {
        value: 0
      },
      uStarRotationAxis: {
        value: new Vector3(0, 1, 0)
      },
      uStarRotationRadians: {
        value: 0
      },
      uCloudCoverage: {
        value: 0.55
      },
      uCloudDensity: {
        value: 0.6
      },
      uCloudSoftness: {
        value: 0.4
      },
      uCloudScale: {
        value: 1.2
      },
      uCloudHeight: {
        value: 0.6
      },
      uCloudHeightVariation: {
        value: 0.2
      },
      uCloudTint: {
        value: new Color("#ffffff")
      },
      uCloudOpacity: {
        value: 0.65
      },
      uCloudOpacityRandomness: {
        value: 0.2
      },
      uCloudDriftOffset: {
        value: new Vector2(0, 0)
      },
      uAuroraVisibility: {
        value: 0
      },
      uAuroraIntensity: {
        value: 1
      },
      uAuroraHeight: {
        value: 0.66
      },
      uAuroraThickness: {
        value: 0.42
      },
      uAuroraSpeed: {
        value: 0.12
      },
      uAuroraPrimaryColor: {
        value: new Color("#6df7d0")
      },
      uAuroraSecondaryColor: {
        value: new Color("#6e8dff")
      },
      uAuroraRotationRadians: {
        value: 0
      },
      uAuroraTimeHours: {
        value: 0
      }
    },
    vertexShader: GRADIENT_VERTEX_SHADER,
    fragmentShader: DEFAULT_SKY_FRAGMENT_SHADER,
    side: BackSide,
    depthTest: false,
    depthWrite: false,
    fog: false
  });
}

function applyShaderSkyStateToMaterial(
  material: ShaderMaterial,
  state: WorldShaderSkyRenderState | null
) {
  if (state === null) {
    material.uniforms.uHorizonHeight.value = 0;
    material.uniforms.uStarVisibility.value = 0;
    material.uniforms.uStarHorizonFadeOffset.value = 0;
    material.uniforms.uStarRotationAxis.value.set(0, 1, 0);
    material.uniforms.uStarRotationRadians.value = 0;
    material.uniforms.uSunVisible.value = 0;
    material.uniforms.uMoonVisible.value = 0;
    material.uniforms.uAuroraVisibility.value = 0;
    return;
  }

  material.uniforms.uSkyTopColor.value.set(state.sky.topColorHex);
  material.uniforms.uSkyBottomColor.value.set(state.sky.bottomColorHex);
  material.uniforms.uHorizonHeight.value = state.sky.horizonHeight;
  material.uniforms.uSunDirection.value.set(
    state.celestial.sunDirection.x,
    state.celestial.sunDirection.y,
    state.celestial.sunDirection.z
  );
  material.uniforms.uSunColor.value.set(state.celestial.sunColorHex);
  material.uniforms.uSunIntensity.value = state.celestial.sunIntensity;
  material.uniforms.uSunDiscSizeDegrees.value =
    state.celestial.sunDiscSizeDegrees;
  material.uniforms.uSunVisible.value = state.celestial.sunVisible ? 1 : 0;
  material.uniforms.uMoonDirection.value.set(
    state.celestial.moonDirection.x,
    state.celestial.moonDirection.y,
    state.celestial.moonDirection.z
  );
  material.uniforms.uMoonColor.value.set(state.celestial.moonColorHex);
  material.uniforms.uMoonIntensity.value = state.celestial.moonIntensity;
  material.uniforms.uMoonDiscSizeDegrees.value =
    state.celestial.moonDiscSizeDegrees;
  material.uniforms.uMoonVisible.value = state.celestial.moonVisible ? 1 : 0;
  material.uniforms.uDaylightFactor.value = state.time.daylightFactor;
  material.uniforms.uTwilightFactor.value = state.time.twilightFactor;
  material.uniforms.uStarDensity.value = state.stars.density;
  material.uniforms.uStarBrightness.value = state.stars.brightness;
  material.uniforms.uStarVisibility.value = state.stars.visibility;
  material.uniforms.uStarHorizonFadeOffset.value =
    state.stars.horizonFadeOffset;
  material.uniforms.uStarRotationAxis.value.set(
    state.stars.rotationAxis.x,
    state.stars.rotationAxis.y,
    state.stars.rotationAxis.z
  );
  material.uniforms.uStarRotationRadians.value = state.stars.rotationRadians;
  material.uniforms.uCloudCoverage.value = state.clouds.coverage;
  material.uniforms.uCloudDensity.value = state.clouds.density;
  material.uniforms.uCloudSoftness.value = state.clouds.softness;
  material.uniforms.uCloudScale.value = state.clouds.scale;
  material.uniforms.uCloudHeight.value = state.clouds.height;
  material.uniforms.uCloudHeightVariation.value = state.clouds.heightVariation;
  material.uniforms.uCloudTint.value.set(state.clouds.tintHex);
  material.uniforms.uCloudOpacity.value = state.clouds.opacity;
  material.uniforms.uCloudOpacityRandomness.value =
    state.clouds.opacityRandomness;
  material.uniforms.uCloudDriftOffset.value.set(
    state.clouds.driftOffset.x,
    state.clouds.driftOffset.y
  );
  material.uniforms.uAuroraVisibility.value = state.aurora.visibility;
  material.uniforms.uAuroraIntensity.value = state.aurora.intensity;
  material.uniforms.uAuroraHeight.value = state.aurora.height;
  material.uniforms.uAuroraThickness.value = state.aurora.thickness;
  material.uniforms.uAuroraSpeed.value = state.aurora.speed;
  material.uniforms.uAuroraPrimaryColor.value.set(state.aurora.primaryColorHex);
  material.uniforms.uAuroraSecondaryColor.value.set(
    state.aurora.secondaryColorHex
  );
  material.uniforms.uAuroraRotationRadians.value = state.aurora.rotationRadians;
  material.uniforms.uAuroraTimeHours.value = state.aurora.timeHours;
}

export class WorldBackgroundRenderer {
  readonly scene = new Scene();
  readonly environmentCaptureScene = new Scene();

  private readonly anchor = new Group();
  private readonly environmentCaptureAnchor = new Group();
  private readonly geometry = new SphereGeometry(
    BACKGROUND_SPHERE_RADIUS,
    BACKGROUND_SPHERE_WIDTH_SEGMENTS,
    BACKGROUND_SPHERE_HEIGHT_SEGMENTS
  );
  private readonly gradientMaterial = new ShaderMaterial({
    uniforms: {
      uTopColor: {
        value: new Color(DEFAULT_IMAGE_BACKGROUND_FALLBACK_COLOR)
      },
      uBottomColor: {
        value: new Color(DEFAULT_IMAGE_BACKGROUND_FALLBACK_COLOR)
      }
    },
    vertexShader: GRADIENT_VERTEX_SHADER,
    fragmentShader: GRADIENT_FRAGMENT_SHADER,
    side: BackSide,
    depthTest: false,
    depthWrite: false,
    fog: false
  });
  private readonly shaderSkyMaterial = createShaderSkyMaterial();
  private readonly environmentCaptureShaderMaterial = createShaderSkyMaterial();
  private readonly imageMaterial = new MeshBasicMaterial({
    color: 0xffffff,
    side: BackSide,
    depthTest: false,
    depthWrite: false,
    fog: false
  });
  private readonly overlayMaterial = new MeshBasicMaterial({
    color: 0xffffff,
    side: BackSide,
    depthTest: false,
    depthWrite: false,
    fog: false,
    transparent: true,
    opacity: 0
  });
  private readonly celestialGeometry = new PlaneGeometry(1, 1);
  private readonly sunMaterial =
    createCelestialBodyMaterial(SUN_FRAGMENT_SHADER);
  private readonly moonMaterial =
    createCelestialBodyMaterial(MOON_FRAGMENT_SHADER);
  private readonly shaderMesh = new Mesh(this.geometry, this.shaderSkyMaterial);
  private readonly environmentCaptureShaderMesh = new Mesh(
    this.geometry,
    this.environmentCaptureShaderMaterial
  );
  private readonly gradientMesh = new Mesh(
    this.geometry,
    this.gradientMaterial
  );
  private readonly imageMesh = new Mesh(this.geometry, this.imageMaterial);
  private readonly overlayMesh = new Mesh(this.geometry, this.overlayMaterial);
  private readonly sunMesh = new Mesh(this.celestialGeometry, this.sunMaterial);
  private readonly moonMesh = new Mesh(
    this.celestialGeometry,
    this.moonMaterial
  );
  private readonly celestialBodyPosition = new Vector3();
  private sunState: WorldCelestialBodyState | null = null;
  private moonState: WorldCelestialBodyState | null = null;

  constructor() {
    this.shaderMesh.renderOrder = -1003;
    this.gradientMesh.renderOrder = -1002;
    this.imageMesh.renderOrder = -1001;
    this.overlayMesh.renderOrder = -1000;
    this.moonMesh.renderOrder = -999;
    this.sunMesh.renderOrder = -998;

    for (const mesh of [
      this.shaderMesh,
      this.environmentCaptureShaderMesh,
      this.gradientMesh,
      this.imageMesh,
      this.overlayMesh,
      this.sunMesh,
      this.moonMesh
    ]) {
      mesh.frustumCulled = false;
    }

    this.anchor.add(this.shaderMesh);
    this.anchor.add(this.gradientMesh);
    this.anchor.add(this.imageMesh);
    this.anchor.add(this.overlayMesh);
    this.anchor.add(this.moonMesh);
    this.anchor.add(this.sunMesh);
    this.scene.add(this.anchor);
    this.environmentCaptureAnchor.add(this.environmentCaptureShaderMesh);
    this.environmentCaptureScene.add(this.environmentCaptureAnchor);

    this.shaderMesh.visible = false;
    this.environmentCaptureShaderMesh.visible = false;
    this.imageMesh.visible = false;
    this.overlayMesh.visible = false;
    this.sunMesh.visible = false;
    this.moonMesh.visible = false;
  }

  update(
    background: WorldBackgroundSettings,
    backgroundTexture: Texture | null,
    overlay: WorldBackgroundOverlayState | null,
    celestialBodies: WorldCelestialBodiesState | null = null,
    shaderSkyState: WorldShaderSkyRenderState | null = null
  ) {
    const gradientColors = resolveGradientColors(background);
    this.gradientMaterial.uniforms.uTopColor.value.set(
      gradientColors.topColorHex
    );
    this.gradientMaterial.uniforms.uBottomColor.value.set(
      gradientColors.bottomColorHex
    );

    const showShaderBackground =
      background.mode === "shader" && shaderSkyState !== null;
    const showImageBackground =
      !showShaderBackground &&
      background.mode === "image" &&
      backgroundTexture !== null;

    if (this.imageMaterial.map !== backgroundTexture) {
      this.imageMaterial.map = backgroundTexture;
      this.imageMaterial.needsUpdate = true;
    }

    applyShaderSkyStateToMaterial(this.shaderSkyMaterial, shaderSkyState);
    this.syncEnvironmentCaptureState(
      showShaderBackground ? shaderSkyState : null
    );
    this.shaderMesh.visible = showShaderBackground;
    this.gradientMesh.visible = !showShaderBackground && !showImageBackground;
    this.imageMesh.visible = showImageBackground;

    const overlayTexture = overlay?.texture ?? null;
    const overlayOpacity =
      overlayTexture === null ? 0 : clamp(overlay?.opacity ?? 0, 0, 1);

    if (this.overlayMaterial.map !== overlayTexture) {
      this.overlayMaterial.map = overlayTexture;
      this.overlayMaterial.needsUpdate = true;
    }

    this.overlayMaterial.opacity = overlayOpacity;
    this.overlayMesh.visible =
      !showShaderBackground && overlayOpacity > NIGHT_BACKGROUND_EPSILON;
    this.sunState = showShaderBackground
      ? null
      : (celestialBodies?.sun ?? null);
    this.moonState = showShaderBackground
      ? null
      : (celestialBodies?.moon ?? null);
    this.syncCelestialBodyVisualState(
      this.sunMesh,
      this.sunMaterial,
      this.sunState
    );
    this.syncCelestialBodyVisualState(
      this.moonMesh,
      this.moonMaterial,
      this.moonState
    );
  }

  syncToCamera(camera: Camera) {
    this.anchor.position.copy(camera.position);
    this.syncCelestialBodyPose(this.sunMesh, this.sunState, camera);
    this.syncCelestialBodyPose(this.moonMesh, this.moonState, camera);
  }

  dispose() {
    this.geometry.dispose();
    this.celestialGeometry.dispose();
    this.gradientMaterial.dispose();
    this.shaderSkyMaterial.dispose();
    this.environmentCaptureShaderMaterial.dispose();
    this.imageMaterial.dispose();
    this.overlayMaterial.dispose();
    this.sunMaterial.dispose();
    this.moonMaterial.dispose();
  }

  syncEnvironmentCaptureState(state: WorldShaderSkyRenderState | null) {
    applyShaderSkyStateToMaterial(this.environmentCaptureShaderMaterial, state);
    this.environmentCaptureShaderMesh.visible = state !== null;
  }

  getEnvironmentCaptureFarPlane() {
    return BACKGROUND_SPHERE_RADIUS + 8;
  }

  private syncCelestialBodyVisualState(
    mesh: Mesh,
    material: ShaderMaterial,
    state: WorldCelestialBodyState | null
  ) {
    if (state === null) {
      mesh.visible = false;
      material.uniforms.uIntensity.value = 0;
      return;
    }

    material.uniforms.uColor.value.set(state.colorHex);
    material.uniforms.uIntensity.value = state.intensity;
    mesh.scale.setScalar(state.size);
    mesh.visible = true;
  }

  private syncCelestialBodyPose(
    mesh: Mesh,
    state: WorldCelestialBodyState | null,
    camera: Camera
  ) {
    if (state === null) {
      mesh.visible = false;
      return;
    }

    this.celestialBodyPosition
      .set(state.direction.x, state.direction.y, state.direction.z)
      .normalize()
      .multiplyScalar(CELESTIAL_BODY_DISTANCE);
    mesh.position.copy(this.celestialBodyPosition);
    mesh.lookAt(camera.position);
  }
}
