--------
-- Most of weather stuff happens here: it sets lighting, fog, scene brightness and prepares a few globally defined
-- cloud materials.
--------

CloudsLightDirection = vec3()
CloudsLightColor = rgb()

-- Various local variables, changing with each update, something easy to deal with things. There is 
-- no need to edit any of those values if you want to change anything, please proceed further to
-- actual functions setting that stuff
local sunsetK = 0 -- grows when sun is at sunset stage
local horizonK = 0 -- grows when sun is near horizon
local eclipseK = 0 -- starts growing when moon touches sun, 1 at total eclipse
local lightBrightness = 1
local belowHorizonCorrection = 0
local initialSet = 3
local cameraOcclusion = 1
local sunColor = rgb(1, 1, 1)
local skyTopColor = rgb(1, 1, 1)
local skySunColor = rgb(1, 1, 1)
local lightDir = vec3(0, 1, 0)
local lightColor = rgb(0, 0, 0)
local fogRangeMult = 1
local realNightK = 0

-- Sky gradient covering everything, for sky-wide color correction
local skyGeneralMult = nil
skyGeneralMult = ac.SkyExtraGradient()
skyGeneralMult.isAdditive = false
skyGeneralMult.sizeFull = 2
skyGeneralMult.sizeStart = 2
skyGeneralMult.direction = vec3(0, 1, 0)
ac.addSkyExtraGradient(skyGeneralMult)

-- Another sky gradient for cloudy and foggy look
local skyCoverAddition = nil
skyCoverAddition = ac.SkyExtraGradient()
skyCoverAddition.isAdditive = true
skyCoverAddition.sizeFull = 2
skyCoverAddition.sizeStart = 2
skyCoverAddition.direction = vec3(0, 1, 0)
ac.addSkyExtraGradient(skyCoverAddition)

-- Gradient to boost brightness towards horizon
local skyHorizonAddition = nil
skyHorizonAddition = ac.SkyExtraGradient()
skyHorizonAddition.isAdditive = true
skyHorizonAddition.sizeFull = 0.8
skyHorizonAddition.sizeStart = 1.2
skyHorizonAddition.direction = vec3(0, -1, 0)
ac.addSkyExtraGradient(skyHorizonAddition)

-- Gradient to boost brightness towards horizon
local cityHaze = ac.SkyExtraGradient()
cityHaze.isAdditive = false
cityHaze.sizeFull = 1
cityHaze.sizeStart = 1.2
cityHaze.exponent = 20
cityHaze.direction = vec3(0, -1, 0)

-- Gradient to darken zenith during solar eclipse
local eclipseCover = ac.SkyExtraGradient()
eclipseCover.isAdditive = false
eclipseCover.sizeFull = 0
eclipseCover.sizeStart = 2
eclipseCover.exponent = 0.1
eclipseCover.direction = vec3(0, 1, 0)
eclipseCover.color = rgb(0, 0, 0)

-- Custom post-processing brightness adjustment
-- TODO: Disable if gamma fix is active
local ppBrightnessCorrection = ac.ColorCorrectionBrightness()
ac.addWeatherColorCorrection(ppBrightnessCorrection)

-- A bit of optimization to reduce garbage generated per frame
local vec3Up = vec3(0, 1, 0)

-- Cheap thunder effect
local thunderActiveFor = 0
local thunderFlashAdded = false
local thunderFlash = ac.SkyExtraGradient()
thunderFlash.direction = vec3(0, 1, 0)
thunderFlash.sizeFull = 0
thunderFlash.sizeStart = 1
thunderFlash.exponent = 1
thunderFlash.isIncludedInCalculate = false

-- Strong wind blows estimated city pollution away (estimation is done based on light pollution, specific weather
-- types can boost pollution further)
local windSpeedSmoothed = -1
local baseCityPollution = 0
local totalPollution = 0
local prevEclipseK = -1

-- Temporary, for debugging
function OnGammaToggle()
  if prevEclipseK > 0 then
    ac.skyExtraGradients:erase(eclipseCover)
  end
  if CityHaze > 0 then
    ac.skyExtraGradients:erase(cityHaze)
  end
  prevEclipseK = -1
  CityHaze = -1
end

-- Updates sky color
function ApplySky(dt)
  ac.getSunDirectionTo(SunDir)
  ac.getMoonDirectionTo(MoonDir)

  windSpeedSmoothed = windSpeedSmoothed < 0 and CurrentConditions.windSpeed or math.applyLag(windSpeedSmoothed, CurrentConditions.windSpeed, 0.99, dt)
  baseCityPollution = LightPollutionValue * (1 - 0.3 * math.min(windSpeedSmoothed / 10, 1)) * math.saturateN(0.2 + ac.getSim().ambientTemperature / 60)
  totalPollution = math.lerp(baseCityPollution * 0.5, 1, CurrentConditions.pollution)

  SpaceLook = math.saturateN(ac.getAltitude() / 5e4 - 1)
  CloudsMult = math.saturateN(2 - ac.getAltitude() / 2e3)
  realNightK = math.lerpInvSat(SunDir.y, 0.05, -0.2)

  -- Eclipse coefficients. Full eclipse happens on Brasov track on 08/11/1999:
  -- https://www.racedepartment.com/downloads/brasov-romania.28239/
  local sunMoonAngle = ac.getSunMoonAngle()
  local hadAnyFullEclipse = EclipseFullK > 0
  eclipseK = math.lerpInvSat(sunMoonAngle, 0.0077, 0.0005) * (1 - realNightK)
  EclipseFullK = math.lerpInvSat(sunMoonAngle, 0.00032, 0.00021) * (1 - realNightK)
  if hadAnyFullEclipse ~= (EclipseFullK > 0) then
    if hadAnyFullEclipse then
      ForceRapidUpdates = ForceRapidUpdates - 1
    else
      ForceRapidUpdates = ForceRapidUpdates + 1
    end
    UpdateEclipseGlare(EclipseFullK > 0)
  end

  if SpaceLook > 0 then
    realNightK = math.lerp(realNightK, 0, SpaceLook)
    eclipseK = math.lerp(eclipseK, 0, SpaceLook)
    EclipseFullK = math.lerp(EclipseFullK, 0, SpaceLook)
  end

  NightK = realNightK
  FinalFog = math.pow(CurrentConditions.fog, 1 - 0.5 * NightK)
  
  sunsetK = math.lerpInvSat(math.max(0, SunDir.y), 0.12, 0)
  horizonK = math.lerpInvSat(math.abs(SunDir.y), 0.4, 0.12)

  if UseGammaFix and EclipseFullK then
    NightK = math.lerp(NightK, 1, EclipseFullK)
  end

  if SpaceLook > 0 then
    sunsetK = math.lerp(sunsetK, 0, SpaceLook)
    horizonK = math.lerp(horizonK, 0, SpaceLook)
    NightK = math.lerp(NightK, 0, SpaceLook)
    FinalFog = math.lerp(FinalFog, 0, SpaceLook)
  end

  -- Generally the same:
  ac.setSkyV2MieKCoefficient(ac.SkyRegion.All, vec3(0.686, 0.678, 0.666))
  ac.setSkyV2NumMolecules(ac.SkyRegion.All, 2.542e25)
  ac.setSkyV2MieDirectionalG(ac.SkyRegion.All, 0.8)
  ac.setSkyV2DepolarizationFactor(ac.SkyRegion.All, 0.035)
  ac.setSkyV2MieV(ac.SkyRegion.All, 3.96)
  ac.setSkyV2MieZenithLength(ac.SkyRegion.All, 1.25e3)
  ac.setSkyV2SunIntensityFactor(ac.SkyRegion.All, 1000.0)
  ac.setSkyV2SunIntensityFalloffSteepness(ac.SkyRegion.All, 1.5)

  -- Few sky adjustments
  local purpleAdjustment = sunsetK -- slightly alter color for sunsets
  local skyVisibility = (1 - FinalFog) * CurrentConditions.clear

  -- Brightness adjustments:
  if UseGammaFix then
    local refractiveIndex = math.lerp(1.000317, 1.00029, NightK) + 0.0001 * purpleAdjustment -- TODO: Tie to pollution, make purple value dynamic 
    ac.setSkyV2Primaries(ac.SkyRegion.All, vec3(6.8e-7, 5.5e-7, 4.5e-7))
    ac.setSkyV2Turbidity(ac.SkyRegion.All, 1.25 + sunsetK * (1 - NightK) * 3.45)
    ac.setSkyV2Rayleigh(ac.SkyRegion.Sun, 1 + sunsetK * 0.28)
    ac.setSkyV2RayleighZenithLength(ac.SkyRegion.All, (8400 - 6000 * sunsetK) * (1 - 0.5 * NightK) + baseCityPollution * math.lerp(8000, 4000, horizonK))

    ac.setSkyV2Luminance(ac.SkyRegion.All, 0.03)
    ac.setSkyV2Gamma(ac.SkyRegion.All, 1)

    ac.setSkyV2BackgroundLight(ac.SkyRegion.All, 0) -- what does this thing do?
    ac.setSkyV2SunShapeMult(ac.SkyRegion.All, 3e4 * ((CurrentConditions.clear * (1 - FinalFog)) ^ 5) * (1 - EclipseFullK) ^ 8)
    ac.setSkyV2SunSaturation(ac.SkyRegion.All, 1)
    -- ac.setSkyV2Saturation(ac.SkyRegion.All, 1.2 - Sim.weatherConditions.humidity * 0.4)
    ac.setSkyV2Saturation(ac.SkyRegion.All, 1)

    local mieC = math.lerp(0.0065, 0.0045, sunsetK) * (1 - EclipseFullK) * CurrentConditions.clear
    refractiveIndex = refractiveIndex * math.lerp(0.99997, 1.00003, Sim.weatherConditions.humidity)
    if SpaceLook > 0 then
      refractiveIndex = math.lerp(refractiveIndex, 1, SpaceLook)
      mieC = math.lerp(mieC, 0, SpaceLook)
    end

    ac.setSkyV2RefractiveIndex(ac.SkyRegion.All, refractiveIndex)
    ac.setSkyV2MieCoefficient(ac.SkyRegion.All, mieC)

    -- Shifting sky using Earth radius and current altitude
    local earthR = 6371e3
    local cameraR = earthR + math.max(1, ac.getAltitude())
    local n = cameraR * cameraR - earthR * earthR
    local x = n / cameraR
    local d = math.sqrt(n - x * x)
    fogRangeMult = 1 + x / d
    local shiftScale = 1 / fogRangeMult
    ac.setSkyV2YOffset(ac.SkyRegion.All, 1 - shiftScale)
    ac.setSkyV2YScale(ac.SkyRegion.All, shiftScale)
  else
    local brightDayAdjustment = math.lerpInvSat(math.max(0, SunDir.y), 0.2, 0.6) -- make sky clearer during the day
    ac.setSkyV2RefractiveIndex(ac.SkyRegion.All, 1.0003)
    ac.setSkyV2Primaries(ac.SkyRegion.All, vec3(6.8e-7, 5.5e-7, math.lerp(4.5e-7, 5.1e-7, purpleAdjustment)))
    ac.setSkyV2Turbidity(ac.SkyRegion.All, 2.0)
    ac.setSkyV2Rayleigh(ac.SkyRegion.All, math.lerp(3.0, 1.0, brightDayAdjustment))
    ac.setSkyV2RayleighZenithLength(ac.SkyRegion.All, 8400)
    ac.setSkyV2MieCoefficient(ac.SkyRegion.All, 0.005 * CurrentConditions.clear)

    local darkNightSky = math.max(NightK, EclipseFullK * 0.85) -- sky getting black
    ac.setSkyV2Luminance(ac.SkyRegion.All, math.lerp(0, 0.3, math.pow(1 - darkNightSky, 4)))
    ac.setSkyV2Gamma(ac.SkyRegion.All, 2.5)

    ac.setSkyV2BackgroundLight(ac.SkyRegion.All, 0) -- what does this thing do?
    ac.setSkyV2SunShapeMult(ac.SkyRegion.All, 10 * (CurrentConditions.clear ^ 2))
    ac.setSkyV2SunSaturation(ac.SkyRegion.All, 0.9)
    ac.setSkyV2Saturation(ac.SkyRegion.All, math.lerp(0.5, 1.2, CurrentConditions.clear))

    if SpaceLook > 0 then
      ac.setSkyV2RefractiveIndex(ac.SkyRegion.All, math.lerp(1.0003, 1, SpaceLook))
      ac.setSkyV2MieCoefficient(ac.SkyRegion.All, math.lerp(0.005 * CurrentConditions.clear, 0, SpaceLook))
    end
    
    -- Crappy old approach kept for compatibility
    ac.setSkyV2YOffset(ac.SkyRegion.All, 0.1)
    ac.setSkyV2YScale(ac.SkyRegion.All, 0.9)
  end

  ac.setSkyBrightnessMult(1)

  -- Boosting deep blue at nights
  local deepBlue = NightK ^ 2
  skyGeneralMult.color
    :set(math.lerp(1, 0.2, deepBlue), math.lerp(1, 0.8, math.max(horizonK * 0.5, deepBlue)), math.lerp(1, 1.6, deepBlue))
    :mul(CurrentConditions.tint)
    :scale(skyVisibility 
      * (1 - math.smoothstep(realNightK) * 0.99)) -- TODO: verify compatibility, add variance to 0.99

  -- Covering layer
  ac.calculateSkyColorNoGradientsTo(skyTopColor, vec3Up, false, false, false)
  skyCoverAddition.color
    :set(math.lerp(1, 0.2, deepBlue), math.lerp(1, 0.8, deepBlue) * 1.1, math.lerp(1, 2, deepBlue) * 1.2)
    :scale((1 - NightK * 0.99) * (1 - CurrentConditions.cloudsDensity * 0.5))
    :mul(CurrentConditions.tint)
  if UseGammaFix then
    skyHorizonAddition.exponent = 2
    skyCoverAddition.color:pow(2.2):scale(skyTopColor.b * 0.3 --[[ actually defines how dark nonclear weathers are ]] * (1 - skyVisibility))
  else
    skyHorizonAddition.exponent = 1
    skyCoverAddition.color:scale(skyTopColor.g * (1 - skyVisibility))
  end
  skyHorizonAddition.color:set(skyCoverAddition.color)
  skyHorizonAddition.direction.x = SunDir.x * 0.2
  skyHorizonAddition.direction.z = SunDir.z * 0.2

  -- Haze alteration (with gamma fixed, sky parameters are tweaked instead for a dirtier atmosphere look)
  local newCityHaze = UseGammaFix and 0 or (1 - sunsetK) * (1 - CurrentConditions.clouds) * CurrentConditions.clear 
    * math.pow(math.saturateN(LightPollutionValue), 2) * math.saturateN(0.2 + ac.getSim().ambientTemperature / 60)
  if math.abs(newCityHaze - CityHaze) > 0.01 then
    if (newCityHaze ~= 0) ~= (CityHaze ~= 0) then
      if newCityHaze ~= 0 then
        ac.addSkyExtraGradient(cityHaze)
      else
        ac.skyExtraGradients:erase(cityHaze)
      end
    end
    CityHaze = newCityHaze
    cityHaze.color.r = math.lerp(1, 1.8, newCityHaze)
    cityHaze.color.g = math.lerp(1, 0.9, newCityHaze)
    cityHaze.color.b = math.lerp(1, 0.5, newCityHaze)
  end

  if UseGammaFix and prevEclipseK ~= eclipseK then
    if (prevEclipseK > 0) ~= (eclipseK > 0) then
      if eclipseK > 0 then
        ac.addSkyExtraGradient(eclipseCover)
      else
        ac.skyExtraGradients:erase(eclipseCover)
      end
    end
    eclipseCover.color:set(1 - eclipseK * 0.9 - EclipseFullK * 0.1)
    eclipseCover.direction:set(SunDir)
    prevEclipseK = eclipseK
  end

  local rainbowIntensity = Overrides.rainbowIntensity or 
    math.saturateN(CurrentConditions.rain * 50) * CurrentConditions.clear * math.lerpInvSat(SunDir.y, 0.02, 0.06)
  ac.setSkyV2Rainbow(rainbowIntensity)
  ac.setSkyV2RainbowSecondary(0.2 * rainbowIntensity)
  ac.setSkyV2RainbowDarkening(math.lerp(1, UseGammaFix and 0.4 or 0.8, rainbowIntensity))

  -- Getting a few colors from sky
  ac.calculateSkyColorTo(skyTopColor, vec3Up, false, false, false)
  ac.calculateSkyColorTo(skySunColor, vec3(SunDir.x, math.max(SunDir.y, 0.0), SunDir.z), false, false, true)

  -- Small adjustment for balancing
  if UseGammaFix then
    skySunColor:scale(0.5)
    skyTopColor:scale(0.5)
  else
    skySunColor:scale(0.25)
    skyTopColor:scale(0.25)
  end

  if SpaceLook > 0 then
    ac.setSkyV2Rainbow(rainbowIntensity * (1 - SpaceLook))
    skySunColor:setLerp(skySunColor, rgb.colors.white, SpaceLook)
    skyTopColor:setLerp(skyTopColor, rgb.colors.black, SpaceLook)
  end
end

---@param color rgb
function ApplyExtraVibe(color)
  local sunsetBlue = math.sin(NightK * math.pi)
  local intensity = 0.05 + 0.95 * CurrentConditions.clear
  color.r = color.r * (1 + (horizonK * 2 - sunsetBlue) * intensity)
  color.b = color.b * (1 + (sunsetK * 0.5 + sunsetBlue * 2) * intensity)
  color:normalize()
end

-- Updates main scene light: could be either sun or moon light, dims down with eclipses
local moonAbsorption = rgb()
local cloudLightColor = rgb()

function ApplyLight()
  local eclipseLightMult = (1 - eclipseK * (UseGammaFix and 0.98 or 0.8)) -- up to 80% general occlusion
    * (1 - EclipseFullK * (UseGammaFix and 1 or 0.98)) -- up to 98% occlusion for real full eclipse
  
  -- Calculating sun color based on sky absorption (boosted at horizon)
  ac.getSkyAbsorptionTo(sunColor, SunDir)
  if UseGammaFix then
    sunColor:scale(SunIntensity * eclipseLightMult)
  else
    sunColor:pow(1 + horizonK):mul(SunColor):scale(SunIntensity * eclipseLightMult * math.lerp(1, 2, horizonK))
  end

  -- Initially, it starts as a sun light
  lightColor:set(sunColor)

  -- If it’s deep night and moon is high enough, change it to moon light
  ac.getSkyAbsorptionTo(moonAbsorption, MoonDir)
  local sunThreshold = math.lerpInvSat(realNightK, 0.7, 0.5)
  local moonThreshold = math.lerpInvSat(realNightK, 0.7, 0.95)
  local moonLight = moonThreshold * math.lerpInvSat(MoonDir.y, 0, 0.12) * (1 - SpaceLook)

  -- Calculate light direction, similar rules
  if moonLight > 0 then
    local moonPartialEclipseK = math.lerpInvSat(math.dot(SunDir, MoonDir), -0.99964, -0.99984)
    local moonEclipseK = math.lerpInvSat(math.dot(SunDir, MoonDir), -0.99996, -0.99985)
    local finalMoonEclipseMult = moonEclipseK * (0.8 + 0.2 * moonPartialEclipseK)
    if UseGammaFix then
      finalMoonEclipseMult = finalMoonEclipseMult ^ 2
    end
    moonLight = moonLight * finalMoonEclipseMult

    lightDir:set(MoonDir)
    lightColor:set(moonAbsorption):mul(MoonColor)
      :scale(MoonLightMult * LightPollutionSkyFeaturesMult * ac.getMoonFraction() * moonLight * CurrentConditions.clear)
  else
    lightDir:set(SunDir)
  end

  -- Adjust light color
  lightColor:scale(UseGammaFix and CurrentConditions.clear ^ 2 or CurrentConditions.clear)
    :adjustSaturation(CurrentConditions.saturation * (UseGammaFix and 1 or 0.8) * (1.1 - Sim.weatherConditions.humidity * 0.2))
    :mul(CurrentConditions.tint)

  -- Clouds have their own lighting, so sun would work even if it’s below the horizon
  local cloudSunLight = math.lerpInvSat(SunDir.y, -0.23, -0.115)
  cloudLightColor:set(sunColor):scale(cloudSunLight * sunThreshold)
  cloudLightColor:setLerp(cloudLightColor, lightColor, moonLight)
  cloudLightColor:scale(CurrentConditions.clear)
  cloudLightColor:adjustSaturation(CurrentConditions.saturation)
  cloudLightColor:mul(CurrentConditions.tint)
  ac.setCloudsLight(lightDir, cloudLightColor, 6371e3 / 20)

  -- Dim light if light source is very low
  lightColor:scale(math.lerpInvSat(lightDir.y, -0.03, 0) * SunLightIntensity)

  -- Dim godrays even more
  GodraysColor:set(lightColor):scale(math.lerpInvSat(lightDir.y, 0.01, 0.02) * (1 - FinalFog ^ 2))

  if SpaceLook > 0 then
    GodraysColor:scale(1 - SpaceLook)
  end

  -- And godrays!
  if SunRaysCustom then
    ac.setGodraysCustomColor(GodraysColor)
    ac.setGodraysCustomDirection(lightDir)
    ac.setGodraysLength(0.3)
    ac.setGodraysGlareRatio(0)
    ac.setGodraysAngleAttenuation(1)
  else
    ac.setGodraysCustomColor(GodraysColor:scale(SunRaysIntensity))
    ac.setGodraysCustomDirection(lightDir)
  end

  -- Adjust light dir for case where sun is below horizon, but a bit is still visible
  belowHorizonCorrection = math.lerpInvSat(lightDir.y, 0.04, 0.01)
  if belowHorizonCorrection > 0 then
    lightColor:scale(math.lerpInvSat(lightDir.y, -0.01, 0.01))
    lightDir.y = math.lerp(lightDir.y, 0.02, belowHorizonCorrection ^ 2)
  end

  if SpaceLook > 0 then
    lightDir:setLerp(lightDir, SunDir, SpaceLook)
    lightColor:setLerp(lightColor, rgb.colors.white, SpaceLook)
  elseif thunderFlashAdded and SunDir.y < 0 then
    lightDir:set(thunderFlash.direction)
    lightColor:setScaled(thunderFlash.color, 10)
  end

  -- Applying everything
  ac.setLightDirection(lightDir)
  ac.setLightColor(lightColor)
  ac.setSpecularColor(lightColor)
  ac.setSunSpecularMultiplier(CurrentConditions.clear ^ 2)

  ac.setCloudsLight(lightDir, lightColor, 6371e3)
  CloudsLightDirection:set(lightDir)
  CloudsLightColor:set(lightColor)
end

-- Updates ambient lighting based on sky color without taking sun or moon into account
local ambientBaseColor = rgb(1, 1, 1)
local ambientAdjColor = rgb(1, 1, 1)
local ambientDistantColor = rgb()
local ambientExtraColor = rgb()
local ambientExtraDirection = vec3()
local ambientLuminance = 1

function ApplyAmbient()
  if UseGammaFix then
    -- Computing sky color on horizon 90° off sun direction 
    local d = math.sqrt(SunDir.x ^ 2 + SunDir.z ^ 2)
    ambientExtraDirection.x = SunDir.z / d
    ambientExtraDirection.z = -SunDir.x / d
    ambientExtraDirection.y = 0.15 - 0.05 * sunsetK
    ac.calculateSkyColorV2To(ambientBaseColor, ambientExtraDirection, false, false, false)
    ambientBaseColor:scale(0.5 + 0.5 * CurrentConditions.clear)

    -- Syncing luminance with top sky point for more even lighting
    local targetLuminance = skyTopColor:luminance()
    ambientBaseColor:scale(targetLuminance / math.max(1e-9, ambientBaseColor:luminance()))

    -- If there are a lot of clouds around, desaturating ambient light and shifting it a bit closer to sun color
    local ambientDesaturate = math.lerp(Sim.weatherConditions.humidity ^ 2 * 0.25, 1, CurrentConditions.clouds)
    local ambientSaturate = (1 - ambientDesaturate) * CurrentConditions.saturation
    local sunColorSynced = ambientAdjColor:set(sunColor):scale(targetLuminance / math.max(1e-9, sunColor:luminance()))
    ambientBaseColor:adjustSaturation(ambientSaturate):mul(CurrentConditions.tint)

    local basicSunColorContribution = ac.isBouncedLightActive() and 0.1 or 0.2
    ambientBaseColor:setLerp(ambientBaseColor, sunColorSynced, (basicSunColorContribution + ambientDesaturate * 0.4) * (CurrentConditions.clear ^ 2))
    
    -- Ambient light is ready
    ac.setAmbientColor(ambientBaseColor)
    ambientLuminance = ambientBaseColor:luminance()

    -- Distant ambient lighting is a tiny bit more bluish because why not
    ac.setDistantAmbientColor(ambientDistantColor:set(0.95, 1, 1.05):mul(ambientBaseColor), 20e3)
    ambientExtraColor:set(skyTopColor):adjustSaturation(ambientSaturate):mul(CurrentConditions.tint)
    ambientExtraColor:setLerp(ambientExtraColor, sunColorSynced, (0.1 + ambientDesaturate * 0.4) * (CurrentConditions.clear ^ 2)):sub(ambientBaseColor)
    ambientExtraColor:add(LightPollutionColor)
    ac.setExtraAmbientColor(ambientExtraColor)
    ac.setExtraAmbientDirection(vec3Up)
  
    -- Adjusting fake shadows under cars
    ac.setWeatherFakeShadowOpacity(1 - SpaceLook)
    ac.setWeatherFakeShadowConcentration(0)
  
    -- Adjusting vertex AO
    ac.adjustTrackVAO(1, 0, 1)
    ac.adjustDynamicAOSamples(1, 0, 1)
  else
    -- Base ambient color: uses sky color at zenith with extra addition of light pollution, adjusted for conditions
    ambientBaseColor
      :set(skyTopColor)
      :adjustSaturation(CurrentConditions.saturation)
  
    local rain = math.min(CurrentConditions.wetness * 50, 1)
    ambientBaseColor.r = ambientBaseColor.r * math.lerp(1, 0.8, rain)
  
    -- Actual scene ambient color
    ambientAdjColor:set(ambientBaseColor)
    if not ac.isBouncedLightActive() then
      -- If bounced light from Extra FX is disabled, let’s mix in a bit of sun light
      ambientAdjColor:add(lightColor * (0.01 * math.saturate(lightDir.y * 2)))
    end
    ambientAdjColor:scale(AmbientLightIntensity):adjustSaturation(math.lerp(0.25, 0.5, horizonK))
    ac.setAmbientColor(ambientAdjColor)
  
    ac.setDistantAmbientColor(ambientDistantColor:set(0.7 - rain * 0.2, 1, 1.3):mul(ambientAdjColor), 4000)
  
    -- Extra ambient for sunsets, to take into account sky glow even when sun is below horizon
    local extraAmbientMult = sunsetK * (1 - NightK) * math.lerpInvSat(CurrentConditions.clear, 0.3, 1)
    if extraAmbientMult > 0 then
      ambientExtraColor
        :set(skySunColor)
        :adjustSaturation(CurrentConditions.saturation)
        :mul(rgb.tmp():set(CurrentConditions.tint):add(1):scale(0.5))
        :scale((1 + CurrentConditions.clouds * 0.6) * extraAmbientMult * 2)
      ac.setExtraAmbientColor(ambientExtraColor)
      ac.setExtraAmbientDirection(ambientExtraDirection:set(SunDir.x, math.max(SunDir.y, 0.1), SunDir.z))
    else
      local directedAmbient = math.lerpInvSat(CurrentConditions.clear, 0.3, 0) * math.lerp(0.3, 0.6, CurrentConditions.cloudsDensity) 
        * (1 - NightK) * (1 - FinalFog)
      if directedAmbient > 0 then
        ambientExtraColor:set(ambientAdjColor):scale(1 - directedAmbient)
        ac.setAmbientColor(ambientExtraColor)
        ambientExtraColor:set(ambientAdjColor):scale(directedAmbient)
        ac.setExtraAmbientColor(ambientExtraColor)
        ac.setExtraAmbientDirection(ambientExtraDirection:set(0, 0.3 / (0.1 + directedAmbient), 0):add(SunDir))
      else
        ambientExtraColor:set(0)
        ac.setExtraAmbientColor(ambientExtraColor)
      end
    end

    -- ac.debug('lightBrightness', lightBrightness)
    -- ac.debug('horizonK', horizonK)
    -- ac.debug('headlights', lightBrightness < 8 or horizonK > 0.5)
    -- ac.debug('sugg', math.max(math.lerpInvSat(lightBrightness, 8, 6), math.lerpInvSat(horizonK, 0.8, 0.9)))
  
    -- Turning on headlights when it’s too dark outside
    -- ac.setAiHeadlights(lightBrightness < 8 or horizonK > 0.5)
    ac.setAiHeadlightsSuggestion(math.max(math.lerpInvSat(lightBrightness, 6, 4), 
      math.max(math.lerpInvSat(horizonK, 0.8, 0.9), math.lerpInvSat(CurrentConditions.rain, 0.003, 0.01))))
  
    -- Adjusting fake shadows under cars
    ac.setWeatherFakeShadowOpacity(1)
    ac.setWeatherFakeShadowConcentration(math.lerp(0.15, 1, NightK))
  
    -- Adjusting vertex AO
    ac.adjustTrackVAO(math.lerp(0.55, 0.65, CurrentConditions.clear), 0, 1)
    ac.adjustDynamicAOSamples(math.lerp(0.2, 0.25, CurrentConditions.clear), 0, 1)
  end
end

function ApplySceneTweaks(dt)
  if UseGammaFix then
    local grassThrive = math.saturateN(Sim.ambientTemperature / 20) * Sim.weatherConditions.humidity
    ac.configureGrassShading(0.1 * (1 + sunsetK), 0.03 * (1 + sunsetK), grassThrive * 2, 0.25 + grassThrive)
  else
    ac.configureGrassShading(0, 0, 0, 0)
  end
  ac.setSnowMix(math.lerpInvSat(Sim.ambientTemperature, 1, 0))
end

-- Updates fog, fog color is based on ambient color, so sometimes this fog can get red with sunsets
local skyHorizonColor = rgb(1, 1, 1)
local secondaryFogColor = rgb(1, 1, 1)
local fogNoise = LowFrequency2DNoise:new{ frequency = 0.003 }
function ApplyFog(dt)
  ac.calculateSkyColorTo(skyHorizonColor, vec3(SunDir.z, 0, -SunDir.x), false, false)

  local ccFog = FinalFog
  if UseGammaFix then
    local occlusionMult = math.lerpInvSat(cameraOcclusion, 0.1, 1) ^ 2
    ac.setFogColor(skyHorizonColor:scale(SkyBrightness * occlusionMult * 0.5))

    local pressureMult = 101325 / Sim.weatherConditions.pressure
    local fogBlend = math.lerpInvSat(ac.getAltitude(), 10e3, 5e3)
    local fogDistance = math.lerp(28.58e3 * pressureMult * (1 - Sim.weatherConditions.humidity * 0.6), 1e3, totalPollution) * math.lerp(1, 0.1, math.lerp((1 - CurrentConditions.clear) * 0.5, 1, ccFog))
    ac.setFogDistance(fogDistance)
    ac.setFogExponent(1 - CurrentConditions.pollution * 0.5)
    ac.setFogBlend(fogBlend)

    local atmosphereFade = math.lerp(ccFog, 1, math.max(CurrentConditions.clouds, 1 - CurrentConditions.clear))
    ac.setFogAtmosphere(fogDistance * (1 - atmosphereFade * 0.5) / (22.5e3 * pressureMult) * (0.65 + Sim.weatherConditions.humidity * 0.7))

    -- ac.debug('occlusionMult', occlusionMult)
    local distanceBoost = math.max(0, Sim.cameraPosition.y - GroundYAveraged) * math.lerp(4, 0.4, NightK)
    ac.setNearbyFog(secondaryFogColor:set(ambientDistantColor)
        :addScaled(lightColor, lightDir.y):scale(1 - NightK):addScaled(skyHorizonColor, NightK):scale((0.05 + 0.95 * occlusionMult) * 2), --:mul(rgbm(1, 1, 0, 1)),
      math.lerp(math.lerp(5e3, 1e3, NightK), math.lerp(50, 30, NightK), ccFog) + distanceBoost, math.lerp(-20, -10, ccFog),
      fogBlend * math.min(1, 1.2 * ccFog / (0.1 + ccFog)),
      math.lerp(0.9, 1.1, fogNoise:get(Sim.cameraPosition)) * (1 + ccFog ^ 2))

    local horizonFog = math.min(1, 1.5 * ccFog / (0.5 + ccFog))
    ac.setSkyFogMultiplier(horizonFog * 0.8)
    ac.setHorizonFogMultiplier(1, math.lerp(math.lerp(10, 4, horizonK), 0.5, horizonFog), fogRangeMult)
  
    ac.setFogBacklitExponent(12)
    ac.setFogBacklitMultiplier(math.lerp(4, 0.2, CurrentConditions.clouds))
  else
    ccFog = math.lerp(ccFog, 1, CityHaze * 0.5)
    ac.setFogColor(skyHorizonColor:scale(SkyBrightness))

    local fogDistance = math.lerp(1500, 35, ccFog)
    local fogHorizon = math.min(1, math.lerp(0.3, 1.1, ccFog ^ 0.5))
    local fogDensity = math.lerp(0.01, 1, ccFog ^ 2)
    local fogExponent = math.lerp(0.3, 0.5, fogNoise:get(Sim.cameraPosition))
  
    ac.setFogExponent(fogExponent)
    ac.setFogDensity(fogDensity)
    ac.setFogDistance(fogDistance)
    ac.setFogHeight(GroundYAveraged - fogDistance - math.lerp(100, 20, ccFog))
    ac.setFogAtmosphere(0)
    ac.setSkyFogMultiplier(ccFog ^ 2)
    ac.setHorizonFogMultiplier(fogHorizon, math.lerp(8, 1, ccFog), math.lerp(0.95, 0.75, ccFog ^ 2))
  
    ac.setFogBlend(fogHorizon)
    ac.setFogBacklitExponent(12)
    ac.setFogBacklitMultiplier(math.lerp(0.2, 1, ccFog * horizonK) * FogBacklitIntensity)
  end
end

-- Calculates heat factor for wobbling air above heated track and that wet road/mirage effect
function ApplyHeatFactor()
  local heatFactor = math.lerpInvSat(SunDir.y, 0.6, 0.7) 
    * math.lerpInvSat(CurrentConditions.clear, 0.7, 0.9) 
    * math.lerpInvSat(CurrentConditions.clouds, 0.6, 0.3)
    * math.lerpInvSat(CurrentConditions.windSpeed, 7, 3)
  ac.setTrackHeatFactor(heatFactor)
end

-- Updates stuff like moon, stars and planets
local moonBaseColor = rgb()
function ApplySkyFeatures()
  -- local brightness = ((0.25 / math.max(lightBrightness, 0.05)) ^ 2) * LightPollutionSkyFeaturesMult 
  --   * (CurrentConditions.clear ^ 4) * 0.1

  local moonBrightness = math.lerp(50, 10 - CurrentConditions.clear * 9, NightK ^ 0.1)
  local moonOpacity = math.lerp(0.1, 1, NightK) * CurrentConditions.clear * LightPollutionSkyFeaturesMult
  local starsBrightness = 2

  if UseGammaFix then
    local starsMultBase = CurrentConditions.clear * (1 - FinalFog)
    starsMultBase = starsMultBase * (1 - AuroraIntensity * 0.3)

    local starsMult = starsMultBase * (1 - LightPollutionValue)
    moonOpacity = 0.005
    moonBrightness = 50 * starsMult
    starsBrightness = (1 + 9 * NightK) * starsMult
    ac.setSkyMoonBaseColor(MoonColor)

    ac.setSkyPlanetsBrightness(5)
    ac.setSkyPlanetsOpacity(NightK * starsMultBase)
  else
    moonBrightness = math.lerp(50, 10 - CurrentConditions.clear * 9, NightK ^ 0.1)
    moonOpacity = math.lerp(0.1, 1, NightK) * CurrentConditions.clear * LightPollutionSkyFeaturesMult
    starsBrightness = 2 * NightK * moonBrightness
    ac.setSkyMoonBaseColor(moonBaseColor:setScaled(MoonColor, 0.2 + NightK))

    ac.setSkyPlanetsBrightness(moonBrightness)
    ac.setSkyPlanetsOpacity(moonOpacity)
  end

  if SpaceLook > 0 then
    moonBrightness = math.lerp(moonBrightness, 1, SpaceLook)
    starsBrightness = math.lerp(starsBrightness, 1, SpaceLook)
    moonOpacity = math.lerp(moonOpacity, 1, SpaceLook)
  end

  ac.setSkyMoonMieMultiplier(0.00003 * (1 - CurrentConditions.clear) * (1 - FinalFog))
  ac.setSkyMoonBrightness(moonBrightness)
  ac.setSkyMoonOpacity(moonOpacity)
  ac.setSkyMoonMieExp(120)
  ac.setSkyMoonDepthSkip(true)

  -- boosting stars brightness for low FOV for some extra movie magic
  starsBrightness = starsBrightness / math.clampN(math.atan(math.rad(Sim.cameraFOV)), 0.05, 1)

  ac.setSkyStarsColor(MoonColor)
  ac.setSkyStarsBrightness(starsBrightness * moonOpacity)
  StarsBrightness = starsBrightness * moonOpacity

  -- easiest way to take light pollution into account is
  -- to raise stars map in power: with stars map storing values from 0 to 1, it gets rid of dimmer stars only leaving
  -- brightest ones    

  local augustK = math.lerpInvSat(math.abs(1 - ac.getDayOfTheYear() / 200), 0.2, 0.1)
  if UseGammaFix then
    local pollutionK = LightPollutionValue
    pollutionK = math.lerp(pollutionK, 1, 1 - NightK)
    pollutionK = math.lerp(pollutionK, 1, math.lerpInvSat(MoonDir.y, -0.1, 0.1) * 0.5)

    -- augustK = 1
    -- pollutionK = 0

    ac.setSkyStarsSaturation(math.lerp(0.3, 0.1, pollutionK) * CurrentConditions.saturation)
    ac.setSkyStarsExponent(math.lerp(4 - augustK, 12, pollutionK))

    -- ac.setSkyStarsBrightness(0.1)
    -- ac.setSkyStarsExponent(1)
    -- ac.setSkyStarsBrightness(1)
    -- ac.setSkyStarsExponent(2)
  else
    local pollutionK = math.sqrt(LightPollutionValue)
    pollutionK = math.max(pollutionK, 1 - NightK ^ 4)
    pollutionK = math.max(pollutionK, math.lerpInvSat(MoonDir.y, -0.3, -0.1))
  
    pollutionK = math.max(pollutionK, (1 - augustK) / 4)
    
    ac.setSkyStarsSaturation(math.lerp(0.3, 0.1, pollutionK) * CurrentConditions.saturation)
    ac.setSkyStarsExponent(math.lerp(1.3, 4, pollutionK ^ 2))
  end

  ac.setSkyPlanetsSizeBase(1)
  ac.setSkyPlanetsSizeVariance(1)
  ac.setSkyPlanetsSizeMultiplier(1)
end

-- local function sunBehindHorizon(sunDir, distanceToCenter, earthRadius)
--   return (sunDir.y * distanceToCenter) ^ 2 + earthRadius ^ 2 > distanceToCenter ^ 2
-- end

-- Thing thing disables shadows if it’s too cloudy or light is not bright enough, or downsizes shadow map resolution
-- making shadows look blurry
function ApplyAdaptiveShadows()
  if lightColor:value() < (UseGammaFix and 1e-5 or 0.001) then
    ac.setShadows(ac.ShadowsState.Off)
  elseif SpaceLook > 0 then
    ac.resetShadowsResolution()
    ac.setShadows(ac.ShadowsState.On)
    -- if SunDir.y > 0 then
    --   ac.setShadows(ac.ShadowsState.On)
    -- elseif sunBehindHorizon(SunDir, ac.getAltitude() + 6400e3, 6400e3) then
    --   ac.setShadows(ac.ShadowsState.EverythingShadowed)
    -- else
    --   ac.setShadows(ac.ShadowsState.On)
    -- end
  elseif belowHorizonCorrection > 0 and BlurShadowsWhenSunIsLow then
    if belowHorizonCorrection > 0.8 then ac.setShadowsResolution(256)
    elseif belowHorizonCorrection > 0.6 then ac.setShadowsResolution(384)
    elseif belowHorizonCorrection > 0.4 then ac.setShadowsResolution(512)
    elseif belowHorizonCorrection > 0.2 then ac.setShadowsResolution(768)
    else ac.setShadowsResolution(1024) end
    ac.setShadows(ac.ShadowsState.On)
  elseif BlurShadowsWithFog then
    if FinalFog > 0.96 then ac.setShadowsResolution(256)
    elseif FinalFog > 0.92 then ac.setShadowsResolution(384)
    elseif FinalFog > 0.88 then ac.setShadowsResolution(512)
    elseif FinalFog > 0.84 then ac.setShadowsResolution(768)
    elseif FinalFog > 0.8 then ac.setShadowsResolution(1024)
    else ac.resetShadowsResolution() end
    ac.setShadows(ac.ShadowsState.On)
  else
    ac.resetShadowsResolution()
    ac.setShadows(ac.ShadowsState.On)
  end
end

-- The idea here is to use scene brightness for adapting camera to darkness in tunnels
-- unlike auto-exposure approach, it would be smoother and wouldn’t jump as much if camera
-- simply rotates and, for example, looks down in car interior
ac.setCameraOcclusionDepthBoost(2.5)
local function getSceneBrightness(dt)
  local aoNow = math.lerp(ac.getCameraLookOcclusion(), ac.getCameraOcclusion(vec3Up), 0.5)
  cameraOcclusion = math.applyLag(cameraOcclusion, aoNow, RecentlyJumped > 0 and 0 or 0.95, dt)
  return cameraOcclusion ^ 2
end

local brightnessMult = 1
function ApplyFakeExposure_postponed()
  if not UseGammaFix then return false end

  local delta = math.abs(brightnessMult - BrightnessMultApplied)
  if delta < 1e-5 then return false end

  ac.adjustCubemapReprojectionBrightness(brightnessMult / BrightnessMultApplied)

  BrightnessMultApplied = brightnessMult
  ac.setBrightnessMult(GammaFixBrightnessOffset * brightnessMult)

  ac.setHDRToLDRConversionHints(1 / (brightnessMult * GammaFixBrightnessOffset), 0.4545)
  ac.setOverallSkyBrightnessMult(1)
  ac.setSkyV2DitherScale(0)

  if ac.isPpActive() then
    ppBrightnessCorrection.value = 1.6
  end

  -- Lights can be pretty dark now
  local lightsMult = 0.004

  -- A funny trick: split multiplier into v1 and v2 since v1 is the one that can turn the lights
  -- off, so there could be a bit of extra optimization in a sunny day
  local v1LightsMult = 0.001 + 0.999 * math.lerpInvSat(brightnessMult, 2, 5)
  ac.setWeatherLightsMultiplier(math.pow(v1LightsMult, 1 / 2.2))
  ac.setWeatherLightsMultiplier2((lightsMult / v1LightsMult * GammaFixBrightnessOffset) * brightnessMult)
  ac.setBaseAmbientColor(rgb.tmp():set(0.00002))
  ac.setEmissiveMultiplier(ScriptSettings.LINEAR_COLOR_SPACE.DIM_EMISSIVES and 0.07 or 2.5 / brightnessMult) -- how bright are emissives
  ac.setTrueEmissiveMultiplier(1) -- how bright are extrafx emissives
  ac.setGlowBrightness(1) -- how bright are those distant emissive glows
  ac.setWeatherTrackLightsMultiplierThreshold(0.01)

  ac.setWhiteReferencePoint(10 / brightnessMult) -- white ref point is always bright

  -- No need to boost reflections here (and fresnel gamma wouldn’t even work anyway)
  ac.setFresnelGamma(1)
  ac.setReflectionEmissiveBoost(1)

  return delta > 0.01
end

-- There are two problems fake exposure solves:
-- 1. We need days to be much brighter than nights, to such an extend that lights wouldn’t be visible in sunny days.
--    That also hugely helps with performance.
-- 2. In dark tunnels, brightness should go up, revealing those lights and overexposing everything outside.
-- Ideally, HDR should’ve solved that task, but it introduces some other problems: for example, emissives go too dark,
-- or too bright during the day. That’s why instead this thing uses fake exposure, adjusting brightness a bit, but 
-- also, adjusting intensity of all dynamic lights and emissives to make it seem like the difference is bigger.
function ApplyFakeExposure(dt)
  if UseGammaFix then
    local envBrightness = ambientLuminance * 3 + lightColor:luminance() * math.saturate(lightDir.y * 20)
    local envMix = getSceneBrightness(dt)

    local localBrightness = math.lerp(0, envBrightness, envMix)
    localBrightness = (localBrightness ^ 2 + 1) ^ 0.5 -- soft max instead of basic math.max(localBrightness, 1)

    if SpaceLook > 0 then
      localBrightness = math.lerp(localBrightness, 1, SpaceLook)
    end

    local brightnessMultTarget = 50 / localBrightness
    if initialSet > 0 or RecentlyJumped > 0 then
      brightnessMult = brightnessMultTarget
      initialSet = initialSet - 1
    else
      brightnessMult = math.applyLag(brightnessMult, brightnessMultTarget, 0.98, dt)
    end

    local aiHeadlights = math.lerp(math.lerpInvSat(ambientLuminance, 0.05, 0.03), 1,
      math.max(FinalFog, math.min(CurrentConditions.rain * 10, 1)))
    ac.setAiHeadlightsSuggestion(aiHeadlights, 0.3)
  else  
    ac.setHDRToLDRConversionHints(1, 1)

    local lightBrightnessRaw = ambientAdjColor:value() * 1.5 + lightColor:value() * math.saturate(lightDir.y * 1.4) * 0.5
    local aoK = math.lerpInvSat(getSceneBrightness(dt), 0.1, 0.4)
    lightBrightnessRaw = lightBrightnessRaw * aoK
  
    if initialSet > 0 then
      lightBrightness = lightBrightnessRaw
      initialSet = initialSet - 1
    elseif lightBrightness < lightBrightnessRaw then
      lightBrightness = math.min(lightBrightness + dt * AdaptationSpeed, lightBrightnessRaw)
    elseif lightBrightness > lightBrightnessRaw then
      lightBrightness = math.max(lightBrightness - dt * AdaptationSpeed, lightBrightnessRaw)
    end
  
    local sceneBrightness = 5 / (lightBrightness + 5)
    local lightsMult = math.sqrt(math.max(sceneBrightness - 0.2, 0))
    -- ac.debug('sceneBrightness', sceneBrightness)
  
    if ac.isPpActive() then
      -- with post-processing, adjusting scene brightness and post-processing brightness
      ac.setBrightnessMult(sceneBrightness * SceneBrightnessMultPP)
      ac.setOverallSkyBrightnessMult(SkyBrightness)
      ppBrightnessCorrection.value = math.lerp(2, 1, sceneBrightness) * FilterBrightnessMultPP
    else
      -- without post-processing, we can only adjust scene brightness, but that’s enough
      ac.setBrightnessMult(sceneBrightness * SceneBrightnessMultNoPP)
      ac.setOverallSkyBrightnessMult(SkyBrightness)
    end
  
    -- ac.debug('lightsMult', lightsMult)
  
    ac.setWeatherLightsMultiplier(math.max(lightsMult * 1.25 - 0.25, 0) * 2) -- how bright are lights
    ac.setTrueEmissiveMultiplier(lightsMult) -- how bright are extrafx emissives
    ac.setGlowBrightness(lightsMult * 0.15) -- how bright are those distant emissive glows
    ac.setEmissiveMultiplier(0.9 + lightsMult * 0.3) -- how bright are emissives
    ac.setWeatherTrackLightsMultiplierThreshold(0.01) -- let make lights switch on early for smoothness
    ac.setWhiteReferencePoint(0) 
  
    ac.setBaseAmbientColor(rgb.tmp():set((UseGammaFix and 0.004 or 0.04) * lightsMult)) -- base ambient adds a bit of extra ambient lighting not
      -- affected by ambient occlusion, so even pitch black tunnels become a tiny bit lit after “eye” adapts.
  
    -- Trying to get reflections to work better
    ac.setFresnelGamma(math.lerp(1, 0.8, NightK))
    ac.setReflectionEmissiveBoost(1 + 2 * NightK)
  end
end

-- Creates generic cloud material
---@return ac.SkyCloudMaterial
local function createGenericCloudMaterial(props)
  local ret = ac.SkyCloudMaterial()
  ret.baseColor = rgb(0.15, 0.15, 0.15)
  ret.useSceneAmbient = false
  ret.ambientConcentration = 0.35
  ret.frontlitMultiplier = 1
  ret.frontlitDiffuseConcentration = 0.45
  ret.backlitMultiplier = 0
  ret.backlitExponent = 30
  ret.backlitOpacityMultiplier = 0.6
  ret.backlitOpacityExponent = 1.7
  ret.specularPower = 1
  ret.specularExponent = 5
  ret.alphaSmoothTransition = 1
  ret.normalFacingExponent = 2

  if props ~= nil then
    for k, v in pairs(props) do
      ret[k] = v
    end
  end

  return ret
end

-- Global cloud materials
CloudMaterials = {}

-- Initialization for some static values
CloudMaterials.Main = createGenericCloudMaterial({ 
  contourExponent = 2,
  contourIntensity = 0.2,
  ambientConcentration = 0.1, 
  frontlitDiffuseConcentration = 0.8,
  backlitMultiplier = 4,
  backlitOpacityMultiplier = 0.5,
  backlitOpacityExponent = 1,
  backlitExponent = 20,
  specularExponent = 2,
  receiveShadowsOpacity = 0.9,
  fogMultiplier = 1
})

CloudMaterials.Bottom = createGenericCloudMaterial({
  contourExponent = 4,
  contourIntensity = 0.1,
  ambientConcentration = 0.1,  
  frontlitDiffuseConcentration = 0.5,
  backlitMultiplier = 4,
  backlitOpacityMultiplier = 0.8,
  backlitOpacityExponent = 3,
  backlitExponent = 15,
  specularPower = 0,
  specularExponent = 1,
  receiveShadowsOpacity = 0.9,
  fogMultiplier = 1
})

CloudMaterials.Hovering = createGenericCloudMaterial({
  frontlitMultiplier = 1,
  frontlitDiffuseConcentration = 0.3,
  ambientConcentration = 0.1,
  backlitMultiplier = 2,
  backlitOpacityMultiplier = 0.8,
  backlitOpacityExponent = 3,
  backlitExponent = 20,
  specularPower = 0,
  specularExponent = 1,
  fogMultiplier = 1
})

CloudMaterials.Spread = createGenericCloudMaterial({
  frontlitMultiplier = 1,
  frontlitDiffuseConcentration = 0,
  ambientConcentration = 0,
  backlitMultiplier = 4,
  backlitOpacityMultiplier = 0,
  backlitOpacityExponent = 1,
  backlitExponent = 20,
  specularPower = 0,
  specularExponent = 1,
  fogMultiplier = 1
})

local cloudMateralsList = {CloudMaterials.Main, CloudMaterials.Bottom, CloudMaterials.Hovering, CloudMaterials.Spread}
local prevSunsetK = -1
local prevCloudDensityK = -1

-- Update cloud materials for chanding lighting conditions
function UpdateCloudMaterials()
  ac.setLightShadowOpacity(math.lerp(0, 0.6 + 0.3 * CurrentConditions.clouds, CurrentConditions.clear))

  local main = CloudMaterials.Main
  local ccCloudsDensity = CurrentConditions.cloudsDensity

  if UseGammaFix then
    local densityMult = 1 - ccCloudsDensity * 0.8
    main.ambientColor:setScaled(skyTopColor, 3 * densityMult)
    main.extraDownlit:setScaled(lightColor, 0.03 * lightDir.y * densityMult)
      :addScaled(LightPollutionColor, 0.5 * densityMult)
    main.extraDownlit.r, main.extraDownlit.b = main.extraDownlit.r * 0.9, main.extraDownlit.b * 0.8

    if math.abs(prevCloudDensityK - ccCloudsDensity) > 0.001
      or math.abs(prevSunsetK - SunDir.y) > 0.001 then
      prevSunsetK = SunDir.y
      prevCloudDensityK = ccCloudsDensity
      for _, v in ipairs(cloudMateralsList) do
        v.baseColor:set(0.3 * densityMult)
        v.ambientConcentration = math.lerp(0.25, 0.45, ccCloudsDensity)
        v.frontlitMultiplier = math.lerp(2.5, 1, horizonK) * densityMult
        v.frontlitDiffuseConcentration = math.lerp(0.5, 0.75, sunsetK)
        v.receiveShadowsOpacity = 0.9
        v.specularPower = math.lerp(1, 8, sunsetK)
        v.specularExponent = 4
        v.backlitMultiplier = 4 * densityMult
        v.backlitExponent = 10
        v.backlitOpacityMultiplier = 0.5
        v.backlitOpacityExponent = 1
        v.contourIntensity = 0.2 * densityMult
        v.contourExponent = 1
        v.fogMultiplier = 1
        v.alphaSmoothTransition = 1
      end
      CloudMaterials.Bottom.contourExponent = 2
    end
    
    CloudMaterials.Bottom.ambientColor:set(main.ambientColor)
    CloudMaterials.Bottom.extraDownlit:set(main.extraDownlit)
    CloudMaterials.Hovering.ambientColor:set(main.ambientColor)
    CloudMaterials.Hovering.extraDownlit:set(main.extraDownlit)
    CloudMaterials.Spread.ambientColor:set(main.ambientColor)
    CloudMaterials.Spread.extraDownlit:set(main.extraDownlit)
  else
    local ccClear = CurrentConditions.clear
    local ccClouds = CurrentConditions.clouds
    local clearSunset = sunsetK * ccClear

    prevSunsetK = -1
    main.ambientColor
      :set(skyTopColor):adjustSaturation(math.lerp(0.8, 1.4, clearSunset)):scale(math.lerp(3.2, 2, clearSunset) * math.lerp(1.5, 1, ccCloudsDensity))
      -- :add(lightColor:clone():scale(0.1 * math.lerp(0.4, 0.2, sunsetK)))
      -- :add(LightPollutionExtraAmbient)
    main.ambientConcentration = math.lerp(0.2, 0, NightK) * (0.3 + ccCloudsDensity) * (1 - ccClouds * 0.5)
    main.extraDownlit:set(skySunColor):scale(math.lerp(0.2, 0.1, sunsetK) * ccClear)
    main.frontlitMultiplier = math.lerp(1 + ccClear, 0.5 + ccClear * 0.8, NightK)
    main.specularPower = math.lerp(1, 0.1, NightK)
    main.frontlitDiffuseConcentration = math.lerp(0.3, 0.8, sunsetK)
    main.backlitMultiplier = math.lerp(0, 6, ccClear)
    
    -- Wet look
    local colorValue = math.lerp(0.15, 0.05, ccCloudsDensity)
    main.baseColor:set(colorValue, colorValue, colorValue)
    main.ambientConcentration = math.lerp(main.ambientConcentration, 0.5, ccCloudsDensity)
    main.frontlitDiffuseConcentration = math.lerp(main.frontlitDiffuseConcentration, 1.0, ccCloudsDensity)
    main.specularPower = math.lerp(main.frontlitDiffuseConcentration, 0, ccCloudsDensity)
  
    local bottom = CloudMaterials.Bottom
    bottom.baseColor:set(main.baseColor)
    bottom.ambientColor:set(main.ambientColor)
    bottom.ambientConcentration = main.ambientConcentration * 0.5
    bottom.extraDownlit:set(main.extraDownlit)
    bottom.frontlitMultiplier = main.frontlitMultiplier
    bottom.frontlitDiffuseConcentration = main.frontlitDiffuseConcentration
    bottom.frontlitDiffuseConcentration = main.frontlitDiffuseConcentration
    bottom.specularPower = main.specularPower
    bottom.contourIntensity = main.contourIntensity
    
    local hovering = CloudMaterials.Hovering
    hovering.ambientColor:set(main.ambientColor)
    
    local spread = CloudMaterials.Spread
    spread.ambientColor:set(main.ambientColor)
  end
end

function ApplyThunder(dt)
  if dt == 0 then
    return false
  end

  local cc = CurrentConditions
  local chance = cc.thunder * cc.clouds
  if math.random() > math.lerp(1.03, 0.97, chance) and SpaceLook == 0 then
    thunderActiveFor = 0.1 + math.random() * 0.3
  end

  local showFlash = false
  if thunderActiveFor > 0 then
    thunderActiveFor = thunderActiveFor - dt
    showFlash = math.random() > 0.95
  end

  if showFlash then
    if not thunderFlashAdded then
      thunderFlash.direction = vec3(
        math.random() - 0.5 - 0.1 * CurrentConditions.windDir.x, math.random() ^ 2,
        math.random() - 0.5 - 0.1 * CurrentConditions.windDir.y):normalize()

      local drawBolt = thunderFlash.direction.y < 0.4
      if drawBolt then
        AddVisualLightning(thunderFlash.direction:clone())
      end

      thunderFlash.exponent = UseGammaFix and 1 or 1
      thunderFlash.color = rgb(0.3, 0.3, 0.5):scale(1 + math.random()):scale(UseGammaFix and 0.001 or 1)
      thunderFlashAdded = true
      ac.skyExtraGradients:push(thunderFlash)
      ac.pauseCubemapUpdates(true)
      return true
    end
  elseif thunderFlashAdded then
    thunderFlashAdded = false
    ac.skyExtraGradients:erase(thunderFlash)
    setTimeout(function ()
      ac.pauseCubemapUpdates(false)
    end)
    return true
  end
end

local tornadoEffect

local function findNewTornadoPos()
  local distance = math.lerp(3e3, 12e3, math.random())
  local angle = math.random() * math.tau
  do
    return vec3(-5317.17, -19.31, -1253.33)
  end
  return vec3(Sim.cameraPosition.x + math.sin(angle) * distance, ac.getGroundYApproximation(), Sim.cameraPosition.z + math.cos(angle) * distance)
end

function ApplyTornado(intensity)
  if not tornadoEffect then
    if intensity == 0 then return end
    tornadoEffect = {
      intensity = 0,
      instances = { 
        { pos = findNewTornadoPos(), size = 2e3 * (0.8 + 0.4 * math.random()), life = 1, start = os.preciseClock() + math.random() * 1e3 },
        -- { pos = findNewTornadoPos(), size = 2e3 * (0.8 + 0.4 * math.random()), life = 7, start = os.preciseClock() + math.random() * 1e3 },
      },
      update = function (s, intensity)
        if s.intensity == s then return end
        if (s.intensity > 0) ~= (intensity > 0) then
          if s.listener then 
            s.listener()
          end
          if intensity > 0 then
            s.listener = render.on('main.track.transparent', function ()
              for _, v in ipairs(s.instances) do
                local distanceMult = math.lerpInvSat(Sim.cameraPosition:distance(v.pos), 1e3, 2e3)
                local mult = tornadoEffect.intensity
                  * distanceMult
                  * math.lerpInvSat(v.life, 0, 2)
                  * math.lerpInvSat(v.life, 10, 8)
                if distanceMult < 0.99 and v.life < 8 then
                  v.life = math.max(8, 10 - v.life)
                end
                render.tornado(v.pos, v.size, mult, os.preciseClock() - v.start + 5e3)
              end
            end)
          end
        end
        for _, v in ipairs(s.instances) do
          v.life = v.life + Sim.dt * 0.05
          if v.life > 10 then
            v.life = 0
            v.pos = findNewTornadoPos()
            v.size = 2e3 * (0.8 + 0.4 * math.random())
            v.start = os.preciseClock() + math.random() * 1e3
          end
        end
        s.intensity = intensity
      end
    }
  end
  tornadoEffect:update(intensity)
end
