use ecow::EcoString;
use typst_utils::Numeric;

use crate::diag::{HintedStrResult, SourceResult};
use crate::foundations::{
    Args, Cast, Dict, Fold, FromValue, NoneValue, Repr, Resolve, Smart, StyleChain,
    Value, cast, dict, func, scope, ty,
};
use crate::layout::{Abs, Length, Ratio};
use crate::visualize::{Color, Gradient, Paint, Tiling};

/// Defines how to draw a line.
///
/// A stroke has a _paint_ (a solid color or gradient), a _thickness,_ a line
/// _cap,_ a line _join,_ a _miter limit,_ and a _dash_ pattern. All of these
/// values are optional and have sensible defaults.
///
/// = Example <example>
/// ```example
/// #set line(length: 100%)
/// #stack(
///   spacing: 1em,
///   line(stroke: 2pt + red),
///   line(stroke: (paint: blue, thickness: 4pt, cap: "round")),
///   line(stroke: (paint: blue, thickness: 1pt, dash: "dashed")),
///   line(stroke: 2pt + gradient.linear(..color.map.rainbow)),
/// )
/// ```
///
/// = Simple strokes <simple-strokes>
/// You can create a simple solid stroke from a color, a thickness, or a
/// combination of the two. Specifically, wherever a stroke is expected you can
/// pass any of the following values:
///
/// - A length specifying the stroke's thickness. The color is inherited,
///   defaulting to black.
/// - A color to use for the stroke. The thickness is inherited, defaulting to
///   `{1pt}`.
/// - A stroke combined from color and thickness using the `+` operator as in
///   `{2pt + red}`.
///
/// For full control, you can also provide a @dictionary[dictionary] or a
/// `{stroke}` object to any function that expects a stroke. The dictionary's
/// keys may include any of the parameters for the constructor function, shown
/// below.
///
/// = Fields <fields>
/// On a stroke object, you can access any of the fields listed in the
/// constructor function. For example, `{(2pt + blue).thickness}` is `{2pt}`.
/// Meanwhile, `{stroke(red).cap}` is `{auto}` because it's unspecified. Fields
/// set to `{auto}` are inherited.
#[ty(scope, cast)]
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
pub struct Stroke<T: Numeric = Length> {
    /// The stroke's paint.
    pub paint: Smart<Paint>,
    /// The stroke's thickness.
    pub thickness: Smart<T>,
    /// The stroke's line cap.
    pub cap: Smart<LineCap>,
    /// The stroke's line join.
    pub join: Smart<LineJoin>,
    /// The stroke's line dash pattern.
    pub dash: Smart<Option<DashPattern<T>>>,
    /// The miter limit.
    pub miter_limit: Smart<Ratio>,
}

impl Stroke {
    /// Create a stroke from a paint and a thickness.
    pub fn from_pair(paint: impl Into<Paint>, thickness: Length) -> Self {
        Self {
            paint: Smart::Custom(paint.into()),
            thickness: Smart::Custom(thickness),
            ..Default::default()
        }
    }
}

#[scope]
impl Stroke {
    /// Converts a value to a stroke or constructs a stroke with the given
    /// parameters.
    ///
    /// Note that in most cases you do not need to convert values to strokes in
    /// order to use them, as they will be converted automatically. However,
    /// this constructor can be useful to ensure a value has all the fields of a
    /// stroke.
    ///
    /// ```example
    /// #let my-func(x) = {
    ///     x = stroke(x) // Convert to a stroke
    ///     [Stroke has thickness #x.thickness.]
    /// }
    /// #my-func(3pt) \
    /// #my-func(red) \
    /// #my-func(stroke(cap: "round", thickness: 1pt))
    /// ```
    #[func(constructor)]
    pub fn construct(
        args: &mut Args,

        /// The color or gradient to use for the stroke.
        ///
        /// If set to `{auto}`, the value is inherited, defaulting to `{black}`.
        #[external]
        paint: Smart<Paint>,

        /// The stroke's thickness.
        ///
        /// If set to `{auto}`, the value is inherited, defaulting to `{1pt}`.
        #[external]
        thickness: Smart<Length>,

        /// How the ends of the stroke are rendered.
        ///
        /// If set to `{auto}`, the value is inherited, defaulting to
        /// `{"butt"}`.
        #[external]
        cap: Smart<LineCap>,

        /// How sharp turns are rendered.
        ///
        /// If set to `{auto}`, the value is inherited, defaulting to
        /// `{"miter"}`.
        #[external]
        join: Smart<LineJoin>,

        /// The dash pattern to use. This can be:
        ///
        /// - One of the predefined patterns listed below.
        /// - `{none}`, as an equivalent to the predefined pattern `{"solid"}`.
        /// - An @array[array] with alternating lengths for dashes and gaps. You
        ///   can also use the string `{"dot"}` for a length equal to the line
        ///   thickness.
        /// - A @dictionary[dictionary] with the keys `array` (same as the array
        ///   above), and `phase` (of type @length[length]), which defines where
        ///   in the pattern to start drawing.
        ///
        /// If set to `{auto}`, the value is inherited, defaulting to `{none}`.
        ///
        /// #example(
        ///   title: "Usage",
        ///   ```
        ///   #set line(length: 100%, stroke: 2pt)
        ///   #stack(
        ///     spacing: 1em,
        ///     line(stroke: (dash: "dashed")),
        ///     line(stroke: (dash: (10pt, 5pt, "dot", 5pt))),
        ///     line(stroke: (dash: (array: (10pt, 5pt, "dot", 5pt), phase: 10pt))),
        ///   )
        ///   ```
        /// )
        ///
        /// #example(
        ///   title: "Predefined patterns",
        ///   ```
        ///   #set page(width: auto, height: auto)
        ///
        ///   #let patterns = (
        ///     ("dotted", "dashed", "dash-dotted").map(d => (
        ///       "loosely-" + d,
        ///       d,
        ///       "densely-" + d,
        ///     ))
        ///   ).flatten()
        ///
        ///
        ///   #let display(dash) = {
        ///     set par(spacing: 0.3em)
        ///
        ///     show "loosely": set text(eastern.darken(17%))
        ///     show "densely": set text(purple)
        ///
        ///     let derived = dash.starts-with("loosely-") or dash.starts-with("densely-")
        ///     if derived { dash } else { strong(dash) }
        ///
        ///     rect(
        ///       line(stroke: (dash: dash), length: 100%),
        ///       stroke: none,
        ///       fill: yellow.transparentize(80%),
        ///     )
        ///   }
        ///
        ///   #table(
        ///     columns: 3,
        ///     stroke: none,
        ///     align: (left, center, left),
        ///
        ///     table.cell(colspan: 3, align: center, display("solid")),
        ///     ..patterns.map(display),
        ///   )
        ///   ```
        /// )
        #[external]
        dash: Smart<Option<DashPattern>>,

        /// Number at which protruding sharp bends are rendered with a bevel
        /// instead or a miter join. The higher the number, the sharper an angle
        /// can be before it is bevelled. Only applicable if `join` is
        /// `{"miter"}`.
        ///
        /// Specifically, the miter limit is the maximum ratio between the
        /// corner's protrusion length and the stroke's thickness.
        ///
        /// If set to `{auto}`, the value is inherited, defaulting to `{4.0}`.
        ///
        /// ```example
        /// #let items = (
        ///   curve.move((15pt, 0pt)),
        ///   curve.line((0pt, 30pt)),
        ///   curve.line((30pt, 30pt)),
        ///   curve.line((10pt, 20pt)),
        /// )
        ///
        /// #set curve(stroke: 6pt + blue)
        /// #stack(
        ///   dir: ltr,
        ///   spacing: 1cm,
        ///   curve(stroke: (miter-limit: 1), ..items),
        ///   curve(stroke: (miter-limit: 4), ..items),
        ///   curve(stroke: (miter-limit: 5), ..items),
        /// )
        /// ```
        #[external]
        miter_limit: Smart<f64>,
    ) -> SourceResult<Stroke> {
        if let Some(stroke) = args.eat::<Stroke>()? {
            return Ok(stroke);
        }

        fn take<T: FromValue>(args: &mut Args, arg: &str) -> SourceResult<Smart<T>> {
            Ok(args.named::<Smart<T>>(arg)?.unwrap_or(Smart::Auto))
        }

        let paint = take::<Paint>(args, "paint")?;
        let thickness = take::<Length>(args, "thickness")?;
        let cap = take::<LineCap>(args, "cap")?;
        let join = take::<LineJoin>(args, "join")?;
        let dash = take::<Option<DashPattern>>(args, "dash")?;
        let miter_limit = take::<f64>(args, "miter-limit")?.map(Ratio::new);

        Ok(Self { paint, thickness, cap, join, dash, miter_limit })
    }
}

impl<T: Numeric> Stroke<T> {
    /// Map the contained lengths with `f`.
    pub fn map<F, U: Numeric>(self, f: F) -> Stroke<U>
    where
        F: Fn(T) -> U,
    {
        Stroke {
            paint: self.paint,
            thickness: self.thickness.map(&f),
            cap: self.cap,
            join: self.join,
            dash: self.dash.map(|dash| {
                dash.map(|dash| DashPattern {
                    array: dash
                        .array
                        .into_iter()
                        .map(|l| match l {
                            DashLength::Length(v) => DashLength::Length(f(v)),
                            DashLength::LineWidth => DashLength::LineWidth,
                        })
                        .collect(),
                    phase: f(dash.phase),
                })
            }),
            miter_limit: self.miter_limit,
        }
    }
}

impl Stroke<Abs> {
    /// Unpack the stroke, filling missing fields from the `default`.
    pub fn unwrap_or(self, default: FixedStroke) -> FixedStroke {
        let thickness = self.thickness.unwrap_or(default.thickness);
        let dash = self
            .dash
            .map(|dash| {
                dash.map(|dash| DashPattern {
                    array: dash.array.into_iter().map(|l| l.finish(thickness)).collect(),
                    phase: dash.phase,
                })
            })
            .unwrap_or(default.dash);

        FixedStroke {
            paint: self.paint.unwrap_or(default.paint),
            thickness,
            cap: self.cap.unwrap_or(default.cap),
            join: self.join.unwrap_or(default.join),
            dash,
            miter_limit: self.miter_limit.unwrap_or(default.miter_limit),
        }
    }

    /// Unpack the stroke, filling missing fields with the default values.
    pub fn unwrap_or_default(self) -> FixedStroke {
        // we want to do this; the Clippy lint is not type-aware
        #[allow(clippy::unwrap_or_default)]
        self.unwrap_or(FixedStroke::default())
    }
}

impl<T: Numeric + Repr> Repr for Stroke<T> {
    fn repr(&self) -> EcoString {
        let mut r = EcoString::new();
        let Self { paint, thickness, cap, join, dash, miter_limit } = &self;
        if cap.is_auto() && join.is_auto() && dash.is_auto() && miter_limit.is_auto() {
            match (&self.paint, &self.thickness) {
                (Smart::Custom(paint), Smart::Custom(thickness)) => {
                    r.push_str(&thickness.repr());
                    r.push_str(" + ");
                    r.push_str(&paint.repr());
                }
                (Smart::Custom(paint), Smart::Auto) => r.push_str(&paint.repr()),
                (Smart::Auto, Smart::Custom(thickness)) => r.push_str(&thickness.repr()),
                (Smart::Auto, Smart::Auto) => r.push_str("1pt + black"),
            }
        } else {
            r.push('(');
            let mut sep = "";
            if let Smart::Custom(paint) = &paint {
                r.push_str(sep);
                r.push_str("paint: ");
                r.push_str(&paint.repr());
                sep = ", ";
            }
            if let Smart::Custom(thickness) = &thickness {
                r.push_str(sep);
                r.push_str("thickness: ");
                r.push_str(&thickness.repr());
                sep = ", ";
            }
            if let Smart::Custom(cap) = &cap {
                r.push_str(sep);
                r.push_str("cap: ");
                r.push_str(&cap.repr());
                sep = ", ";
            }
            if let Smart::Custom(join) = &join {
                r.push_str(sep);
                r.push_str("join: ");
                r.push_str(&join.repr());
                sep = ", ";
            }
            if let Smart::Custom(dash) = &dash {
                r.push_str(sep);
                r.push_str("dash: ");
                if let Some(dash) = dash {
                    r.push_str(&dash.repr());
                } else {
                    r.push_str(&NoneValue.repr());
                }
                sep = ", ";
            }
            if let Smart::Custom(miter_limit) = &miter_limit {
                r.push_str(sep);
                r.push_str("miter-limit: ");
                r.push_str(&miter_limit.get().repr());
            }
            r.push(')');
        }
        r
    }
}

impl<T: Numeric + Fold> Fold for Stroke<T> {
    fn fold(self, outer: Self) -> Self {
        Self {
            paint: self.paint.or(outer.paint),
            thickness: self.thickness.or(outer.thickness),
            cap: self.cap.or(outer.cap),
            join: self.join.or(outer.join),
            dash: self.dash.or(outer.dash),
            miter_limit: self.miter_limit.or(outer.miter_limit),
        }
    }
}

impl Resolve for Stroke {
    type Output = Stroke<Abs>;

    fn resolve(self, styles: StyleChain) -> Self::Output {
        Stroke {
            paint: self.paint,
            thickness: self.thickness.resolve(styles),
            cap: self.cap,
            join: self.join,
            dash: self.dash.resolve(styles),
            miter_limit: self.miter_limit,
        }
    }
}

cast! {
    type Stroke,
    thickness: Length => Self {
        thickness: Smart::Custom(thickness),
        ..Default::default()
    },
    color: Color => Self {
        paint: Smart::Custom(color.into()),
        ..Default::default()
    },
    gradient: Gradient => Self {
        paint: Smart::Custom(gradient.into()),
        ..Default::default()
    },
    tiling: Tiling => Self {
        paint: Smart::Custom(tiling.into()),
        ..Default::default()
    },
    mut dict: Dict => {
        // Get a value by key, accepting either Auto or something convertible to type T.
        fn take<T: FromValue>(dict: &mut Dict, key: &str) -> HintedStrResult<Smart<T>> {
            Ok(dict.take(key).ok().map(Smart::<T>::from_value)
                .transpose()?.unwrap_or(Smart::Auto))
        }

        let paint = take::<Paint>(&mut dict, "paint")?;
        let thickness = take::<Length>(&mut dict, "thickness")?;
        let cap = take::<LineCap>(&mut dict, "cap")?;
        let join = take::<LineJoin>(&mut dict, "join")?;
        let dash = take::<Option<DashPattern>>(&mut dict, "dash")?;
        let miter_limit = take::<f64>(&mut dict, "miter-limit")?;
        dict.finish(&["paint", "thickness", "cap", "join", "dash", "miter-limit"])?;

        Self {
            paint,
            thickness,
            cap,
            join,
            dash,
            miter_limit: miter_limit.map(Ratio::new),
        }
    },
}

cast! {
    Stroke<Abs>,
    self => self.map(Length::from).into_value(),
}

/// The line cap of a stroke
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)]
pub enum LineCap {
    /// Square stroke cap with the edge at the stroke's end point.
    Butt,
    /// Circular stroke cap centered at the stroke's end point.
    Round,
    /// Square stroke cap centered at the stroke's end point.
    Square,
}

impl Repr for LineCap {
    fn repr(&self) -> EcoString {
        match self {
            Self::Butt => "butt".repr(),
            Self::Round => "round".repr(),
            Self::Square => "square".repr(),
        }
    }
}

/// The line join of a stroke
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Cast)]
pub enum LineJoin {
    /// Segments are joined with sharp edges. Sharp bends exceeding the miter
    /// limit are bevelled instead.
    Miter,
    /// Segments are joined with circular corners.
    Round,
    /// Segments are joined with a bevel (a straight edge connecting the butts
    /// of the joined segments).
    Bevel,
}

impl Repr for LineJoin {
    fn repr(&self) -> EcoString {
        match self {
            Self::Miter => "miter".repr(),
            Self::Round => "round".repr(),
            Self::Bevel => "bevel".repr(),
        }
    }
}

/// A line dash pattern.
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub struct DashPattern<T: Numeric = Length, DT = DashLength<T>> {
    /// The dash array.
    pub array: Vec<DT>,
    /// The dash phase.
    pub phase: T,
}

impl<T: Numeric + Repr, DT: Repr> Repr for DashPattern<T, DT> {
    fn repr(&self) -> EcoString {
        let mut r = EcoString::from("(array: (");
        for (i, elem) in self.array.iter().enumerate() {
            if i != 0 {
                r.push_str(", ")
            }
            r.push_str(&elem.repr())
        }
        r.push_str("), phase: ");
        r.push_str(&self.phase.repr());
        r.push(')');
        r
    }
}

impl<T: Numeric + Default> From<Vec<DashLength<T>>> for DashPattern<T> {
    fn from(array: Vec<DashLength<T>>) -> Self {
        Self { array, phase: T::default() }
    }
}

impl Resolve for DashPattern {
    type Output = DashPattern<Abs>;

    fn resolve(self, styles: StyleChain) -> Self::Output {
        DashPattern {
            array: self.array.into_iter().map(|l| l.resolve(styles)).collect(),
            phase: self.phase.resolve(styles),
        }
    }
}

// Same names as tikz:
// https://tex.stackexchange.com/questions/45275/tikz-get-values-for-predefined-dash-patterns
cast! {
    DashPattern,
    self => dict! { "array" => self.array, "phase" => self.phase }.into_value(),

    /// `{()}`
    "solid" => Vec::new().into(),
    /// `{("dot", 2pt)}`
    "dotted" => vec![DashLength::LineWidth, Abs::pt(2.0).into()].into(),
    /// `{("dot", 1pt)}`
    "densely-dotted" => vec![DashLength::LineWidth, Abs::pt(1.0).into()].into(),
    /// `{("dot", 4pt)}`
    "loosely-dotted" => vec![DashLength::LineWidth, Abs::pt(4.0).into()].into(),
    /// `{(3pt, 3pt)}`
    "dashed" => vec![Abs::pt(3.0).into(), Abs::pt(3.0).into()].into(),
    /// `{(3pt, 2pt)}`
    "densely-dashed" => vec![Abs::pt(3.0).into(), Abs::pt(2.0).into()].into(),
    /// `{(3pt, 6pt)}`
    "loosely-dashed" => vec![Abs::pt(3.0).into(), Abs::pt(6.0).into()].into(),
    /// `{(3pt, 2pt, "dot", 2pt)}`
    "dash-dotted" => vec![Abs::pt(3.0).into(), Abs::pt(2.0).into(), DashLength::LineWidth, Abs::pt(2.0).into()].into(),
    /// `{(3pt, 1pt, "dot", 1pt)}`
    "densely-dash-dotted" => vec![Abs::pt(3.0).into(), Abs::pt(1.0).into(), DashLength::LineWidth, Abs::pt(1.0).into()].into(),
    /// `{(3pt, 4pt, "dot", 4pt)}`
    "loosely-dash-dotted" => vec![Abs::pt(3.0).into(), Abs::pt(4.0).into(), DashLength::LineWidth, Abs::pt(4.0).into()].into(),

    array: Vec<DashLength> => Self { array, phase: Length::zero() },
    mut dict: Dict => {
        let array: Vec<DashLength> = dict.take("array")?.cast()?;
        let phase = dict.take("phase").ok().map(Value::cast)
            .transpose()?.unwrap_or(Length::zero());
        dict.finish(&["array", "phase"])?;
        Self {
            array,
            phase,
        }
    },
}

/// The length of a dash in a line dash pattern.
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub enum DashLength<T: Numeric = Length> {
    LineWidth,
    Length(T),
}

impl<T: Numeric> DashLength<T> {
    fn finish(self, line_width: T) -> T {
        match self {
            Self::LineWidth => line_width,
            Self::Length(l) => l,
        }
    }
}

impl<T: Numeric + Repr> Repr for DashLength<T> {
    fn repr(&self) -> EcoString {
        match self {
            Self::LineWidth => "dot".repr(),
            Self::Length(v) => v.repr(),
        }
    }
}

impl Resolve for DashLength {
    type Output = DashLength<Abs>;

    fn resolve(self, styles: StyleChain) -> Self::Output {
        match self {
            Self::LineWidth => DashLength::LineWidth,
            Self::Length(v) => DashLength::Length(v.resolve(styles)),
        }
    }
}

impl From<Abs> for DashLength {
    fn from(l: Abs) -> Self {
        DashLength::Length(l.into())
    }
}

cast! {
    DashLength,
    self => match self {
        Self::LineWidth => "dot".into_value(),
        Self::Length(v) => v.into_value(),
    },
    "dot" => Self::LineWidth,
    v: Length => Self::Length(v),
}

/// A fully specified stroke of a geometric shape.
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub struct FixedStroke {
    /// The stroke's paint.
    pub paint: Paint,
    /// The stroke's thickness.
    pub thickness: Abs,
    /// The stroke's line cap.
    pub cap: LineCap,
    /// The stroke's line join.
    pub join: LineJoin,
    /// The stroke's line dash pattern.
    pub dash: Option<DashPattern<Abs, Abs>>,
    /// The miter limit. Defaults to 4.0, same as `tiny-skia`.
    pub miter_limit: Ratio,
}

impl FixedStroke {
    /// Create a stroke from a paint and a thickness.
    pub fn from_pair(paint: impl Into<Paint>, thickness: Abs) -> Self {
        Self {
            paint: paint.into(),
            thickness,
            ..Default::default()
        }
    }
}

impl Default for FixedStroke {
    fn default() -> Self {
        Self {
            paint: Paint::Solid(Color::BLACK),
            thickness: Abs::pt(1.0),
            cap: LineCap::Butt,
            join: LineJoin::Miter,
            dash: None,
            miter_limit: Ratio::new(4.0),
        }
    }
}
