///|
pub(all) enum PcmEncoding {
  PcmU8
  PcmS16
  PcmS24
  PcmS32
} derive(Eq, @debug.Debug)

///|
pub fn PcmEncoding::bits_per_sample(self : PcmEncoding) -> Int {
  match self {
    PcmU8 => 8
    PcmS16 => 16
    PcmS24 => 24
    PcmS32 => 32
  }
}

///|
pub fn PcmEncoding::label(self : PcmEncoding) -> String {
  match self {
    PcmU8 => "pcm-u8"
    PcmS16 => "pcm-s16"
    PcmS24 => "pcm-s24"
    PcmS32 => "pcm-s32"
  }
}

///|
pub(all) struct SampleRegion {
  start : Int
  length : Int
  threshold : Double
} derive(Eq, @debug.Debug)

///|
pub fn SampleRegion::new(
  start : Int,
  length : Int,
  threshold? : Double = 0.0,
) -> SampleRegion {
  { start, length, threshold }
}

///|
pub fn SampleRegion::end_exclusive(self : SampleRegion) -> Int {
  self.start + self.length
}

///|
pub fn SampleRegion::is_valid(self : SampleRegion) -> Bool {
  self.start >= 0 && self.length >= 0
}

///|
pub(all) struct ChannelStats {
  channel : Int
  peak : Double
  rms : Double
  average : Double
  silent_samples : Int
  clipped_samples : Int
} derive(Eq, @debug.Debug)

///|
pub fn ChannelStats::empty(channel : Int) -> ChannelStats {
  {
    channel,
    peak: 0.0,
    rms: 0.0,
    average: 0.0,
    silent_samples: 0,
    clipped_samples: 0,
  }
}

///|
pub(all) struct NormalizeReport {
  ok : Bool
  gain : Double
  target_peak : Double
  before_peak : Double
  after_peak : Double
  buffer : FloatBuffer
  error : WavError
} derive(Eq, @debug.Debug)

///|
pub fn NormalizeReport::failure(error : WavError) -> NormalizeReport {
  {
    ok: false,
    gain: 0.0,
    target_peak: 0.0,
    before_peak: 0.0,
    after_peak: 0.0,
    buffer: FloatBuffer::empty(),
    error,
  }
}

///|
pub fn NormalizeReport::success(
  gain : Double,
  target_peak : Double,
  before_peak : Double,
  buffer : FloatBuffer,
) -> NormalizeReport {
  let stats = analyze_audio(buffer)
  {
    ok: true,
    gain,
    target_peak,
    before_peak,
    after_peak: stats.peak,
    buffer,
    error: WavError::none(),
  }
}

///|
pub(all) struct TrimResult {
  ok : Bool
  start_frame : Int
  end_frame : Int
  removed_start_frames : Int
  removed_end_frames : Int
  buffer : FloatBuffer
  error : WavError
} derive(Eq, @debug.Debug)

///|
pub fn TrimResult::failure(error : WavError) -> TrimResult {
  {
    ok: false,
    start_frame: 0,
    end_frame: 0,
    removed_start_frames: 0,
    removed_end_frames: 0,
    buffer: FloatBuffer::empty(),
    error,
  }
}

///|
pub fn TrimResult::success(
  start_frame : Int,
  end_frame : Int,
  source_frames : Int,
  buffer : FloatBuffer,
) -> TrimResult {
  {
    ok: true,
    start_frame,
    end_frame,
    removed_start_frames: start_frame,
    removed_end_frames: source_frames - end_frame,
    buffer,
    error: WavError::none(),
  }
}

///|
pub(all) struct ResampleResult {
  ok : Bool
  source_sample_rate : Int
  target_sample_rate : Int
  source_frames : Int
  target_frames : Int
  buffer : FloatBuffer
  error : WavError
} derive(Eq, @debug.Debug)

///|
pub fn ResampleResult::failure(error : WavError) -> ResampleResult {
  {
    ok: false,
    source_sample_rate: 0,
    target_sample_rate: 0,
    source_frames: 0,
    target_frames: 0,
    buffer: FloatBuffer::empty(),
    error,
  }
}

///|
pub fn ResampleResult::success(
  source_sample_rate : Int,
  target_sample_rate : Int,
  source_frames : Int,
  buffer : FloatBuffer,
) -> ResampleResult {
  {
    ok: true,
    source_sample_rate,
    target_sample_rate,
    source_frames,
    target_frames: buffer.frame_count(),
    buffer,
    error: WavError::none(),
  }
}

///|
pub(all) struct ByteEncodeResult {
  ok : Bool
  encoding : PcmEncoding
  bytes : Array[Int]
  error : WavError
} derive(Eq, @debug.Debug)

///|
pub fn ByteEncodeResult::failure(
  encoding : PcmEncoding,
  error : WavError,
) -> ByteEncodeResult {
  { ok: false, encoding, bytes: [], error }
}

///|
pub fn ByteEncodeResult::success(
  encoding : PcmEncoding,
  bytes : Array[Int],
) -> ByteEncodeResult {
  { ok: true, encoding, bytes, error: WavError::none() }
}

///|
pub(all) struct AudioFingerprint {
  frame_count : Int
  sample_rate : Int
  channels : Int
  peak_milli : Int
  rms_milli : Int
  zero_crossings : Int
  checksum : Int
} derive(Eq, @debug.Debug)

///|
pub fn AudioFingerprint::empty() -> AudioFingerprint {
  {
    frame_count: 0,
    sample_rate: 0,
    channels: 0,
    peak_milli: 0,
    rms_milli: 0,
    zero_crossings: 0,
    checksum: 0,
  }
}

///|
fn clamp_range(value : Double, low : Double, high : Double) -> Double {
  if value < low {
    low
  } else if value > high {
    high
  } else {
    value
  }
}

///|
fn rounded_to_int(value : Double) -> Int {
  value.round().to_int()
}

///|
fn sample_frame_has_signal(
  buffer : FloatBuffer,
  frame : Int,
  threshold : Double,
) -> Bool {
  for channel in 0.. threshold {
      break true
    }
  } nobreak {
    false
  }
}

///|
fn copy_frames(
  buffer : FloatBuffer,
  start_frame : Int,
  end_frame : Int,
) -> Array[Double] {
  let frame_count = if end_frame > start_frame {
    end_frame - start_frame
  } else {
    0
  }
  Array::makei(frame_count * buffer.channels, i => {
    let source = start_frame * buffer.channels + i
    buffer.samples[source]
  })
}

///|
pub fn make_silence(
  channels : Int,
  sample_rate : Int,
  frames : Int,
) -> FloatBuffer {
  if channels <= 0 || sample_rate <= 0 || frames < 0 {
    FloatBuffer::empty()
  } else {
    FloatBuffer::new(
      channels,
      sample_rate,
      Array::makei(channels * frames, _ => 0.0),
    )
  }
}

///|
pub fn apply_gain(buffer : FloatBuffer, gain : Double) -> FloatBuffer {
  if !buffer.is_valid() {
    FloatBuffer::empty()
  } else {
    FloatBuffer::new(
      buffer.channels,
      buffer.sample_rate,
      Array::makei(buffer.samples.length(), i => buffer.samples[i] * gain),
    )
  }
}

///|
pub fn clamp_buffer(buffer : FloatBuffer) -> FloatBuffer {
  if !buffer.is_valid() {
    FloatBuffer::empty()
  } else {
    FloatBuffer::new(
      buffer.channels,
      buffer.sample_rate,
      Array::makei(buffer.samples.length(), i => {
        clamp_range(buffer.samples[i], -1.0, 1.0)
      }),
    )
  }
}

///|
pub fn normalize_peak(
  buffer : FloatBuffer,
  target_peak? : Double = 0.95,
) -> NormalizeReport {
  if !buffer.is_valid() {
    NormalizeReport::failure(
      WavError::new(ErrorInvalidArgument, "invalid float buffer"),
    )
  } else if target_peak <= 0.0 || target_peak > 1.0 {
    NormalizeReport::failure(
      WavError::new(ErrorInvalidArgument, "target peak must be in 0..1"),
    )
  } else {
    let stats = analyze_audio(buffer)
    if stats.peak <= 0.0 {
      NormalizeReport::success(1.0, target_peak, stats.peak, buffer)
    } else {
      let gain = target_peak / stats.peak
      NormalizeReport::success(
        gain,
        target_peak,
        stats.peak,
        clamp_buffer(apply_gain(buffer, gain)),
      )
    }
  }
}

///|
pub fn trim_silence(
  buffer : FloatBuffer,
  threshold? : Double = 0.0001,
  keep_frames? : Int = 0,
) -> TrimResult {
  if !buffer.is_valid() {
    TrimResult::failure(
      WavError::new(ErrorInvalidArgument, "invalid float buffer"),
    )
  } else if threshold < 0.0 || keep_frames < 0 {
    TrimResult::failure(
      WavError::new(ErrorInvalidArgument, "invalid trim options"),
    )
  } else {
    let frames = buffer.frame_count()
    let first_signal = for frame in 0..= 0; {
        if sample_frame_has_signal(buffer, frame, threshold) {
          break frame
        }
        continue frame - 1
      } nobreak {
        first_signal
      }
      let start_frame = if first_signal > keep_frames {
        first_signal - keep_frames
      } else {
        0
      }
      let end_frame = if last_signal + 1 + keep_frames < frames {
        last_signal + 1 + keep_frames
      } else {
        frames
      }
      let samples = copy_frames(buffer, start_frame, end_frame)
      TrimResult::success(
        start_frame,
        end_frame,
        frames,
        FloatBuffer::new(buffer.channels, buffer.sample_rate, samples),
      )
    }
  }
}

///|
pub fn reverse_audio(buffer : FloatBuffer) -> FloatBuffer {
  if !buffer.is_valid() {
    FloatBuffer::empty()
  } else {
    let frames = buffer.frame_count()
    let samples = Array::makei(buffer.samples.length(), i => {
      let target_frame = i / buffer.channels
      let channel = i % buffer.channels
      let source_frame = frames - 1 - target_frame
      buffer.samples[source_frame * buffer.channels + channel]
    })
    FloatBuffer::new(buffer.channels, buffer.sample_rate, samples)
  }
}

///|
fn interpolate_channel(
  buffer : FloatBuffer,
  position : Double,
  channel : Int,
) -> Double {
  let frames = buffer.frame_count()
  if frames == 0 {
    0.0
  } else {
    let left = position.floor().to_int()
    let right = if left + 1 < frames { left + 1 } else { left }
    let weight = position - left.to_double()
    let left_value = buffer.samples[left * buffer.channels + channel]
    let right_value = buffer.samples[right * buffer.channels + channel]
    left_value * (1.0 - weight) + right_value * weight
  }
}

///|
pub fn resample_linear(
  buffer : FloatBuffer,
  target_sample_rate : Int,
) -> ResampleResult {
  if !buffer.is_valid() {
    ResampleResult::failure(
      WavError::new(ErrorInvalidArgument, "invalid float buffer"),
    )
  } else if target_sample_rate <= 0 {
    ResampleResult::failure(
      WavError::new(ErrorInvalidArgument, "target sample rate must be positive"),
    )
  } else if target_sample_rate == buffer.sample_rate {
    ResampleResult::success(
      buffer.sample_rate,
      target_sample_rate,
      buffer.frame_count(),
      buffer,
    )
  } else {
    let source_frames = buffer.frame_count()
    let target_frames = rounded_to_int(
      source_frames.to_double() *
      target_sample_rate.to_double() /
      buffer.sample_rate.to_double(),
    )
    let safe_target_frames = if target_frames < 1 { 1 } else { target_frames }
    let samples = Array::makei(safe_target_frames * buffer.channels, i => {
      let frame = i / buffer.channels
      let channel = i % buffer.channels
      let source_position = if safe_target_frames == 1 {
        0.0
      } else {
        frame.to_double() *
        (source_frames - 1).to_double() /
        (safe_target_frames - 1).to_double()
      }
      interpolate_channel(buffer, source_position, channel)
    })
    ResampleResult::success(
      buffer.sample_rate,
      target_sample_rate,
      source_frames,
      FloatBuffer::new(buffer.channels, target_sample_rate, samples),
    )
  }
}

///|
pub fn channel_stats(
  buffer : FloatBuffer,
  silence_threshold? : Double = 0.0001,
  clip_threshold? : Double = 0.999,
) -> Array[ChannelStats] {
  if !buffer.is_valid() {
    []
  } else {
    Array::makei(buffer.channels, channel => {
      let frames = buffer.frame_count()
      let peak = for frame in 0..= clip_threshold {
          continue count + 1
        } else {
          continue count
        }
      } nobreak {
        count
      }
      {
        channel,
        peak,
        rms: (sum_sq / frames.to_double()).sqrt(),
        average: sum / frames.to_double(),
        silent_samples: silent,
        clipped_samples: clipped,
      }
    })
  }
}

///|
pub fn zero_crossing_count(buffer : FloatBuffer) -> Int {
  if !buffer.is_valid() || buffer.samples.length() <= buffer.channels {
    0
  } else {
    let mono = if buffer.channels == 1 {
      buffer
    } else {
      downmix_to_mono(buffer)
    }
    for i in 1..= 0.0) || (prev >= 0.0 && current < 0.0) {
        continue count + 1
      } else {
        continue count
      }
    } nobreak {
      count
    }
  }
}

///|
pub fn zero_crossing_rate(buffer : FloatBuffer) -> Double {
  let frames = buffer.frame_count()
  if frames <= 1 {
    0.0
  } else {
    zero_crossing_count(buffer).to_double() / (frames - 1).to_double()
  }
}

///|
pub fn crest_factor(buffer : FloatBuffer) -> Double {
  let stats = analyze_audio(buffer)
  if stats.rms <= 0.0 {
    0.0
  } else {
    stats.peak / stats.rms
  }
}

///|
fn region_scan(
  samples : Array[Double],
  threshold : Double,
  min_run : Int,
  want_above : Bool,
) -> Array[SampleRegion] {
  let regions : Array[SampleRegion] = []
  let mut start = -1
  for i in 0..= threshold
    } else {
      abs_double(samples[i]) <= threshold
    }
    if hit && start < 0 {
      start = i
    } else if !hit && start >= 0 {
      let length = i - start
      if length >= min_run {
        regions.push(SampleRegion::new(start, length, threshold~))
      }
      start = -1
    }
  }
  if start >= 0 {
    let length = samples.length() - start
    if length >= min_run {
      regions.push(SampleRegion::new(start, length, threshold~))
    }
  }
  regions
}

///|
pub fn detect_silence_sample_regions(
  buffer : FloatBuffer,
  min_run : Int,
  threshold? : Double = 0.0001,
) -> Array[SampleRegion] {
  if !buffer.is_valid() || min_run <= 0 || threshold < 0.0 {
    []
  } else {
    region_scan(buffer.samples, threshold, min_run, false)
  }
}

///|
pub fn find_clipping_regions(
  buffer : FloatBuffer,
  min_run? : Int = 1,
  threshold? : Double = 0.999,
) -> Array[SampleRegion] {
  if !buffer.is_valid() || min_run <= 0 || threshold <= 0.0 {
    []
  } else {
    region_scan(buffer.samples, threshold, min_run, true)
  }
}

///|
fn encode_sample_to_int(sample : Double, encoding : PcmEncoding) -> Int {
  let clamped = clamp_range(sample, -1.0, 1.0)
  match encoding {
    PcmU8 => rounded_to_int(clamped * 127.0 + 128.0)
    PcmS16 => rounded_to_int(clamped * 32767.0)
    PcmS24 => rounded_to_int(clamped * 8388607.0)
    PcmS32 => rounded_to_int(clamped * 2147483647.0)
  }
}

///|
fn push_u16_le(out : Array[Int], value : Int) -> Unit {
  out.push(value & 0xff)
  out.push((value >> 8) & 0xff)
}

///|
fn push_u32_le(out : Array[Int], value : Int) -> Unit {
  out.push(value & 0xff)
  out.push((value >> 8) & 0xff)
  out.push((value >> 16) & 0xff)
  out.push((value >> 24) & 0xff)
}

///|
fn push_pcm_sample(
  out : Array[Int],
  sample : Double,
  encoding : PcmEncoding,
) -> Unit {
  let value = encode_sample_to_int(sample, encoding)
  match encoding {
    PcmU8 => out.push(value & 0xff)
    PcmS16 => {
      out.push(value & 0xff)
      out.push((value >> 8) & 0xff)
    }
    PcmS24 => {
      out.push(value & 0xff)
      out.push((value >> 8) & 0xff)
      out.push((value >> 16) & 0xff)
    }
    PcmS32 => {
      out.push(value & 0xff)
      out.push((value >> 8) & 0xff)
      out.push((value >> 16) & 0xff)
      out.push((value >> 24) & 0xff)
    }
  }
}

///|
pub fn encode_float_to_pcm_bytes(
  buffer : FloatBuffer,
  encoding : PcmEncoding,
) -> ByteEncodeResult {
  if !buffer.is_valid() {
    ByteEncodeResult::failure(
      encoding,
      WavError::new(ErrorInvalidArgument, "invalid float buffer"),
    )
  } else {
    let out : Array[Int] = []
    for sample in buffer.samples {
      push_pcm_sample(out, sample, encoding)
    }
    ByteEncodeResult::success(encoding, out)
  }
}

///|
pub fn build_wav_from_float(
  buffer : FloatBuffer,
  encoding? : PcmEncoding = PcmS16,
) -> ByteEncodeResult {
  let encoded = encode_float_to_pcm_bytes(buffer, encoding)
  if !encoded.ok {
    encoded
  } else {
    let bits = encoding.bits_per_sample()
    let block_align = buffer.channels * bits / 8
    let byte_rate = buffer.sample_rate * block_align
    let data_size = encoded.bytes.length()
    let riff_size = 4 + 8 + 16 + 8 + data_size
    let out : Array[Int] = []
    out.push(82)
    out.push(73)
    out.push(70)
    out.push(70)
    push_u32_le(out, riff_size)
    out.push(87)
    out.push(65)
    out.push(86)
    out.push(69)
    out.push(102)
    out.push(109)
    out.push(116)
    out.push(32)
    push_u32_le(out, 16)
    push_u16_le(out, 1)
    push_u16_le(out, buffer.channels)
    push_u32_le(out, buffer.sample_rate)
    push_u32_le(out, byte_rate)
    push_u16_le(out, block_align)
    push_u16_le(out, bits)
    out.push(100)
    out.push(97)
    out.push(116)
    out.push(97)
    push_u32_le(out, data_size)
    for byte in encoded.bytes {
      out.push(byte)
    }
    if data_size % 2 != 0 {
      out.push(0)
    }
    ByteEncodeResult::success(encoding, out)
  }
}

///|
pub fn audio_fingerprint(buffer : FloatBuffer) -> AudioFingerprint {
  if !buffer.is_valid() {
    AudioFingerprint::empty()
  } else {
    let stats = analyze_audio(buffer)
    let checksum = for sample in buffer.samples; value = 17 {
      let milli = rounded_to_int(clamp_range(sample, -1.0, 1.0) * 1000.0)
      continue (value * 31 + milli) & 0x7fffffff
    } nobreak {
      value
    }
    {
      frame_count: buffer.frame_count(),
      sample_rate: buffer.sample_rate,
      channels: buffer.channels,
      peak_milli: rounded_to_int(stats.peak * 1000.0),
      rms_milli: rounded_to_int(stats.rms * 1000.0),
      zero_crossings: zero_crossing_count(buffer),
      checksum,
    }
  }
}