// 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.

///|
/// Platform-level dynamic host/device wrappers.
///
/// This is a small MoonBit analogue of upstream `cpal::platform` dynamic dispatch types.
///
/// Native backends:
/// - Null (always available)
/// - macOS CoreAudio (native; stubbed on non-Apple native builds for linking)
/// - Linux ALSA + JACK
/// - Windows WASAPI

///|
pub enum Host {
  Null(@null.Host)
  CoreAudio(@macos.Host)
  Alsa(@alsa.Host)
  Jack(@jack.Host)
  Wasapi(@wasapi.Host)
} derive(Debug, Eq)

///|
pub enum Device {
  Null(@null.Device)
  CoreAudio(@macos.Device)
  Alsa(@alsa.Device)
  Jack(@jack.Device)
  Wasapi(@wasapi.Device)
} derive(Debug, Eq)

///|
pub enum Stream {
  Null(@null.Stream)
  CoreAudio(@macos.Stream)
  Alsa(@alsa.Stream)
  Jack(@jack.Stream)
  Wasapi(@wasapi.Stream)
} derive(Debug, Eq)

///|
pub fn Host::id(self : Host) -> @core.HostId {
  match self {
    Null(_) => Null
    CoreAudio(_) => CoreAudio
    Alsa(_) => Alsa
    Jack(_) => Jack
    Wasapi(_) => Wasapi
  }
}

///|
pub fn available_hosts() -> Array[@core.HostId] {
  // Upstream `cpal::available_hosts()` filters by runtime availability (HostTrait::is_available).
  //
  // We implement the runtime filtering at the `platform` layer (which can depend on backend
  // packages without introducing cycles).
  match @core.native_os() {
    Macos => [CoreAudio]
    Windows => [Wasapi]
    // Match upstream ordering (platform `ALL_HOSTS`): JACK (feature-gated upstream) before ALSA.
    Linux => [Jack, Alsa]
    // Match upstream: the Null backend exists as a fallback, but `available_hosts()` yields
    // only hosts that are currently available for real audio I/O.
    Unknown => []
  }
}

///|
pub fn all_hosts() -> Array[@core.HostId] {
  // Mirrors upstream platform `ALL_HOSTS`: the set of hosts supported by the current compilation
  // target. This includes hosts that may not be currently available at runtime.
  //
  // Note: In upstream, `HostId` is platform-specific; on supported platforms it does not include
  // `Null`. In MoonBit we keep a cross-platform superset `HostId`, so we expose the platform
  // subset via this function.
  match @core.native_os() {
    Macos => [CoreAudio]
    Windows => [Wasapi]
    Linux => [Jack, Alsa]
    Unknown => [Null]
  }
}

///|
pub fn host_from_id(id : @core.HostId) -> Host raise @core.HostUnavailable {
  match id {
    // Upstream: platform `HostId` includes `Null` only on unsupported targets.
    Null =>
      if @core.native_os() == Unknown {
        Null(@null.default_host())
      } else {
        raise @core.host_unavailable()
      }
    CoreAudio =>
      if @core.native_os() == Macos {
        CoreAudio(@macos.default_host())
      } else {
        raise @core.host_unavailable()
      }
    Wasapi =>
      if @core.native_os() == Windows {
        Wasapi(@wasapi.default_host())
      } else {
        raise @core.host_unavailable()
      }
    Alsa =>
      if @core.native_os() == Linux {
        Alsa(@alsa.default_host())
      } else {
        raise @core.host_unavailable()
      }
    Jack =>
      if @core.native_os() == Linux {
        Jack(@jack.default_host())
      } else {
        raise @core.host_unavailable()
      }
    _ => raise @core.host_unavailable()
  }
}

///|
pub fn try_host_from_id(id : @core.HostId) -> Host? {
  try host_from_id(id) catch {
    _ => None
  } noraise {
    h => Some(h)
  }
}

///|
pub fn Host::default_output_device(self : Host) -> Device? {
  match self {
    Null(h) =>
      match h.default_output_device() {
        None => None
        Some(d) => Some(Null(d))
      }
    CoreAudio(h) =>
      match h.default_output_device() {
        None => None
        Some(d) => Some(CoreAudio(d))
      }
    Alsa(h) =>
      match h.default_output_device() {
        None => None
        Some(d) => Some(Alsa(d))
      }
    Jack(h) =>
      match h.default_output_device() {
        None => None
        Some(d) => Some(Jack(d))
      }
    Wasapi(h) =>
      match h.default_output_device() {
        None => None
        Some(d) => Some(Wasapi(d))
      }
  }
}

///|
pub fn Host::default_input_device(self : Host) -> Device? {
  match self {
    Null(h) =>
      match h.default_input_device() {
        None => None
        Some(d) => Some(Null(d))
      }
    CoreAudio(h) =>
      match h.default_input_device() {
        None => None
        Some(d) => Some(CoreAudio(d))
      }
    Alsa(h) =>
      match h.default_input_device() {
        None => None
        Some(d) => Some(Alsa(d))
      }
    Jack(h) =>
      match h.default_input_device() {
        None => None
        Some(d) => Some(Jack(d))
      }
    Wasapi(h) =>
      match h.default_input_device() {
        None => None
        Some(d) => Some(Wasapi(d))
      }
  }
}

///|
pub fn Host::devices(self : Host) -> Iter[Device] raise @core.DevicesError {
  match self {
    Null(h) => {
      let ds = h.devices()
      ds.iter().map(fn(d) { Null(d) })
    }
    CoreAudio(h) =>
      try h.devices() catch {
        DeviceNotAvailable(op, status) =>
          raise @core.devices_error_backend_specific(
            "\{op} (OSStatus \{status})",
          )
        CoreAudioError(op, status) =>
          raise @core.devices_error_backend_specific(
            "\{op} (OSStatus \{status})",
          )
      } noraise {
        ds => ds.iter().map(fn(d) { CoreAudio(d) })
      }
    Alsa(h) => {
      let ds = h.devices()
      ds.iter().map(fn(d) { Alsa(d) })
    }
    Jack(h) => {
      let ds = h.devices()
      ds.iter().map(fn(d) { Jack(d) })
    }
    Wasapi(h) => {
      let ds = h.devices()
      ds.iter().map(fn(d) { Wasapi(d) })
    }
  }
}

///|
pub fn Host::device_by_id(self : Host, id : @core.DeviceId) -> Device? {
  let ds = try Host::devices(self) catch {
    _ => return None
  } noraise {
    xs => xs
  }
  ds.find_first(fn(d) {
    try d.id() catch {
      _ => false
    } noraise {
      did => did == id
    }
  })
}

///|
pub fn Host::input_devices(
  self : Host,
) -> Iter[Device] raise @core.DevicesError {
  Host::devices(self).filter(fn(d) { d.supports_input() })
}

///|
pub fn Host::output_devices(
  self : Host,
) -> Iter[Device] raise @core.DevicesError {
  Host::devices(self).filter(fn(d) { d.supports_output() })
}

///|
pub fn Device::name(self : Device) -> String raise @core.DeviceNameError {
  match self {
    Null(d) => d.name()
    CoreAudio(d) =>
      try d.name() catch {
        DeviceNotAvailable(op, status) =>
          raise @core.device_name_error_backend_specific(
            "\{op} (OSStatus \{status})",
          )
        CoreAudioError(op, status) =>
          raise @core.device_name_error_backend_specific(
            "\{op} (OSStatus \{status})",
          )
      } noraise {
        s => s
      }
    Alsa(d) => d.name()
    Jack(d) => d.name()
    Wasapi(d) => d.name()
  }
}

///|
pub fn Device::id(self : Device) -> @core.DeviceId raise @core.DeviceIdError {
  let did = match self {
    Null(d) => d.id()
    CoreAudio(d) => d.id()
    Alsa(d) => d.id()
    Jack(d) => d.id()
    Wasapi(d) => d.id()
  }
  if did.device_id().is_empty() {
    raise @core.device_id_error_backend_specific(
      "Device returned an empty device_id",
    )
  }
  did
}

///|
pub fn Device::description(
  self : Device,
) -> @core.DeviceDescription raise @core.DeviceNameError {
  match self {
    Null(d) => d.description()
    CoreAudio(d) =>
      try d.description() catch {
        DeviceNotAvailable(op, status) =>
          raise @core.device_name_error_backend_specific(
            "\{op} (OSStatus \{status})",
          )
        CoreAudioError(op, status) =>
          raise @core.device_name_error_backend_specific(
            "\{op} (OSStatus \{status})",
          )
      } noraise {
        x => x
      }
    Alsa(d) => d.description()
    Jack(d) => d.description()
    Wasapi(d) => d.description()
  }
}

///|
pub fn Device::supports_input(self : Device) -> Bool {
  match self {
    CoreAudio(d) => d.supports_input()
    Alsa(d) => d.supports_input()
    Jack(d) => d.supports_input()
    Wasapi(d) => d.supports_input()
    _ =>
      try (Device::supported_input_configs(self).next() is Some(_)) catch {
        _ => false
      }
  }
}

///|
pub fn Device::supports_output(self : Device) -> Bool {
  match self {
    CoreAudio(d) => d.supports_output()
    Alsa(d) => d.supports_output()
    Jack(d) => d.supports_output()
    Wasapi(d) => d.supports_output()
    _ =>
      try (Device::supported_output_configs(self).next() is Some(_)) catch {
        _ => false
      }
  }
}

///|
pub fn Device::supported_output_configs(
  self : Device,
) -> Iter[@core.SupportedStreamConfigRange] raise @core.SupportedStreamConfigsError {
  match self {
    Null(d) => {
      let xs = d.supported_output_configs()
      xs.iter()
    }
    CoreAudio(d) =>
      try d.supported_output_configs() catch {
        DeviceNotAvailable(_, _) =>
          raise @core.supported_stream_configs_error_device_not_available()
        CoreAudioError(op, status) =>
          raise @macos.supported_configs_error_from_osstatus(op, status)
      } noraise {
        xs => xs.iter()
      }
    Alsa(d) => {
      let xs = d.supported_output_configs()
      xs.iter()
    }
    Jack(d) => {
      let xs = d.supported_output_configs()
      xs.iter()
    }
    Wasapi(d) => {
      let xs = d.supported_output_configs()
      xs.iter()
    }
  }
}

///|
pub fn Device::supported_input_configs(
  self : Device,
) -> Iter[@core.SupportedStreamConfigRange] raise @core.SupportedStreamConfigsError {
  match self {
    Null(d) => {
      let xs = d.supported_input_configs()
      xs.iter()
    }
    CoreAudio(d) =>
      try d.supported_input_configs() catch {
        DeviceNotAvailable(_, _) =>
          raise @core.supported_stream_configs_error_device_not_available()
        CoreAudioError(op, status) =>
          raise @macos.supported_configs_error_from_osstatus(op, status)
      } noraise {
        xs => xs.iter()
      }
    Alsa(d) => {
      let xs = d.supported_input_configs()
      xs.iter()
    }
    Jack(d) => {
      let xs = d.supported_input_configs()
      xs.iter()
    }
    Wasapi(d) => {
      let xs = d.supported_input_configs()
      xs.iter()
    }
  }
}

///|
pub fn Device::try_default_output_config(
  self : Device,
) -> @core.SupportedStreamConfig? {
  match self {
    Null(d) =>
      try d.default_output_config() catch {
        _ => None
      } noraise {
        cfg => Some(cfg)
      }
    CoreAudio(d) =>
      try d.default_output_config() catch {
        _ => None
      } noraise {
        cfg => Some(cfg)
      }
    Alsa(d) =>
      try d.default_output_config() catch {
        _ => None
      } noraise {
        cfg => Some(cfg)
      }
    Jack(d) =>
      try d.default_output_config() catch {
        _ => None
      } noraise {
        cfg => Some(cfg)
      }
    Wasapi(d) =>
      try d.default_output_config() catch {
        _ => None
      } noraise {
        cfg => Some(cfg)
      }
  }
}

///|
pub fn Device::try_default_input_config(
  self : Device,
) -> @core.SupportedStreamConfig? {
  match self {
    Null(d) =>
      try d.default_input_config() catch {
        _ => None
      } noraise {
        cfg => Some(cfg)
      }
    CoreAudio(d) =>
      try d.default_input_config() catch {
        _ => None
      } noraise {
        cfg => Some(cfg)
      }
    Alsa(d) =>
      try d.default_input_config() catch {
        _ => None
      } noraise {
        cfg => Some(cfg)
      }
    Jack(d) =>
      try d.default_input_config() catch {
        _ => None
      } noraise {
        cfg => Some(cfg)
      }
    Wasapi(d) =>
      try d.default_input_config() catch {
        _ => None
      } noraise {
        cfg => Some(cfg)
      }
  }
}

///|
pub fn Device::default_output_config(
  self : Device,
) -> @core.SupportedStreamConfig raise @core.DefaultStreamConfigError {
  match self {
    Null(d) => d.default_output_config()
    CoreAudio(d) => d.default_output_config()
    Alsa(d) => d.default_output_config()
    Jack(d) => d.default_output_config()
    Wasapi(d) => d.default_output_config()
  }
}

///|
pub fn Device::default_input_config(
  self : Device,
) -> @core.SupportedStreamConfig raise @core.DefaultStreamConfigError {
  match self {
    Null(d) => d.default_input_config()
    CoreAudio(d) => d.default_input_config()
    Alsa(d) => d.default_input_config()
    Jack(d) => d.default_input_config()
    Wasapi(d) => d.default_input_config()
  }
}

///|
pub fn Device::build_output_stream(
  self : Device,
  config : @core.StreamConfig,
  data_callback : (@core.Data, @core.OutputCallbackInfo) -> Unit,
  error_callback : (@core.StreamError) -> Unit,
  timeout : @core.Duration?,
) -> Stream raise @core.BuildStreamError {
  match self {
    Null(d) =>
      Null(
        d.build_output_stream(config, data_callback, error_callback, timeout),
      )
    CoreAudio(d) =>
      CoreAudio(
        d.build_output_stream(config, data_callback, error_callback, timeout),
      )
    Alsa(d) =>
      Alsa(
        d.build_output_stream(config, data_callback, error_callback, timeout),
      )
    Jack(d) =>
      Jack(
        d.build_output_stream(config, data_callback, error_callback, timeout),
      )
    Wasapi(d) =>
      Wasapi(
        d.build_output_stream(config, data_callback, error_callback, timeout),
      )
  }
}

///|
pub fn Device::build_output_stream_raw(
  self : Device,
  config : @core.StreamConfig,
  sample_format : @core.SampleFormat,
  data_callback : (@core.Data, @core.OutputCallbackInfo) -> Unit,
  error_callback : (@core.StreamError) -> Unit,
  timeout : @core.Duration?,
) -> Stream raise @core.BuildStreamError {
  match self {
    Null(d) =>
      Null(
        d.build_output_stream_raw(
          config, sample_format, data_callback, error_callback, timeout,
        ),
      )
    CoreAudio(d) =>
      CoreAudio(
        d.build_output_stream_raw(
          config, sample_format, data_callback, error_callback, timeout,
        ),
      )
    Alsa(d) =>
      Alsa(
        d.build_output_stream_raw(
          config, sample_format, data_callback, error_callback, timeout,
        ),
      )
    Jack(d) =>
      Jack(
        d.build_output_stream_raw(
          config, sample_format, data_callback, error_callback, timeout,
        ),
      )
    Wasapi(d) =>
      Wasapi(
        d.build_output_stream_raw(
          config, sample_format, data_callback, error_callback, timeout,
        ),
      )
  }
}

///|
pub fn Device::build_input_stream(
  self : Device,
  config : @core.StreamConfig,
  data_callback : (@core.Data, @core.InputCallbackInfo) -> Unit,
  error_callback : (@core.StreamError) -> Unit,
  timeout : @core.Duration?,
) -> Stream raise @core.BuildStreamError {
  match self {
    Null(d) =>
      Null(d.build_input_stream(config, data_callback, error_callback, timeout))
    CoreAudio(d) =>
      CoreAudio(
        d.build_input_stream(config, data_callback, error_callback, timeout),
      )
    Alsa(d) =>
      Alsa(d.build_input_stream(config, data_callback, error_callback, timeout))
    Jack(d) =>
      Jack(d.build_input_stream(config, data_callback, error_callback, timeout))
    Wasapi(d) =>
      Wasapi(
        d.build_input_stream(config, data_callback, error_callback, timeout),
      )
  }
}

///|
pub fn Device::build_input_stream_raw(
  self : Device,
  config : @core.StreamConfig,
  sample_format : @core.SampleFormat,
  data_callback : (@core.Data, @core.InputCallbackInfo) -> Unit,
  error_callback : (@core.StreamError) -> Unit,
  timeout : @core.Duration?,
) -> Stream raise @core.BuildStreamError {
  match self {
    Null(d) =>
      Null(
        d.build_input_stream_raw(
          config, sample_format, data_callback, error_callback, timeout,
        ),
      )
    CoreAudio(d) =>
      CoreAudio(
        d.build_input_stream_raw(
          config, sample_format, data_callback, error_callback, timeout,
        ),
      )
    Alsa(d) =>
      Alsa(
        d.build_input_stream_raw(
          config, sample_format, data_callback, error_callback, timeout,
        ),
      )
    Jack(d) =>
      Jack(
        d.build_input_stream_raw(
          config, sample_format, data_callback, error_callback, timeout,
        ),
      )
    Wasapi(d) =>
      Wasapi(
        d.build_input_stream_raw(
          config, sample_format, data_callback, error_callback, timeout,
        ),
      )
  }
}

///|
pub fn Stream::play(self : Stream) -> Unit raise @core.PlayStreamError {
  match self {
    Null(s) => s.play()
    CoreAudio(s) => s.play()
    Alsa(s) => s.play()
    Jack(s) => s.play()
    Wasapi(s) => s.play()
  }
}

///|
pub fn Stream::pause(self : Stream) -> Unit raise @core.PauseStreamError {
  match self {
    Null(s) => s.pause()
    CoreAudio(s) => s.pause()
    Alsa(s) => s.pause()
    Jack(s) => s.pause()
    Wasapi(s) => s.pause()
  }
}

///|
pub fn Stream::close(self : Stream) -> Unit {
  match self {
    Null(s) => s.close()
    CoreAudio(s) => s.close()
    Alsa(s) => s.close()
    Jack(s) => s.close()
    Wasapi(s) => s.close()
  }
}