use crate::{
    paint::{
        IntoResources,
        Resource,
        ResourceIri,
    },
    primitives::{
        ColorInterpolation,
        GradientTransform,
        GradientUnits,
        IntoOptionalLength,
        Length,
        SpreadMethod,
        Stop,
    },
    utils::{
        ElementWriter,
        IsDefault,
        angle_to_line,
    },
};
use smart_default::SmartDefault;
use std::fmt::{
    Display,
    Formatter,
};

type GradientUnit = Length<false, true>;

#[derive(Debug, Hash, Eq, PartialEq, Clone, SmartDefault)]
pub struct LinearGradient {
    x1: GradientUnit,
    y1: GradientUnit,
    #[default(GradientUnit::percent(100.0))]
    x2: GradientUnit,
    y2: GradientUnit,
    stops: Vec<Stop>,
    units: GradientUnits,
    spread_method: SpreadMethod,
    transform: GradientTransform,
    #[default(ColorInterpolation::SRgb)]
    color_interpolation: ColorInterpolation,
}

impl LinearGradient {
    pub fn new() -> Self {
        LinearGradient::default()
    }

    pub fn angle(angle: f32) -> Self {
        let (x1, y1, x2, y2) = angle_to_line(angle);
        LinearGradient {
            x1: GradientUnit::percent_normalized(x1),
            y1: GradientUnit::percent_normalized(y1),
            x2: GradientUnit::percent_normalized(x2),
            y2: GradientUnit::percent_normalized(y2),
            ..Default::default()
        }
    }

    pub fn top() -> Self {
        Self::new()
            .x1(Length::zero())
            .y1(Length::percent(100.0))
            .x2(Length::zero())
            .y2(Length::zero())
    }

    pub fn right() -> Self {
        Self::new()
    }

    pub fn bottom() -> Self {
        Self::new()
            .x1(Length::zero())
            .y1(Length::zero())
            .x2(Length::zero())
            .y2(Length::percent(100.0))
    }

    pub fn left() -> Self {
        Self::new()
            .x1(Length::percent(100.0))
            .y1(Length::zero())
            .x2(Length::zero())
            .y2(Length::zero())
    }

    pub fn top_left() -> Self {
        Self::new()
            .x1(Length::percent(100.0))
            .y1(Length::percent(100.0))
            .x2(Length::zero())
            .y2(Length::zero())
    }

    pub fn top_right() -> Self {
        Self::new()
            .x1(Length::zero())
            .y1(Length::percent(100.0))
            .x2(Length::percent(100.0))
            .y2(Length::zero())
    }

    pub fn bottom_left() -> Self {
        Self::new()
            .x1(Length::percent(100.0))
            .y1(Length::zero())
            .x2(Length::zero())
            .y2(Length::percent(100.0))
    }

    pub fn bottom_right() -> Self {
        Self::new()
            .x1(Length::zero())
            .y1(Length::zero())
            .x2(Length::percent(100.0))
            .y2(Length::percent(100.0))
    }

    //

    pub fn stop<T>(mut self, value: T) -> Self
    where
        T: Into<Stop>,
    {
        self.stops.push(value.into());
        self
    }

    pub fn stops<I, T>(mut self, stops: I) -> Self
    where
        I: IntoIterator<Item = T>,
        T: Into<Stop>,
    {
        self.stops.extend(stops.into_iter().map(Into::into));
        self
    }

    pub fn units<T>(mut self, value: T) -> Self
    where
        T: Into<Option<GradientUnits>>,
    {
        self.units = value.into().unwrap_or_default();
        self
    }

    pub fn x1<T>(mut self, value: T) -> Self
    where
        T: IntoOptionalLength<false, true>,
    {
        self.x1 = value.into_optional_length().unwrap_or_default();
        self
    }

    pub fn y1<T>(mut self, value: T) -> Self
    where
        T: IntoOptionalLength<false, true>,
    {
        self.y1 = value.into_optional_length().unwrap_or_default();
        self
    }

    pub fn x2<T>(mut self, value: T) -> Self
    where
        T: IntoOptionalLength<false, true>,
    {
        self.x2 = value
            .into_optional_length()
            .unwrap_or(GradientUnit::percent(100.0));
        self
    }

    pub fn y2<T>(mut self, value: T) -> Self
    where
        T: IntoOptionalLength<false, true>,
    {
        self.y2 = value.into_optional_length().unwrap_or_default();
        self
    }

    pub fn spread_method<T>(mut self, value: T) -> Self
    where
        T: Into<Option<SpreadMethod>>,
    {
        self.spread_method = value.into().unwrap_or_default();
        self
    }

    pub fn transform<T>(mut self, value: T) -> Self
    where
        T: Into<Option<GradientTransform>>,
    {
        self.transform = value.into().unwrap_or_default();
        self
    }

    pub fn color_interpolation<T>(mut self, value: T) -> Self
    where
        T: Into<Option<ColorInterpolation>>,
    {
        self.color_interpolation = value.into().unwrap_or(ColorInterpolation::SRgb);
        self
    }
}

impl ResourceIri for LinearGradient {}

impl IntoResources for LinearGradient {
    fn into_resources(self) -> Vec<Resource> {
        vec![self.into()]
    }
}

impl Display for LinearGradient {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        let gradient = ElementWriter::new(f, "linearGradient")?
            .attr("id", (self.iri(),))?
            .attr_if("x1", self.x1, !self.x1.is_zero())?
            .attr_if("y1", self.y1, !self.y1.is_zero())?
            .attr_if("x2", self.x2, self.x2 != GradientUnit::percent(100.0))?
            .attr_if("y2", self.y2, !self.y2.is_zero())?
            .attr_if("gradientUnits", (&self.units,), !self.units.is_default())?
            .attr_if(
                "spreadMethod",
                (&self.spread_method,),
                !self.spread_method.is_default(),
            )?
            .attr_if(
                "color-interpolation",
                (&self.color_interpolation,),
                self.color_interpolation != ColorInterpolation::SRgb,
            )?
            .write(|out| self.transform.write(out, "gradientTransform"))?;

        if self.stops.is_empty() {
            gradient.close()
        } else {
            gradient
                .content(|out| self.stops.iter().try_for_each(|stop| stop.fmt(out)))?
                .close()
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::test_utils::assert_xml;

    #[test]
    fn renders_default_gradient() {
        let lg = LinearGradient::new();
        assert_xml(
            lg.to_string(),
            format!(r#"<linearGradient id="{}" />"#, lg.iri()),
        );
    }

    #[test]
    fn renders_to_top() {
        let lg = LinearGradient::top();
        assert_xml(
            lg.to_string(),
            format!(r#"<linearGradient id="{}" y1="100%" x2="0" />"#, lg.iri()),
        );
    }

    #[test]
    fn renders_to_right() {
        let lg = LinearGradient::right();
        assert_xml(
            lg.to_string(),
            format!(r#"<linearGradient id="{}" />"#, lg.iri()),
        );
    }

    #[test]
    fn renders_to_bottom() {
        let lg = LinearGradient::bottom();
        assert_xml(
            lg.to_string(),
            format!(r#"<linearGradient id="{}" x2="0" y2="100%" />"#, lg.iri()),
        );
    }

    #[test]
    fn renders_to_left() {
        let lg = LinearGradient::left();
        assert_xml(
            lg.to_string(),
            format!(r#"<linearGradient id="{}" x1="100%" x2="0" />"#, lg.iri()),
        );
    }

    #[test]
    fn renders_to_top_left() {
        let lg = LinearGradient::top_left();
        assert_xml(
            lg.to_string(),
            format!(
                r#"<linearGradient id="{}" x1="100%" y1="100%" x2="0" />"#,
                lg.iri()
            ),
        );
    }

    #[test]
    fn renders_to_top_right() {
        let lg = LinearGradient::top_right();
        assert_xml(
            lg.to_string(),
            format!(r#"<linearGradient id="{}" y1="100%" />"#, lg.iri()),
        );
    }

    #[test]
    fn renders_to_bottom_left() {
        let lg = LinearGradient::bottom_left();
        assert_xml(
            lg.to_string(),
            format!(
                r#"<linearGradient id="{}" x1="100%" x2="0" y2="100%" />"#,
                lg.iri()
            ),
        );
    }

    #[test]
    fn renders_to_bottom_right() {
        let lg = LinearGradient::bottom_right();
        assert_xml(
            lg.to_string(),
            format!(r#"<linearGradient id="{}" y2="100%" />"#, lg.iri()),
        );
    }

    #[test]
    fn renders_with_angle() {
        let lg = LinearGradient::angle(90.0);
        assert_xml(
            lg.to_string(),
            format!(r#"<linearGradient id="{}" y1="50%" y2="50%" />"#, lg.iri()),
        );
    }

    #[test]
    fn self_closes_when_no_stops() {
        let lg = LinearGradient::new();
        assert_xml(
            lg.to_string(),
            format!(r#"<linearGradient id="{}" />"#, lg.iri()),
        );
    }

    #[test]
    fn renders_stops() {
        let lg = LinearGradient::new()
            .stop(Stop::new().offset(0.0).color("#000"))
            .stop(Stop::new().offset(1.0).color("#fff"));

        assert_xml(
            lg.to_string(),
            format!(
                r#"
<linearGradient id="{}">
    <stop stop-color="rgb(0,0,0)" offset="0" />
    <stop stop-color="rgb(255,255,255)" offset="1" />
</linearGradient>"#,
                lg.iri()
            ),
        );
    }

    #[test]
    fn renders_with_attrs() {
        let gradient_units = GradientUnits::UserSpaceOnUse;
        let spread_method = SpreadMethod::Reflect;
        let color_interpolation = ColorInterpolation::LinearRgb;
        let lg = LinearGradient::new()
            .units(gradient_units)
            .spread_method(spread_method)
            .transform(GradientTransform::new().translate_x(10.0))
            .color_interpolation(color_interpolation);

        assert_xml(
            lg.to_string(),
            format!(
                r#"
<linearGradient
    id="{}"
    gradientUnits="{gradient_units}"
    spreadMethod="{spread_method}"
    gradientTransform="matrix(1 0 0 1 10 0)"
    color-interpolation="{color_interpolation}"
/>
"#,
                lg.iri(),
            ),
        );
    }
}
