// Copyright © 2017 Mozilla Foundation
//
// This program is made available under an ISC-style license.  See the
// accompanying file LICENSE for details.

use ffi;
use std::ffi::CStr;
use std::mem::{forget, MaybeUninit};
use std::os::raw::{c_int, c_void};
use std::ptr;
use util::UnwrapCStr;
use *;

// A note about `wrapped` functions
//
// C FFI demands `unsafe extern fn(*mut pa_context, ...) -> i32`, etc,
// but we want to allow such callbacks to be safe. This means no
// `unsafe` or `extern`, and callbacks should be called with a safe
// wrapper of `*mut pa_context`. Since the callback doesn't take
// ownership, this is `&Context`. `fn wrapped<T>(...)` defines a
// function that converts from our safe signature to the unsafe
// signature.
//
// Currently, we use a property of Rust, namely that each function
// gets its own unique type.  These unique types can't be written
// directly, so we use generic and a type parameter, and let the Rust
// compiler fill in the name for us:
//
// fn get_sink_input_info<CB>(&self, ..., _: CB, ...) -> ...
//     where CB: Fn(&Context, *const SinkInputInfo, i32, *mut c_void)
//
// Because we aren't storing or passing any state, we assert, at run-time :-(,
// that our functions are zero-sized:
//
//	assert!(mem::size_of::<F>() == 0);
//
// We need to obtain a value of type F in order to call it. Since we
// can't name the function, we have to unsafely construct that value
// somehow - we do this using mem::uninitialized. Then, we call that
// function with a reference to the Context, and save the result:
//
//              |       generate value        ||  call it  |
// let result = ::std::mem::uninitialized::<F>()(&mut object);
//
// Lastly, since our Object is an owned type, we need to avoid
// dropping it, then return the result we just generated.
//
//		mem::forget(object);
//		result

// Aid in returning Operation from callbacks
macro_rules! op_or_err {
    ($self_:ident, $e:expr) => {{
        let o = unsafe { $e };
        if o.is_null() {
            Err(ErrorCode::from_error_code($self_.errno()))
        } else {
            Ok(unsafe { operation::from_raw_ptr(o) })
        }
    }};
}

#[repr(C)]
#[derive(Debug)]
pub struct Context(*mut ffi::pa_context);

impl Context {
    pub fn new<'a, OPT>(api: &MainloopApi, name: OPT) -> Option<Self>
    where
        OPT: Into<Option<&'a CStr>>,
    {
        let ptr = unsafe { ffi::pa_context_new(api.raw_mut(), name.unwrap_cstr()) };
        if ptr.is_null() {
            None
        } else {
            Some(Context(ptr))
        }
    }

    #[doc(hidden)]
    pub fn raw_mut(&self) -> &mut ffi::pa_context {
        unsafe { &mut *self.0 }
    }

    pub fn unref(self) {
        unsafe {
            ffi::pa_context_unref(self.raw_mut());
        }
    }

    pub fn clear_state_callback(&self) {
        unsafe {
            ffi::pa_context_set_state_callback(self.raw_mut(), None, ptr::null_mut());
        }
    }

    pub fn set_state_callback<CB>(&self, _: CB, userdata: *mut c_void)
    where
        CB: Fn(&Context, *mut c_void),
    {
        assert_eq!(::std::mem::size_of::<CB>(), 0);

        // See: A note about `wrapped` functions
        unsafe extern "C" fn wrapped<F>(c: *mut ffi::pa_context, userdata: *mut c_void)
        where
            F: Fn(&Context, *mut c_void),
        {
            let ctx = context::from_raw_ptr(c);
            let cb = MaybeUninit::<F>::uninit();
            let result = (*cb.as_ptr())(&ctx, userdata);
            forget(ctx);

            result
        }

        unsafe {
            ffi::pa_context_set_state_callback(self.raw_mut(), Some(wrapped::<CB>), userdata);
        }
    }

    pub fn errno(&self) -> ffi::pa_error_code_t {
        unsafe { ffi::pa_context_errno(self.raw_mut()) }
    }

    pub fn get_state(&self) -> ContextState {
        ContextState::try_from(unsafe { ffi::pa_context_get_state(self.raw_mut()) })
            .expect("pa_context_get_state returned invalid ContextState")
    }

    pub fn connect<'a, OPT>(
        &self,
        server: OPT,
        flags: ContextFlags,
        api: *const ffi::pa_spawn_api,
    ) -> Result<()>
    where
        OPT: Into<Option<&'a CStr>>,
    {
        let r = unsafe {
            ffi::pa_context_connect(
                self.raw_mut(),
                server.into().unwrap_cstr(),
                flags.into(),
                api,
            )
        };
        error_result!((), r)
    }

    pub fn disconnect(&self) {
        unsafe {
            ffi::pa_context_disconnect(self.raw_mut());
        }
    }

    pub fn drain<CB>(&self, _: CB, userdata: *mut c_void) -> Result<Operation>
    where
        CB: Fn(&Context, *mut c_void),
    {
        assert_eq!(::std::mem::size_of::<CB>(), 0);

        // See: A note about `wrapped` functions
        unsafe extern "C" fn wrapped<F>(c: *mut ffi::pa_context, userdata: *mut c_void)
        where
            F: Fn(&Context, *mut c_void),
        {
            let ctx = context::from_raw_ptr(c);
            let cb = MaybeUninit::<F>::uninit();
            let result = (*cb.as_ptr())(&ctx, userdata);
            forget(ctx);

            result
        }

        op_or_err!(
            self,
            ffi::pa_context_drain(self.raw_mut(), Some(wrapped::<CB>), userdata)
        )
    }

    pub fn rttime_new<CB>(
        &self,
        usec: USec,
        _: CB,
        userdata: *mut c_void,
    ) -> *mut ffi::pa_time_event
    where
        CB: Fn(&MainloopApi, *mut ffi::pa_time_event, &TimeVal, *mut c_void),
    {
        assert_eq!(::std::mem::size_of::<CB>(), 0);

        // See: A note about `wrapped` functions
        unsafe extern "C" fn wrapped<F>(
            a: *mut ffi::pa_mainloop_api,
            e: *mut ffi::pa_time_event,
            tv: *const TimeVal,
            userdata: *mut c_void,
        ) where
            F: Fn(&MainloopApi, *mut ffi::pa_time_event, &TimeVal, *mut c_void),
        {
            let api = mainloop_api::from_raw_ptr(a);
            let timeval = &*tv;
            let cb = MaybeUninit::<F>::uninit();
            let result = (*cb.as_ptr())(&api, e, timeval, userdata);
            forget(api);

            result
        }

        unsafe { ffi::pa_context_rttime_new(self.raw_mut(), usec, Some(wrapped::<CB>), userdata) }
    }

    pub fn get_server_info<CB>(&self, _: CB, userdata: *mut c_void) -> Result<Operation>
    where
        CB: Fn(&Context, Option<&ServerInfo>, *mut c_void),
    {
        assert_eq!(::std::mem::size_of::<CB>(), 0);

        // See: A note about `wrapped` functions
        unsafe extern "C" fn wrapped<F>(
            c: *mut ffi::pa_context,
            i: *const ffi::pa_server_info,
            userdata: *mut c_void,
        ) where
            F: Fn(&Context, Option<&ServerInfo>, *mut c_void),
        {
            let info = if i.is_null() { None } else { Some(&*i) };
            let ctx = context::from_raw_ptr(c);
            let cb = MaybeUninit::<F>::uninit();
            let result = (*cb.as_ptr())(&ctx, info, userdata);
            forget(ctx);

            result
        }

        op_or_err!(
            self,
            ffi::pa_context_get_server_info(self.raw_mut(), Some(wrapped::<CB>), userdata)
        )
    }

    pub fn get_sink_info_by_name<'str, CS, CB>(
        &self,
        name: CS,
        _: CB,
        userdata: *mut c_void,
    ) -> Result<Operation>
    where
        CB: Fn(&Context, *const SinkInfo, i32, *mut c_void),
        CS: Into<Option<&'str CStr>>,
    {
        assert_eq!(::std::mem::size_of::<CB>(), 0);

        // See: A note about `wrapped` functions
        unsafe extern "C" fn wrapped<F>(
            c: *mut ffi::pa_context,
            info: *const ffi::pa_sink_info,
            eol: c_int,
            userdata: *mut c_void,
        ) where
            F: Fn(&Context, *const SinkInfo, i32, *mut c_void),
        {
            let ctx = context::from_raw_ptr(c);
            let cb = MaybeUninit::<F>::uninit();
            let result = (*cb.as_ptr())(&ctx, info, eol, userdata);
            forget(ctx);

            result
        }

        op_or_err!(
            self,
            ffi::pa_context_get_sink_info_by_name(
                self.raw_mut(),
                name.into().unwrap_cstr(),
                Some(wrapped::<CB>),
                userdata
            )
        )
    }

    pub fn get_sink_info_list<CB>(&self, _: CB, userdata: *mut c_void) -> Result<Operation>
    where
        CB: Fn(&Context, *const SinkInfo, i32, *mut c_void),
    {
        assert_eq!(::std::mem::size_of::<CB>(), 0);

        // See: A note about `wrapped` functions
        unsafe extern "C" fn wrapped<F>(
            c: *mut ffi::pa_context,
            info: *const ffi::pa_sink_info,
            eol: c_int,
            userdata: *mut c_void,
        ) where
            F: Fn(&Context, *const SinkInfo, i32, *mut c_void),
        {
            let ctx = context::from_raw_ptr(c);
            let cb = MaybeUninit::<F>::uninit();
            let result = (*cb.as_ptr())(&ctx, info, eol, userdata);
            forget(ctx);

            result
        }

        op_or_err!(
            self,
            ffi::pa_context_get_sink_info_list(self.raw_mut(), Some(wrapped::<CB>), userdata)
        )
    }

    pub fn get_sink_input_info<CB>(
        &self,
        idx: u32,
        _: CB,
        userdata: *mut c_void,
    ) -> Result<Operation>
    where
        CB: Fn(&Context, *const SinkInputInfo, i32, *mut c_void),
    {
        assert_eq!(::std::mem::size_of::<CB>(), 0);

        // See: A note about `wrapped` functions
        unsafe extern "C" fn wrapped<F>(
            c: *mut ffi::pa_context,
            info: *const ffi::pa_sink_input_info,
            eol: c_int,
            userdata: *mut c_void,
        ) where
            F: Fn(&Context, *const SinkInputInfo, i32, *mut c_void),
        {
            let ctx = context::from_raw_ptr(c);
            let cb = MaybeUninit::<F>::uninit();
            let result = (*cb.as_ptr())(&ctx, info, eol, userdata);
            forget(ctx);

            result
        }

        op_or_err!(
            self,
            ffi::pa_context_get_sink_input_info(self.raw_mut(), idx, Some(wrapped::<CB>), userdata)
        )
    }

    pub fn get_source_info_list<CB>(&self, _: CB, userdata: *mut c_void) -> Result<Operation>
    where
        CB: Fn(&Context, *const SourceInfo, i32, *mut c_void),
    {
        assert_eq!(::std::mem::size_of::<CB>(), 0);

        // See: A note about `wrapped` functions
        unsafe extern "C" fn wrapped<F>(
            c: *mut ffi::pa_context,
            info: *const ffi::pa_source_info,
            eol: c_int,
            userdata: *mut c_void,
        ) where
            F: Fn(&Context, *const SourceInfo, i32, *mut c_void),
        {
            let ctx = context::from_raw_ptr(c);
            let cb = MaybeUninit::<F>::uninit();
            let result = (*cb.as_ptr())(&ctx, info, eol, userdata);
            forget(ctx);

            result
        }

        op_or_err!(
            self,
            ffi::pa_context_get_source_info_list(self.raw_mut(), Some(wrapped::<CB>), userdata)
        )
    }

    pub fn set_sink_input_volume<CB>(
        &self,
        idx: u32,
        volume: &CVolume,
        _: CB,
        userdata: *mut c_void,
    ) -> Result<Operation>
    where
        CB: Fn(&Context, i32, *mut c_void),
    {
        assert_eq!(::std::mem::size_of::<CB>(), 0);

        // See: A note about `wrapped` functions
        unsafe extern "C" fn wrapped<F>(
            c: *mut ffi::pa_context,
            success: c_int,
            userdata: *mut c_void,
        ) where
            F: Fn(&Context, i32, *mut c_void),
        {
            let ctx = context::from_raw_ptr(c);
            let cb = MaybeUninit::<F>::uninit();
            let result = (*cb.as_ptr())(&ctx, success, userdata);
            forget(ctx);

            result
        }

        op_or_err!(
            self,
            ffi::pa_context_set_sink_input_volume(
                self.raw_mut(),
                idx,
                volume,
                Some(wrapped::<CB>),
                userdata
            )
        )
    }

    pub fn subscribe<CB>(
        &self,
        m: SubscriptionMask,
        _: CB,
        userdata: *mut c_void,
    ) -> Result<Operation>
    where
        CB: Fn(&Context, i32, *mut c_void),
    {
        assert_eq!(::std::mem::size_of::<CB>(), 0);

        // See: A note about `wrapped` functions
        unsafe extern "C" fn wrapped<F>(
            c: *mut ffi::pa_context,
            success: c_int,
            userdata: *mut c_void,
        ) where
            F: Fn(&Context, i32, *mut c_void),
        {
            let ctx = context::from_raw_ptr(c);
            let cb = MaybeUninit::<F>::uninit();
            let result = (*cb.as_ptr())(&ctx, success, userdata);
            forget(ctx);

            result
        }

        op_or_err!(
            self,
            ffi::pa_context_subscribe(self.raw_mut(), m.into(), Some(wrapped::<CB>), userdata)
        )
    }

    pub fn clear_subscribe_callback(&self) {
        unsafe {
            ffi::pa_context_set_subscribe_callback(self.raw_mut(), None, ptr::null_mut());
        }
    }

    pub fn set_subscribe_callback<CB>(&self, _: CB, userdata: *mut c_void)
    where
        CB: Fn(&Context, SubscriptionEvent, u32, *mut c_void),
    {
        assert_eq!(::std::mem::size_of::<CB>(), 0);

        // See: A note about `wrapped` functions
        unsafe extern "C" fn wrapped<F>(
            c: *mut ffi::pa_context,
            t: ffi::pa_subscription_event_type_t,
            idx: u32,
            userdata: *mut c_void,
        ) where
            F: Fn(&Context, SubscriptionEvent, u32, *mut c_void),
        {
            let ctx = context::from_raw_ptr(c);
            let event = SubscriptionEvent::try_from(t)
                .expect("pa_context_subscribe_cb_t passed invalid pa_subscription_event_type_t");
            let cb = MaybeUninit::<F>::uninit();
            let result = (*cb.as_ptr())(&ctx, event, idx, userdata);
            forget(ctx);

            result
        }

        unsafe {
            ffi::pa_context_set_subscribe_callback(self.raw_mut(), Some(wrapped::<CB>), userdata);
        }
    }
}

#[doc(hidden)]
pub unsafe fn from_raw_ptr(ptr: *mut ffi::pa_context) -> Context {
    Context(ptr)
}
