use crate::{
    exceptions, ffi, AsPyPointer, FromPyObject, IntoPy, PyAny, PyErr, PyNativeType, PyObject,
    PyResult, Python, ToPyObject,
};
use std::convert::TryFrom;
use std::i64;
use std::os::raw::{c_int, c_long, c_uchar};
fn err_if_invalid_value<T: PartialEq>(
    py: Python,
    invalid_value: T,
    actual_value: T,
) -> PyResult<T> {
    if actual_value == invalid_value && PyErr::occurred(py) {
        Err(PyErr::fetch(py))
    } else {
        Ok(actual_value)
    }
}
macro_rules! int_fits_larger_int {
    ($rust_type:ty, $larger_type:ty) => {
        impl ToPyObject for $rust_type {
            #[inline]
            fn to_object(&self, py: Python) -> PyObject {
                (*self as $larger_type).into_py(py)
            }
        }
        impl IntoPy<PyObject> for $rust_type {
            fn into_py(self, py: Python) -> PyObject {
                (self as $larger_type).into_py(py)
            }
        }
        impl<'source> FromPyObject<'source> for $rust_type {
            fn extract(obj: &'source PyAny) -> PyResult<Self> {
                let val: $larger_type = obj.extract()?;
                <$rust_type>::try_from(val).map_err(|_| exceptions::OverflowError.into())
            }
        }
    };
}
#[cfg(target_endian = "little")]
const IS_LITTLE_ENDIAN: c_int = 1;
#[cfg(not(target_endian = "little"))]
const IS_LITTLE_ENDIAN: c_int = 0;
macro_rules! int_convert_128 {
    ($rust_type: ty, $byte_size: expr, $is_signed: expr) => {
        impl ToPyObject for $rust_type {
            #[inline]
            fn to_object(&self, py: Python) -> PyObject {
                (*self).into_py(py)
            }
        }
        impl IntoPy<PyObject> for $rust_type {
            fn into_py(self, py: Python) -> PyObject {
                unsafe {
                    let bytes = self.to_ne_bytes();
                    let obj = ffi::_PyLong_FromByteArray(
                        bytes.as_ptr() as *const c_uchar,
                        $byte_size,
                        IS_LITTLE_ENDIAN,
                        $is_signed,
                    );
                    PyObject::from_owned_ptr_or_panic(py, obj)
                }
            }
        }
        impl<'source> FromPyObject<'source> for $rust_type {
            fn extract(ob: &'source PyAny) -> PyResult<$rust_type> {
                unsafe {
                    let num = ffi::PyNumber_Index(ob.as_ptr());
                    if num.is_null() {
                        return Err(PyErr::fetch(ob.py()));
                    }
                    let buffer: [c_uchar; $byte_size] = [0; $byte_size];
                    let ok = ffi::_PyLong_AsByteArray(
                        num as *mut ffi::PyLongObject,
                        buffer.as_ptr() as *const c_uchar,
                        $byte_size,
                        IS_LITTLE_ENDIAN,
                        $is_signed,
                    );
                    if ok == -1 {
                        Err(PyErr::fetch(ob.py()))
                    } else {
                        Ok(<$rust_type>::from_ne_bytes(buffer))
                    }
                }
            }
        }
    };
}
#[repr(transparent)]
pub struct PyLong(PyAny);
pyobject_native_var_type!(PyLong, ffi::PyLong_Type, ffi::PyLong_Check);
macro_rules! int_fits_c_long {
    ($rust_type:ty) => {
        impl ToPyObject for $rust_type {
            #![cfg_attr(feature = "cargo-clippy", allow(clippy::cast_lossless))]
            fn to_object(&self, py: Python) -> PyObject {
                unsafe {
                    PyObject::from_owned_ptr_or_panic(py, ffi::PyLong_FromLong(*self as c_long))
                }
            }
        }
        impl IntoPy<PyObject> for $rust_type {
            #![cfg_attr(feature = "cargo-clippy", allow(clippy::cast_lossless))]
            fn into_py(self, py: Python) -> PyObject {
                unsafe {
                    PyObject::from_owned_ptr_or_panic(py, ffi::PyLong_FromLong(self as c_long))
                }
            }
        }
        impl<'source> FromPyObject<'source> for $rust_type {
            fn extract(obj: &'source PyAny) -> PyResult<Self> {
                let ptr = obj.as_ptr();
                let val = unsafe {
                    let num = ffi::PyNumber_Index(ptr);
                    if num.is_null() {
                        Err(PyErr::fetch(obj.py()))
                    } else {
                        let val = err_if_invalid_value(obj.py(), -1, ffi::PyLong_AsLong(num));
                        ffi::Py_DECREF(num);
                        val
                    }
                }?;
                <$rust_type>::try_from(val).map_err(|_| exceptions::OverflowError.into())
            }
        }
    };
}
macro_rules! int_convert_u64_or_i64 {
    ($rust_type:ty, $pylong_from_ll_or_ull:expr, $pylong_as_ll_or_ull:expr) => {
        impl ToPyObject for $rust_type {
            #[inline]
            fn to_object(&self, py: Python) -> PyObject {
                unsafe { PyObject::from_owned_ptr_or_panic(py, $pylong_from_ll_or_ull(*self)) }
            }
        }
        impl IntoPy<PyObject> for $rust_type {
            #[inline]
            fn into_py(self, py: Python) -> PyObject {
                unsafe { PyObject::from_owned_ptr_or_panic(py, $pylong_from_ll_or_ull(self)) }
            }
        }
        impl<'source> FromPyObject<'source> for $rust_type {
            fn extract(ob: &'source PyAny) -> PyResult<$rust_type> {
                let ptr = ob.as_ptr();
                unsafe {
                    let num = ffi::PyNumber_Index(ptr);
                    if num.is_null() {
                        Err(PyErr::fetch(ob.py()))
                    } else {
                        let result = err_if_invalid_value(ob.py(), !0, $pylong_as_ll_or_ull(num));
                        ffi::Py_DECREF(num);
                        result
                    }
                }
            }
        }
    };
}
int_fits_c_long!(i8);
int_fits_c_long!(u8);
int_fits_c_long!(i16);
int_fits_c_long!(u16);
int_fits_c_long!(i32);
#[cfg(all(target_pointer_width = "64", not(target_os = "windows")))]
int_fits_c_long!(u32);
#[cfg(any(target_pointer_width = "32", target_os = "windows"))]
int_fits_larger_int!(u32, u64);
#[cfg(all(target_pointer_width = "64", not(target_os = "windows")))]
int_fits_c_long!(i64);
#[cfg(any(target_pointer_width = "32", target_os = "windows"))]
int_convert_u64_or_i64!(i64, ffi::PyLong_FromLongLong, ffi::PyLong_AsLongLong);
#[cfg(all(target_pointer_width = "64", not(target_os = "windows")))]
int_fits_c_long!(isize);
#[cfg(any(target_pointer_width = "32", target_os = "windows"))]
int_fits_larger_int!(isize, i64);
int_fits_larger_int!(usize, u64);
int_convert_u64_or_i64!(
    u64,
    ffi::PyLong_FromUnsignedLongLong,
    ffi::PyLong_AsUnsignedLongLong
);
#[cfg(not(Py_LIMITED_API))]
int_convert_128!(i128, 16, 1);
#[cfg(not(Py_LIMITED_API))]
int_convert_128!(u128, 16, 0);
#[cfg(all(feature = "num-bigint", not(Py_LIMITED_API)))]
mod bigint_conversion {
    use super::*;
    use num_bigint::{BigInt, BigUint};
    unsafe fn extract_small(ob: &PyAny, n: usize, is_signed: c_int) -> PyResult<[c_uchar; 128]> {
        let buffer = [0; 128];
        let ok = ffi::_PyLong_AsByteArray(
            ob.as_ptr() as *mut ffi::PyLongObject,
            buffer.as_ptr() as *const c_uchar,
            n,
            1,
            is_signed,
        );
        if ok == -1 {
            Err(PyErr::fetch(ob.py()))
        } else {
            Ok(buffer)
        }
    }
    unsafe fn extract_large(ob: &PyAny, n: usize, is_signed: c_int) -> PyResult<Vec<c_uchar>> {
        let buffer = vec![0; n];
        let ok = ffi::_PyLong_AsByteArray(
            ob.as_ptr() as *mut ffi::PyLongObject,
            buffer.as_ptr() as *const c_uchar,
            n,
            1,
            is_signed,
        );
        if ok == -1 {
            Err(PyErr::fetch(ob.py()))
        } else {
            Ok(buffer)
        }
    }
    macro_rules! bigint_conversion {
        ($rust_ty: ty, $is_signed: expr, $to_bytes: path, $from_bytes: path) => {
            impl ToPyObject for $rust_ty {
                fn to_object(&self, py: Python) -> PyObject {
                    unsafe {
                        let bytes = $to_bytes(self);
                        let obj = ffi::_PyLong_FromByteArray(
                            bytes.as_ptr() as *const c_uchar,
                            bytes.len(),
                            1,
                            $is_signed,
                        );
                        PyObject::from_owned_ptr_or_panic(py, obj)
                    }
                }
            }
            impl IntoPy<PyObject> for $rust_ty {
                fn into_py(self, py: Python) -> PyObject {
                    self.to_object(py)
                }
            }
            impl<'source> FromPyObject<'source> for $rust_ty {
                fn extract(ob: &'source PyAny) -> PyResult<$rust_ty> {
                    unsafe {
                        let num = ffi::PyNumber_Index(ob.as_ptr());
                        if num.is_null() {
                            return Err(PyErr::fetch(ob.py()));
                        }
                        let n_bits = ffi::_PyLong_NumBits(num);
                        let n_bytes = if n_bits < 0 {
                            return Err(PyErr::fetch(ob.py()));
                        } else if n_bits == 0 {
                            0
                        } else {
                            (n_bits as usize - 1 + $is_signed) / 8 + 1
                        };
                        if n_bytes <= 128 {
                            extract_small(ob, n_bytes, $is_signed)
                                .map(|b| $from_bytes(&b[..n_bytes]))
                        } else {
                            extract_large(ob, n_bytes, $is_signed).map(|b| $from_bytes(&b))
                        }
                    }
                }
            }
        };
    }
    bigint_conversion!(BigUint, 0, BigUint::to_bytes_le, BigUint::from_bytes_le);
    bigint_conversion!(
        BigInt,
        1,
        BigInt::to_signed_bytes_le,
        BigInt::from_signed_bytes_le
    );
    #[cfg(test)]
    mod test {
        use super::*;
        use crate::types::{PyDict, PyModule};
        use indoc::indoc;
        fn python_fib(py: Python) -> &PyModule {
            let fib_code = indoc!(
                r#"
                def fib(n):
                    f0, f1 = 0, 1
                    for _ in range(n):
                        f0, f1 = f1, f0 + f1
                    return f0
                def fib_neg(n):
                    return -fib(n)
        "#
            );
            PyModule::from_code(py, fib_code, "fib.py", "fib").unwrap()
        }
        fn rust_fib<T>(n: usize) -> T
        where
            T: From<u16>,
            for<'a> &'a T: std::ops::Add<Output = T>,
        {
            let mut f0: T = T::from(0);
            let mut f1: T = T::from(1);
            for _ in 0..n {
                let f2 = &f0 + &f1;
                f0 = std::mem::replace(&mut f1, f2);
            }
            f0
        }
        #[test]
        fn convert_biguint() {
            let gil = Python::acquire_gil();
            let py = gil.python();
            let rs_result: BigUint = rust_fib(400);
            let fib = python_fib(py);
            let locals = PyDict::new(py);
            locals.set_item("rs_result", &rs_result).unwrap();
            locals.set_item("fib", fib).unwrap();
            
            py.run("assert fib.fib(400) == rs_result", None, Some(locals))
                .unwrap();
            
            let py_result: BigUint =
                FromPyObject::extract(fib.call1("fib", (400,)).unwrap()).unwrap();
            assert_eq!(rs_result, py_result);
            
            let rs_result: BigUint = rust_fib(2000);
            let py_result: BigUint =
                FromPyObject::extract(fib.call1("fib", (2000,)).unwrap()).unwrap();
            assert_eq!(rs_result, py_result);
        }
        #[test]
        fn convert_bigint() {
            let gil = Python::acquire_gil();
            let py = gil.python();
            let rs_result = rust_fib::<BigInt>(400) * -1;
            let fib = python_fib(py);
            let locals = PyDict::new(py);
            locals.set_item("rs_result", &rs_result).unwrap();
            locals.set_item("fib", fib).unwrap();
            
            py.run("assert fib.fib_neg(400) == rs_result", None, Some(locals))
                .unwrap();
            
            let py_result: BigInt =
                FromPyObject::extract(fib.call1("fib_neg", (400,)).unwrap()).unwrap();
            assert_eq!(rs_result, py_result);
            
            let rs_result = rust_fib::<BigInt>(2000) * -1;
            let py_result: BigInt =
                FromPyObject::extract(fib.call1("fib_neg", (2000,)).unwrap()).unwrap();
            assert_eq!(rs_result, py_result);
        }
        #[test]
        fn handle_zero() {
            let gil = Python::acquire_gil();
            let py = gil.python();
            let fib = python_fib(py);
            let zero: BigInt = FromPyObject::extract(fib.call1("fib", (0,)).unwrap()).unwrap();
            assert_eq!(zero, BigInt::from(0));
        }
        
        #[test]
        fn check_overflow() {
            let gil = Python::acquire_gil();
            let py = gil.python();
            macro_rules! test {
                ($T:ty, $value:expr, $py:expr) => {
                    let value = $value;
                    println!("{}: {}", stringify!($T), value);
                    let python_value = value.clone().to_object(py);
                    let roundtrip_value = python_value.extract::<$T>(py).unwrap();
                    assert_eq!(value, roundtrip_value);
                };
            }
            for i in 0..=256usize {
                
                test!(BigInt, BigInt::from(i), py);
                test!(BigUint, BigUint::from(i), py);
                test!(BigInt, -BigInt::from(i), py);
                test!(BigInt, BigInt::from(1) << i, py);
                test!(BigUint, BigUint::from(1u32) << i, py);
                test!(BigInt, -BigInt::from(1) << i, py);
                test!(BigInt, (BigInt::from(1) << i) + 1u32, py);
                test!(BigUint, (BigUint::from(1u32) << i) + 1u32, py);
                test!(BigInt, (-BigInt::from(1) << i) + 1u32, py);
                test!(BigInt, (BigInt::from(1) << i) - 1u32, py);
                test!(BigUint, (BigUint::from(1u32) << i) - 1u32, py);
                test!(BigInt, (-BigInt::from(1) << i) - 1u32, py);
            }
        }
    }
}
#[cfg(test)]
mod test {
    use crate::Python;
    use crate::ToPyObject;
    #[test]
    fn test_u32_max() {
        let gil = Python::acquire_gil();
        let py = gil.python();
        let v = std::u32::MAX;
        let obj = v.to_object(py);
        assert_eq!(v, obj.extract::<u32>(py).unwrap());
        assert_eq!(u64::from(v), obj.extract::<u64>(py).unwrap());
        assert!(obj.extract::<i32>(py).is_err());
    }
    #[test]
    fn test_i64_max() {
        let gil = Python::acquire_gil();
        let py = gil.python();
        let v = std::i64::MAX;
        let obj = v.to_object(py);
        assert_eq!(v, obj.extract::<i64>(py).unwrap());
        assert_eq!(v as u64, obj.extract::<u64>(py).unwrap());
        assert!(obj.extract::<u32>(py).is_err());
    }
    #[test]
    fn test_i64_min() {
        let gil = Python::acquire_gil();
        let py = gil.python();
        let v = std::i64::MIN;
        let obj = v.to_object(py);
        assert_eq!(v, obj.extract::<i64>(py).unwrap());
        assert!(obj.extract::<i32>(py).is_err());
        assert!(obj.extract::<u64>(py).is_err());
    }
    #[test]
    fn test_u64_max() {
        let gil = Python::acquire_gil();
        let py = gil.python();
        let v = std::u64::MAX;
        let obj = v.to_object(py);
        assert_eq!(v, obj.extract::<u64>(py).unwrap());
        assert!(obj.extract::<i64>(py).is_err());
    }
    #[test]
    #[cfg(not(Py_LIMITED_API))]
    fn test_i128_max() {
        let gil = Python::acquire_gil();
        let py = gil.python();
        let v = std::i128::MAX;
        let obj = v.to_object(py);
        assert_eq!(v, obj.extract::<i128>(py).unwrap());
        assert_eq!(v as u128, obj.extract::<u128>(py).unwrap());
        assert!(obj.extract::<u64>(py).is_err());
    }
    #[test]
    #[cfg(not(Py_LIMITED_API))]
    fn test_i128_min() {
        let gil = Python::acquire_gil();
        let py = gil.python();
        let v = std::i128::MIN;
        let obj = v.to_object(py);
        assert_eq!(v, obj.extract::<i128>(py).unwrap());
        assert!(obj.extract::<i64>(py).is_err());
        assert!(obj.extract::<u128>(py).is_err());
    }
    #[test]
    #[cfg(not(Py_LIMITED_API))]
    fn test_u128_max() {
        let gil = Python::acquire_gil();
        let py = gil.python();
        let v = std::u128::MAX;
        let obj = v.to_object(py);
        assert_eq!(v, obj.extract::<u128>(py).unwrap());
        assert!(obj.extract::<i128>(py).is_err());
    }
    #[test]
    #[cfg(not(Py_LIMITED_API))]
    fn test_u128_overflow() {
        use crate::exceptions;
        use crate::ffi;
        use crate::object::PyObject;
        use std::os::raw::c_uchar;
        let gil = Python::acquire_gil();
        let py = gil.python();
        let overflow_bytes: [c_uchar; 20] = [255; 20];
        unsafe {
            let obj = ffi::_PyLong_FromByteArray(
                overflow_bytes.as_ptr() as *const c_uchar,
                20,
                super::IS_LITTLE_ENDIAN,
                0,
            );
            let obj = PyObject::from_owned_ptr_or_panic(py, obj);
            let err = obj.extract::<u128>(py).unwrap_err();
            assert!(err.is_instance::<exceptions::OverflowError>(py));
        }
    }
    macro_rules! test_common (
        ($test_mod_name:ident, $t:ty) => (
            mod $test_mod_name {
                use crate::exceptions;
                use crate::ToPyObject;
                use crate::Python;
                #[test]
                fn from_py_string_type_error() {
                    let gil = Python::acquire_gil();
                    let py = gil.python();
                    let obj = ("123").to_object(py);
                    let err = obj.extract::<$t>(py).unwrap_err();
                    assert!(err.is_instance::<exceptions::TypeError>(py));
                }
                #[test]
                fn from_py_float_type_error() {
                    let gil = Python::acquire_gil();
                    let py = gil.python();
                    let obj = (12.3).to_object(py);
                    let err = obj.extract::<$t>(py).unwrap_err();
                    assert!(err.is_instance::<exceptions::TypeError>(py));
                }
                #[test]
                fn to_py_object_and_back() {
                    let gil = Python::acquire_gil();
                    let py = gil.python();
                    let val = 123 as $t;
                    let obj = val.to_object(py);
                    assert_eq!(obj.extract::<$t>(py).unwrap(), val as $t);
                }
            }
        )
    );
    test_common!(i8, i8);
    test_common!(u8, u8);
    test_common!(i16, i16);
    test_common!(u16, u16);
    test_common!(i32, i32);
    test_common!(u32, u32);
    test_common!(i64, i64);
    test_common!(u64, u64);
    test_common!(isize, isize);
    test_common!(usize, usize);
    #[cfg(not(Py_LIMITED_API))]
    test_common!(i128, i128);
    #[cfg(not(Py_LIMITED_API))]
    test_common!(u128, u128);
}