use super::writer::Statement;

/// Structs that implement [`Attributes`] are used for writing the attributes
/// of a diagraph, graph, subgraph, subgraph cluster, node or edge.
///
/// The raw [`Attributes::set`] function is provided for setting arbitary atrributes,
/// but it is recommended to use the more typesafe functions where available.
/// If a typesafe function is missing and you have to use the `set` function, please do file an issue in the github and we can add it.
pub trait Attributes: Sized {
    /// Set the name of the font family for label text
    fn set_font(&mut self, label: &str) -> &mut Self {
        self.set("fontname", label, true)
    }

    /// Set arbitary html, useful for constructing more complex nodes
    fn set_html(&mut self, label: &str) -> &mut Self {
        self.set("label", label, false)
    }

    /// Set the display label for a graph, node or edge
    fn set_label(&mut self, label: &str) -> &mut Self {
        self.set("label", label, true)
    }

    /// Set the label to appear at the head of an edge
    fn set_head_label(&mut self, label: &str) -> &mut Self {
        self.set("headlabel", label, true)
    }

    /// Set the label to appear at the tail of an edge
    fn set_tail_label(&mut self, label: &str) -> &mut Self {
        self.set("taillabel", label, true)
    }

    /// Set the edge or line color
    fn set_color(&mut self, color: Color) -> &mut Self {
        self.set("color", color.as_str(), false)
    }

    /// Set the color to fill the area with
    fn set_fill_color(&mut self, color: Color) -> &mut Self {
        self.set("fillcolor", color.as_str(), false)
    }

    /// Set the color of the font used for text
    fn set_font_color(&mut self, color: Color) -> &mut Self {
        self.set("fontcolor", color.as_str(), false)
    }

    /// Set the background color
    fn set_background_color(&mut self, color: Color) -> &mut Self {
        self.set("bgcolor", color.as_str(), false)
    }

    /// Set the shape of a graph, subgraph, cluster or node
    fn set_shape(&mut self, shape: Shape) -> &mut Self {
        self.set("shape", shape.as_str(), false)
    }

    /// Set the style
    fn set_style(&mut self, style: Style) -> &mut Self {
        self.set("style", style.as_str(), true)
    }

    /// Set type of arrow head for edge lines (the arrow at the destination)
    fn set_arrow_head(&mut self, arrow_type: ArrowType) -> &mut Self {
        self.set("arrowhead", arrow_type.as_str(), false)
    }

    /// Set type of arrow tail for edge lines (the arrow at the source)
    fn set_arrow_tail(&mut self, arrow_type: ArrowType) -> &mut Self {
        self.set("arrowtail", arrow_type.as_str(), false)
    }

    /// Set the relative rank, which affects layout
    fn set_rank(&mut self, rank: Rank) -> &mut Self {
        self.set("rank", rank.as_str(), false)
    }

    /// Set the pen width for drawing lines
    fn set_pen_width(&mut self, width: f32) -> &mut Self {
        self.set("penwidth", &width.to_string(), false)
    }

    /// Set the arrow size
    fn set_arrow_size(&mut self, size: f32) -> &mut Self {
        self.set("arrowsize", &size.to_string(), false)
    }

    /// Set the font size
    fn set_font_size(&mut self, size: f32) -> &mut Self {
        self.set("fontsize", &size.to_string(), false)
    }

    // Set direction of graph layout
    fn set_rank_direction(&mut self, rank_direction: RankDirection) -> &mut Self {
        self.set("rankdir", rank_direction.as_str(), false)
    }

    /// Sets an attribute. See the Graphviz [documentation](https://graphviz.org/doc/info/attrs.html)
    /// for a full list of available names and values.
    /// Set the arguement `quote` to true if the `value` should be written in quotes `"`, to escape
    /// any special characters. Note that any quote in the string need to be escaped before calling.
    /// This function does NOT check that `name` or `value` are valid DOT strings.
    fn set(&mut self, name: &str, value: &str, quote: bool) -> &mut Self;
}

/// An [`AttributesList`] sets the attributes of an edge or node.
/// See the [`Attributes`] trait for more information on what fields can be set.
pub struct AttributesList<'d, 'w> {
    statement: Statement<'d, 'w>,
    at_least_one_set: bool,
}

impl<'d, 'w> AttributesList<'d, 'w> {
    pub(crate) fn new(statement: Statement<'d, 'w>) -> Self {
        Self {
            statement,
            at_least_one_set: false,
        }
    }
}

impl<'d, 'w> Attributes for AttributesList<'d, 'w> {
    fn set(&mut self, name: &str, value: &str, quote: bool) -> &mut Self {
        if !self.at_least_one_set {
            self.at_least_one_set = true;
            self.statement.write(b" [");
        } else {
            self.statement.write(b", ");
        }
        self.statement.write(name.as_bytes());
        self.statement.write(b"=");
        if quote {
            self.statement.write_quoted(value.as_bytes());
        } else {
            self.statement.write(value.as_bytes());
        }
        self
    }
}

impl<'d, 'w> Drop for AttributesList<'d, 'w> {
    fn drop(&mut self) {
        if self.at_least_one_set {
            self.statement.write(b"]");
        }
    }
}

/// Shape of a component. This list is not comprehensive, the full list
/// is visible [here](https://graphviz.org/doc/info/shapes.html#polygon)
/// and can instead be set using [`Attributes::set`].
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub enum Shape {
    Circle,
    Record,
    Rectangle,
    None,
    Mdiamond,
    Mrecord,
    Msquare,
}

impl Shape {
    fn as_str(&self) -> &str {
        match self {
            Self::Circle => "circle",
            Self::Record => "record",
            Self::Mrecord => "Mrecord",
            Self::Mdiamond => "Mdiamond",
            Self::Rectangle => "rectangle",
            Self::Msquare => "Msquare",
            Self::None => "none",
        }
    }
}

/// Color of a line or fill. This list is far from comprehensive, the
/// full list is visible [here](https://graphviz.org/doc/info/colors.html),
/// and can instead be set using [`Attributes::set`]
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub enum Color {
    Black,
    Grey,
    LightGrey,
    PaleGreen,
    PaleTurquoise,
    Red,
    White,
    Blue,
    Gray20,
}

impl Color {
    pub fn as_str(&self) -> &str {
        match self {
            Self::Black => "black",
            Self::Grey => "gray",
            Self::LightGrey => "lightgray",
            Self::PaleGreen => "palegreen",
            Self::PaleTurquoise => "paleturquoise",
            Self::Red => "red",
            Self::White => "white",
            Self::Blue => "blue",
            Self::Gray20 => "gray20",
        }
    }
}

/// Style of design. Note that some of these or only valid for
/// certain combinations of node, edge and cluster. See documentation
/// [here](https://graphviz.org/docs/attr-types/style/) for more information.
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub enum Style {
    /// Nodes and edges
    Dashed,
    /// Nodes and edges
    Dotted,
    /// Nodes and edges
    Solid,
    /// Nodes and edges
    Invisible,
    /// Nodes and edges
    Bold,

    /// Edges only
    Tapered,

    /// Nodes only
    Wedged,
    /// Only for elliptically-shaped nodes
    Diagonals,

    /// Node and clusters
    Filled,
    /// Node and clusters
    Striped,
    /// Node and clusters
    Rounded,

    /// Any
    Radial,
    /// Any
    Unfilled,
}

impl Style {
    fn as_str(&self) -> &str {
        match self {
            Self::Dashed => "dashed",
            Self::Dotted => "dotted",
            Self::Solid => "solid",
            Self::Invisible => "invis",
            Self::Bold => "bold",

            Self::Tapered => "tapered",

            Self::Wedged => "wedged",
            Self::Diagonals => "diagonals",

            Self::Filled => "filled",
            Self::Striped => "striped",
            Self::Rounded => "rounded",

            Self::Radial => "radial",
            Self::Unfilled => "",
        }
    }
}

/// Node rank type, for more information see
/// [Graphviz documentation](https://graphviz.org/docs/attr-types/rankType/).
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub enum Rank {
    Min,
    Same,
    Max,
    Source,
    Sink,
}

impl Rank {
    fn as_str(&self) -> &str {
        match self {
            Self::Source => "source",
            Self::Min => "min",
            Self::Same => "same",
            Self::Max => "max",
            Self::Sink => "sink",
        }
    }
}

/// Arrow types, used to set either head or tail, for more information see
/// [Graphviz documentation](https://graphviz.org/docs/attr-types/arrowType/).
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub enum ArrowType {
    Normal,
    Dot,
    Inv,
    InvDot,
    ODit,
    InvODot,
    None,
    Tee,
    Empty,
    InvEmpty,
    Diamond,
    ODiamond,
    EDiamond,
    Crow,
    Box,
    OBox,
    Open,
    HalfOpen,
    Vee,
}

impl ArrowType {
    fn as_str(&self) -> &str {
        match self {
            Self::Normal => "normal",
            Self::Dot => "dot",
            Self::Inv => "inv",
            Self::InvDot => "invdot",
            Self::ODit => "odot",
            Self::InvODot => "invdot",
            Self::None => "none",
            Self::Tee => "tee",
            Self::Empty => "empty",
            Self::InvEmpty => "invempty",
            Self::Diamond => "diamond",
            Self::ODiamond => "odiamond",
            Self::EDiamond => "ediamond",
            Self::Crow => "crow",
            Self::Box => "box",
            Self::OBox => "obox",
            Self::Open => "open",
            Self::HalfOpen => "halfopen",
            Self::Vee => "vee",
        }
    }
}

/// Direction to layout graph, for more information see
/// [Graphviz documentation](https://graphviz.org/docs/attr-types/rankdir/).
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub enum RankDirection {
    TopBottom,
    BottomTop,
    LeftRight,
    RightLeft,
}

impl RankDirection {
    fn as_str(&self) -> &str {
        match self {
            Self::TopBottom => "TB",
            Self::BottomTop => "BT",
            Self::LeftRight => "LR",
            Self::RightLeft => "RL",
        }
    }
}
