-- Experimental RainFX audio implementation. Might be moved to C++ side later once everything is sorted out.

local sim = nil ---@type ac.StateSim
local rainAmbientAudio = nil ---@type ac.AudioEvent
local rainAmbientThunderAudio = nil ---@type ac.AudioEvent
local rainCarInterior = nil ---@type ac.AudioEvent
local rainCarExterior = nil ---@type ac.AudioEvent
local rainGrass = nil ---@type ac.AudioEvent
local rainGravel = nil ---@type ac.AudioEvent
local rainSkidsInterior = nil ---@type ac.AudioEvent[]
local rainSkidsExterior = nil ---@type ac.AudioEvent[]

local rainActive = false
local thunderActive = false
local rainGrassActive = false
local rainGravelActive = false

local function startRainAudio()
  if not rainAmbientAudio then
    sim = ac.getSim()
    rainAmbientAudio = ac.AudioEvent('event:/extension_common/rain_amb', true)
    rainAmbientThunderAudio = ac.AudioEvent('event:/extension_common/rain_amb_thunder', true)
    rainCarInterior = ac.AudioEvent('event:/extension_common/rain_car_int', false)
    rainCarExterior = ac.AudioEvent('event:/extension_common/rain_car_ext', true)
    rainGrass = ac.AudioEvent('event:/extension_common/rain_grass', true):setVolumeChannel(ac.AudioChannel.Surfaces)
    rainGravel = ac.AudioEvent('event:/extension_common/rain_gravel', true):setVolumeChannel(ac.AudioChannel.Surfaces)
    rainSkidsInterior = table.range(4, function ()
      return ac.AudioEvent('event:/extension_common/rain_skid_int', false):setVolumeChannel(ac.AudioChannel.Tyres)
    end)
    rainSkidsExterior = table.range(4, function ()
      return ac.AudioEvent('event:/extension_common/rain_skid_ext', true):setVolumeChannel(ac.AudioChannel.Tyres)
    end)

    rainAmbientAudio.cameraInteriorMultiplier = 0.5
    rainAmbientThunderAudio.cameraInteriorMultiplier = 0.5
    rainCarInterior.cameraInteriorMultiplier = 1
    rainGrass.cameraInteriorMultiplier = 1
    rainGravel.cameraInteriorMultiplier = 1
    for i = 1, 4 do
      rainSkidsInterior[i].cameraInteriorMultiplier = 1
      rainSkidsExterior[i].cameraInteriorMultiplier = 0.5
    end

    rainGrass:setDistanceMin(4)
    rainGrass:setDistanceMax(10)
    rainGrass:setConeSettings(360, 360, 1)
  end

  rainAmbientAudio:start()
end

local cameraPos = vec3()
local cameraDir = vec3()
local groundPos = vec3()
local dirUp = vec3(0, 1, 0)
local dirDown = vec3(0, -1, 0)
local dirFwd = vec3(0, 0, 1)
local dirBack = vec3(0, 0, -1)
local carAudioPlaying = false
local grassPos = vec3()
local gravelPos = vec3()

---@return ac.StateCar?
local function nearestCar()
  if sim.focusedCar == -1 then return nil end
  local car = ac.getCar(sim.focusedCar)
  return car and car.position:closerToThan(cameraPos, 10) and car or nil
end

local appliedWet = 0
local puddleHitSmooth = 0
local wheelGrassSmooth = {0,0,0,0}
local wheelDirtSmooth = {0,0,0,0}
local resetSet = false

local function setWetMultiplier(mult)
  ac.setAudioEventMultiplier('event:/surfaces/grass', mult)
  ac.setAudioEventMultiplier('event:/surfaces/sand', mult)
  ac.setAudioEventMultiplier('event:/cars/?/skid_ext', mult)
  ac.setAudioEventMultiplier('event:/cars/?/skid_int', mult)

  if not resetSet then
    resetSet = true
    ac.onRelease(function()
      setWetMultiplier(1)
    end)
  end
end

local function tonemap(x)
  if x < 0 then return 0 end
  return x / (1 + x)
end

local function updateRainAudio(rainAmount, rainWetness, dt)
  ac.getCameraPositionTo(cameraPos)
  ac.getCameraForwardTo(cameraDir)
  groundPos:set(cameraPos.x, ac.getGroundYApproximation(), cameraPos.z)

  local localOcclusion = ac.sampleCameraAO()
  if localOcclusion == 1 then
    localOcclusion = ac.getCameraOcclusion(dirUp)
  end

  local volume = 1
  local wetTrack = localOcclusion * math.lerpInvSat(rainWetness, 0.002, 0.01)
  local wetVolume = wetTrack * volume
  local thunderMix = CurrentConditions.thunder
  local cloudsDensity = CurrentConditions.cloudsDensity
  local localRain = rainAmount * localOcclusion

  if math.abs(wetVolume - appliedWet) > 0.01 then
    appliedWet = wetVolume
    setWetMultiplier(1 - wetVolume)
  end

  if thunderMix > 0 and not thunderActive then
    thunderActive = true
    rainAmbientThunderAudio:start()
  end
  
  local localRainVolume = volume * (math.lerpInvSat(localRain, 0, 0.1) + localRain)
  local localRainIntensity = math.lerp(0.15, 1, localRain ^ 0.5)
  rainAmbientAudio.volume = localRainVolume * (1 - thunderMix)
  rainAmbientThunderAudio.volume = localRainVolume * thunderMix

  local car = nearestCar()
  rainAmbientAudio:setPosition(groundPos, dirUp, dirFwd, car and car.velocity or nil)
  rainAmbientAudio:setParam('intensity\n' --[[ yup, that’s a bug with that FMOD soundbank I use ]], localRainIntensity)

  if thunderActive then
    rainAmbientThunderAudio:setPosition(cameraPos, dirUp, dirFwd, car and car.velocity or nil)
    rainAmbientThunderAudio:setParam('intensity\n', localRainIntensity)
  end

  local speed = 400 * cloudsDensity + (car and car.speedKmh or 0)
  if thunderActive then
    rainAmbientThunderAudio:setParam('speed', speed)
  end

  if carAudioPlaying ~= (car ~= nil) then
    carAudioPlaying = car ~= nil
    rainCarExterior:resumeIf(carAudioPlaying)

    if not car then
      rainCarInterior:stop()
      for i = 1, 4 do
        rainSkidsExterior[i]:stop()
        rainSkidsInterior[i]:stop()
      end
    end
  end

  if not car then
    if rainGrassActive then
      rainGrassActive = false
      rainGrass:stop()
    end
    if rainGravelActive then
      rainGravelActive = false
      rainGravel:stop()
    end
    return
  end

  rainCarExterior.volume = volume * (1 + thunderMix) * (1 + cloudsDensity) * math.lerpInvSat(car.position:distance(cameraPos), 10, 0) * (0.2 + math.saturateN(car.speedKmh / 200))
  rainCarExterior:setPosition(car.position, dirDown, dirBack, car and car.velocity or nil)
  rainCarExterior:setParam('intensity', localRain * (1 + 2 * math.sqrt(CurrentConditions.rain)))
  rainCarExterior:setParam('speed', car.speedKmh)
  rainCarExterior.cameraInteriorMultiplier = car.focusedOnInterior and car.isOpenWheeler and 1 or 0.5
  
  rainCarInterior.volume = volume * (1 + thunderMix) * (1 + cloudsDensity) * (car.isOpenWheeler and 0.15 or 1)
  rainCarInterior:resumeIf(car.focusedOnInterior)
  rainCarInterior:setPosition(car.position, dirDown, dirBack, car and car.velocity or nil)
  rainCarInterior:setParam('intensity', localRain * (1 + 2 * math.sqrt(CurrentConditions.rain)))
  rainCarInterior:setParam('speed', car.speedKmh)

  local surfaceGrass = 0
  local surfaceGravel = 0
  local puddleHit = false
  grassPos:set(0, 0, 0)
  gravelPos:set(0, 0, 0)
  for i = 0, 3 do
    local wheel = car.wheels[i]
    local surfaceType = wheel.surfaceType

    local puddleHitWheel = wheel.waterThickness > 0.003
    local newWaterGrassSmooth = math.applyLag(wheelGrassSmooth[i + 1], surfaceType == ac.SurfaceType.Grass and 0.25 * wheel.loadK * wetTrack
      or (math.lerpInvSat(wheel.waterThickness, 0.0002, 0.001) * 0.1 + math.lerpInvSat(wheel.waterThickness, 0.003, 0.005)) * wheel.loadK, puddleHitWheel and 0.6 or 0.8, dt)
    wheelGrassSmooth[i + 1] = newWaterGrassSmooth
    if newWaterGrassSmooth > 0.001 then
      surfaceGrass = surfaceGrass + newWaterGrassSmooth
      grassPos:addScaled(wheel.contactPoint, newWaterGrassSmooth)
      if puddleHitWheel then puddleHit = true end
    end
    
    local newWaterDirtSmooth = math.applyLag(wheelDirtSmooth[i + 1], surfaceType == ac.SurfaceType.Dirt and 0.25 * wheel.loadK or 0, 0.6, dt)
    wheelDirtSmooth[i + 1] = newWaterDirtSmooth
    if newWaterDirtSmooth > 0.001 then
      surfaceGravel = surfaceGravel + newWaterDirtSmooth
      gravelPos:addScaled(wheel.contactPoint, newWaterDirtSmooth)
    end

    local slipAdjusted = wheel.ndSlip - wheel.waterThickness * 800
    local targetVolume = wheel.surfaceGrip > 0.9 and not wheel.isBlown and not wheel.isSpecialSurface
      and tonemap(slipAdjusted - 1) * math.lerpInvSat(car.speedKmh, 4, 40) * 0.4 or 0
    local newVolume = math.applyLag(rainSkidsExterior[i + 1].volume, targetVolume, 0.6, dt)
    rainSkidsExterior[i + 1].volume = newVolume
    rainSkidsExterior[i + 1].pitch = 1 + tonemap(wheel.angularSpeed / 50)
    rainSkidsExterior[i + 1]:resumeIf(newVolume > 0.01)
    rainSkidsExterior[i + 1]:setPosition(wheel.position, dirUp, dirFwd, wheel.velocity)
    if car.focusedOnInterior then
      rainSkidsInterior[i + 1].volume = newVolume
      rainSkidsInterior[i + 1].pitch = 1 + tonemap(wheel.angularSpeed / 50)
      rainSkidsInterior[i + 1]:resumeIf(newVolume > 0.01)
      rainSkidsInterior[i + 1]:setPosition(wheel.position, dirUp, dirFwd, wheel.velocity)
    else
      rainSkidsInterior[i + 1]:resumeIf(false)
    end
  end

  puddleHitSmooth = math.applyLag(puddleHitSmooth, puddleHit and 1 or 0, puddleHit and 0.6 or 0.8, dt)

  if rainGrassActive ~= (surfaceGrass > 0) then
    rainGrassActive = not rainGrassActive
    rainGrass:resumeIf(rainGrassActive)
  end

  if rainGrassActive then
    rainGrass.volume = 2.4 * surfaceGrass
    rainGrass.pitch = car.focusedOnInterior and math.lerp(1, 0.5, puddleHitSmooth) or 1
    grassPos:scale(1 / surfaceGrass)
    if car.focusedOnInterior then
      grassPos:addScaled(cameraPos, 4):scale(0.2):addScaled(cameraDir, 1)
    end
    rainGrass:setPosition(grassPos, dirUp, dirFwd, car and car.velocity or nil)
    rainGrass:setParam('speed', car.speedKmh * (1 + puddleHitSmooth))
  end

  if rainGravelActive ~= (surfaceGravel > 0 and wetVolume > 0.01) then
    rainGravelActive = not rainGravelActive
    rainGravel:resumeIf(rainGravelActive)
  end

  if rainGravelActive then
    rainGravel.volume = 4 * wetVolume * math.sqrt(surfaceGravel)
    rainGravel:setPosition(gravelPos:scale(1 / surfaceGravel), dirUp, dirFwd, car and car.velocity or nil)
    rainGravel:setParam('speed', car.speedKmh)
  end
end

local function stopRainAudio()
  print('stopping rain audio')
  rainActive = false
  rainAmbientAudio:stop()
  rainAmbientThunderAudio:stop()
  rainCarInterior:stop()
  rainCarExterior:stop()
  rainGrass:stop()
  rainGravel:stop()

  for i = 1, 4 do
    rainSkidsExterior[i]:stop()
    rainSkidsInterior[i]:stop()
  end
  
  if appliedWet ~= 0 then
    appliedWet = 0
    setWetMultiplier(1)
  end
end

function ApplyAudio(dt)
  local rain = 1.1 * CurrentConditions.rain / (0.1 + CurrentConditions.rain)
  local wetness = CurrentConditions.wetness
  if rain > 0.001 or wetness > 0.001 then
    if not rainActive then
      print('starting rain audio')
      rainActive = true
      startRainAudio()
    end
    updateRainAudio(rain, wetness, dt)
  elseif rainActive then
    stopRainAudio()
  end
end
