use super::utils::{
    test_audiounit_get_buffer_frame_size, test_get_default_audiounit, test_get_default_device,
    test_ops_context_operation, PropertyScope, Scope,
};
use super::*;
use std::thread;

// Ignore the test by default to avoid overwritting the buffer frame size of the device that is
// currently used by other streams in other tests.
#[ignore]
#[test]
fn test_parallel_ops_init_streams_in_parallel_input() {
    const THREADS: u32 = 50;
    create_streams_by_ops_in_parallel_with_different_latency(
        THREADS,
        StreamType::Input,
        |streams| {
            // All the latency frames should be the same value as the first stream's one, since the
            // latency frames cannot be changed if another stream is operating in parallel.
            let mut latency_frames = vec![];
            let mut in_buffer_frame_sizes = vec![];

            for stream in streams {
                latency_frames.push(stream.latency_frames);

                assert!(!stream.core_stream_data.input_unit.is_null());
                let in_buffer_frame_size = test_audiounit_get_buffer_frame_size(
                    stream.core_stream_data.input_unit,
                    Scope::Input,
                    PropertyScope::Output,
                )
                .unwrap();
                in_buffer_frame_sizes.push(in_buffer_frame_size);

                assert!(stream.core_stream_data.output_unit.is_null());
            }

            // Make sure all the latency frames are same as the first stream's one.
            for i in 0..latency_frames.len() - 1 {
                assert_eq!(latency_frames[i], latency_frames[i + 1]);
            }

            // Make sure all the buffer frame sizes on output scope of the input audiounit are same
            // as the defined latency of the first initial stream.
            for i in 0..in_buffer_frame_sizes.len() - 1 {
                assert_eq!(in_buffer_frame_sizes[i], in_buffer_frame_sizes[i + 1]);
            }
        },
    );
}

// Ignore the test by default to avoid overwritting the buffer frame size of the device that is
// currently used by other streams in other tests.
#[ignore]
#[test]
fn test_parallel_ops_init_streams_in_parallel_output() {
    const THREADS: u32 = 50;
    create_streams_by_ops_in_parallel_with_different_latency(
        THREADS,
        StreamType::Output,
        |streams| {
            // All the latency frames should be the same value as the first stream's one, since the
            // latency frames cannot be changed if another stream is operating in parallel.
            let mut latency_frames = vec![];
            let mut out_buffer_frame_sizes = vec![];

            for stream in streams {
                latency_frames.push(stream.latency_frames);

                assert!(stream.core_stream_data.input_unit.is_null());

                assert!(!stream.core_stream_data.output_unit.is_null());
                let out_buffer_frame_size = test_audiounit_get_buffer_frame_size(
                    stream.core_stream_data.output_unit,
                    Scope::Output,
                    PropertyScope::Input,
                )
                .unwrap();
                out_buffer_frame_sizes.push(out_buffer_frame_size);
            }

            // Make sure all the latency frames are same as the first stream's one.
            for i in 0..latency_frames.len() - 1 {
                assert_eq!(latency_frames[i], latency_frames[i + 1]);
            }

            // Make sure all the buffer frame sizes on input scope of the output audiounit are same
            // as the defined latency of the first initial stream.
            for i in 0..out_buffer_frame_sizes.len() - 1 {
                assert_eq!(out_buffer_frame_sizes[i], out_buffer_frame_sizes[i + 1]);
            }
        },
    );
}

// Ignore the test by default to avoid overwritting the buffer frame size of the device that is
// currently used by other streams in other tests.
#[ignore]
#[test]
fn test_parallel_ops_init_streams_in_parallel_duplex() {
    const THREADS: u32 = 50;
    create_streams_by_ops_in_parallel_with_different_latency(
        THREADS,
        StreamType::Duplex,
        |streams| {
            // All the latency frames should be the same value as the first stream's one, since the
            // latency frames cannot be changed if another stream is operating in parallel.
            let mut latency_frames = vec![];
            let mut in_buffer_frame_sizes = vec![];
            let mut out_buffer_frame_sizes = vec![];

            for stream in streams {
                latency_frames.push(stream.latency_frames);

                assert!(!stream.core_stream_data.input_unit.is_null());
                let in_buffer_frame_size = test_audiounit_get_buffer_frame_size(
                    stream.core_stream_data.input_unit,
                    Scope::Input,
                    PropertyScope::Output,
                )
                .unwrap();
                in_buffer_frame_sizes.push(in_buffer_frame_size);

                assert!(!stream.core_stream_data.output_unit.is_null());
                let out_buffer_frame_size = test_audiounit_get_buffer_frame_size(
                    stream.core_stream_data.output_unit,
                    Scope::Output,
                    PropertyScope::Input,
                )
                .unwrap();
                out_buffer_frame_sizes.push(out_buffer_frame_size);
            }

            // Make sure all the latency frames are same as the first stream's one.
            for i in 0..latency_frames.len() - 1 {
                assert_eq!(latency_frames[i], latency_frames[i + 1]);
            }

            // Make sure all the buffer frame sizes on output scope of the input audiounit are same
            // as the defined latency of the first initial stream.
            for i in 0..in_buffer_frame_sizes.len() - 1 {
                assert_eq!(in_buffer_frame_sizes[i], in_buffer_frame_sizes[i + 1]);
            }

            // Make sure all the buffer frame sizes on input scope of the output audiounit are same
            // as the defined latency of the first initial stream.
            for i in 0..out_buffer_frame_sizes.len() - 1 {
                assert_eq!(out_buffer_frame_sizes[i], out_buffer_frame_sizes[i + 1]);
            }
        },
    );
}

fn create_streams_by_ops_in_parallel_with_different_latency<F>(
    amount: u32,
    stm_type: StreamType,
    callback: F,
) where
    F: FnOnce(Vec<&AudioUnitStream>),
{
    let default_input = test_get_default_device(Scope::Input);
    let default_output = test_get_default_device(Scope::Output);

    let has_input = stm_type == StreamType::Input || stm_type == StreamType::Duplex;
    let has_output = stm_type == StreamType::Output || stm_type == StreamType::Duplex;

    if has_input && default_input.is_none() {
        println!("No input device to perform the test.");
        return;
    }

    if has_output && default_output.is_none() {
        println!("No output device to perform the test.");
        return;
    }

    test_ops_context_operation("context: init and destroy", |context_ptr| {
        let context_ptr_value = context_ptr as usize;

        let mut join_handles = vec![];
        for i in 0..amount {
            // Make sure the parameters meet the requirements of AudioUnitContext::stream_init
            // (in the comments).
            let mut input_params = ffi::cubeb_stream_params::default();
            input_params.format = ffi::CUBEB_SAMPLE_FLOAT32NE;
            input_params.rate = 48_000;
            input_params.channels = 1;
            input_params.layout = ffi::CUBEB_LAYOUT_UNDEFINED;
            input_params.prefs = ffi::CUBEB_STREAM_PREF_NONE;

            let mut output_params = ffi::cubeb_stream_params::default();
            output_params.format = ffi::CUBEB_SAMPLE_FLOAT32NE;
            output_params.rate = 44100;
            output_params.channels = 2;
            output_params.layout = ffi::CUBEB_LAYOUT_UNDEFINED;
            output_params.prefs = ffi::CUBEB_STREAM_PREF_NONE;

            // Latency cannot be changed if another stream is operating in parallel. All the latecy
            // should be set to the same latency value of the first stream that is operating in the
            // context.
            let latency_frames = SAFE_MIN_LATENCY_FRAMES + i;
            assert!(latency_frames < SAFE_MAX_LATENCY_FRAMES);

            // Create many streams within the same context. The order of the stream creation
            // is random (The order of execution of the spawned threads is random.).assert!
            // It's super dangerous to pass `context_ptr_value` across threads and convert it back
            // to a pointer. However, it's the cheapest way to make sure the inside mutex works.
            let thread_name = format!("stream {} @ context {:?}", i, context_ptr);
            join_handles.push(
                thread::Builder::new()
                    .name(thread_name)
                    .spawn(move || {
                        let context_ptr = context_ptr_value as *mut ffi::cubeb;
                        let mut stream: *mut ffi::cubeb_stream = ptr::null_mut();
                        let stream_name = CString::new(format!("stream {}", i)).unwrap();
                        assert_eq!(
                            unsafe {
                                OPS.stream_init.unwrap()(
                                    context_ptr,
                                    &mut stream,
                                    stream_name.as_ptr(),
                                    ptr::null_mut(), // Use default input device.
                                    if has_input {
                                        &mut input_params
                                    } else {
                                        ptr::null_mut()
                                    },
                                    ptr::null_mut(), // Use default output device.
                                    if has_output {
                                        &mut output_params
                                    } else {
                                        ptr::null_mut()
                                    },
                                    latency_frames,
                                    None,            // No data callback.
                                    None,            // No state callback.
                                    ptr::null_mut(), // No user data pointer.
                                )
                            },
                            ffi::CUBEB_OK
                        );
                        assert!(!stream.is_null());
                        stream as usize
                    })
                    .unwrap(),
            );
        }

        let mut streams = vec![];
        // Wait for finishing the tasks on the different threads.
        for handle in join_handles {
            let stream_ptr_value = handle.join().unwrap();
            let stream = unsafe { Box::from_raw(stream_ptr_value as *mut AudioUnitStream) };
            streams.push(stream);
        }

        let stream_refs: Vec<&AudioUnitStream> = streams.iter().map(|stm| stm.as_ref()).collect();
        callback(stream_refs);
    });
}

// Ignore the test by default to avoid overwritting the buffer frame size of the device that is
// currently used by other streams in other tests.
#[ignore]
#[test]
fn test_parallel_init_streams_in_parallel_input() {
    const THREADS: u32 = 10;
    create_streams_in_parallel_with_different_latency(THREADS, StreamType::Input, |streams| {
        // All the latency frames should be the same value as the first stream's one, since the
        // latency frames cannot be changed if another stream is operating in parallel.
        let mut latency_frames = vec![];
        let mut in_buffer_frame_sizes = vec![];

        for stream in streams {
            latency_frames.push(stream.latency_frames);

            assert!(!stream.core_stream_data.input_unit.is_null());
            let in_buffer_frame_size = test_audiounit_get_buffer_frame_size(
                stream.core_stream_data.input_unit,
                Scope::Input,
                PropertyScope::Output,
            )
            .unwrap();
            in_buffer_frame_sizes.push(in_buffer_frame_size);

            assert!(stream.core_stream_data.output_unit.is_null());
        }

        // Make sure all the latency frames are same as the first stream's one.
        for i in 0..latency_frames.len() - 1 {
            assert_eq!(latency_frames[i], latency_frames[i + 1]);
        }

        // Make sure all the buffer frame sizes on output scope of the input audiounit are same
        // as the defined latency of the first initial stream.
        for i in 0..in_buffer_frame_sizes.len() - 1 {
            assert_eq!(in_buffer_frame_sizes[i], in_buffer_frame_sizes[i + 1]);
        }
    });
}

// Ignore the test by default to avoid overwritting the buffer frame size of the device that is
// currently used by other streams in other tests.
#[ignore]
#[test]
fn test_parallel_init_streams_in_parallel_output() {
    const THREADS: u32 = 10;
    create_streams_in_parallel_with_different_latency(THREADS, StreamType::Output, |streams| {
        // All the latency frames should be the same value as the first stream's one, since the
        // latency frames cannot be changed if another stream is operating in parallel.
        let mut latency_frames = vec![];
        let mut out_buffer_frame_sizes = vec![];

        for stream in streams {
            latency_frames.push(stream.latency_frames);

            assert!(stream.core_stream_data.input_unit.is_null());

            assert!(!stream.core_stream_data.output_unit.is_null());
            let out_buffer_frame_size = test_audiounit_get_buffer_frame_size(
                stream.core_stream_data.output_unit,
                Scope::Output,
                PropertyScope::Input,
            )
            .unwrap();
            out_buffer_frame_sizes.push(out_buffer_frame_size);
        }

        // Make sure all the latency frames are same as the first stream's one.
        for i in 0..latency_frames.len() - 1 {
            assert_eq!(latency_frames[i], latency_frames[i + 1]);
        }

        // Make sure all the buffer frame sizes on input scope of the output audiounit are same
        // as the defined latency of the first initial stream.
        for i in 0..out_buffer_frame_sizes.len() - 1 {
            assert_eq!(out_buffer_frame_sizes[i], out_buffer_frame_sizes[i + 1]);
        }
    });
}

// Ignore the test by default to avoid overwritting the buffer frame size of the device that is
// currently used by other streams in other tests.
#[ignore]
#[test]
fn test_parallel_init_streams_in_parallel_duplex() {
    const THREADS: u32 = 10;
    create_streams_in_parallel_with_different_latency(THREADS, StreamType::Duplex, |streams| {
        // All the latency frames should be the same value as the first stream's one, since the
        // latency frames cannot be changed if another stream is operating in parallel.
        let mut latency_frames = vec![];
        let mut in_buffer_frame_sizes = vec![];
        let mut out_buffer_frame_sizes = vec![];

        for stream in streams {
            latency_frames.push(stream.latency_frames);

            assert!(!stream.core_stream_data.input_unit.is_null());
            let in_buffer_frame_size = test_audiounit_get_buffer_frame_size(
                stream.core_stream_data.input_unit,
                Scope::Input,
                PropertyScope::Output,
            )
            .unwrap();
            in_buffer_frame_sizes.push(in_buffer_frame_size);

            assert!(!stream.core_stream_data.output_unit.is_null());
            let out_buffer_frame_size = test_audiounit_get_buffer_frame_size(
                stream.core_stream_data.output_unit,
                Scope::Output,
                PropertyScope::Input,
            )
            .unwrap();
            out_buffer_frame_sizes.push(out_buffer_frame_size);
        }

        // Make sure all the latency frames are same as the first stream's one.
        for i in 0..latency_frames.len() - 1 {
            assert_eq!(latency_frames[i], latency_frames[i + 1]);
        }

        // Make sure all the buffer frame sizes on output scope of the input audiounit are same
        // as the defined latency of the first initial stream.
        for i in 0..in_buffer_frame_sizes.len() - 1 {
            assert_eq!(in_buffer_frame_sizes[i], in_buffer_frame_sizes[i + 1]);
        }

        // Make sure all the buffer frame sizes on input scope of the output audiounit are same
        // as the defined latency of the first initial stream.
        for i in 0..out_buffer_frame_sizes.len() - 1 {
            assert_eq!(out_buffer_frame_sizes[i], out_buffer_frame_sizes[i + 1]);
        }
    });
}

fn create_streams_in_parallel_with_different_latency<F>(
    amount: u32,
    stm_type: StreamType,
    callback: F,
) where
    F: FnOnce(Vec<&AudioUnitStream>),
{
    let default_input = test_get_default_device(Scope::Input);
    let default_output = test_get_default_device(Scope::Output);

    let has_input = stm_type == StreamType::Input || stm_type == StreamType::Duplex;
    let has_output = stm_type == StreamType::Output || stm_type == StreamType::Duplex;

    if has_input && default_input.is_none() {
        println!("No input device to perform the test.");
        return;
    }

    if has_output && default_output.is_none() {
        println!("No output device to perform the test.");
        return;
    }

    let context = AudioUnitContext::new();

    let context_ptr_value = &context as *const AudioUnitContext as usize;

    let mut join_handles = vec![];
    for i in 0..amount {
        // Make sure the parameters meet the requirements of AudioUnitContext::stream_init
        // (in the comments).
        let mut input_params = ffi::cubeb_stream_params::default();
        input_params.format = ffi::CUBEB_SAMPLE_FLOAT32NE;
        input_params.rate = 48_000;
        input_params.channels = 1;
        input_params.layout = ffi::CUBEB_LAYOUT_UNDEFINED;
        input_params.prefs = ffi::CUBEB_STREAM_PREF_NONE;

        let mut output_params = ffi::cubeb_stream_params::default();
        output_params.format = ffi::CUBEB_SAMPLE_FLOAT32NE;
        output_params.rate = 44100;
        output_params.channels = 2;
        output_params.layout = ffi::CUBEB_LAYOUT_UNDEFINED;
        output_params.prefs = ffi::CUBEB_STREAM_PREF_NONE;

        // Latency cannot be changed if another stream is operating in parallel. All the latecy
        // should be set to the same latency value of the first stream that is operating in the
        // context.
        let latency_frames = SAFE_MIN_LATENCY_FRAMES + i;
        assert!(latency_frames < SAFE_MAX_LATENCY_FRAMES);

        // Create many streams within the same context. The order of the stream creation
        // is random. (The order of execution of the spawned threads is random.)
        // It's super dangerous to pass `context_ptr_value` across threads and convert it back
        // to a reference. However, it's the cheapest way to make sure the inside mutex works.
        let thread_name = format!("stream {} @ context {:?}", i, context_ptr_value);
        join_handles.push(
            thread::Builder::new()
                .name(thread_name)
                .spawn(move || {
                    let context = unsafe { &mut *(context_ptr_value as *mut AudioUnitContext) };
                    let input_params = unsafe { StreamParamsRef::from_ptr(&mut input_params) };
                    let output_params = unsafe { StreamParamsRef::from_ptr(&mut output_params) };
                    let stream = context
                        .stream_init(
                            None,
                            ptr::null_mut(), // Use default input device.
                            if has_input { Some(input_params) } else { None },
                            ptr::null_mut(), // Use default output device.
                            if has_output {
                                Some(output_params)
                            } else {
                                None
                            },
                            latency_frames,
                            None,            // No data callback.
                            None,            // No state callback.
                            ptr::null_mut(), // No user data pointer.
                        )
                        .unwrap();
                    assert!(!stream.as_ptr().is_null());
                    let stream_ptr_value = stream.as_ptr() as usize;
                    // Prevent the stream from being destroyed by leaking this stream.
                    mem::forget(stream);
                    stream_ptr_value
                })
                .unwrap(),
        );
    }

    let mut streams = vec![];
    // Wait for finishing the tasks on the different threads.
    for handle in join_handles {
        let stream_ptr_value = handle.join().unwrap();
        // Retake the leaked stream.
        let stream = unsafe { Box::from_raw(stream_ptr_value as *mut AudioUnitStream) };
        streams.push(stream);
    }

    let stream_refs: Vec<&AudioUnitStream> = streams.iter().map(|stm| stm.as_ref()).collect();
    callback(stream_refs);
}

#[derive(Debug, PartialEq)]
enum StreamType {
    Input,
    Output,
    Duplex,
}

// This is used to interfere other active streams.
// From this testing, it's ok to set the buffer frame size of a device that is currently used by
// other tests. It works on OSX 10.13, not sure if it works on other versions.
// However, other tests may check the buffer frame size they set at the same time,
// so we ignore this by default incase those checks fail.
#[ignore]
#[test]
fn test_set_buffer_frame_size_in_parallel() {
    test_set_buffer_frame_size_in_parallel_in_scope(Scope::Input);
    test_set_buffer_frame_size_in_parallel_in_scope(Scope::Output);
}

fn test_set_buffer_frame_size_in_parallel_in_scope(scope: Scope) {
    const THREADS: u32 = 100;

    let unit = test_get_default_audiounit(scope.clone());
    if unit.is_none() {
        println!("No unit for {:?}", scope);
        return;
    }

    let (unit_scope, unit_element, prop_scope) = match scope {
        Scope::Input => (kAudioUnitScope_Output, AU_IN_BUS, PropertyScope::Output),
        Scope::Output => (kAudioUnitScope_Input, AU_OUT_BUS, PropertyScope::Input),
    };

    let mut units = vec![];
    let mut join_handles = vec![];
    for i in 0..THREADS {
        let latency_frames = SAFE_MIN_LATENCY_FRAMES + i;
        assert!(latency_frames < SAFE_MAX_LATENCY_FRAMES);
        units.push(test_get_default_audiounit(scope.clone()).unwrap());
        let unit_value = units.last().unwrap().get_inner() as usize;
        join_handles.push(thread::spawn(move || {
            let status = audio_unit_set_property(
                unit_value as AudioUnit,
                kAudioDevicePropertyBufferFrameSize,
                unit_scope,
                unit_element,
                &latency_frames,
                mem::size_of::<u32>(),
            );
            (latency_frames, status)
        }));
    }

    let mut latencies = vec![];
    let mut statuses = vec![];
    for handle in join_handles {
        let (latency, status) = handle.join().unwrap();
        latencies.push(latency);
        statuses.push(status);
    }

    let mut buffer_frames_list = vec![];
    for unit in units.iter() {
        buffer_frames_list.push(unit.get_buffer_frame_size(scope.clone(), prop_scope.clone()));
    }

    for status in statuses {
        assert_eq!(status, NO_ERR);
    }

    for i in 0..buffer_frames_list.len() - 1 {
        assert_eq!(buffer_frames_list[i], buffer_frames_list[i + 1]);
    }
}
