#![feature(pattern)]
use fmod::{self, ChannelControl};
use vec_map::VecMap;
use rs_utils;
use signal_utils::{pcm, generator, Generator};

use fmod_utils::*;

const MAX_CHANNELS : u16 = 1024;   // max virtual channels
const INITFLAGS    : fmod::system::Initflags =
  fmod::system::Initflags::_3D_RIGHTHANDED;
//const SOUNDFONT    : &'static str = "data/OPL-3_FM_128M_22050s.dls";
//const SOUNDFONT    : &'static str = "data/Roland_SC-88.dls";
const SOUNDFONT    : &str = "data/gm.dls";
const SAMPLE_WAV   : sampler::Id  = sampler::Id (0);
const SONG_MID     : music::Id = music::Id (0);
const VOICE_WAV    : usize = 0;
const VOICE_MID    : usize = 1;
const VOICE_NOISE  : usize = 2;

macro_rules! show {
  ($e:expr) => { println!("{}: {:?}", stringify!($e), $e); }
}

fn noise_gen (system : &mut fmod::System, generator : &mut Generator) -> fmod::Sound {
  const SECONDS : usize = 10;
  let mut pcm_data = vec![0i16; SECONDS * 44100];
  generator.generate (&mut pcm_data[..]);
  let pcm_data =
    pcm::trim_zeros (pcm::normalize_peak (pcm_data.as_mut_slice(), i16::MAX));
  println!("noise len: {}", pcm_data.len());
  system.create_sound_from_pcm_default (pcm_data).unwrap()
}

fn main() {
  println!("fmod-utils example main...");

  // command-line options
  let opts = clap::Command::new ("Fmod Utils Example App")
    .arg (clap::Arg::new ("wav-file")
      .short ('w')
      .value_name ("WAV_FILE")
      .help ("Sample wav file"))
    .arg (clap::Arg::new ("mid-file")
      .short ('m')
      .value_name ("MID_FILE")
      .help ("Midi song file"))
    .arg (clap::Arg::new ("dump-info")
      .short ('d')
      .help ("Write info files to ./dump"))
    .get_matches();
  let wav_file = opts.get_one ("wav-file")
    .unwrap_or (&"data/bite.wav".to_string()).clone();
  let mid_file = opts.get_one ("mid-file")
    .unwrap_or (&"data/gshop.mid".to_string()).clone();
  let dumpinfo = opts.contains_id ("dump-info");

  println!("using soundfont: {SOUNDFONT}");

  //
  // init
  //
  // default 64 software channels
  let mut audition = {
    use std::iter::FromIterator;
    let system = fmod::System::new (None, MAX_CHANNELS, INITFLAGS).unwrap();
    let mut audition = Audition::with_system (system);
    audition.load_samples (
      VecMap::from_iter (vec![wav_file].into_iter().enumerate()));
    audition.load_songs_midi (
      VecMap::from_iter (vec![(mid_file, SOUNDFONT.into())]
        .into_iter().enumerate())
    );
    audition.voices.insert (VOICE_WAV,   Voice::default());
    audition.voices.insert (VOICE_MID,   Voice::default());
    audition.voices.insert (VOICE_NOISE, Voice::default());
    audition
  };
  println!("...fmod system initialized...");
  println!("reverb max instances: {}", fmod::dsp::REVERB_MAXINSTANCES);
  // default reverb properties
  let mut reverb = false;
  let mut preset = fmod::reverb3d::Presets::Generic;
  const REVERB_INSTANCE : i32 = 0;
  audition.system.set_reverb_properties (REVERB_INSTANCE, &fmod::reverb3d::OFF)
    .unwrap();
  println!("...fmod system reverb properties set...");
  // setup listener
  let listener_attributes = fmod::ListenerAttributes {
    pos:     [0.0, 0.0,  2.0],
    vel:     [0.0, 0.0,  0.0],
    forward: [0.0, 0.0, -1.0],
    up:      [0.0, 1.0,  0.0]
  };
  const LISTENER_ID : i32 = 0;
  audition.system.set_3d_listener_attributes (LISTENER_ID, &listener_attributes)
    .unwrap();
  println!("...fmod system 3d listener attributes set...");

  // NOTE: by default DSP index 0 exists and is a fader

  // new dsp echo
  const DSP_UNIT_ECHO : i32 = 1;
  let mut dsp_echo = audition.system.create_dsp_by_type (fmod::dsp::Type::Echo)
    .unwrap();
  dsp_echo.set_parameter_float (fmod::dsp::Echo::Delay  as i32, 100.0).unwrap();
  dsp_echo.set_parameter_float (fmod::dsp::Echo::Feedback as i32, 70.0).unwrap();
  dsp_echo.set_parameter_float (fmod::dsp::Echo::DryLevel as i32, 0.0).unwrap();
  dsp_echo.set_parameter_float (fmod::dsp::Echo::WetLevel as i32, 0.0).unwrap();
  dsp_echo.set_bypass (true).unwrap();

  // new dsp lowpass
  const DSP_UNIT_LOWPASS : i32 = 2;
  let mut dsp_lowpass = audition.system
    .create_dsp_by_type (fmod::dsp::Type::Lowpass).unwrap();
  dsp_lowpass.set_parameter_float (fmod::dsp::Lowpass::Cutoff as i32, 360.0)
    .unwrap();
  dsp_lowpass.set_bypass (true).unwrap();

  let mut master_channel_group = audition.system.get_master_channel_group()
    .unwrap();
  master_channel_group.add_dsp (DSP_UNIT_ECHO,    &mut dsp_echo).unwrap();
  master_channel_group.add_dsp (DSP_UNIT_LOWPASS, &mut dsp_lowpass).unwrap();

  println!("...fmod dsps created...");

  println!("...generating noise...");

  // generate noise
  let mut generator = {
    let kind = generator::Noise::Brown { intensity: 410 }.into();
    let freq = 44100.0;
    Generator::new (kind, freq)
  };
  let mut sound_noise = noise_gen (&mut audition.system, &mut generator);
  audition.system.update().unwrap();

  // readline loop
  println!("readline loop...");
  println!("commands:\n    \
    'play'    -- play the sample wav\n    \
    'demo'    -- demo that pans a repeating sample from left to right\n    \
    'midi'    -- play the midi song file\n    \
    'noise'   -- loop generated noise\n    \
    'regen'   -- regenerate noise data\n    \
    'stop'    -- stop looping noise and midi song playback\n    \
    'echo'    -- toggle echo dsp\n    \
    'reverb'  -- toggle 3D reverb\n    \
    'lowpass' -- toggle lowpass filter\n    \
    'next'    -- choose the next reverb preset\n    \
    'quit'    -- quit");
  'readline_loop: loop {
    use std::io::{self, Write};
    print!(" > ");
    let _     = io::stdout().flush();
    let mut s = String::new();
    let _     = io::stdin().read_line (&mut s);
    if !s.trim_end().is_empty() {
      let word_ct     = s.as_str().split_whitespace().count();
      let mut words   = s.as_str().split_whitespace();
      let mut handled = true;
      match word_ct {
        0 => unreachable!("ERROR: zero words in server input readline parse"),
        1 => {
          let command = words.next().unwrap();
          use std::str::pattern::Pattern;
          if command.is_prefix_of ("quit") {
            // quit
            break 'readline_loop;
          } else if command.is_prefix_of ("play") {
            // play wav sample
            audition.voices[VOICE_WAV].play (
              audition.sampler.get_mut (SAMPLE_WAV).unwrap(),
              None);
          } else if command.is_prefix_of ("midi") {
            // play midi song
            audition.voices[VOICE_MID].play (
              audition.music.get_mut (SONG_MID).unwrap(),
              None);
          } else if command.is_prefix_of ("noise") {
            // loop generated noise
            audition.voices[VOICE_NOISE].loop_ (&mut sound_noise, None);
          } else if command.is_prefix_of ("regen") {
            // regenerate noise
            sound_noise = noise_gen (&mut audition.system, &mut generator);
            if audition.voices[VOICE_NOISE].is_playing() == Some (true) {
              audition.voices[VOICE_NOISE].loop_ (&mut sound_noise, None);
            }
          } else if command.is_prefix_of ("stop") {
            // stop midi song
            let _ = audition.voices[VOICE_MID].stop();
            // stop noise loop
            let _ = audition.voices[VOICE_NOISE].stop();
          } else if command.is_prefix_of ("echo") {
            if dsp_echo.get_bypass().unwrap() {
              dsp_echo.set_bypass (false).unwrap();
            } else {
              dsp_echo.set_bypass (true).unwrap();
            }
          } else if command.is_prefix_of ("reverb") {
            if reverb {
              audition.system.set_reverb_properties (
                REVERB_INSTANCE, &fmod::reverb3d::OFF
              ).unwrap();
              reverb = false;
            } else {
              audition.system.set_reverb_properties (
                REVERB_INSTANCE, &preset.into()
              ).unwrap();
              reverb = true;
            }
          } else if command.is_prefix_of ("lowpass") {
            if dsp_lowpass.get_bypass().unwrap() {
              dsp_lowpass.set_bypass (false).unwrap();
            } else {
              dsp_lowpass.set_bypass (true).unwrap();
            }
          } else if command.is_prefix_of ("next") {
            let i = preset as u32 + 1;
            if i >= fmod::reverb3d::Presets::MAX as u32 {
              preset = fmod::reverb3d::Presets::Generic;
            } else {
              use fmod::FromPrimitive;
              preset = fmod::reverb3d::Presets::from_u32 (i).unwrap();
            }
            if reverb {
              audition.system.set_reverb_properties (
                REVERB_INSTANCE, &preset.into()
              ).unwrap();
            }
            show!(preset);
          } else if command.is_prefix_of ("demo") {
            // demo
            let mut position = [0.0, 0.0, -2.0];
            let velocity     = [20.0, 0.0, 0.0];
            for i in 0..101 {
              #[expect(clippy::suboptimal_flops)]
              let x = -100.0 + 2.0*(i as f32);
              position[0] = x;
              let mode    = fmod::Mode::LOOP_OFF | fmod::Mode::_3D;
              let mut channel = audition.sampler
                .cue (SAMPLE_WAV, Some (mode), None);
              channel.set_3d_attributes (position, velocity).unwrap();
              channel.set_paused (false).unwrap();
              // system update
              audition.system.update().unwrap();
              std::thread::sleep (std::time::Duration::from_millis (100));
            }
          } else {
            handled = false;
          }
        },
        _ => handled = false
      } // end match word count
      if !handled {
        println!("unrecognized command: \"{}\"", s.trim());
      }
    } // end input not empty
  } // end 'readline_loop
  println!("...readline loop");

  if dumpinfo {
    // dump system info
    let file_path = std::path::Path::new ("dump/system-info");
    let (file_path, mut file) =
      rs_utils::file::file_new_append_incremental (file_path).unwrap();
    println!("{:<40} {:<40?}", "...dumping system info to:",
      file_path.to_str().unwrap());
    info::write_info_system (&mut file, &audition.system).unwrap();
    drop (file);
    // dump sound info
    let file_path = std::path::Path::new ("dump/sound-info");
    let (file_path, mut file) =
      rs_utils::file::file_new_append_incremental (file_path).unwrap();
    println!("{:<40} {:<40?}", "...dumping sound info to:", file_path.to_str().unwrap());
    info::write_info_sound (&mut file, audition.sampler.get (SAMPLE_WAV).unwrap())
      .unwrap();
    drop (file);
    // dump master channel group info
    let file_path = std::path::Path::new ("dump/channel-group-info");
    let (file_path, mut file) =
      rs_utils::file::file_new_append_incremental (file_path).unwrap();
    println!("{:<40} {:<40?}", "...dumping master channel group info to:",
      file_path.to_str().unwrap());
    info::write_info_channel_group (&mut file, &master_channel_group).unwrap();
    drop (file);
  }

  println!("...fmod-utils example main");
}
