use std::{f32::consts::PI, fmt, ops::Deref};

use specs::{Component, DenseVecStorage, FlaggedStorage};

use crate::{
    nalgebra::{DVector, Isometry2, Point2, RealField, Unit, Vector2},
    ncollide::{
        pipeline::CollisionGroups,
        shape::{
            Ball, Capsule, Compound, Cuboid, HeightField, Plane, Polyline, Segment, ShapeHandle,
        },
    },
    nphysics::{
        material::{BasicMaterial, MaterialHandle},
        object::DefaultColliderHandle,
    },
};
use ncollide2d::shape::ConvexPolygon;

pub type MeshData<N> = (Vec<Point2<N>>, Vec<Point2<usize>>, Option<Vec<Point2<N>>>);

pub trait IntoMesh: objekt::Clone + Send + Sync {
    type N: RealField;

    fn points(&self) -> MeshData<Self::N>;
}

impl<'clone, N: RealField> IntoMesh for Box<(dyn IntoMesh<N = N> + 'clone)> {
    type N = N;

    fn points(&self) -> MeshData<Self::N> {
        self.deref().points()
    }
}

impl<'clone, N: RealField> Clone for Box<dyn IntoMesh<N = N> + 'clone> {
    fn clone(&self) -> Self {
        objekt::clone_box(&*self)
    }
}

/// `Shape` serves as an abstraction over nphysics `ShapeHandle`s and makes it
/// easier to configure and define said `ShapeHandle`s for the user without
/// having to know the underlying nphysics API.
#[derive(Clone)]
pub enum Shape<N: RealField> {
    Ball {
        radius: N,
    },
    Capsule {
        half_height: N,
        radius: N,
    },
    Compound {
        parts: Vec<(Isometry2<N>, Shape<N>)>,
    },
    Cuboid {
        half_extents: Vector2<N>,
    },
    HeightField {
        heights: DVector<N>,
        scale: Vector2<N>,
    },
    Plane {
        normal: Unit<Vector2<N>>,
    },
    Polyline {
        points: Vec<Point2<N>>,
        indices: Option<Vec<Point2<usize>>>,
    },
    Segment {
        a: Point2<N>,
        b: Point2<N>,
    },
    Triangle {
        a: Point2<N>,
        b: Point2<N>,
        c: Point2<N>,
    },
}

impl<N: RealField> Shape<N> {
    /// Converts a `Shape` and its values into its corresponding `ShapeHandle`
    /// type. The `ShapeHandle` is used to define a `Collider` in the
    /// `PhysicsWorld`.
    pub fn handle(&self) -> ShapeHandle<N> {
        match self {
            Shape::Ball { radius } => ShapeHandle::new(Ball::<N>::new(*radius)),
            Shape::Capsule {
                half_height,
                radius,
            } => ShapeHandle::new(Capsule::new(*half_height, *radius)),
            Shape::Compound { parts } => ShapeHandle::new(Compound::new(
                parts.iter().map(|part| (part.0, part.1.handle())).collect(),
            )),
            Shape::Cuboid { half_extents } => ShapeHandle::new(Cuboid::new(*half_extents)),
            Shape::HeightField { heights, scale } => {
                ShapeHandle::new(HeightField::new(heights.clone(), *scale))
            }
            Shape::Plane { normal } => ShapeHandle::new(Plane::new(*normal)),
            Shape::Polyline { points, indices } => {
                ShapeHandle::new(Polyline::new(points.clone(), indices.clone()))
            }
            Shape::Segment { a, b } => ShapeHandle::new(Segment::new(*a, *b)),
            Shape::Triangle { a, b, c } => {
                ShapeHandle::new(ConvexPolygon::try_from_points(&[*a, *b, *c]).unwrap())
            }
        }
    }
}

/// The `PhysicsCollider` `Component` represents a `Collider` in the physics
/// world. A physics `Collider` is automatically created when this `Component`
/// is added to an `Entity`. Value changes are automatically synchronised with
/// the physic worlds `Collider`.
#[derive(Clone)]
pub struct PhysicsCollider<N: RealField> {
    /// The handle to the collider in the physics world.
    pub(crate) handle: Option<DefaultColliderHandle>,
    /// The shape of this collider.
    pub shape: Shape<N>,
    /// The position/rotation offset of the collider from the entity it is
    /// attached to.
    pub offset_from_parent: Isometry2<N>,
    pub density: N,
    /// The physics material of which this collider is composed.
    /// Defines properties like bounciness and others.
    pub material: MaterialHandle<N>,
    /// Margin between the detection zone of what is "near" the collider and the
    /// actual collider.
    pub margin: N,
    /// Collision groups this collider is part of.
    /// Defines with which other colliders this collider can interact.
    pub collision_groups: CollisionGroups,
    /// Prediction amount of the linear momentum.
    pub linear_prediction: N,
    /// Prediction amount of the angular momentum.
    pub angular_prediction: N,
    /// Whether this collider is a sensor and only emits events without
    /// interacting (true) or if it is a regular collider (false).
    pub sensor: bool,
}

impl<N: RealField> Component for PhysicsCollider<N> {
    type Storage = FlaggedStorage<Self, DenseVecStorage<Self>>;
}

impl<N: RealField> fmt::Debug for PhysicsCollider<N> {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(
            f,
            "PhysicsCollider {{ \
             handle: {:?}, \
             offset_from_parent: {:?}, \
             density: {}, \
             margin: {}, \
             collision_group: {:?}, \
             linear_prediction: {}, \
             angular_prediction: {}, \
             sensor: {} \
             }}",
            self.handle,
            self.offset_from_parent,
            self.density,
            self.margin,
            self.collision_groups,
            self.linear_prediction,
            self.angular_prediction,
            self.sensor,
        )?;
        Ok(())
    }
}

impl<N: RealField> PhysicsCollider<N> {
    /// Returns the `ShapeHandle` for `shape`, taking the `margin` into
    /// consideration.
    pub(crate) fn shape_handle(&self) -> ShapeHandle<N> {
        self.shape.handle()
    }
}

/// The `PhysicsColliderBuilder` implements the builder pattern for
/// `PhysicsCollider`s and is the recommended way of instantiating and
/// customising new `PhysicsCollider` instances.
///
/// # Example
///
/// ```rust
/// use specs_physics::{
///     colliders::Shape,
///     nalgebra::{Isometry2, Vector2},
///     ncollide::pipeline::CollisionGroups,
///     nphysics::material::{BasicMaterial, MaterialHandle},
///     PhysicsColliderBuilder,
/// };
///
/// let physics_collider = PhysicsColliderBuilder::from(Shape::Cuboid{ half_extents: Vector2::new(10.0, 10.0) })
///     .offset_from_parent(Isometry2::identity())
///     .density(1.2)
///     .material(MaterialHandle::new(BasicMaterial::default()))
///     .margin(0.02)
///     .collision_groups(CollisionGroups::default())
///     .linear_prediction(0.001)
///     .angular_prediction(0.0)
///     .sensor(true)
///     .build();
/// ```
pub struct PhysicsColliderBuilder<N: RealField> {
    shape: Shape<N>,
    offset_from_parent: Isometry2<N>,
    density: N,
    material: MaterialHandle<N>,
    margin: N,
    collision_groups: CollisionGroups,
    linear_prediction: N,
    angular_prediction: N,
    sensor: bool,
}

impl<N: RealField> From<Shape<N>> for PhysicsColliderBuilder<N> {
    /// Creates a new `PhysicsColliderBuilder` from the given `Shape`. This
    //  also populates the `PhysicsCollider` with sane defaults.
    fn from(shape: Shape<N>) -> Self {
        Self {
            shape,
            offset_from_parent: Isometry2::identity(),
            density: N::from_f32(1.3).unwrap(),
            material: MaterialHandle::new(BasicMaterial::default()),
            margin: N::from_f32(0.2).unwrap(), // default was: 0.01
            collision_groups: CollisionGroups::default(),
            linear_prediction: N::from_f32(0.002).unwrap(),
            angular_prediction: N::from_f32(PI / 180.0 * 5.0).unwrap(),
            sensor: false,
        }
    }
}

impl<N: RealField> PhysicsColliderBuilder<N> {
    /// Sets the `offset_from_parent` value of the `PhysicsColliderBuilder`.
    pub fn offset_from_parent(mut self, offset_from_parent: Isometry2<N>) -> Self {
        self.offset_from_parent = offset_from_parent;
        self
    }

    /// Sets the `density` value of the `PhysicsColliderBuilder`.
    pub fn density(mut self, density: N) -> Self {
        self.density = density;
        self
    }

    /// Sets the `material` value of the `PhysicsColliderBuilder`.
    pub fn material(mut self, material: MaterialHandle<N>) -> Self {
        self.material = material;
        self
    }

    /// Sets the `margin` value of the `PhysicsColliderBuilder`.
    pub fn margin(mut self, margin: N) -> Self {
        self.margin = margin;
        self
    }

    /// Sets the `collision_groups` value of the `PhysicsColliderBuilder`.
    pub fn collision_groups(mut self, collision_groups: CollisionGroups) -> Self {
        self.collision_groups = collision_groups;
        self
    }

    /// Sets the `linear_prediction` value of the `PhysicsColliderBuilder`.
    pub fn linear_prediction(mut self, linear_prediction: N) -> Self {
        self.linear_prediction = linear_prediction;
        self
    }

    /// Sets the `angular_prediction` value of the `PhysicsColliderBuilder`.
    pub fn angular_prediction(mut self, angular_prediction: N) -> Self {
        self.angular_prediction = angular_prediction;
        self
    }

    /// Sets the `sensor` value of the `PhysicsColliderBuilder`.
    pub fn sensor(mut self, sensor: bool) -> Self {
        self.sensor = sensor;
        self
    }

    /// Builds the `PhysicsCollider` from the values set in the
    /// `PhysicsColliderBuilder` instance.
    pub fn build(self) -> PhysicsCollider<N> {
        PhysicsCollider {
            handle: None,
            shape: self.shape,
            offset_from_parent: self.offset_from_parent,
            density: self.density,
            material: self.material,
            margin: self.margin,
            collision_groups: self.collision_groups,
            linear_prediction: self.linear_prediction,
            angular_prediction: self.angular_prediction,
            sensor: self.sensor,
        }
    }
}
