import type { Vec2, Vec3 } from "../core/vector";
import {
  HOURS_PER_DAY,
  normalizeTimeOfDayHours,
  type ProjectTimeSettings
} from "../document/project-time-settings";
import {
  createDefaultWorldTimePhaseProfile,
  resolveWorldCelestialOrbitPeakDirection,
  type WorldCelestialOrbitSettings,
  type WorldBackgroundSettings,
  type WorldSettings
} from "../document/world-settings";
import {
  resolveRuntimeDayNightWorldState,
  resolveRuntimeDayNightPhaseWeights,
  resolveRuntimeTimeState,
  type RuntimeDayNightPhaseWeights,
  type RuntimeDayPhase,
  type RuntimeDayNightWorldState,
  type RuntimeResolvedTimeState
} from "../runtime-three/runtime-project-time";

export interface WorldShaderSkyRenderState {
  presetId: "defaultSky";
  time: {
    dayCount: number;
    timeOfDayHours: number;
    dayPhase: RuntimeResolvedTimeState["dayPhase"];
    daylightFactor: number;
    twilightFactor: number;
    phaseWeights: RuntimeDayNightPhaseWeights;
  };
  sky: {
    topColorHex: string;
    bottomColorHex: string;
    horizonHeight: number;
  };
  celestial: {
    sunDirection: Vec3;
    sunColorHex: string;
    sunIntensity: number;
    sunDiscSizeDegrees: number;
    sunVisible: boolean;
    moonDirection: Vec3;
    moonColorHex: string;
    moonIntensity: number;
    moonDiscSizeDegrees: number;
    moonVisible: boolean;
  };
  stars: {
    density: number;
    brightness: number;
    visibility: number;
    horizonFadeOffset: number;
    rotationAxis: Vec3;
    rotationRadians: number;
  };
  clouds: {
    coverage: number;
    density: number;
    softness: number;
    scale: number;
    height: number;
    heightVariation: number;
    tintHex: string;
    opacity: number;
    opacityRandomness: number;
    driftOffset: Vec2;
  };
  aurora: {
    visibility: number;
    intensity: number;
    height: number;
    thickness: number;
    speed: number;
    primaryColorHex: string;
    secondaryColorHex: string;
    rotationRadians: number;
    timeHours: number;
  };
}

export interface WorldShaderSkyEnvironmentPhaseStates {
  day: WorldShaderSkyRenderState | null;
  dawn: WorldShaderSkyRenderState | null;
  dusk: WorldShaderSkyRenderState | null;
  night: WorldShaderSkyRenderState | null;
}

export interface WorldShaderSkyEnvironmentPhaseBlend {
  basePhase: RuntimeDayPhase;
  overlayPhase: RuntimeDayPhase | null;
  blendAmount: number;
}

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

function wrapTimeForward(hours: number, originHours: number): number {
  let wrappedHours = normalizeTimeOfDayHours(hours);

  while (wrappedHours < originHours) {
    wrappedHours += HOURS_PER_DAY;
  }

  return wrappedHours;
}

function resolveTimeWindowMidpoint(
  startTimeOfDayHours: number,
  endTimeOfDayHours: number
): number {
  const wrappedEndTime = wrapTimeForward(
    endTimeOfDayHours,
    startTimeOfDayHours
  );

  return normalizeTimeOfDayHours(
    startTimeOfDayHours + (wrappedEndTime - startTimeOfDayHours) / 2
  );
}

function quantizeNumberToBucket(value: number, step: number): number {
  return Math.round(value / step);
}

const AURORA_WORLD_ROTATION_RADIANS = 0;
const DEFAULT_SKY_ROTATION_AXIS: Vec3 = {
  x: 0,
  y: 1,
  z: 0
};

function parseHexColor(colorHex: string): { r: number; g: number; b: number } {
  return {
    r: Number.parseInt(colorHex.slice(1, 3), 16),
    g: Number.parseInt(colorHex.slice(3, 5), 16),
    b: Number.parseInt(colorHex.slice(5, 7), 16)
  };
}

function formatHexColor(color: { r: number; g: number; b: number }): string {
  const toHex = (value: number) =>
    Math.round(clamp(value, 0, 255))
      .toString(16)
      .padStart(2, "0");

  return `#${toHex(color.r)}${toHex(color.g)}${toHex(color.b)}`;
}

function blendHexColorsByWeights(
  colors: {
    day: string;
    dawn: string;
    dusk: string;
    night: string;
  },
  weights: RuntimeDayNightPhaseWeights
): string {
  const totalWeight = weights.day + weights.dawn + weights.dusk + weights.night;

  if (totalWeight <= 1e-6) {
    return colors.day;
  }

  const day = parseHexColor(colors.day);
  const dawn = parseHexColor(colors.dawn);
  const dusk = parseHexColor(colors.dusk);
  const night = parseHexColor(colors.night);

  return formatHexColor({
    r:
      (day.r * weights.day +
        dawn.r * weights.dawn +
        dusk.r * weights.dusk +
        night.r * weights.night) /
      totalWeight,
    g:
      (day.g * weights.day +
        dawn.g * weights.dawn +
        dusk.g * weights.dusk +
        night.g * weights.night) /
      totalWeight,
    b:
      (day.b * weights.day +
        dawn.b * weights.dawn +
        dusk.b * weights.dusk +
        night.b * weights.night) /
      totalWeight
  });
}

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

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

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

export function resolveWorldShaderSkyStarRotation(
  timeOfDayHours: number,
  sunOrbit: WorldCelestialOrbitSettings
): {
  rotationAxis: Vec3;
  rotationRadians: number;
} {
  const sunPeakDirection = resolveWorldCelestialOrbitPeakDirection(sunOrbit);
  const rotationAxis = normalizeDirection(
    {
      x: -sunPeakDirection.x * sunPeakDirection.y,
      y: 1 - sunPeakDirection.y * sunPeakDirection.y,
      z: -sunPeakDirection.z * sunPeakDirection.y
    },
    DEFAULT_SKY_ROTATION_AXIS
  );

  return {
    rotationAxis,
    rotationRadians:
      (normalizeTimeOfDayHours(timeOfDayHours) / HOURS_PER_DAY) * Math.PI * 2
  };
}

function offsetDirectionForHorizon(
  direction: Vec3,
  horizonHeight: number
): Vec3 {
  if (Math.abs(horizonHeight) <= 1e-6) {
    return direction;
  }

  return normalizeDirection(
    {
      x: direction.x,
      y: direction.y - horizonHeight,
      z: direction.z
    },
    direction
  );
}

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

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

  return {
    topColorHex: fallbackTopColorHex,
    bottomColorHex: fallbackBottomColorHex
  };
}

export function resolveWorldShaderSkyRenderState(
  world: WorldSettings,
  resolvedWorld: RuntimeDayNightWorldState,
  resolvedTime: RuntimeResolvedTimeState | null,
  timeSettings: ProjectTimeSettings | null
): WorldShaderSkyRenderState | null {
  if (world.background.mode !== "shader") {
    return null;
  }

  const phaseWeights =
    world.projectTimeLightingEnabled &&
    resolvedTime !== null &&
    timeSettings !== null
      ? resolveRuntimeDayNightPhaseWeights(
          timeSettings,
          resolvedTime.timeOfDayHours
        )
      : {
          day: 1,
          dawn: 0,
          dusk: 0,
          night: 0
        };
  const twilightFactor = clamp(phaseWeights.dawn + phaseWeights.dusk, 0, 1);
  const dayCount = resolvedTime?.dayCount ?? 0;
  const timeOfDayHours = resolvedTime?.timeOfDayHours ?? 12;
  const continuousHours = dayCount * 24 + timeOfDayHours;
  const dawnGradient = resolveGradientBasis(
    world.timeOfDay.dawn.background,
    world.timeOfDay.dawn.skyTopColorHex,
    world.timeOfDay.dawn.skyBottomColorHex
  );
  const duskGradient = resolveGradientBasis(
    world.timeOfDay.dusk.background,
    world.timeOfDay.dusk.skyTopColorHex,
    world.timeOfDay.dusk.skyBottomColorHex
  );
  const defaultNightGradient = createDefaultWorldTimePhaseProfile("night");
  const nightGradient = resolveGradientBasis(
    world.timeOfDay.night.background,
    defaultNightGradient.skyTopColorHex,
    defaultNightGradient.skyBottomColorHex
  );
  const sunDirection = normalizeDirection(
    resolvedWorld.sunLight.direction,
    world.sunLight.direction
  );
  const moonDirection = normalizeDirection(
    resolvedWorld.moonLight?.direction ?? {
      x: -sunDirection.x,
      y: -sunDirection.y,
      z: -sunDirection.z
    },
    {
      x: -sunDirection.x,
      y: -sunDirection.y,
      z: -sunDirection.z
    }
  );
  const sunRenderDirection = offsetDirectionForHorizon(
    sunDirection,
    world.shaderSky.horizonHeight
  );
  const moonRenderDirection = offsetDirectionForHorizon(
    moonDirection,
    world.shaderSky.horizonHeight
  );
  const starVisibility =
    clamp(phaseWeights.night + twilightFactor * 0.45, 0, 1) *
    clamp(1 - resolvedWorld.daylightFactor * 0.85, 0, 1);
  const auroraVisibility = world.shaderSky.aurora.enabled
    ? clamp(phaseWeights.night + twilightFactor * 0.32, 0, 1) *
      clamp(1 - resolvedWorld.daylightFactor * 1.15, 0, 1)
    : 0;
  const cloudDriftDirectionRadians =
    (world.shaderSky.clouds.driftDirectionDegrees * Math.PI) / 180;
  const cloudDriftDistance =
    continuousHours * world.shaderSky.clouds.driftSpeed;
  const starRotation = resolveWorldShaderSkyStarRotation(
    timeOfDayHours,
    world.celestialOrbits.sun
  );

  return {
    presetId: world.shaderSky.presetId,
    time: {
      dayCount,
      timeOfDayHours,
      dayPhase: resolvedTime?.dayPhase ?? "day",
      daylightFactor: resolvedWorld.daylightFactor,
      twilightFactor,
      phaseWeights
    },
    sky: {
      topColorHex: blendHexColorsByWeights(
        {
          day: world.shaderSky.dayTopColorHex,
          dawn: dawnGradient.topColorHex,
          dusk: duskGradient.topColorHex,
          night: nightGradient.topColorHex
        },
        phaseWeights
      ),
      bottomColorHex: blendHexColorsByWeights(
        {
          day: world.shaderSky.dayBottomColorHex,
          dawn: dawnGradient.bottomColorHex,
          dusk: duskGradient.bottomColorHex,
          night: nightGradient.bottomColorHex
        },
        phaseWeights
      ),
      horizonHeight: world.shaderSky.horizonHeight
    },
    celestial: {
      sunDirection: sunRenderDirection,
      sunColorHex: resolvedWorld.sunLight.colorHex,
      sunIntensity: resolvedWorld.sunLight.intensity,
      sunDiscSizeDegrees: world.shaderSky.celestial.sunDiscSizeDegrees,
      sunVisible:
        world.showCelestialBodies && resolvedWorld.sunLight.intensity > 1e-4,
      moonDirection: moonRenderDirection,
      moonColorHex: resolvedWorld.moonLight?.colorHex ?? "#d7e4ff",
      moonIntensity: resolvedWorld.moonLight?.intensity ?? 0,
      moonDiscSizeDegrees: world.shaderSky.celestial.moonDiscSizeDegrees,
      moonVisible:
        world.showCelestialBodies &&
        (resolvedWorld.moonLight?.intensity ?? 0) > 1e-4
    },
    stars: {
      density: world.shaderSky.stars.density,
      brightness: world.shaderSky.stars.brightness,
      visibility: starVisibility,
      horizonFadeOffset: world.shaderSky.stars.horizonFadeOffset,
      rotationAxis: starRotation.rotationAxis,
      rotationRadians: starRotation.rotationRadians
    },
    clouds: {
      coverage: world.shaderSky.clouds.coverage,
      density: world.shaderSky.clouds.density,
      softness: world.shaderSky.clouds.softness,
      scale: world.shaderSky.clouds.scale,
      height: world.shaderSky.clouds.height,
      heightVariation: world.shaderSky.clouds.heightVariation,
      tintHex: world.shaderSky.clouds.tintHex,
      opacity: world.shaderSky.clouds.opacity,
      opacityRandomness: world.shaderSky.clouds.opacityRandomness,
      driftOffset: {
        x: Math.cos(cloudDriftDirectionRadians) * cloudDriftDistance,
        y: Math.sin(cloudDriftDirectionRadians) * cloudDriftDistance * 0.35
      }
    },
    aurora: {
      visibility: auroraVisibility,
      intensity: world.shaderSky.aurora.intensity,
      height: world.shaderSky.aurora.height,
      thickness: world.shaderSky.aurora.thickness,
      speed: world.shaderSky.aurora.speed,
      primaryColorHex: world.shaderSky.aurora.primaryColorHex,
      secondaryColorHex: world.shaderSky.aurora.secondaryColorHex,
      rotationRadians: AURORA_WORLD_ROTATION_RADIANS,
      timeHours: continuousHours
    }
  };
}

export function resolveWorldShaderSkyEnvironmentPhaseStates(
  world: WorldSettings,
  timeSettings: ProjectTimeSettings | null
): WorldShaderSkyEnvironmentPhaseStates {
  if (world.background.mode !== "shader") {
    return {
      day: null,
      dawn: null,
      dusk: null,
      night: null
    };
  }

  if (!world.projectTimeLightingEnabled || timeSettings === null) {
    const resolvedWorld: RuntimeDayNightWorldState = {
      ambientLight: {
        ...world.ambientLight
      },
      sunLight: {
        ...world.sunLight,
        direction: {
          ...world.sunLight.direction
        }
      },
      moonLight: null,
      background: {
        ...world.background
      },
      nightBackgroundOverlay: null,
      daylightFactor: 1
    };
    const dayState = resolveWorldShaderSkyRenderState(
      world,
      resolvedWorld,
      null,
      null
    );

    return {
      day: dayState,
      dawn: dayState,
      dusk: dayState,
      night: dayState
    };
  }

  const dawnHalfDuration = Math.max(timeSettings.dawnDurationHours, 0.001) / 2;
  const duskHalfDuration = Math.max(timeSettings.duskDurationHours, 0.001) / 2;
  const dawnStart = timeSettings.sunriseTimeOfDayHours - dawnHalfDuration;
  const dawnEnd = timeSettings.sunriseTimeOfDayHours + dawnHalfDuration;
  const duskStart = timeSettings.sunsetTimeOfDayHours - duskHalfDuration;
  const duskEnd = timeSettings.sunsetTimeOfDayHours + duskHalfDuration;
  const representativeTimes = {
    dawn: resolveTimeWindowMidpoint(dawnStart, dawnEnd),
    day: resolveTimeWindowMidpoint(dawnEnd, duskStart),
    dusk: resolveTimeWindowMidpoint(duskStart, duskEnd),
    night: resolveTimeWindowMidpoint(duskEnd, dawnStart)
  };
  const resolveRepresentativeState = (
    timeOfDayHours: number
  ): WorldShaderSkyRenderState | null => {
    const resolvedTime = resolveRuntimeTimeState(timeSettings, {
      timeOfDayHours,
      dayCount: 0,
      dayLengthMinutes: timeSettings.dayLengthMinutes
    });
    const resolvedWorld = resolveRuntimeDayNightWorldState(
      world,
      timeSettings,
      {
        timeOfDayHours,
        dayCount: 0,
        dayLengthMinutes: timeSettings.dayLengthMinutes
      },
      resolvedTime
    );

    return resolveWorldShaderSkyRenderState(
      world,
      resolvedWorld,
      resolvedTime,
      timeSettings
    );
  };

  return {
    day: resolveRepresentativeState(representativeTimes.day),
    dawn: resolveRepresentativeState(representativeTimes.dawn),
    dusk: resolveRepresentativeState(representativeTimes.dusk),
    night: resolveRepresentativeState(representativeTimes.night)
  };
}

export function createWorldShaderSkyEnvironmentCacheKey(
  state: WorldShaderSkyRenderState
): string {
  return JSON.stringify({
    presetId: state.presetId,
    sky: [
      state.sky.topColorHex,
      state.sky.bottomColorHex,
      quantizeNumberToBucket(state.sky.horizonHeight, 0.01)
    ],
    sun: [
      state.celestial.sunColorHex,
      quantizeNumberToBucket(state.celestial.sunDirection.x, 0.01),
      quantizeNumberToBucket(state.celestial.sunDirection.y, 0.01),
      quantizeNumberToBucket(state.celestial.sunDirection.z, 0.01),
      quantizeNumberToBucket(state.celestial.sunIntensity, 0.05),
      quantizeNumberToBucket(state.celestial.sunDiscSizeDegrees, 0.05),
      state.celestial.sunVisible ? 1 : 0
    ],
    moon: [
      state.celestial.moonColorHex,
      quantizeNumberToBucket(state.celestial.moonDirection.x, 0.01),
      quantizeNumberToBucket(state.celestial.moonDirection.y, 0.01),
      quantizeNumberToBucket(state.celestial.moonDirection.z, 0.01),
      quantizeNumberToBucket(state.celestial.moonIntensity, 0.05),
      quantizeNumberToBucket(state.celestial.moonDiscSizeDegrees, 0.05),
      state.celestial.moonVisible ? 1 : 0
    ],
    stars: [
      quantizeNumberToBucket(state.stars.density, 0.05),
      quantizeNumberToBucket(state.stars.brightness, 0.05),
      quantizeNumberToBucket(state.stars.visibility, 0.05),
      quantizeNumberToBucket(state.stars.horizonFadeOffset, 0.01),
      quantizeNumberToBucket(state.stars.rotationAxis.x, 0.01),
      quantizeNumberToBucket(state.stars.rotationAxis.y, 0.01),
      quantizeNumberToBucket(state.stars.rotationAxis.z, 0.01),
      quantizeNumberToBucket(state.stars.rotationRadians, 0.02)
    ],
    clouds: [
      state.clouds.tintHex,
      quantizeNumberToBucket(state.clouds.coverage, 0.05),
      quantizeNumberToBucket(state.clouds.density, 0.05),
      quantizeNumberToBucket(state.clouds.softness, 0.05),
      quantizeNumberToBucket(state.clouds.scale, 0.05),
      quantizeNumberToBucket(state.clouds.height, 0.05),
      quantizeNumberToBucket(state.clouds.heightVariation, 0.05),
      quantizeNumberToBucket(state.clouds.opacity, 0.05),
      quantizeNumberToBucket(state.clouds.opacityRandomness, 0.05),
      quantizeNumberToBucket(state.clouds.driftOffset.x, 0.02),
      quantizeNumberToBucket(state.clouds.driftOffset.y, 0.02)
    ],
    aurora: [
      state.aurora.primaryColorHex,
      state.aurora.secondaryColorHex,
      quantizeNumberToBucket(state.aurora.visibility, 0.05),
      quantizeNumberToBucket(state.aurora.intensity, 0.05),
      quantizeNumberToBucket(state.aurora.height, 0.05),
      quantizeNumberToBucket(state.aurora.thickness, 0.05),
      quantizeNumberToBucket(state.aurora.speed, 0.05),
      quantizeNumberToBucket(state.aurora.rotationRadians, 0.02)
    ]
  });
}

export function createWorldShaderSkyEnvironmentPhaseCacheKey(
  states: WorldShaderSkyEnvironmentPhaseStates
): string {
  return JSON.stringify({
    day:
      states.day === null
        ? null
        : createWorldShaderSkyEnvironmentCacheKey(states.day),
    dawn:
      states.dawn === null
        ? null
        : createWorldShaderSkyEnvironmentCacheKey(states.dawn),
    dusk:
      states.dusk === null
        ? null
        : createWorldShaderSkyEnvironmentCacheKey(states.dusk),
    night:
      states.night === null
        ? null
        : createWorldShaderSkyEnvironmentCacheKey(states.night)
  });
}

export function resolveWorldShaderSkyEnvironmentPhaseBlend(
  state: Pick<WorldShaderSkyRenderState, "time">
): WorldShaderSkyEnvironmentPhaseBlend | null {
  const activePhases = (["day", "dawn", "dusk", "night"] as const).filter(
    (phase) => state.time.phaseWeights[phase] > 1e-4
  );

  if (activePhases.length === 0) {
    return null;
  }

  if (activePhases.length === 1) {
    return {
      basePhase: activePhases[0],
      overlayPhase: null,
      blendAmount: 0
    };
  }

  const [basePhase, overlayPhase] = activePhases;
  const baseWeight = state.time.phaseWeights[basePhase];
  const overlayWeight = state.time.phaseWeights[overlayPhase];
  const totalWeight = Math.max(baseWeight + overlayWeight, 1e-6);

  return {
    basePhase,
    overlayPhase,
    blendAmount: overlayWeight / totalWeight
  };
}
