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

///|
/// JACK stream bindings (Linux-only).

///|
#borrow(out_handles)
#owned(device_id_utf8, data_callback, error_callback)
extern "C" fn jack_stream_build_output(
  device_id_utf8 : Bytes,
  device_id_len : Int,
  sample_rate : Double,
  channels : UInt,
  sample_format_tag : UInt,
  buffer_frames : UInt,
  call_data_callback : FuncRef[
    (
      (@core.Data, @core.OutputCallbackInfo) -> Unit,
      UInt,
      FixedArray[Byte],
      Int64,
      Int,
      Int64,
      Int,
    ) -> Unit,
  ],
  data_callback : (@core.Data, @core.OutputCallbackInfo) -> Unit,
  call_error_callback : FuncRef[((@core.StreamError) -> Unit, Int, Int) -> Unit],
  error_callback : (@core.StreamError) -> Unit,
  out_handles : FixedArray[UInt64],
  out_len : Int,
) -> Int = "moon_cpal_jack_stream_build_output"

///|
#borrow(out_handles)
#owned(device_id_utf8, data_callback, error_callback)
extern "C" fn jack_stream_build_input(
  device_id_utf8 : Bytes,
  device_id_len : Int,
  sample_rate : Double,
  channels : UInt,
  sample_format_tag : UInt,
  buffer_frames : UInt,
  call_data_callback : FuncRef[
    (
      (@core.Data, @core.InputCallbackInfo) -> Unit,
      UInt,
      FixedArray[Byte],
      Int64,
      Int,
      Int64,
      Int,
    ) -> Unit,
  ],
  data_callback : (@core.Data, @core.InputCallbackInfo) -> Unit,
  call_error_callback : FuncRef[((@core.StreamError) -> Unit, Int, Int) -> Unit],
  error_callback : (@core.StreamError) -> Unit,
  out_handles : FixedArray[UInt64],
  out_len : Int,
) -> Int = "moon_cpal_jack_stream_build_input"

///|
extern "C" fn jack_stream_owner_new(handle : UInt64) -> StreamOwner = "moon_cpal_jack_stream_owner_new"

///|
#borrow(owner)
extern "C" fn jack_stream_owner_play(owner : StreamOwner) -> Int = "moon_cpal_jack_stream_owner_play"

///|
#borrow(owner)
extern "C" fn jack_stream_owner_pause(owner : StreamOwner) -> Int = "moon_cpal_jack_stream_owner_pause"

///|
#borrow(owner)
extern "C" fn jack_stream_owner_close(owner : StreamOwner) -> Int = "moon_cpal_jack_stream_owner_close"

///|
fn[A] raise_build_error(
  op : String,
  status : Int,
) -> A raise @core.BuildStreamError {
  raise jack_build_error_from_status(status, op)
}

///|
fn jack_build_error_from_status(
  status : Int,
  op : String,
) -> @core.BuildStreamError {
  // Best-effort errno-like mapping (Linux).
  if status == -2 || status == -1 || status == -13 || status == -19 {
    // ENOENT / EPERM / EACCES / ENODEV
    @core.build_stream_error_device_not_available()
  } else if status == -16 || status == -11 {
    // EBUSY / EAGAIN
    @core.build_stream_error_device_busy()
  } else if status == -22 {
    // EINVAL
    @core.build_stream_error_invalid_argument()
  } else {
    @core.build_stream_error_backend_specific("\{op}: \{status}")
  }
}

///|
fn[A] raise_play_error(
  op : String,
  status : Int,
) -> A raise @core.PlayStreamError {
  raise @core.play_stream_error_backend_specific("\{op}: \{status}")
}

///|
fn[A] raise_pause_error(
  op : String,
  status : Int,
) -> A raise @core.PauseStreamError {
  raise @core.pause_stream_error_backend_specific("\{op}: \{status}")
}

///|
fn buffer_frames_from_config(config : @core.StreamConfig) -> UInt {
  match config.buffer_size {
    Default => 0
    Fixed(n) => if n > 0 { n.reinterpret_as_uint() } else { 0 }
  }
}

///|
fn validate_buffer_size_or_raise(
  config : @core.StreamConfig,
) -> Unit raise @core.BuildStreamError {
  if config.channels <= 0 || config.sample_rate <= 0 {
    raise @core.build_stream_error_invalid_argument()
  }
  match config.buffer_size {
    Default => ()
    Fixed(n) => {
      if n < 0 {
        raise @core.build_stream_error_invalid_argument()
      }
      if n == 0 {
        raise @core.build_stream_error_stream_config_not_supported()
      }
      match default_server_config() {
        None => ()
        Some((_sr, buf)) =>
          if n != buf {
            raise @core.build_stream_error_stream_config_not_supported()
          }
      }
    }
  }
}

///|
fn validate_sample_format_or_raise(
  sample_format : @core.SampleFormat,
) -> Unit raise @core.BuildStreamError {
  if sample_format != F32 {
    raise @core.build_stream_error_stream_config_not_supported()
  }
}

///|
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 {
  if @core.native_os() != Linux {
    raise_build_error("jack build_output_stream_raw", -1)
  }
  if self.direction != @core.DeviceDirection::output() {
    raise @core.build_stream_error_stream_config_not_supported()
  }
  validate_buffer_size_or_raise(config)
  validate_sample_format_or_raise(sample_format)
  let fmt_tag = (1 : UInt)
  let dev = @core.string_to_utf8_bytes(self.id)
  let out = FixedArray::make(1, (0 : UInt64))
  let st = jack_stream_build_output(
    dev,
    dev.length(),
    config.sample_rate.to_double(),
    config.channels.reinterpret_as_uint(),
    fmt_tag,
    buffer_frames_from_config(config),
    call_output_data_callback_fr,
    data_callback,
    call_stream_error_callback_fr,
    error_callback,
    out,
    1,
  )
  if st < 0 {
    raise_build_error("jack_stream_build_output", st)
  }
  { kind: Output, handle: out[0], owner: jack_stream_owner_new(out[0]) }
}

///|
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 {
  Device::build_output_stream_raw(
    self,
    config,
    F32,
    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 {
  if @core.native_os() != Linux {
    raise_build_error("jack build_input_stream_raw", -1)
  }
  if self.direction != @core.DeviceDirection::input() {
    raise @core.build_stream_error_stream_config_not_supported()
  }
  validate_buffer_size_or_raise(config)
  validate_sample_format_or_raise(sample_format)
  let fmt_tag = (1 : UInt)
  let dev = @core.string_to_utf8_bytes(self.id)
  let out = FixedArray::make(1, (0 : UInt64))
  let st = jack_stream_build_input(
    dev,
    dev.length(),
    config.sample_rate.to_double(),
    config.channels.reinterpret_as_uint(),
    fmt_tag,
    buffer_frames_from_config(config),
    call_input_data_callback_fr,
    data_callback,
    call_stream_error_callback_fr,
    error_callback,
    out,
    1,
  )
  if st < 0 {
    raise_build_error("jack_stream_build_input", st)
  }
  { kind: Input, handle: out[0], owner: jack_stream_owner_new(out[0]) }
}

///|
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 {
  Device::build_input_stream_raw(
    self,
    config,
    F32,
    data_callback,
    error_callback,
    _timeout,
  )
}

///|
pub fn Stream::play(self : Stream) -> Unit raise @core.PlayStreamError {
  let st = jack_stream_owner_play(self.owner)
  if st < 0 {
    raise_play_error("jack_stream_owner_play", st)
  }
}

///|
pub fn Stream::pause(self : Stream) -> Unit raise @core.PauseStreamError {
  let st = jack_stream_owner_pause(self.owner)
  if st < 0 {
    raise_pause_error("jack_stream_owner_pause", st)
  }
}

///|
pub fn Stream::close(self : Stream) -> Unit {
  if self.handle == 0 {
    return
  }
  jack_stream_owner_close(self.owner) |> ignore
}