// Copyright 2025 International Digital Economy Academy
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

///|
pub type AudioInstance = Int

///|

///|
const AUDIO_PLAY_STATE_BLOCKED = 2

///|
const AUDIO_PLAY_STATE_FAILED = 3

///|
priv struct CachedAudioSource {
  src : String
  blob_owned : Bool
}

///|
priv struct AudioPlayback {
  audio : Audio
  mut requested_paused : Bool
  mut last_play_attempt_activation_epoch : Int
}

///|
let audio_instances : Map[AudioInstance, AudioPlayback] = Map([])

///|
let next_audio_instance : Ref[Int] = Ref(1)

///|
let audio_user_activation_epoch : Ref[Int] = Ref(0)

///|
let cached_audio_sources : Map[String, CachedAudioSource] = Map([])

///|
let retired_blob_audio_sources : Array[String] = []

///|
let preloaded_audio_templates : Map[String, Audio] = Map([])

///|
fn clamp_playback_speed(speed : Double) -> Double {
  if speed <= 0.0 {
    0.01
  } else {
    speed
  }
}

///|
fn note_audio_user_activation() -> Unit {
  audio_user_activation_epoch.val += 1
}

///|
fn current_audio_activation_epoch() -> Int {
  audio_user_activation_epoch.val
}

///|
fn audio_prefers_byte_backed_source(path : String) -> Bool {
  if path.has_prefix("http://") || path.has_prefix("https://") {
    return false
  }
  if path.has_prefix("data:") || path.has_prefix("blob:") {
    return false
  }
  path.has_prefix("assets/")
}

///|
fn browser_audio_mime_type(path : String) -> String {
  if path.has_suffix(".ogg") || path.has_suffix(".oga") {
    return "audio/ogg"
  }
  if path.has_suffix(".mp3") {
    return "audio/mpeg"
  }
  if path.has_suffix(".wav") {
    return "audio/wav"
  }
  if path.has_suffix(".flac") {
    return "audio/flac"
  }
  if path.has_suffix(".aac") {
    return "audio/aac"
  }
  "application/octet-stream"
}

///|
fn should_retry_blocked_audio_playback(
  play_state : Int,
  requested_paused : Bool,
  activation_epoch : Int,
  last_play_attempt_activation_epoch : Int,
) -> Bool {
  play_state == AUDIO_PLAY_STATE_BLOCKED &&
  !requested_paused &&
  activation_epoch > last_play_attempt_activation_epoch
}

///|
fn retire_cached_audio_source(entry : CachedAudioSource) -> Unit {
  if entry.blob_owned {
    retired_blob_audio_sources.push(entry.src)
  }
}

///|
fn clear_cached_audio_source(path : String, retire? : Bool = true) -> Unit {
  if cached_audio_sources.get(path) is Some(entry) {
    if retire {
      retire_cached_audio_source(entry)
    } else if entry.blob_owned {
      revoke_object_url(entry.src)
    }
    cached_audio_sources.remove(path)
  }
}

///|
fn resolved_audio_source(path : String, refresh? : Bool = false) -> String {
  if !refresh {
    if cached_audio_sources.get(path) is Some(entry) {
      return entry.src
    }
  } else {
    clear_cached_audio_source(path)
  }
  let src = if audio_prefers_byte_backed_source(path) {
    match get_asset_bytes(path) {
      Some(bytes) => {
        let blob_url = create_audio_object_url(
          bytes,
          browser_audio_mime_type(path),
        )
        cached_audio_sources.set(path, { src: blob_url, blob_owned: true })
        blob_url
      }
      None => {
        let resolved = resolve_asset_path(path)
        cached_audio_sources.set(path, { src: resolved, blob_owned: false })
        resolved
      }
    }
  } else {
    let resolved = resolve_asset_path(path)
    cached_audio_sources.set(path, { src: resolved, blob_owned: false })
    resolved
  }
  src
}

///|
fn clear_preloaded_audio_template(path : String) -> Unit {
  if preloaded_audio_templates.get(path) is Some(audio) {
    audio.pause()
    preloaded_audio_templates.remove(path)
  }
}

///|
fn ensure_preloaded_audio_template(
  path : String,
  refresh? : Bool = false,
) -> Audio {
  if !refresh {
    if preloaded_audio_templates.get(path) is Some(audio) {
      return audio
    }
  } else {
    clear_preloaded_audio_template(path)
  }
  let audio = Audio::new(resolved_audio_source(path, refresh~))
  audio.load()
  preloaded_audio_templates.set(path, audio)
  audio
}

///|
fn request_audio_playback(playback : AudioPlayback) -> Unit {
  let activation_epoch = current_audio_activation_epoch()
  playback.last_play_attempt_activation_epoch = activation_epoch
  request_audio_play(playback.audio, activation_epoch)
}

///|
fn create_audio_instance(path : String) -> (AudioInstance, AudioPlayback) {
  let id = next_audio_instance.val
  next_audio_instance.val += 1
  let template = ensure_preloaded_audio_template(path)
  let audio = template.clone_audio()
  (
    id,
    { audio, requested_paused: false, last_play_attempt_activation_epoch: -1 },
  )
}

///|
fn reset_audio_runtime() -> Unit {
  for pair in audio_instances.to_array() {
    let playback = pair.1
    playback.audio.pause()
    playback.audio.set_current_time(0.0)
  }
  audio_instances.clear()
  for pair in preloaded_audio_templates.to_array() {
    let template = pair.1
    template.pause()
    template.set_current_time(0.0)
  }
  preloaded_audio_templates.clear()
  for pair in cached_audio_sources.to_array() {
    let entry = pair.1
    if entry.blob_owned {
      revoke_object_url(entry.src)
    }
  }
  cached_audio_sources.clear()
  for src in retired_blob_audio_sources {
    revoke_object_url(src)
  }
  retired_blob_audio_sources.clear()
}

///|
pub fn play_audio(
  audio_path~ : String,
  volume~ : Double,
  speed~ : Double,
  loop_~ : Bool,
  paused~ : Bool,
) -> AudioInstance {
  let (instance, playback) = create_audio_instance(audio_path)
  playback.audio.set_volume(volume)
  playback.audio.set_loop(loop_)
  playback.audio.set_playback_rate(clamp_playback_speed(speed))
  playback.requested_paused = paused
  audio_instances.set(instance, playback)
  if paused {
    playback.audio.pause()
  } else {
    request_audio_playback(playback)
  }
  instance
}

///|
pub fn set_volume(instance~ : AudioInstance, volume~ : Double) -> Unit {
  if audio_instances.get(instance) is Some(playback) {
    playback.audio.set_volume(volume)
  }
}

///|
pub fn set_speed(instance~ : AudioInstance, speed~ : Double) -> Unit {
  if audio_instances.get(instance) is Some(playback) {
    playback.audio.set_playback_rate(clamp_playback_speed(speed))
  }
}

///|
pub fn set_loop(instance~ : AudioInstance, loop_~ : Bool) -> Unit {
  if audio_instances.get(instance) is Some(playback) {
    playback.audio.set_loop(loop_)
  }
}

///|
pub fn set_paused(instance~ : AudioInstance, paused~ : Bool) -> Unit {
  if audio_instances.get(instance) is Some(playback) {
    if playback.requested_paused == paused {
      return
    }
    playback.requested_paused = paused
    if paused {
      playback.audio.pause()
    } else if !playback.audio.is_ended() {
      request_audio_playback(playback)
    }
  }
}

///|
pub fn stop(instance~ : AudioInstance) -> Unit {
  if audio_instances.get(instance) is Some(playback) {
    playback.requested_paused = true
    playback.audio.pause()
    playback.audio.set_current_time(0.0)
  }
  audio_instances.remove(instance)
}

///|
pub fn is_finished(instance~ : AudioInstance) -> Bool {
  match audio_instances.get(instance) {
    Some(playback) =>
      playback.audio.is_ended() ||
      audio_play_state(playback.audio) == AUDIO_PLAY_STATE_FAILED
    None => true
  }
}

///|
pub fn tick_audio() -> Unit {
  let activation_epoch = current_audio_activation_epoch()
  for pair in audio_instances.to_array() {
    let playback = pair.1
    let play_state = audio_play_state(playback.audio)
    if playback.audio.is_ended() || play_state == AUDIO_PLAY_STATE_FAILED {
      audio_instances.remove(pair.0)
    } else if should_retry_blocked_audio_playback(
        play_state,
        playback.requested_paused,
        activation_epoch,
        playback.last_play_attempt_activation_epoch,
      ) {
      request_audio_playback(playback)
    }
  }
}

///|
pub fn preload_audio(audio_path : String) -> Unit {
  ignore(ensure_preloaded_audio_template(audio_path, refresh=true))
}