use rithm::{big_int, fraction};
use std::cmp::Ordering;
use std::convert::TryFrom;
use std::ops::{Add, Mul, Sub};
use traiter::numbers::{Endianness, FromBytes, Sign, Signed, ToBytes, Zero};

use crate::locatable::Location;
use crate::operations::{
    CrossMultiply, DotMultiply, LocatePointInPointPointPointCircle, Square,
    SquaredMetric,
};
use crate::traits::{
    Elemental, Multipolygonal, Multisegmental, Multivertexal, Polygonal,
};

use super::impl_box_wrapper::impl_box_wrapper;
use super::impl_constrained_delaunay_triangulation_wrapper::impl_constrained_delaunay_triangulation_wrapper;
use super::impl_contour_wrapper::impl_contour_wrapper;
use super::impl_delaunay_triangulation_wrapper::impl_delaunay_triangulation_wrapper;
use super::impl_empty_wrapper::impl_empty_wrapper;
use super::impl_multipolygon_wrapper::impl_multipolygon_wrapper;
use super::impl_multisegment_wrapper::impl_multisegment_wrapper;
use super::impl_point_wrapper::impl_point_wrapper;
use super::impl_polygon_wrapper::impl_polygon_wrapper;
use super::impl_py_sequence::impl_py_sequence;
use super::impl_segment_wrapper::impl_segment_wrapper;
use super::impl_trapezoidation_wrapper::impl_trapezoidation_wrapper;
use super::reference;
use super::traits::{TryFromPyAny, TryToPyAny};

#[pyo3::pymodule]
fn _cexact(
    py: pyo3::Python<'_>,
    module: &pyo3::Bound<'_, pyo3::types::PyModule>,
) -> pyo3::PyResult<()> {
    use pyo3::types::PyModuleMethods;
    module.add_class::<PyBox>()?;
    module.add_class::<PyConstrainedDelaunayTriangulation>()?;
    module.add_class::<PyContour>()?;
    module.add_class::<PyDelaunayTriangulation>()?;
    module.add_class::<PyEmpty>()?;
    module.add_class::<PyTrapezoidation>()?;
    module.add_class::<PyMultipolygon>()?;
    module.add_class::<PyMultisegment>()?;
    module.add_class::<PyPoint>()?;
    module.add_class::<PyPolygon>()?;
    module.add_class::<PySegment>()?;
    pyo3::types::PySequence::register::<PyContourSegments>(py)?;
    pyo3::types::PySequence::register::<PyContourVertices>(py)?;
    pyo3::types::PySequence::register::<PyMultipolygonPolygons>(py)?;
    pyo3::types::PySequence::register::<PyMultisegmentSegments>(py)?;
    pyo3::types::PySequence::register::<PyPolygonHoles>(py)?;
    Ok(())
}

#[cfg(target_arch = "x86")]
type Digit = u16;
#[cfg(not(target_arch = "x86"))]
type Digit = u32;

const DIGIT_BITNESS: usize = (Digit::BITS - 1) as usize;
const _: () =
    assert!(big_int::is_valid_digit_bitness::<Digit, DIGIT_BITNESS>());

type BigInt = big_int::BigInt<Digit, DIGIT_BITNESS>;
type Fraction = fraction::Fraction<BigInt>;

impl From<Box> for PyBox {
    fn from(value: Box) -> Self {
        Self(value)
    }
}

impl From<Contour> for PyContour {
    fn from(value: Contour) -> Self {
        Self(value)
    }
}

impl From<Vec<Point>> for PyContour {
    fn from(value: Vec<Point>) -> Self {
        Self(Contour::new(value))
    }
}

impl From<Vec<Polygon>> for PyMultipolygon {
    fn from(value: Vec<Polygon>) -> Self {
        Self(Multipolygon::new(value))
    }
}

impl From<Vec<Segment>> for PyMultisegment {
    fn from(value: Vec<Segment>) -> Self {
        Self(Multisegment::new(value))
    }
}

impl From<Point> for PyPoint {
    fn from(value: Point) -> Self {
        Self(value)
    }
}

impl From<Polygon> for PyPolygon {
    fn from(value: Polygon) -> Self {
        Self(value)
    }
}

impl From<Segment> for PySegment {
    fn from(value: Segment) -> Self {
        Self(value)
    }
}

impl<Point> CrossMultiply for &Point
where
    Fraction: Mul<Output = Fraction> + Sub<Output = Fraction>,
    for<'a> &'a Fraction: Sub<Output = Fraction>,
    for<'a> &'a Point: Elemental<Coordinate = &'a Fraction>,
{
    type Output = Fraction;

    fn cross_multiply(
        first_start: Self,
        first_end: Self,
        second_start: Self,
        second_end: Self,
    ) -> Self::Output {
        let (first_start_x, first_start_y) = first_start.coordinates();
        let (first_end_x, first_end_y) = first_end.coordinates();
        let (second_start_x, second_start_y) = second_start.coordinates();
        let (second_end_x, second_end_y) = second_end.coordinates();
        (first_end_x - first_start_x) * (second_end_y - second_start_y)
            - (first_end_y - first_start_y) * (second_end_x - second_start_x)
    }
}

impl<Point> DotMultiply for &Point
where
    Fraction: Add<Output = Fraction> + Mul<Output = Fraction>,
    for<'a> &'a Fraction: Sub<Output = Fraction>,
    for<'a> &'a Point: Elemental<Coordinate = &'a Fraction>,
{
    type Output = Fraction;

    fn dot_multiply(
        first_start: Self,
        first_end: Self,
        second_start: Self,
        second_end: Self,
    ) -> Self::Output {
        let (first_start_x, first_start_y) = first_start.coordinates();
        let (first_end_x, first_end_y) = first_end.coordinates();
        let (second_start_x, second_start_y) = second_start.coordinates();
        let (second_end_x, second_end_y) = second_end.coordinates();
        (first_end_x - first_start_x) * (second_end_x - second_start_x)
            + (first_end_y - first_start_y) * (second_end_y - second_start_y)
    }
}

impl<'a, Point> LocatePointInPointPointPointCircle for &'a Point
where
    &'a Point: Elemental<Coordinate = &'a Fraction>,
    Fraction: Add<Output = Fraction>
        + Mul<Output = Fraction>
        + Sub<Output = Fraction>,
    for<'b> &'b Fraction:
        Mul<Output = Fraction> + Signed + Sub<Output = Fraction>,
{
    fn locate_point_in_point_point_point_circle(
        self,
        first: Self,
        second: Self,
        third: Self,
    ) -> Location {
        let (first_dx, first_dy) =
            (first.x() - self.x(), first.y() - self.y());
        let (second_dx, second_dy) =
            (second.x() - self.x(), second.y() - self.y());
        let (third_dx, third_dy) =
            (third.x() - self.x(), third.y() - self.y());
        match ((&first_dx * &first_dx + &first_dy * &first_dy)
            * (&second_dx * &third_dy - &second_dy * &third_dx)
            - (&second_dx * &second_dx + &second_dy * &second_dy)
                * (&first_dx * &third_dy - &first_dy * &third_dx)
            + (&third_dx * &third_dx + &third_dy * &third_dy)
                * (first_dx * second_dy - first_dy * second_dx))
            .sign()
        {
            Sign::Negative => Location::Exterior,
            Sign::Positive => Location::Interior,
            Sign::Zero => Location::Boundary,
        }
    }
}

impl Square for Fraction
where
    Fraction: Clone + Mul<Output = Fraction>,
{
    type Output = Self;

    fn square(self) -> Self::Output {
        self.clone() * self
    }
}

impl<Point> SquaredMetric for &Point
where
    Fraction: Add<Output = Fraction> + Square<Output = Fraction>,
    for<'a> &'a Fraction: Sub<Output = Fraction>,
    for<'a> &'a Point: Elemental<Coordinate = &'a Fraction>,
{
    type Output = Fraction;

    fn squared_distance_to(self, other: Self) -> Self::Output {
        let (start_x, start_y) = self.coordinates();
        let (other_start_x, other_start_y) = other.coordinates();
        (start_x - other_start_x).square() + (start_y - other_start_y).square()
    }
}

const INVALID_SCALAR_TYPE_ERROR_MESSAGE: &str =
    "Scalar should be a rational number.";
const UNDEFINED_DIVISION_ERROR_MESSAGE: &str =
    "Division by zero is undefined.";

impl TryFromPyAny for Fraction {
    fn try_from_py_any(
        value: &pyo3::Bound<'_, pyo3::PyAny>,
        py: pyo3::Python<'_>,
    ) -> pyo3::PyResult<Self> {
        use pyo3::types::PyAnyMethods;
        if value.is_instance(&<pyo3::types::PyFloat as pyo3::type_object::PyTypeInfo>::type_object(py))? {
            Fraction::try_from(value.extract::<f64>()?).map_err(|error| {
                match error {
                    fraction::FromFloatConstructionError::Infinity => {
                        pyo3::exceptions::PyOverflowError::new_err(error.to_string())
                    }
                    _ => pyo3::exceptions::PyValueError::new_err(error.to_string()),
                }
            })
        } else {
            let numerator = try_py_integral_to_big_int(
                value.getattr(pyo3::intern!(py, "numerator")).map_err(
                    |_| {
                        pyo3::exceptions::PyTypeError::new_err(
                            INVALID_SCALAR_TYPE_ERROR_MESSAGE,
                        )
                    },
                )?,
            )?;
            let denominator = try_py_integral_to_big_int(
                value.getattr(pyo3::intern!(py, "denominator")).map_err(
                    |_| {
                        pyo3::exceptions::PyTypeError::new_err(
                            INVALID_SCALAR_TYPE_ERROR_MESSAGE,
                        )
                    },
                )?,
            )?;
            match Fraction::new(numerator, denominator) {
                Some(value) => Ok(value),
                None => Err(pyo3::exceptions::PyZeroDivisionError::new_err(
                    UNDEFINED_DIVISION_ERROR_MESSAGE,
                )),
            }
        }
    }
}

impl TryToPyAny for &Fraction {
    fn try_to_py_any(
        self,
        py: pyo3::Python<'_>,
    ) -> pyo3::PyResult<pyo3::Bound<'_, pyo3::PyAny>> {
        use pyo3::types::PyAnyMethods;
        static FRACTION_CLS: pyo3::sync::PyOnceLock<pyo3::Py<pyo3::PyAny>> =
            pyo3::sync::PyOnceLock::new();
        FRACTION_CLS
            .get_or_try_init(py, || {
                py.import("rithm.fraction")?
                    .getattr(pyo3::intern!(py, "Fraction"))
                    .map(|value| {
                        pyo3::IntoPyObject::into_pyobject(value, py)
                            .unwrap()
                            .into_any()
                            .unbind()
                    })
            })?
            .call(
                py,
                (
                    big_int_to_py_long(self.numerator(), py),
                    big_int_to_py_long(self.denominator(), py),
                ),
                None,
            )
            .map(|value| value.into_bound(py))
    }
}

impl From<PyContour> for Contour {
    fn from(value: PyContour) -> Self {
        value.0
    }
}

impl From<PyPoint> for Point {
    fn from(value: PyPoint) -> Self {
        value.0
    }
}

impl From<PyPolygon> for Polygon {
    fn from(value: PyPolygon) -> Self {
        value.0
    }
}

impl From<PySegment> for Segment {
    fn from(value: PySegment) -> Self {
        value.0
    }
}

impl<'py> pyo3::IntoPyObject<'py> for Box {
    type Target = <PyBox as pyo3::IntoPyObject<'py>>::Target;
    type Output = <PyBox as pyo3::IntoPyObject<'py>>::Output;
    type Error = <PyBox as pyo3::IntoPyObject<'py>>::Error;

    fn into_pyobject(
        self,
        py: pyo3::Python<'py>,
    ) -> Result<Self::Output, Self::Error> {
        pyo3::IntoPyObject::into_pyobject(PyBox(self), py)
    }
}

impl<'py> pyo3::IntoPyObject<'py> for Contour {
    type Target = <PyContour as pyo3::IntoPyObject<'py>>::Target;
    type Output = <PyContour as pyo3::IntoPyObject<'py>>::Output;
    type Error = <PyContour as pyo3::IntoPyObject<'py>>::Error;

    fn into_pyobject(
        self,
        py: pyo3::Python<'py>,
    ) -> Result<Self::Output, Self::Error> {
        pyo3::IntoPyObject::into_pyobject(PyContour(self), py)
    }
}

impl<'py> pyo3::IntoPyObject<'py> for Multipolygon {
    type Target = <PyMultipolygon as pyo3::IntoPyObject<'py>>::Target;
    type Output = <PyMultipolygon as pyo3::IntoPyObject<'py>>::Output;
    type Error = <PyMultipolygon as pyo3::IntoPyObject<'py>>::Error;

    fn into_pyobject(
        self,
        py: pyo3::Python<'py>,
    ) -> Result<Self::Output, Self::Error> {
        pyo3::IntoPyObject::into_pyobject(PyMultipolygon(self), py)
    }
}

impl<'py> pyo3::IntoPyObject<'py> for Point {
    type Target = <PyPoint as pyo3::IntoPyObject<'py>>::Target;
    type Output = <PyPoint as pyo3::IntoPyObject<'py>>::Output;
    type Error = <PyPoint as pyo3::IntoPyObject<'py>>::Error;

    fn into_pyobject(
        self,
        py: pyo3::Python<'py>,
    ) -> Result<Self::Output, Self::Error> {
        pyo3::IntoPyObject::into_pyobject(PyPoint(self), py)
    }
}

impl<'py> pyo3::IntoPyObject<'py> for Polygon {
    type Target = <PyPolygon as pyo3::IntoPyObject<'py>>::Target;
    type Output = <PyPolygon as pyo3::IntoPyObject<'py>>::Output;
    type Error = <PyPolygon as pyo3::IntoPyObject<'py>>::Error;

    fn into_pyobject(
        self,
        py: pyo3::Python<'py>,
    ) -> Result<Self::Output, Self::Error> {
        pyo3::IntoPyObject::into_pyobject(PyPolygon(self), py)
    }
}

impl<'py> pyo3::IntoPyObject<'py> for Segment {
    type Target = <PySegment as pyo3::IntoPyObject<'py>>::Target;
    type Output = <PySegment as pyo3::IntoPyObject<'py>>::Output;
    type Error = <PySegment as pyo3::IntoPyObject<'py>>::Error;

    fn into_pyobject(
        self,
        py: pyo3::Python<'py>,
    ) -> Result<Self::Output, Self::Error> {
        pyo3::IntoPyObject::into_pyobject(PySegment(self), py)
    }
}

type Box = crate::bounded::Box<Fraction>;
type ConstrainedDelaunayTriangulation =
    crate::triangulation::ConstrainedDelaunayTriangulation<Point>;
type Contour = crate::geometries::Contour<Fraction>;
type DelaunayTriangulation =
    crate::triangulation::DelaunayTriangulation<Point>;
type Empty = crate::geometries::Empty;
type Multipolygon = crate::geometries::Multipolygon<Fraction>;
type Multisegment = crate::geometries::Multisegment<Fraction>;
type Point = crate::geometries::Point<Fraction>;
type Polygon = crate::geometries::Polygon<Fraction>;
type Segment = crate::geometries::Segment<Fraction>;
type Trapezoidation = crate::seidel::Trapezoidation<Point>;

#[pyo3::pyclass(name = "Box", module = "rene.exact", skip_from_py_object)]
#[derive(Clone)]
pub struct PyBox(Box);

#[pyo3::pyclass(
    name = "ConstrainedDelaunayTriangulation",
    module = "rene.exact",
    skip_from_py_object
)]
#[derive(Clone)]
struct PyConstrainedDelaunayTriangulation(ConstrainedDelaunayTriangulation);

#[pyo3::pyclass(name = "Contour", module = "rene.exact", from_py_object)]
#[derive(Clone)]
pub struct PyContour(Contour);

#[pyo3::pyclass(
    name = "DelaunayTriangulation",
    module = "rene.exact",
    skip_from_py_object
)]
#[derive(Clone)]
struct PyDelaunayTriangulation(DelaunayTriangulation);

#[pyo3::pyclass(name = "Empty", module = "rene.exact", skip_from_py_object)]
#[derive(Clone, Default)]
struct PyEmpty(Empty);

#[pyo3::pyclass(
    name = "Multipolygon",
    module = "rene.exact",
    skip_from_py_object
)]
#[derive(Clone)]
pub struct PyMultipolygon(Multipolygon);

#[pyo3::pyclass(
    name = "Multisegment",
    module = "rene.exact",
    skip_from_py_object
)]
#[derive(Clone)]
struct PyMultisegment(Multisegment);

#[pyo3::pyclass(name = "Polygon", module = "rene.exact", from_py_object)]
#[derive(Clone)]
pub struct PyPolygon(Polygon);

#[pyo3::pyclass(name = "Point", module = "rene.exact", from_py_object)]
#[derive(Clone)]
pub struct PyPoint(Point);

#[pyo3::pyclass(name = "Segment", module = "rene.exact", from_py_object)]
#[derive(Clone)]
pub struct PySegment(Segment);

#[pyo3::pyclass(
    name = "Trapezoidation",
    module = "rene.exact",
    skip_from_py_object
)]
#[derive(Clone)]
struct PyTrapezoidation(Trapezoidation);

impl_box_wrapper!();
impl_constrained_delaunay_triangulation_wrapper!();
impl_contour_wrapper!();
impl_delaunay_triangulation_wrapper!();
impl_empty_wrapper!();
impl_multipolygon_wrapper!();
impl_multisegment_wrapper!();
impl_point_wrapper!();
impl_polygon_wrapper!();
impl_segment_wrapper!();
impl_trapezoidation_wrapper!();

type PyContourReference = reference::Reference<PyContour>;
type PyMultisegmentReference = reference::Reference<PyMultisegment>;
type PyMultipolygonReference = reference::Reference<PyMultipolygon>;
type PyPolygonReference = reference::Reference<PyPolygon>;

#[pyo3::pyclass(module = "rene.exact", name = "_ContourSegments", sequence)]
struct PyContourSegments {
    contour: PyContourReference,
    start: isize,
    stop: isize,
    step: isize,
}

#[pyo3::pyclass(module = "rene.exact", name = "_ContourVertices", sequence)]
struct PyContourVertices {
    contour: PyContourReference,
    start: isize,
    stop: isize,
    step: isize,
}

#[pyo3::pyclass(
    module = "rene.exact",
    name = "_MultisegmentSegments",
    sequence
)]
struct PyMultisegmentSegments {
    multisegment: PyMultisegmentReference,
    start: isize,
    stop: isize,
    step: isize,
}

#[pyo3::pyclass(
    module = "rene.exact",
    name = "_MultipolygonPolygons",
    sequence
)]
struct PyMultipolygonPolygons {
    multipolygon: PyMultipolygonReference,
    start: isize,
    stop: isize,
    step: isize,
}

#[pyo3::pyclass(module = "rene.exact", name = "_PolygonHoles", sequence)]
struct PyPolygonHoles {
    polygon: PyPolygonReference,
    start: isize,
    stop: isize,
    step: isize,
}

impl_py_sequence!(
    PyContourSegments,
    contour,
    segment,
    segments,
    PySegment,
    Segment
);

impl_py_sequence!(PyContourVertices, contour, point, vertices, PyPoint, Point);

impl_py_sequence!(
    PyMultisegmentSegments,
    multisegment,
    segment,
    segments,
    PySegment,
    Segment
);

impl_py_sequence!(
    PyMultipolygonPolygons,
    multipolygon,
    polygon,
    polygons,
    PyPolygon,
    Polygon
);

impl_py_sequence!(PyPolygonHoles, polygon, contour, holes, PyContour, Contour);

fn big_int_to_py_long<'py>(
    value: &BigInt,
    py: pyo3::Python<'py>,
) -> pyo3::Bound<'py, pyo3::PyAny> {
    let buffer = value.to_bytes(Endianness::Little);
    unsafe {
        pyo3::Bound::<'py, pyo3::PyAny>::from_owned_ptr(
            py,
            pyo3::ffi::_PyLong_FromByteArray(
                buffer.as_ptr(),
                buffer.len(),
                1,
                1,
            ),
        )
    }
}

fn try_py_integral_to_big_int<'py>(
    value: pyo3::Bound<'py, pyo3::PyAny>,
) -> pyo3::PyResult<BigInt> {
    let ptr = value.as_ptr();
    let py = value.py();
    unsafe {
        let ptr = pyo3::ffi::PyNumber_Long(ptr);
        if ptr.is_null() {
            return Err(pyo3::PyErr::fetch(py));
        }
        let bits_count = pyo3::ffi::_PyLong_NumBits(ptr);
        match bits_count.cmp(&0) {
            Ordering::Less => Err(pyo3::PyErr::fetch(py)),
            Ordering::Equal => Ok(BigInt::zero()),
            Ordering::Greater => {
                let bytes_count = bits_count / (u8::BITS as usize) + 1;
                let mut buffer = vec![0u8; bytes_count];
                if pyo3::ffi::_PyLong_AsByteArray(
                    pyo3::Bound::<'py, pyo3::PyAny>::from_owned_ptr(py, ptr)
                        .as_ptr()
                        .cast::<pyo3::ffi::PyLongObject>(),
                    buffer.as_mut_ptr(),
                    buffer.len(),
                    1,
                    1,
                ) < 0
                {
                    Err(pyo3::PyErr::fetch(py))
                } else {
                    Ok(BigInt::from_bytes(
                        buffer.as_mut_slice(),
                        Endianness::Little,
                    ))
                }
            }
        }
    }
}
