use crate::cmd::Cmd;
use crate::color::Color;
use crate::widget::WaitContext;
use crate::{
    fonts::FontRef,
    widgets::bar_widget::{BarWidget, BarWidgetImpl},
};

use std::cell::RefCell;
use std::collections::HashMap;
use std::ops::Deref;
use std::rc::Rc;
use std::sync::mpsc::{channel, Sender};
use std::sync::{Arc, Mutex};
use std::thread;

use libpulse_binding::callbacks::ListResult;
use libpulse_binding::context::{
    introspect::ServerInfo, introspect::SinkInfo, subscribe::Facility, subscribe::InterestMaskSet,
    subscribe::Operation as SubscribeOperation, Context, FlagSet, State as PulseState,
};
use libpulse_binding::mainloop::standard::IterateResult;
use libpulse_binding::mainloop::standard::Mainloop;
use libpulse_binding::proplist::{properties, Proplist};
use libpulse_binding::volume::{ChannelVolumes, Volume};

struct PulseAudioConnection {
    mainloop: Rc<RefCell<Mainloop>>,
    context: Rc<RefCell<Context>>,
}

#[derive(Clone)]
struct PulseAudioSinkInfo {
    volume: ChannelVolumes,
    mute: bool,
}

struct PulseAudioClient {
    sender: Sender<PulseAudioClientRequest>,
    default_sink: String,
    sinks: HashMap<String, PulseAudioSinkInfo>,
}

enum PulseAudioClientRequest {
    GetDefaultDevice(Option<Sender<bool>>),
    GetSinkInfoByIndex(Option<Sender<bool>>, u32),
    GetSinkInfoByName(Option<Sender<bool>>, String),
    SetSinkVolumeByName(Option<Sender<bool>>, String, ChannelVolumes),
    SetSinkMuteByName(Option<Sender<bool>>, String, bool),
}

#[derive(Debug)]
struct PulseAudioSoundDeviceInner {
    name: Option<String>,
    volume: Option<ChannelVolumes>,
    volume_avg: f32,
    muted: bool,
    default_sink: String,
}

struct PulseAudioSoundDevice {
    client: Arc<Mutex<PulseAudioClient>>,
    inner: Arc<Mutex<PulseAudioSoundDeviceInner>>,
}

impl PulseAudioConnection {
    fn new() -> Result<Self, ::std::io::Error> {
        let mut proplist = Proplist::new().unwrap();
        proplist
            .set_str(properties::APPLICATION_NAME, "wldash")
            .unwrap();

        let mainloop = Rc::new(RefCell::new(Mainloop::new().unwrap()));

        let context = Rc::new(RefCell::new(
            Context::new_with_proplist(mainloop.borrow().deref(), "wldash_context", &proplist)
                .unwrap(),
        ));

        context
            .borrow_mut()
            .connect(None, FlagSet::NOFLAGS, None)
            .map_err(|_e| {
                ::std::io::Error::new(
                    ::std::io::ErrorKind::Other,
                    "unable to connect to pulseaudio context",
                )
            })?;

        let mut connection = PulseAudioConnection { mainloop, context };

        // Wait for context to be ready
        loop {
            connection.iterate(false)?;
            match connection.context.borrow().get_state() {
                PulseState::Ready => {
                    break;
                }
                PulseState::Failed | PulseState::Terminated => {}
                _ => {}
            }
        }

        Ok(connection)
    }

    fn iterate(&mut self, blocking: bool) -> Result<(), ::std::io::Error> {
        match self.mainloop.borrow_mut().iterate(blocking) {
            IterateResult::Quit(_) | IterateResult::Err(_) => Err(::std::io::Error::new(
                ::std::io::ErrorKind::Other,
                "unable to iterate pulseaudio state",
            )),
            IterateResult::Success(_) => Ok(()),
        }
    }
}

impl PulseAudioClient {
    fn new<F>(listener: F) -> Result<Arc<Mutex<Self>>, ::std::io::Error>
    where
        F: Fn(Arc<Mutex<Self>>) -> (),
        F: Send + 'static + Clone,
    {
        let (tx, rx) = channel();

        let client = Arc::new(Mutex::new(PulseAudioClient {
            sender: tx,
            default_sink: "@DEFAULT_SINK@".to_string(),
            sinks: HashMap::new(),
        }));

        let loop_client = client.clone();
        let (tx1, rx1) = channel();
        let _ = thread::Builder::new()
            .name("pa_writer".to_string())
            .spawn(move || {
                let mut conn = match PulseAudioConnection::new() {
                    Ok(v) => {
                        tx1.send(true).unwrap();
                        v
                    }
                    Err(_) => {
                        tx1.send(false).unwrap();
                        return;
                    }
                };

                // make sure mainloop dispatched everything
                for _ in 0..10 {
                    conn.iterate(false).unwrap();
                }

                loop {
                    let cl = loop_client.clone();
                    let l = listener.clone();
                    match rx.recv() {
                        Err(_) => return,
                        Ok(req) => {
                            let mut introspector = conn.context.borrow_mut().introspect();

                            let done = Arc::new(Mutex::new(false));
                            let done_reader = done.clone();
                            let s = match req {
                                PulseAudioClientRequest::GetDefaultDevice(s) => {
                                    introspector.get_server_info(move |info| {
                                        *done.lock().unwrap() = true;
                                        PulseAudioClient::server_info_callback(
                                            cl.clone(),
                                            l.clone(),
                                            info,
                                        );
                                    });
                                    s
                                }
                                PulseAudioClientRequest::GetSinkInfoByIndex(s, index) => {
                                    introspector.get_sink_info_by_index(index, move |res| {
                                        *done.lock().unwrap() = true;
                                        PulseAudioClient::sink_info_callback(
                                            cl.clone(),
                                            l.clone(),
                                            res,
                                        );
                                    });
                                    s
                                }
                                PulseAudioClientRequest::GetSinkInfoByName(s, name) => {
                                    introspector.get_sink_info_by_name(&name, move |res| {
                                        *done.lock().unwrap() = true;
                                        PulseAudioClient::sink_info_callback(
                                            cl.clone(),
                                            l.clone(),
                                            res,
                                        );
                                    });
                                    s
                                }
                                PulseAudioClientRequest::SetSinkVolumeByName(s, name, volumes) => {
                                    introspector.set_sink_volume_by_name(
                                        &name,
                                        &volumes,
                                        Some(Box::new(move |_| {
                                            *done.lock().unwrap() = true;
                                        })),
                                    );
                                    s
                                }
                                PulseAudioClientRequest::SetSinkMuteByName(s, name, mute) => {
                                    introspector.set_sink_mute_by_name(
                                        &name,
                                        mute,
                                        Some(Box::new(move |_| {
                                            *done.lock().unwrap() = true;
                                        })),
                                    );
                                    s
                                }
                            };

                            // send request and receive response
                            loop {
                                if *done_reader.lock().unwrap() {
                                    break;
                                }
                                conn.iterate(true).unwrap();
                            }

                            if let Some(s) = s {
                                let _ = s.send(true);
                            }
                        }
                    }
                }
            });

        // subscribe
        let cl2 = client.clone();
        let (tx2, rx2) = channel();
        let _ = thread::Builder::new()
            .name("pa_reader".to_string())
            .spawn(move || {
                let conn = match PulseAudioConnection::new() {
                    Ok(v) => {
                        tx2.send(true).unwrap();
                        v
                    }
                    Err(_) => {
                        tx2.send(false).unwrap();
                        return;
                    }
                };
                // subcribe for events
                conn.context
                    .borrow_mut()
                    .set_subscribe_callback(Some(Box::new(move |facility, operation, index| {
                        cl2.lock()
                            .unwrap()
                            .subscribe_callback(facility, operation, index)
                    })));
                conn.context
                    .borrow_mut()
                    .subscribe(InterestMaskSet::SERVER | InterestMaskSet::SINK, |_| {});

                conn.mainloop.borrow_mut().run().unwrap();
            });

        let pa_writer_res = rx1.recv().unwrap();
        let pa_reader_res = rx2.recv().unwrap();

        if !pa_writer_res || !pa_reader_res {
            return Err(::std::io::Error::new(
                ::std::io::ErrorKind::Other,
                "unable to start pulseaudio thread",
            ));
        }

        Ok(client)
    }

    fn send(&self, request: PulseAudioClientRequest) -> Result<(), ::std::io::Error> {
        self.sender.send(request).map_err(|_e| {
            ::std::io::Error::new(
                ::std::io::ErrorKind::Other,
                "unable to send pulseaudio request",
            )
        })
    }

    fn server_info_callback<F>(s: Arc<Mutex<Self>>, listener: F, server_info: &ServerInfo)
    where
        F: Fn(Arc<Mutex<Self>>) -> (),
        F: Send + 'static,
    {
        match server_info.default_sink_name.clone() {
            None => {}
            Some(default_sink) => {
                (*s.lock().unwrap()).default_sink = default_sink.into();
                listener(s);
            }
        }
    }

    fn sink_info_callback<F>(s: Arc<Mutex<Self>>, listener: F, result: ListResult<&SinkInfo>)
    where
        F: Fn(Arc<Mutex<Self>>) -> (),
        F: Send + 'static,
    {
        match result {
            ListResult::End | ListResult::Error => {}
            ListResult::Item(sink_info) => match sink_info.name.clone() {
                None => {}
                Some(name) => {
                    let info = PulseAudioSinkInfo {
                        volume: sink_info.volume,
                        mute: sink_info.mute,
                    };
                    s.lock().unwrap().sinks.insert(name.into(), info);
                    listener(s);
                }
            },
        }
    }

    fn subscribe_callback(
        &self,
        facility: Option<Facility>,
        _operation: Option<SubscribeOperation>,
        index: u32,
    ) {
        match facility {
            None => {}
            Some(facility) => match facility {
                Facility::Server => {
                    let _ = self.send(PulseAudioClientRequest::GetDefaultDevice(None));
                }
                Facility::Sink => {
                    let _ = self.send(PulseAudioClientRequest::GetSinkInfoByIndex(None, index));
                }
                _ => {}
            },
        }
    }
}

impl PulseAudioSoundDevice {
    fn new<F>(listener: F) -> Result<Self, ::std::io::Error>
    where
        F: Fn() -> (),
        F: Send + 'static + Clone,
    {
        let inner = Arc::new(Mutex::new(PulseAudioSoundDeviceInner {
            name: None,
            volume: None,
            volume_avg: 0.0,
            muted: false,
            default_sink: "@DEFAULT_SINK@".to_string(),
        }));

        let cb_inner = inner.clone();
        let client = PulseAudioClient::new(move |client| {
            let mut inner = cb_inner.lock().unwrap();
            inner.default_sink = client.lock().unwrap().default_sink.clone();
            let name = inner
                .name
                .clone()
                .unwrap_or_else(|| inner.default_sink.clone());
            let sink_info = match client.lock().unwrap().sinks.get(&name) {
                None => return,
                Some(sink_info) => (*sink_info).clone(),
            };

            inner.volume = Some(sink_info.volume);
            inner.volume_avg = sink_info.volume.avg().0 as f32 / Volume::NORMAL.0 as f32;
            inner.muted = sink_info.mute;

            listener();
        })?;

        let cl = client.clone();
        let (tx, rx) = channel();
        {
            let cl = client.lock().unwrap();
            cl.send(PulseAudioClientRequest::GetDefaultDevice(Some(tx)))?;
        }
        rx.recv().unwrap();
        let name = {
            let cl = client.lock().unwrap();
            cl.default_sink.to_string()
        };
        (*inner.lock().unwrap()).name = Some(name.clone());
        let device = PulseAudioSoundDevice { client: cl, inner };
        let (tx, rx) = channel();
        {
            let cl = client.lock().unwrap();
            cl.send(PulseAudioClientRequest::GetSinkInfoByName(Some(tx), name))?;
        }
        rx.recv().unwrap();

        Ok(device)
    }

    fn muted(&self) -> bool {
        let inner = self.inner.lock().unwrap();
        inner.muted
    }

    fn volume(&self) -> f32 {
        let inner = self.inner.lock().unwrap();
        inner.volume_avg
    }

    fn inc_volume(&mut self, step: f32) -> Result<(), ::std::io::Error> {
        let mut inner = self.inner.lock().unwrap();
        let mut volume = match inner.volume {
            Some(volume) => volume,
            None => {
                return Err(::std::io::Error::new(
                    ::std::io::ErrorKind::Other,
                    "unable to set volume",
                ))
            }
        };

        // apply step to volumes
        let step = (step * Volume::NORMAL.0 as f32).round() as i32;
        if step > 0 {
            volume.inc_clamp(Volume(step as u32), Volume::MAX);
        } else {
            volume.decrease(Volume(-step as u32));
        }

        let name = inner
            .name
            .clone()
            .unwrap_or_else(|| inner.default_sink.clone());

        // update volumes
        inner.volume = Some(volume);
        inner.volume_avg = volume.avg().0 as f32 / Volume::NORMAL.0 as f32;
        self.client
            .lock()
            .unwrap()
            .send(PulseAudioClientRequest::SetSinkVolumeByName(
                None, name, volume,
            ))?;
        Ok(())
    }

    fn set_volume(&mut self, val: f32) -> Result<(), ::std::io::Error> {
        let mut inner = self.inner.lock().unwrap();
        let mut volume = match inner.volume {
            Some(volume) => volume,
            None => {
                return Err(::std::io::Error::new(
                    ::std::io::ErrorKind::Other,
                    "unable to set volume",
                ))
            }
        };

        // apply step to volumes
        volume.scale(Volume((val * Volume::NORMAL.0 as f32).round() as u32));

        let name = inner
            .name
            .clone()
            .unwrap_or_else(|| inner.default_sink.clone());

        // update volumes
        inner.volume = Some(volume);
        inner.volume_avg = volume.avg().0 as f32 / Volume::NORMAL.0 as f32;
        self.client
            .lock()
            .unwrap()
            .send(PulseAudioClientRequest::SetSinkVolumeByName(
                None, name, volume,
            ))?;
        Ok(())
    }

    fn toggle(&mut self) -> Result<(), ::std::io::Error> {
        let mut inner = self.inner.lock().unwrap();
        inner.muted = !inner.muted;
        self.client
            .lock()
            .unwrap()
            .send(PulseAudioClientRequest::SetSinkMuteByName(
                None,
                inner
                    .name
                    .clone()
                    .unwrap_or_else(|| inner.default_sink.clone()),
                inner.muted,
            ))?;
        Ok(())
    }
}

pub struct PulseAudio {
    device: PulseAudioSoundDevice,
}

impl PulseAudio {
    pub fn new(
        font: FontRef,
        font_size: f32,
        length: u32,
        sender: Sender<Cmd>,
    ) -> Result<Box<BarWidget>, ::std::io::Error> {
        BarWidget::new(font, font_size, length, move |dirty| {
            let device = PulseAudioSoundDevice::new(move || {
                *dirty.lock().unwrap() = true;
                sender.send(Cmd::Draw).unwrap();
            })?;
            Ok(Box::new(PulseAudio { device }))
        })
    }
}

impl BarWidgetImpl for PulseAudio {
    fn wait(&mut self, _: &mut WaitContext) {}
    fn name(&self) -> &str {
        "volume"
    }
    fn value(&self) -> f32 {
        self.device.volume()
    }
    fn color(&self) -> Color {
        let muted = self.device.muted();
        if muted {
            Color::new(1.0, 1.0, 0.0, 1.0)
        } else {
            Color::new(1.0, 1.0, 1.0, 1.0)
        }
    }
    fn inc(&mut self, inc: f32) {
        self.device.inc_volume(inc).unwrap();
    }
    fn set(&mut self, val: f32) {
        self.device.set_volume(val).unwrap();
    }
    fn toggle(&mut self) {
        self.device.toggle().unwrap();
    }
}
