use std::ffi::c_void;

pub struct Array {
    items: Vec<usize>,
    destructor: Option<unsafe extern "C" fn(x: *mut c_void)>,
}

pub mod ffi {
    use super::Array;

    use std::{
        ffi::{c_int, c_void},
        ptr,
    };

    pub const NOT_FOUND: c_int = -1;

    pub type WMArray = Array;

    #[unsafe(no_mangle)]
    pub unsafe extern "C" fn WMCreateArray(initial_size: c_int) -> *mut Array {
        let cap = if initial_size < 0 {
            0
        } else {
            initial_size as usize
        };
        Box::leak(Box::new(Array {
            items: Vec::with_capacity(cap),
            destructor: None,
        }))
    }

    #[unsafe(no_mangle)]
    pub unsafe extern "C" fn WMCreateArrayWithDestructor(
        initial_size: c_int,
        destructor: unsafe extern "C" fn(x: *mut c_void),
    ) -> *mut Array {
        let cap = if initial_size < 0 {
            0
        } else {
            initial_size as usize
        };
        Box::leak(Box::new(Array {
            items: Vec::with_capacity(cap),
            destructor: Some(destructor),
        }))
    }

    #[unsafe(no_mangle)]
    pub unsafe extern "C" fn WMCreateArrayWithArray(array: *mut Array) -> *mut Array {
        if array.is_null() {
            return ptr::null_mut();
        }
        let array = unsafe { &*array };
        Box::leak(Box::new(Array {
            items: array.items.clone(),
            destructor: array.destructor,
        }))
    }

    #[unsafe(no_mangle)]
    pub unsafe extern "C" fn WMEmptyArray(array: *mut Array) {
        if array.is_null() {
            return;
        }
        let array = unsafe { &mut *array };
        if let Some(f) = array.destructor {
            for item in &mut array.items {
                unsafe { (f)(*item as *mut c_void) }
            }
        }
        array.items.clear();
    }

    #[unsafe(no_mangle)]
    pub unsafe extern "C" fn WMFreeArray(array: *mut Array) {
        if array.is_null() {
            return;
        }
        unsafe {
            WMEmptyArray(array);
            let _ = ptr::read(array);
        }
    }

    #[unsafe(no_mangle)]
    pub unsafe extern "C" fn WMGetArrayItemCount(array: *mut Array) -> c_int {
        if array.is_null() {
            return 0;
        }
        unsafe { (*array).items.len() as c_int }
    }

    #[unsafe(no_mangle)]
    pub unsafe extern "C" fn WMAddToArray(array: *mut Array, item: *mut c_void) {
        if array.is_null() {
            return;
        }
        unsafe {
            (*array).items.push(item.addr());
        }
    }

    #[unsafe(no_mangle)]
    pub unsafe extern "C" fn WMInsertInArray(array: *mut Array, index: c_int, item: *mut c_void) {
        if array.is_null() {
            return;
        }
        if index < 0 {
            return;
        }
        let array = unsafe { &mut (*array).items };
        let index = index as usize;
        if index >= array.len() {
            return;
        }
        array.insert(index, item.addr());
    }

    #[unsafe(no_mangle)]
    pub unsafe extern "C" fn WMSetInArray(
        array: *mut Array,
        index: c_int,
        item: *mut c_void,
    ) -> *mut c_void {
        if array.is_null() {
            return ptr::null_mut();
        }
        if index < 0 {
            return ptr::null_mut();
        }
        let index = index as usize;

        /* is it really useful to perform append if index == array->itemCount ? -Dan */
        if index == unsafe { (*array).items.len() } {
            unsafe {
                WMAddToArray(array, item);
            }
            return ptr::null_mut();
        }

        let array = unsafe { &mut (*array).items };

        let old = array[index];
        array[index] = item.addr();
        old as *mut c_void
    }

    #[unsafe(no_mangle)]
    pub unsafe extern "C" fn WMDeleteFromArray(array: *mut Array, index: c_int) -> c_int {
        if array.is_null() {
            return 0;
        }
        let array = unsafe { &mut *array };
        if index < 0 {
            return 0;
        }
        let index = index as usize;
        if index >= array.items.len() {
            0
        } else {
            let old = array.items.remove(index);
            if let Some(f) = array.destructor {
                unsafe {
                    (f)(old as *mut c_void);
                }
            }
            1
        }
    }

    #[unsafe(no_mangle)]
    pub unsafe extern "C" fn WMRemoveFromArray(array: *mut Array, item: *mut c_void) -> c_int {
        unsafe { WMRemoveFromArrayMatching(array, None, item) }
    }

    #[unsafe(no_mangle)]
    pub unsafe extern "C" fn WMRemoveFromArrayMatching(
        array: *mut Array,
        pred: Option<unsafe extern "C" fn(item: *const c_void, cdata: *mut c_void) -> c_int>,
        cdata: *mut c_void,
    ) -> c_int {
        if array.is_null() {
            return 1;
        }
        let array = unsafe { &mut *array };
        let original_len = array.items.len();
        match pred {
            Some(f) => array.items.retain(|x| unsafe { f(*x as *const c_void, cdata) != 0 }),
            None => array.items.retain(|x| ptr::eq(*x as *mut c_void, cdata)),
        }
        (original_len - array.items.len()) as c_int
    }

    #[unsafe(no_mangle)]
    pub unsafe extern "C" fn WMGetFromArray(array: *mut Array, index: c_int) -> *mut c_void {
        if array.is_null() || index < 0 {
            return ptr::null_mut();
        }
        unsafe {
            (&(*array))
                .items
                .get(index as usize)
                .map(|p| *p as *mut c_void)
                .unwrap_or(ptr::null_mut())
        }
    }

    #[unsafe(no_mangle)]
    pub unsafe extern "C" fn WMGetFirstInArray(array: *mut Array, item: *mut c_void) -> c_int {
        unsafe { WMFindInArray(array, None, item) }
    }

    #[unsafe(no_mangle)]
    pub unsafe extern "C" fn WMPopFromArray(array: *mut Array) -> *mut c_void {
        if array.is_null() {
            return ptr::null_mut();
        }
        unsafe {
            (*array)
                .items
                .pop()
                .map(|p| p as *mut c_void)
                .unwrap_or(ptr::null_mut())
        }
    }

    #[unsafe(no_mangle)]
    pub unsafe extern "C" fn WMFindInArray(
        array: *mut Array,
        pred: Option<unsafe extern "C" fn(item: *const c_void, cdata: *mut c_void) -> c_int>,
        cdata: *mut c_void,
    ) -> c_int {
        if array.is_null() {
            return NOT_FOUND;
        }
        let array = unsafe { &*array };
        if let Some(f) = pred {
            array
                .items
                .iter()
                .enumerate()
                .find(|(_, item)| unsafe { f(**item as *const c_void, cdata) != 0 })
                .map(|(i, _)| i as c_int)
                .unwrap_or(NOT_FOUND)
        } else {
            array
                .items
                .iter()
                .enumerate()
                .find(|(_, item)| ptr::eq(**item as *const c_void, cdata))
                .map(|(i, _)| i as c_int)
                .unwrap_or(NOT_FOUND)
        }
    }

    #[unsafe(no_mangle)]
    pub unsafe extern "C" fn WMCountInArray(array: *mut Array, item: *const c_void) -> c_int {
        if array.is_null() {
            return 0;
        }
        let array = unsafe { &*array };
        array
            .items
            .iter()
            .filter(|x| ptr::eq(**x as *const c_void, item))
            .count() as c_int
    }

    #[unsafe(no_mangle)]
    pub unsafe extern "C" fn WMSortArray(
        array: *mut Array,
        comparator: unsafe extern "C" fn(a: *const c_void, b: *const c_void) -> c_int,
    ) {
        if array.is_null() {
            return;
        }
        unsafe {
            (*array)
                .items
                .sort_by(|a, b| {
                    let a = a as *const _ as *const c_void;
                    let b = b as *const _ as *const c_void;
                    match comparator(a, b).signum() {
                        -1 => std::cmp::Ordering::Less,
                        0 => std::cmp::Ordering::Equal,
                        1 => std::cmp::Ordering::Greater,
                        _ => unreachable!(),
                    }
                })
       }
    }

    #[unsafe(no_mangle)]
    pub unsafe extern "C" fn WMMapArray(
        array: *mut Array,
        f: unsafe extern "C" fn(*mut c_void, *mut c_void),
        data: *mut c_void,
    ) {
        if array.is_null() {
            return;
        }
        unsafe {
            for a in &mut (*array).items {
                (f)(*a as *mut c_void, data);
            }
        }
    }

    #[unsafe(no_mangle)]
    pub unsafe extern "C" fn WMArrayFirst(array: *mut Array, iter: *mut c_int) -> *mut c_void {
        if array.is_null() || iter.is_null() {
            return ptr::null_mut();
        }
        let array = unsafe { &*array };
        match array.items.get(0) {
            None => {
                unsafe {
                    *iter = NOT_FOUND;
                }
                ptr::null_mut()
            }
            Some(x) => {
                unsafe {
                    *iter = 0;
                }
                *x as *mut c_void
            }
        }
    }

    #[unsafe(no_mangle)]
    pub unsafe extern "C" fn WMArrayLast(array: *mut Array, iter: *mut c_int) -> *mut c_void {
        if array.is_null() || iter.is_null() {
            return ptr::null_mut();
        }
        let array = unsafe { &*array };
        match array.items.last() {
            None => {
                unsafe {
                    *iter = NOT_FOUND;
                }
                ptr::null_mut()
            }
            Some(x) => {
                unsafe {
                    *iter = (array.items.len() - 1) as c_int;
                }
                *x as *mut c_void
            }
        }
    }

    #[unsafe(no_mangle)]
    pub unsafe extern "C" fn WMArrayNext(array: *mut Array, iter: *mut c_int) -> *mut c_void {
        if array.is_null() || iter.is_null() {
            return ptr::null_mut();
        }
        let array = unsafe { &*array };
        let index = unsafe { *iter };
        if index < 0 {
            return ptr::null_mut();
        }
        match array.items.get(index as usize + 1) {
            Some(i) => {
                unsafe {
                    *iter += 1;
                }
                *i as *mut c_void
            }
            None => {
                unsafe {
                    *iter = NOT_FOUND;
                }
                ptr::null_mut()
            }
        }
    }

    #[unsafe(no_mangle)]
    pub unsafe extern "C" fn WMArrayPrevious(array: *mut Array, iter: *mut c_int) -> *mut c_void {
        if array.is_null() || iter.is_null() {
            return ptr::null_mut();
        }
        let array = unsafe { &*array };
        let index = unsafe { *iter };
        if index < 1 {
            return ptr::null_mut();
        }
        match array.items.get(index as usize - 1) {
            Some(i) => {
                unsafe {
                    *iter -= 1;
                }
                *i as *mut c_void
            }
            None => {
                unsafe {
                    *iter = NOT_FOUND;
                }
                ptr::null_mut()
            }
        }
    }
}

#[cfg(test)]
mod test {
    use std::{ffi::c_void, ptr};

    use super::ffi::*;

    #[test]
    fn create_destroy_with_size() {
        unsafe {
            let array = WMCreateArray(10);
            assert_eq!((*array).items.len(), 0);
            assert!((*array).items.capacity() >= 10);
            WMFreeArray(array);
        }
    }

    #[test]
    fn create_push_clear_destroy() {
        static mut SENTINEL: *mut c_void = ptr::null_mut();
        unsafe extern "C" fn destructor(item: *mut c_void) {
            unsafe {
                SENTINEL = item;
            }
        }
        unsafe {
            let array = WMCreateArrayWithDestructor(10, destructor);
            assert!(SENTINEL.is_null());

            let mut x = 0xdeadbeefu32;
            WMAddToArray(array, (&mut x as *mut u32).cast::<c_void>());
            assert_eq!(WMGetArrayItemCount(array), 1);
            WMEmptyArray(array);
            assert!(ptr::eq(SENTINEL, (&x as *const u32).cast::<c_void>()));
            assert_eq!(0xdeadbeefu32, *SENTINEL.cast::<u32>());

            SENTINEL = ptr::null_mut();
            WMFreeArray(array);
            assert!(SENTINEL.is_null());
        }
    }
}
