mod parser;
mod segment;
mod writer;

pub use self::parser::*;
pub use self::segment::*;

/// Representation of the SVG [path data].
///
/// [path data]: https://www.w3.org/TR/SVG11/paths.html#PathData
#[derive(Clone, PartialEq, Default)]
pub struct Path(pub Vec<PathSegment>);

impl Path {
    /// Constructs a new path.
    pub fn new() -> Self {
        Path(Vec::new())
    }

    /// Constructs a new path with a specified capacity.
    pub fn with_capacity(capacity: usize) -> Self {
        Path(Vec::with_capacity(capacity))
    }

    /// Converts path's segments into absolute one in-place.
    ///
    /// Original segments can be mixed (relative, absolute).
    pub fn conv_to_absolute(&mut self) {
        // position of the previous segment
        let mut prev_x = 0.0;
        let mut prev_y = 0.0;

        // Position of the previous MoveTo segment.
        // When we get 'm'(relative) segment, which is not first segment - then it's
        // relative to previous 'M'(absolute) segment, not to first segment.
        let mut prev_mx = 0.0;
        let mut prev_my = 0.0;

        let mut prev_cmd = PathCommand::MoveTo;
        for seg in self.iter_mut() {
            if seg.cmd() == PathCommand::ClosePath {
                prev_x = prev_mx;
                prev_y = prev_my;

                seg.set_absolute(true);
                continue;
            }

            let offset_x;
            let offset_y;
            if seg.is_relative() {
                if seg.cmd() == PathCommand::MoveTo && prev_cmd == PathCommand::ClosePath {
                    offset_x = prev_mx;
                    offset_y = prev_my;
                } else {
                    offset_x = prev_x;
                    offset_y = prev_y;
                }
            } else {
                offset_x = 0.0;
                offset_y = 0.0;
            }

            if seg.is_relative() {
                shift_segment_data(seg, offset_x, offset_y);
            }

            if seg.cmd() == PathCommand::MoveTo {
                prev_mx = seg.x().unwrap();
                prev_my = seg.y().unwrap();
            }

            seg.set_absolute(true);

            if seg.cmd() == PathCommand::HorizontalLineTo {
                prev_x = seg.x().unwrap();
            } else if seg.cmd() == PathCommand::VerticalLineTo {
                prev_y = seg.y().unwrap();
            } else {
                prev_x = seg.x().unwrap();
                prev_y = seg.y().unwrap();
            }

            prev_cmd = seg.cmd();
        }
    }

    /// Converts path's segments into relative one in-place.
    ///
    /// Original segments can be mixed (relative, absolute).
    pub fn conv_to_relative(&mut self) {
        // NOTE: this method may look like 'conv_to_absolute', but it's a bit different.

        // position of the previous segment
        let mut prev_x = 0.0;
        let mut prev_y = 0.0;

        // Position of the previous MoveTo segment.
        // When we get 'm'(relative) segment, which is not first segment - then it's
        // relative to previous 'M'(absolute) segment, not to first segment.
        let mut prev_mx = 0.0;
        let mut prev_my = 0.0;

        let mut prev_cmd = PathCommand::MoveTo;
        for seg in self.iter_mut() {
            if seg.cmd() == PathCommand::ClosePath {
                prev_x = prev_mx;
                prev_y = prev_my;

                seg.set_absolute(false);
                continue;
            }

            let offset_x;
            let offset_y;
            if seg.is_absolute() {
                if seg.cmd() == PathCommand::MoveTo && prev_cmd == PathCommand::ClosePath {
                    offset_x = prev_mx;
                    offset_y = prev_my;
                } else {
                    offset_x = prev_x;
                    offset_y = prev_y;
                }
            } else {
                offset_x = 0.0;
                offset_y = 0.0;
            }

            // unlike in 'to_absolute', we should take prev values before changing segment data
            if seg.is_absolute() {
                if seg.cmd() == PathCommand::HorizontalLineTo {
                    prev_x = seg.x().unwrap();
                } else if seg.cmd() == PathCommand::VerticalLineTo {
                    prev_y = seg.y().unwrap();
                } else {
                    prev_x = seg.x().unwrap();
                    prev_y = seg.y().unwrap();
                }
            } else {
                if seg.cmd() == PathCommand::HorizontalLineTo {
                    prev_x += seg.x().unwrap();
                } else if seg.cmd() == PathCommand::VerticalLineTo {
                    prev_y += seg.y().unwrap();
                } else {
                    prev_x += seg.x().unwrap();
                    prev_y += seg.y().unwrap();
                }
            }

            if seg.cmd() == PathCommand::MoveTo {
                if seg.is_absolute() {
                    prev_mx = seg.x().unwrap();
                    prev_my = seg.y().unwrap();
                } else {
                    prev_mx += seg.x().unwrap();
                    prev_my += seg.y().unwrap();
                }
            }

            if seg.is_absolute() {
                shift_segment_data(seg, -offset_x, -offset_y);
            }

            seg.set_absolute(false);

            prev_cmd = seg.cmd();
        }
    }

    /// Appends an absolute MoveTo segment.
    pub fn push_move_to(&mut self, x: f64, y: f64) {
        self.push(PathSegment::MoveTo { abs: true, x, y });
    }

    /// Appends a relative MoveTo segment.
    pub fn push_rel_move_to(&mut self, x: f64, y: f64) {
        self.push(PathSegment::MoveTo { abs: false, x, y });
    }

    /// Appends an absolute ClosePath segment.
    pub fn push_close_path(&mut self) {
        self.push(PathSegment::ClosePath { abs: true });
    }

    /// Appends a relative ClosePath segment.
    pub fn push_rel_close_path(&mut self) {
        self.push(PathSegment::ClosePath { abs: false });
    }

    /// Appends an absolute LineTo segment.
    pub fn push_line_to(&mut self, x: f64, y: f64) {
        self.push(PathSegment::LineTo { abs: true, x, y });
    }

    /// Appends a relative LineTo segment.
    pub fn push_rel_line_to(&mut self, x: f64, y: f64) {
        self.push(PathSegment::LineTo { abs: false, x, y });
    }

    /// Appends an absolute HorizontalLineTo segment.
    pub fn push_hline_to(&mut self, x: f64) {
        self.push(PathSegment::HorizontalLineTo { abs: true, x });
    }

    /// Appends a relative HorizontalLineTo segment.
    pub fn push_rel_hline_to(&mut self, x: f64) {
        self.push(PathSegment::HorizontalLineTo { abs: false, x });
    }

    /// Appends an absolute VerticalLineTo segment.
    pub fn push_vline_to(&mut self, y: f64) {
        self.push(PathSegment::VerticalLineTo { abs: true, y });
    }

    /// Appends a relative VerticalLineTo segment.
    pub fn push_rel_vline_to(&mut self, y: f64) {
        self.push(PathSegment::VerticalLineTo { abs: false, y });
    }

    /// Appends an absolute CurveTo segment.
    pub fn push_curve_to(&mut self, x1: f64, y1: f64, x2: f64, y2: f64, x: f64, y: f64) {
        self.push(PathSegment::CurveTo { abs: true, x1, y1, x2, y2, x, y });
    }

    /// Appends a relative CurveTo segment.
    pub fn push_rel_curve_to(&mut self, x1: f64, y1: f64, x2: f64, y2: f64, x: f64, y: f64) {
        self.push(PathSegment::CurveTo { abs: false, x1, y1, x2, y2, x, y });
    }

    /// Appends an absolute SmoothCurveTo segment.
    pub fn push_smooth_curve_to(&mut self, x2: f64, y2: f64, x: f64, y: f64) {
        self.push(PathSegment::SmoothCurveTo { abs: true, x2, y2, x, y });
    }

    /// Appends a relative SmoothCurveTo segment.
    pub fn push_rel_smooth_curve_to(&mut self, x2: f64, y2: f64, x: f64, y: f64) {
        self.push(PathSegment::SmoothCurveTo { abs: false, x2, y2, x, y });
    }

    /// Appends an absolute QuadTo segment.
    pub fn push_quad_to(&mut self, x1: f64, y1: f64, x: f64, y: f64) {
        self.push(PathSegment::Quadratic { abs: true, x1, y1, x, y });
    }

    /// Appends a relative QuadTo segment.
    pub fn push_rel_quad_to(&mut self, x1: f64, y1: f64, x: f64, y: f64) {
        self.push(PathSegment::Quadratic { abs: false, x1, y1, x, y });
    }

    /// Appends an absolute SmoothQuadTo segment.
    pub fn push_smooth_quad_to(&mut self, x: f64, y: f64) {
        self.push(PathSegment::SmoothQuadratic { abs: true, x, y });
    }

    /// Appends a relative SmoothQuadTo segment.
    pub fn push_rel_smooth_quad_to(&mut self, x: f64, y: f64) {
        self.push(PathSegment::SmoothQuadratic { abs: false, x, y });
    }

    /// Appends an absolute ArcTo segment.
    pub fn push_arc_to(
        &mut self,
        rx: f64, ry: f64, x_axis_rotation: f64, large_arc: bool, sweep: bool, x: f64, y: f64,
    ) {
        self.push(PathSegment::EllipticalArc {
            abs: true, rx, ry, x_axis_rotation, large_arc, sweep, x, y
        });
    }

    /// Appends a relative ArcTo segment.
    pub fn push_rel_arc_to(
        &mut self,
        rx: f64, ry: f64, x_axis_rotation: f64, large_arc: bool, sweep: bool, x: f64, y: f64,
    ) {
        self.push(PathSegment::EllipticalArc {
            abs: false, rx, ry, x_axis_rotation, large_arc, sweep, x, y
        });
    }
}

fn shift_segment_data(d: &mut PathSegment, offset_x: f64, offset_y: f64) {
    match *d {
        PathSegment::MoveTo { ref mut x, ref mut y, .. } => {
            *x += offset_x;
            *y += offset_y;
        }
        PathSegment::LineTo { ref mut x, ref mut y, .. } => {
            *x += offset_x;
            *y += offset_y;
        }
        PathSegment::HorizontalLineTo { ref mut x, .. } => {
            *x += offset_x;
        }
        PathSegment::VerticalLineTo { ref mut y, .. } => {
            *y += offset_y;
        }
        PathSegment::CurveTo { ref mut x1, ref mut y1, ref mut x2, ref mut y2,
            ref mut x, ref mut y, .. } => {
            *x1 += offset_x;
            *y1 += offset_y;
            *x2 += offset_x;
            *y2 += offset_y;
            *x  += offset_x;
            *y  += offset_y;
        }
        PathSegment::SmoothCurveTo { ref mut x2, ref mut y2, ref mut x, ref mut y, .. } => {
            *x2 += offset_x;
            *y2 += offset_y;
            *x  += offset_x;
            *y  += offset_y;
        }
        PathSegment::Quadratic { ref mut x1, ref mut y1, ref mut x, ref mut y, .. } => {
            *x1 += offset_x;
            *y1 += offset_y;
            *x  += offset_x;
            *y  += offset_y;
        }
        PathSegment::SmoothQuadratic { ref mut x, ref mut y, .. } => {
            *x += offset_x;
            *y += offset_y;
        }
        PathSegment::EllipticalArc { ref mut x, ref mut y, .. } => {
            *x += offset_x;
            *y += offset_y;
        }
        PathSegment::ClosePath { .. } => {}
    }
}

impl_from_vec!(Path, Path, PathSegment);
impl_vec_defer!(Path, PathSegment);

#[cfg(test)]
mod to_absolute {
    use std::str::FromStr;
    use super::*;

    macro_rules! test {
        ($name:ident, $in_text:expr, $out_text:expr) => (
            #[test]
            fn $name() {
                let mut path = Path::from_str($in_text).unwrap();
                path.conv_to_absolute();
                assert_eq!(path.to_string(), $out_text);
            }
        )
    }

    test!(line_to,
          "m 10 20 l 20 20",
          "M 10 20 L 30 40");

    test!(close_path,
          "m 10 20 l 20 20 z",
          "M 10 20 L 30 40 Z");

    // test to check that parses implicit MoveTo as LineTo
    test!(implicit_line_to,
          "m 10 20 20 20",
          "M 10 20 L 30 40");

    test!(hline_vline,
          "m 10 20 v 10 h 10 l 10 10",
          "M 10 20 V 30 H 20 L 30 40");

    test!(curve,
          "m 10 20 c 10 10 10 10 10 10",
          "M 10 20 C 20 30 20 30 20 30");

    test!(move_to_1,
          "m 10 20 l 10 10 m 10 10 l 10 10",
          "M 10 20 L 20 30 M 30 40 L 40 50");

    test!(move_to_2,
          "m 10 20 l 10 10 z m 10 10 l 10 10",
          "M 10 20 L 20 30 Z M 20 30 L 30 40");

    test!(move_to_3,
          "m 10 20 l 10 10 Z m 10 10 l 10 10",
          "M 10 20 L 20 30 Z M 20 30 L 30 40");

    // MoveTo after ClosePath can be skipped
    test!(move_to_4,
          "m 10 20 l 10 10 Z l 10 10",
          "M 10 20 L 20 30 Z L 20 30");

    test!(smooth_curve,
          "m 10 20 s 10 10 10 10",
          "M 10 20 S 20 30 20 30");

    test!(quad,
          "m 10 20 q 10 10 10 10",
          "M 10 20 Q 20 30 20 30");

    test!(arc_mixed,
          "M 30 150 a 40 40 0 0 1 65 50 Z m 30 30 A 20 20 0 0 0 125 230 Z \
           m 40 24 a 20 20 0 0 1 65 50 z",
          "M 30 150 A 40 40 0 0 1 95 200 Z M 60 180 A 20 20 0 0 0 125 230 Z \
           M 100 204 A 20 20 0 0 1 165 254 Z");
}

#[cfg(test)]
mod to_relative {
    use std::str::FromStr;
    use super::*;

    macro_rules! test {
        ($name:ident, $in_text:expr, $out_text:expr) => (
            #[test]
            fn $name() {
                let mut path = Path::from_str($in_text).unwrap();
                path.conv_to_relative();
                assert_eq!(path.to_string(), $out_text);
            }
        )
    }

    test!(line_to,
          "M 10 20 L 30 40",
          "m 10 20 l 20 20");

    test!(close_path,
          "M 10 20 L 30 40 Z",
          "m 10 20 l 20 20 z");

    test!(implicit_line_to,
          "M 10 20 30 40",
          "m 10 20 l 20 20");

    test!(hline_vline,
          "M 10 20 V 30 H 20 L 30 40",
          "m 10 20 v 10 h 10 l 10 10");

    test!(curve,
          "M 10 20 C 20 30 20 30 20 30",
          "m 10 20 c 10 10 10 10 10 10");

    test!(move_to_1,
          "M 10 20 L 20 30 M 30 40 L 40 50",
          "m 10 20 l 10 10 m 10 10 l 10 10");

    test!(move_to_2,
          "M 10 20 L 20 30 Z M 20 30 L 30 40",
          "m 10 20 l 10 10 z m 10 10 l 10 10");

    test!(move_to_3,
          "M 10 20 L 20 30 z M 20 30 L 30 40",
          "m 10 20 l 10 10 z m 10 10 l 10 10");

    // MoveTo after ClosePath can be skipped
    test!(move_to_4,
          "M 10 20 L 20 30 Z L 20 30",
          "m 10 20 l 10 10 z l 10 10");

    test!(smooth_curve,
          "M 10 20 S 20 30 20 30",
          "m 10 20 s 10 10 10 10");

    test!(quad,
          "M 10 20 Q 20 30 20 30",
          "m 10 20 q 10 10 10 10");

    test!(arc_mixed,
          "M 30 150 a 40 40 0 0 1 65 50 Z m 30 30 A 20 20 0 0 0 125 230 Z \
           m 40 24 a 20 20 0 0 1 65 50 z",
          "m 30 150 a 40 40 0 0 1 65 50 z m 30 30 a 20 20 0 0 0 65 50 z \
           m 40 24 a 20 20 0 0 1 65 50 z");
}
