//! Channel management

use fmod::{self, ChannelControl};

/// Manages a single FMOD Channel for monophonic playback.
///
/// When a sound is played by FMOD, it is assigned to play on a virtual channel
/// and a weak handle to the channel is returned.
///
/// This handle will be invalidated after the next call to `system.update()` if
/// playback has reached the end of a non-looping sound, or otherwise if
/// `channel.stop()` is called. The channel can also be *stolen* by the priority
/// system if a new request to play a sound is made and there are no free
/// channels.
///
/// This type will act as a strong reference for a single playing sound,
/// controlling playback using a single channel and re-acquiring it in the case
/// the channel is stolen or invalidated, and stopping the channel when the
/// voice is dropped.
#[derive(Debug, Default, PartialEq)]
pub struct Voice {
  channel       : Option <fmod::Channel>,
  channel_group : Option <fmod::ChannelGroup>
}

pub type IndexType = u16;
#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
pub struct Id (pub IndexType);

impl Voice {
  pub fn new() -> Self {
    Voice::default()
  }

  pub const fn channel (&self) -> Option <&fmod::Channel> {
    self.channel.as_ref()
  }

  pub const fn channel_mut (&mut self) -> Option <&mut fmod::Channel> {
    self.channel.as_mut()
  }

  pub const fn channel_group (&self) -> Option <&fmod::ChannelGroup> {
    self.channel_group.as_ref()
  }

  pub const fn channel_group_mut (&mut self) -> Option <&mut fmod::ChannelGroup> {
    self.channel_group.as_mut()
  }

  pub fn is_playing (&self) -> Option <bool> {
    if let Some (channel) = self.channel.as_ref() {
      let playing = match channel.is_playing() {
        Ok (playing) => playing,
        Err (fmod::Error::ChannelStolen | fmod::Error::InvalidHandle) => false,
        Err (err) => unreachable!("err: {:?}", err)
      };
      Some (playing)
    } else {
      None
    }
  }
  //
  //  acquiring a fresh channel
  //
  /// Play a 2D sound on a fresh channel.
  ///
  /// Note that playing the same sound twice will re-acquire the channel. To restart
  /// playback at the beginning of the sound use `trigger()`.
  #[expect(clippy::missing_panics_doc)]
  pub fn play (&mut self,
    sound         : &mut fmod::Sound,
    mut channel_group : Option <fmod::ChannelGroup>
  ) {
    let _ = self.stop();
    sound.set_mode (fmod::Mode::LOOP_OFF | fmod::Mode::_2D).unwrap();
    self.channel = Some (sound.play (channel_group.as_mut(), false).unwrap());
    self.channel_group = channel_group;
  }

  /// Loop a 2D sound on a fresh channel.
  ///
  /// Note that playing the same sound twice will re-acquire the channel. To restart
  /// playback at the beginning of the sound use `trigger()`.
  #[expect(clippy::missing_panics_doc)]
  pub fn loop_ (&mut self,
    sound         : &mut fmod::Sound,
    mut channel_group : Option <fmod::ChannelGroup>
  ) {
    let _ = self.stop();
    sound.set_mode (fmod::Mode::LOOP_NORMAL | fmod::Mode::_2D).unwrap();
    self.channel = Some (sound.play (channel_group.as_mut(), false).unwrap());
    self.channel_group = channel_group;
  }

  /// Play a 2D sound on a fresh channel starting from the given position.
  ///
  /// Note that playing the same sound twice will re-acquire the channel. To restart
  /// playback at the beginning of the sound use `trigger()`.
  #[expect(clippy::missing_panics_doc)]
  pub fn play_from (&mut self,
    sound         : &mut fmod::Sound,
    channel_group : Option <fmod::ChannelGroup>,
    position      : u32
  ) {
    let _ = self.stop();
    self.cue (sound, channel_group);
    assert!(!self.position (position, None).unwrap());
    assert!(!self.resume().unwrap());
  }

  /// Play a 2D sound on a fresh channel starting from the given position.
  ///
  /// Note that looping will loop back to the beginning of the sound when the end is
  /// reached.
  ///
  /// Note that playing the same sound twice will re-acquire the channel. To restart
  /// playback at the beginning of the sound use `trigger()`.
  #[expect(clippy::missing_panics_doc)]
  pub fn loop_from (&mut self,
    sound         : &mut fmod::Sound,
    mut channel_group : Option <fmod::ChannelGroup>,
    position      : u32
  ) {
    let _ = self.stop();
    sound.set_mode (fmod::Mode::LOOP_NORMAL | fmod::Mode::_2D).unwrap();
    self.channel = Some (sound.play (channel_group.as_mut(), true).unwrap());
    self.channel_group = channel_group;
    assert!(!self.position (position, None).unwrap());
    assert!(!self.resume().unwrap());
  }

  /// "Play" a 2D sound on a fresh channel with 'paused = true'.
  #[expect(clippy::missing_panics_doc)]
  pub fn cue (&mut self,
    sound         : &mut fmod::Sound,
    mut channel_group : Option <fmod::ChannelGroup>
  ) {
    let _ = self.stop();
    sound.set_mode (fmod::Mode::LOOP_OFF | fmod::Mode::_2D).unwrap();
    self.channel = Some (sound.play (channel_group.as_mut(), true).unwrap());
    self.channel_group = channel_group;
  }

  //
  //  sound playback control on an existing channel
  //

  /// Set the position to the beginning of the sound in the un-paused state.
  ///
  /// Returns Some(true) if the channel was playing and None if this voice is
  /// uninitialized.
  #[expect(clippy::missing_panics_doc)]
  pub fn trigger (&mut self) -> Option <bool> {
    if let Some (mut channel) = self.channel.take() {
      let playing = match channel.is_playing() {
        Ok (playing) => {
          channel.set_position (0, fmod::Timeunit::PCM).unwrap();
          channel.set_paused (false).unwrap();
          self.channel = Some (channel);
          playing
        }
        Err (fmod::Error::ChannelStolen | fmod::Error::InvalidHandle) => {
          let mut sound = channel.sound_ref();
          self.play (&mut sound, self.channel_group.clone());
          false
        }
        Err (err) => unreachable!("err: {:?}", err)
      };
      Some (playing)
    } else {
      None
    }
  }

  /// Pauses playback.
  ///
  /// Returns Some(true) if the channel was playing, and None if the voice was
  /// uninitialized.
  #[expect(clippy::missing_panics_doc)]
  pub fn pause (&mut self) -> Option <bool> {
    if let Some (mut channel) = self.channel.take() {
      let playing = match channel.is_playing() {
        Ok (playing) => {
          channel.set_paused (true).unwrap();
          self.channel = Some (channel);
          playing
        }
        Err (fmod::Error::ChannelStolen | fmod::Error::InvalidHandle) => {
          let mut sound = channel.sound_ref();
          self.cue (&mut sound, self.channel_group.clone());
          false
        }
        Err (err) => unreachable!("err: {:?}", err)
      };
      Some (playing)
    } else {
      None
    }
  }

  /// Resumes playback.
  ///
  /// Returns Some(true) if the channel was already playing, and None if the
  /// voice is uninitialized.
  #[expect(clippy::missing_panics_doc)]
  pub fn resume (&mut self) -> Option <bool> {
    if let Some (mut channel) = self.channel.take() {
      let playing = match channel.is_playing() {
        Ok (playing) => {
          channel.set_paused (false).unwrap();
          self.channel = Some (channel);
          playing
        }
        Err (fmod::Error::ChannelStolen | fmod::Error::InvalidHandle) => {
          let mut sound = channel.sound_ref();
          self.play (&mut sound, self.channel_group.clone());
          false
        }
        Err (err) => unreachable!("err: {:?}", err)
      };
      Some (playing)
    } else {
      None
    }
  }

  /// Set playback position.
  ///
  /// Default `timeunit` is `Timeunit::PCM` (samples).
  ///
  /// Returns Some(true) if the channel was currently playing, and None if the voice was
  /// uninitialized.
  #[expect(clippy::missing_panics_doc)]
  pub fn position (&mut self, position : u32, timeunit : Option <fmod::Timeunit>)
    -> Option <bool>
  {
    if let Some (mut channel) = self.channel.take() {
      let timeunit = timeunit.unwrap_or (fmod::Timeunit::PCM);
      let playing = match channel.is_playing() {
        Ok (playing) => {
          channel.set_position (position, timeunit).unwrap();
          self.channel = Some (channel);
          playing
        }
        Err (fmod::Error::ChannelStolen | fmod::Error::InvalidHandle) => {
          let mut sound = channel.sound_ref();
          self.cue (&mut sound, self.channel_group.clone());
          self.channel.as_mut().unwrap().set_position (position, timeunit)
            .unwrap();
          false
        }
        Err (err) => unreachable!("err: {:?}", err)
      };
      Some (playing)
    } else {
      None
    }
  }

  /// Stops playback and releases the channel.
  ///
  /// Returns Some(true) if the channel was playing. Returns None if the voice was
  /// uninitialized.
  ///
  /// To instead pause playback and keep the channel reference, use `pause()`.
  #[expect(clippy::missing_panics_doc)]
  pub fn stop (&mut self) -> Option <bool> {
    // NOTE: always forgets channel group
    self.channel_group = None;
    if let Some (mut channel) = self.channel.take() {
      let playing = match channel.is_playing() {
        Ok (playing) => {
          channel.stop().unwrap();
          playing
        }
        Err (fmod::Error::ChannelStolen | fmod::Error::InvalidHandle) => false,
        Err (err) => unreachable!("err: {:?}", err)
      };
      Some (playing)
    } else {
      None
    }
  }
}

impl Drop for Voice {
  fn drop (&mut self) {
    self.stop();
  }
}
