use super::utils::{test_get_default_device, test_ops_stream_operation, Scope};
use super::*;
use std::sync::atomic::{AtomicI64, Ordering};

#[test]
fn test_dial_tone() {
    use std::f32::consts::PI;
    use std::thread;
    use std::time::Duration;

    const SAMPLE_FREQUENCY: u32 = 48_000;

    // Do nothing if there is no available output device.
    if test_get_default_device(Scope::Output).is_none() {
        println!("No output device.");
        return;
    }

    // Make sure the parameters meet the requirements of AudioUnitContext::stream_init
    // (in the comments).
    let mut output_params = ffi::cubeb_stream_params::default();
    output_params.format = ffi::CUBEB_SAMPLE_S16NE;
    output_params.rate = SAMPLE_FREQUENCY;
    output_params.channels = 1;
    output_params.layout = ffi::CUBEB_LAYOUT_MONO;
    output_params.prefs = ffi::CUBEB_STREAM_PREF_NONE;

    struct Closure {
        buffer_size: AtomicI64,
        phase: i64,
    }
    let mut closure = Closure {
        buffer_size: AtomicI64::new(0),
        phase: 0,
    };
    let closure_ptr = &mut closure as *mut Closure as *mut c_void;

    test_ops_stream_operation(
        "stream: North American dial tone",
        ptr::null_mut(), // Use default input device.
        ptr::null_mut(), // No input parameters.
        ptr::null_mut(), // Use default output device.
        &mut output_params,
        4096, // TODO: Get latency by get_min_latency instead ?
        Some(data_callback),
        Some(state_callback),
        closure_ptr,
        |stream| {
            assert_eq!(unsafe { OPS.stream_start.unwrap()(stream) }, ffi::CUBEB_OK);

            #[derive(Debug)]
            enum State {
                WaitingForStart,
                PositionIncreasing,
                Paused,
                Resumed,
                End,
            }
            let mut state = State::WaitingForStart;
            let mut position: u64 = 0;
            let mut prev_position: u64 = 0;
            let mut count = 0;
            const CHECK_COUNT: i32 = 10;
            loop {
                thread::sleep(Duration::from_millis(50));
                assert_eq!(
                    unsafe { OPS.stream_get_position.unwrap()(stream, &mut position) },
                    ffi::CUBEB_OK
                );
                println!(
                    "State: {:?}, position: {}, previous position: {}",
                    state, position, prev_position
                );
                match &mut state {
                    State::WaitingForStart => {
                        // It's expected to have 0 for a few iterations here: the stream can take
                        // some time to start.
                        if position != prev_position {
                            assert!(position > prev_position);
                            prev_position = position;
                            state = State::PositionIncreasing;
                        }
                    }
                    State::PositionIncreasing => {
                        // wait a few iterations, check monotony
                        if position != prev_position {
                            assert!(position > prev_position);
                            prev_position = position;
                            count += 1;
                            if count > CHECK_COUNT {
                                state = State::Paused;
                                count = 0;
                                assert_eq!(
                                    unsafe { OPS.stream_stop.unwrap()(stream) },
                                    ffi::CUBEB_OK
                                );
                                // Update the position once paused.
                                assert_eq!(
                                    unsafe {
                                        OPS.stream_get_position.unwrap()(stream, &mut position)
                                    },
                                    ffi::CUBEB_OK
                                );
                                prev_position = position;
                            }
                        }
                    }
                    State::Paused => {
                        // The cubeb_stream_stop call above should synchrously stop the callbacks,
                        // hence the clock, the assert below must always holds, modulo the client
                        // side interpolation.
                        assert!(
                            position == prev_position
                                || position - prev_position
                                    <= closure.buffer_size.load(Ordering::SeqCst) as u64
                        );
                        count += 1;
                        prev_position = position;
                        if count > CHECK_COUNT {
                            state = State::Resumed;
                            count = 0;
                            assert_eq!(unsafe { OPS.stream_start.unwrap()(stream) }, ffi::CUBEB_OK);
                        }
                    }
                    State::Resumed => {
                        // wait a few iterations, this can take some time to start
                        if position != prev_position {
                            assert!(position > prev_position);
                            prev_position = position;
                            count += 1;
                            if count > CHECK_COUNT {
                                state = State::End;
                                count = 0;
                                assert_eq!(
                                    unsafe { OPS.stream_stop.unwrap()(stream) },
                                    ffi::CUBEB_OK
                                );
                                assert_eq!(
                                    unsafe {
                                        OPS.stream_get_position.unwrap()(stream, &mut position)
                                    },
                                    ffi::CUBEB_OK
                                );
                                prev_position = position;
                            }
                        }
                    }
                    State::End => {
                        // The cubeb_stream_stop call above should synchrously stop the callbacks,
                        // hence the clock, the assert below must always holds, modulo the client
                        // side interpolation.
                        assert!(
                            position == prev_position
                                || position - prev_position
                                    <= closure.buffer_size.load(Ordering::SeqCst) as u64
                        );
                        if position == prev_position {
                            count += 1;
                            if count > CHECK_COUNT {
                                break;
                            }
                        }
                    }
                }
            }
            assert_eq!(unsafe { OPS.stream_stop.unwrap()(stream) }, ffi::CUBEB_OK);
        },
    );

    extern "C" fn state_callback(
        stream: *mut ffi::cubeb_stream,
        user_ptr: *mut c_void,
        state: ffi::cubeb_state,
    ) {
        assert!(!stream.is_null());
        assert!(!user_ptr.is_null());
        assert_ne!(state, ffi::CUBEB_STATE_ERROR);
    }

    extern "C" fn data_callback(
        stream: *mut ffi::cubeb_stream,
        user_ptr: *mut c_void,
        _input_buffer: *const c_void,
        output_buffer: *mut c_void,
        nframes: i64,
    ) -> i64 {
        assert!(!stream.is_null());
        assert!(!user_ptr.is_null());
        assert!(!output_buffer.is_null());

        let buffer = unsafe {
            let ptr = output_buffer as *mut i16;
            let len = nframes as usize;
            slice::from_raw_parts_mut(ptr, len)
        };

        let closure = unsafe { &mut *(user_ptr as *mut Closure) };

        closure.buffer_size.store(nframes, Ordering::SeqCst);

        // Generate tone on the fly.
        for data in buffer.iter_mut() {
            let t1 = (2.0 * PI * 350.0 * (closure.phase) as f32 / SAMPLE_FREQUENCY as f32).sin();
            let t2 = (2.0 * PI * 440.0 * (closure.phase) as f32 / SAMPLE_FREQUENCY as f32).sin();
            *data = f32_to_i16_sample(0.5 * (t1 + t2));
            closure.phase += 1;
        }

        nframes
    }

    fn f32_to_i16_sample(x: f32) -> i16 {
        (x * f32::from(i16::max_value())) as i16
    }
}
