use cpal::traits::{DeviceTrait, HostTrait};
use cpal::{Device, Host};
use log::{debug, error, info};
use minimp3::{Decoder, Error};
use regex::Regex;
use std::fs::File;
use std::io::Read;
use std::io::{BufReader, Cursor};
use std::sync::mpsc::{self, Sender};
use std::sync::{
    atomic::{AtomicBool, Ordering},
    Arc, Mutex,
};

use std::thread;

#[derive(Clone)]
pub enum SoundDecoder {
    Detect,
    Mp3Mini,
    Rodio,
}

pub enum Command {
    Exit,
    Play(String),
    PlayWhilePressing(String, String),
    Stop,
}

// todo: implement a device selection with this:
pub enum DeviceSelection {
    SelectFirst,
    SelectById(usize),
    FindByPattern(String),
}

fn list_devices(host: &Host) -> Vec<String> {
    let devices = host.output_devices().unwrap();
    devices.map(|device| device.name().unwrap()).collect()
}

fn find_device_index_regex(devices: Vec<String>, pattern: &str) -> Option<usize> {
    let re = Regex::new(pattern).unwrap();
    devices.iter().position(|device| re.is_match(device))
}

fn select_device(host: &Host, i: usize) -> Option<Device> {
    host.output_devices().unwrap().nth(i)
}

fn select_device_by_pattern(pattern: &str) -> Option<Device> {
    let host = cpal::default_host();
    let devices = list_devices(&host);
    info!("Devices: {:?}", devices);

    if let Some(index) = find_device_index_regex(devices, pattern) {
        info!(
            "Selecting Device from pattern: \"{}\" at index {:?}",
            pattern, index
        );
        return select_device(&host, index);
    }
    return None;
}

fn select_device_by_id(index: usize) -> Option<Device> {
    let host = cpal::default_host();
    return select_device(&host, index);
}

pub type AudioThread = Sender<Command>;

pub trait PlaybackAction {
    fn before_playback(&mut self);
    fn after_playback(&mut self);
}

pub struct PlaybackActionEvents {
    actions: Vec<Box<dyn PlaybackAction + Send>>,
}

impl PlaybackActionEvents {
    pub fn new() -> Self {
        PlaybackActionEvents {
            actions: Vec::new(),
        }
    }

    pub fn dispatch_before_playback(&mut self) {
        for action in self.actions.iter_mut() {
            action.before_playback();
        }
    }

    pub fn dispatch_after_playback(&mut self) {
        for action in self.actions.iter_mut() {
            action.after_playback();
        }
    }
}

pub struct AudioHandler {
    pub listener: AudioThread,
    events: Arc<Mutex<PlaybackActionEvents>>,
}

impl AudioHandler {
    pub fn new(select_device: DeviceSelection, select_decoder: SoundDecoder) -> Self {
        let events = Arc::new(Mutex::new(PlaybackActionEvents::new()));
        let tx = audio_thread(select_device, select_decoder, events.clone());
        Self {
            listener: tx,
            events,
        }
    }

    pub fn on_playback(&mut self, event: Box<dyn PlaybackAction + Send>) {
        self.events.lock().unwrap().actions.push(event);
    }

    pub fn exit(&mut self) {
        self.events.lock().unwrap().actions.clear();
        self.listener
            .send(Command::Exit)
            .expect("Error sending Exit Command.");
    }
}

pub mod example_handler {
    use enigo::*;

    pub struct PressMouseForward {
        enigo: Enigo,
    }

    impl PressMouseForward {
        pub fn new() -> Self {
            let enigo_settings = Settings::default();
            let enigo = Enigo::new(&enigo_settings).unwrap();
            PressMouseForward { enigo: enigo }
        }
    }

    impl super::PlaybackAction for PressMouseForward {
        fn before_playback(&mut self) {
            self.enigo
                .button(Button::Forward, Direction::Press)
                .unwrap();
        }

        fn after_playback(&mut self) {
            self.enigo
                .button(Button::Forward, Direction::Release)
                .unwrap();
        }
    }
}

pub fn audio_thread(
    select_device: DeviceSelection,
    select_decoder: SoundDecoder,
    events: Arc<Mutex<PlaybackActionEvents>>,
) -> AudioThread {
    let (tx, rx) = mpsc::channel();

    thread::spawn(move || {
        let device = match select_device {
            DeviceSelection::SelectFirst => {
                select_device_by_id(0).expect("No Audio devices found.")
            }
            DeviceSelection::SelectById(id) => {
                select_device_by_id(id).expect("Audio device for ID not found.")
            }
            DeviceSelection::FindByPattern(pattern) => select_device_by_pattern(pattern.as_ref())
                .expect("Pattern search for audio device yielded no results."),
        };

        // Create a rodio Sink connected to our device. We need to keep stream and stream_handle alive during the thread.
        let (_stream, stream_handle) = rodio::OutputStream::try_from_device(&device).unwrap();
        let new_sink = rodio::Sink::try_new(&stream_handle).unwrap();

        let sink_mutex = Arc::new(Mutex::new(new_sink));
        let is_button_thread_active = Arc::new(AtomicBool::new(false));

        for command in rx {
            match command {
                Command::Exit => {
                    eprintln!("Exiting Soundloop.");
                    if let Ok(sink) = sink_mutex.lock() {
                        sink.stop();
                    }
                    drop(stream_handle);
                    break;
                }
                Command::Play(file_name) => {
                    if let Ok(sink) = sink_mutex.lock() {
                        play_file(&sink, file_name, &select_decoder);
                    } else {
                        error!("Mutex Lock Failure while trying to play sound.")
                    }
                }
                Command::PlayWhilePressing(file_name, _press_button) => {
                    if let Ok(sink) = sink_mutex.lock() {
                        play_file(&sink, file_name, &select_decoder);
                    } else {
                        error!("Mutex Lock Failure while trying to play sound with callbacks.");
                    }

                    if !is_button_thread_active.load(Ordering::SeqCst) {
                        let sink_mutex_clone = sink_mutex.clone();
                        let is_active_clone = is_button_thread_active.clone();
                        let events_clone = events.clone();
                        thread::spawn(move || {
                            events_clone.lock().unwrap().dispatch_before_playback();
                            is_active_clone.store(true, Ordering::SeqCst);

                            while sink_mutex_clone.lock().map_or(false, |sink| !sink.empty()) {
                                thread::sleep(std::time::Duration::from_millis(100));
                            }

                            events_clone.lock().unwrap().dispatch_after_playback();
                            is_active_clone.store(false, Ordering::SeqCst);
                        });
                    }
                }
                Command::Stop => {
                    if let Ok(sink) = sink_mutex.lock() {
                        sink.stop();
                    } else {
                        error!("Mutex Lock Failure while trying to stop sound.");
                    }
                }
            }
        }
        eprintln!("Exiting audio_thread");
    });

    tx
}

fn detect_decoder(_file_name: &str, sound_decoder: &SoundDecoder) -> SoundDecoder {
    // TODO: File detection via ending or whitelisting?
    // This function MUST NOT return SoundDecoder::Detect
    match sound_decoder {
        SoundDecoder::Detect => SoundDecoder::Mp3Mini,
        other => other.clone(),
    }
}

fn play_file(sink: &rodio::Sink, file_name: String, sound_decoder: &SoundDecoder) {
    match detect_decoder(&file_name, sound_decoder) {
        SoundDecoder::Mp3Mini => {
            // MP3 Mini provides low latency playback
            let mut sound_file = File::open(file_name).unwrap();
            let mut file_data = Vec::new();
            sound_file.read_to_end(&mut file_data).unwrap();
            // Iterate over the MP3 frames and play them
            let mut decoder = Decoder::new(Cursor::new(file_data));
            loop {
                match decoder.next_frame() {
                    Ok(frame) => {
                        let source = rodio::buffer::SamplesBuffer::new(
                            2,
                            frame.sample_rate.try_into().unwrap(),
                            &*frame.data,
                        );
                        sink.append(source);
                    }
                    Err(Error::Eof) => {
                        debug!("EOF");
                        break;
                    }
                    Err(e) => {
                        error!("{:?}", e);
                        break;
                    }
                }
            }
        }
        SoundDecoder::Rodio => {
            // Rodio currently supports Ogg, Mp3, WAV and Flac
            let file = File::open(&file_name).unwrap();
            let source = rodio::Decoder::new(BufReader::new(file)).expect("Failed to open Decoder");
            sink.append(source);
            //sink.sleep_until_end();
        }
        SoundDecoder::Detect => error!("This should never happen"),
    }
}