use crate::{error::{PdfMakeError, Result}, model::Fit, vector::{FillRule, PathCommand, VectorShape}};

#[derive(Debug, Clone, PartialEq)]
pub struct ParsedSvg {
    pub view_width: f32,
    pub view_height: f32,
    pub shapes: Vec<VectorShape>,
}

pub fn parse_svg(markup: &str) -> Result<ParsedSvg> {
    for unsupported in ["text", "g", "defs", "clipPath", "linearGradient", "image"] {
        if markup.contains(&format!("<{unsupported}")) || markup.contains(&format!("</{unsupported}>")) {
            return Err(PdfMakeError::UnsupportedSvgElement(unsupported.to_string()));
        }
    }
    let view_width = attr_f32(markup, "width").ok_or_else(|| PdfMakeError::SvgParse("missing svg width".to_string()))?;
    let view_height = attr_f32(markup, "height").ok_or_else(|| PdfMakeError::SvgParse("missing svg height".to_string()))?;
    if view_width <= 0.0 || view_height <= 0.0 {
        return Err(PdfMakeError::SvgParse("invalid svg dimensions".to_string()));
    }
    let mut shapes = Vec::new();
    for tag in tags(markup, "rect") {
        shapes.push(VectorShape::Rect {
            x: attr_f32(tag, "x").unwrap_or(0.0),
            y: attr_f32(tag, "y").unwrap_or(0.0),
            width: attr_f32(tag, "width").ok_or_else(|| PdfMakeError::SvgParse("rect missing width".to_string()))?,
            height: attr_f32(tag, "height").ok_or_else(|| PdfMakeError::SvgParse("rect missing height".to_string()))?,
            fill: attr_str(tag, "fill"),
            stroke: attr_str(tag, "stroke"),
            stroke_width: attr_f32(tag, "stroke-width"),
            rx: attr_f32(tag, "rx"),
            line_join: attr_str(tag, "stroke-linejoin"),
            line_cap: attr_str(tag, "stroke-linecap"),
            dash_array: attr_str(tag, "stroke-dasharray"),
        });
    }
    for tag in tags(markup, "line") {
        shapes.push(VectorShape::Line {
            x1: attr_f32(tag, "x1").unwrap_or(0.0),
            y1: attr_f32(tag, "y1").unwrap_or(0.0),
            x2: attr_f32(tag, "x2").unwrap_or(0.0),
            y2: attr_f32(tag, "y2").unwrap_or(0.0),
            stroke: attr_str(tag, "stroke"),
            stroke_width: attr_f32(tag, "stroke-width"),
            line_join: attr_str(tag, "stroke-linejoin"),
            line_cap: attr_str(tag, "stroke-linecap"),
            dash_array: attr_str(tag, "stroke-dasharray"),
        });
    }
    for tag in tags(markup, "circle") {
        shapes.push(VectorShape::Circle {
            cx: attr_f32(tag, "cx").unwrap_or(0.0),
            cy: attr_f32(tag, "cy").unwrap_or(0.0),
            r: attr_f32(tag, "r").ok_or_else(|| PdfMakeError::SvgParse("circle missing r".to_string()))?,
            fill: attr_str(tag, "fill"),
            stroke: attr_str(tag, "stroke"),
            stroke_width: attr_f32(tag, "stroke-width"),
            line_join: attr_str(tag, "stroke-linejoin"),
            line_cap: attr_str(tag, "stroke-linecap"),
            dash_array: attr_str(tag, "stroke-dasharray"),
        });
    }
    for tag in tags(markup, "ellipse") {
        shapes.push(VectorShape::Ellipse {
            cx: attr_f32(tag, "cx").unwrap_or(0.0),
            cy: attr_f32(tag, "cy").unwrap_or(0.0),
            rx: attr_f32(tag, "rx").ok_or_else(|| PdfMakeError::SvgParse("ellipse missing rx".to_string()))?,
            ry: attr_f32(tag, "ry").ok_or_else(|| PdfMakeError::SvgParse("ellipse missing ry".to_string()))?,
            fill: attr_str(tag, "fill"),
            stroke: attr_str(tag, "stroke"),
            stroke_width: attr_f32(tag, "stroke-width"),
            line_join: attr_str(tag, "stroke-linejoin"),
            line_cap: attr_str(tag, "stroke-linecap"),
            dash_array: attr_str(tag, "stroke-dasharray"),
        });
    }
    for tag in tags(markup, "path") {
        let d = attr_str(tag, "d").ok_or_else(|| PdfMakeError::SvgParse("path missing d".to_string()))?;
        let commands = parse_path_data(&d)?;
        let fill_rule = match attr_str(tag, "fill-rule").as_deref() {
            Some("evenodd") => FillRule::EvenOdd,
            _ => FillRule::NonZero,
        };
        shapes.push(VectorShape::Path {
            commands,
            fill: attr_str(tag, "fill"),
            stroke: attr_str(tag, "stroke"),
            stroke_width: attr_f32(tag, "stroke-width"),
            fill_rule,
            line_join: attr_str(tag, "stroke-linejoin"),
            line_cap: attr_str(tag, "stroke-linecap"),
            dash_array: attr_str(tag, "stroke-dasharray"),
        });
    }
    for tag in tags(markup, "polyline") {
        if let Some(points_str) = attr_str(tag, "points") {
            let points = parse_points(&points_str);
            shapes.push(VectorShape::Polyline {
                points,
                fill: attr_str(tag, "fill"),
                stroke: attr_str(tag, "stroke"),
                stroke_width: attr_f32(tag, "stroke-width"),
                line_join: attr_str(tag, "stroke-linejoin"),
                line_cap: attr_str(tag, "stroke-linecap"),
                dash_array: attr_str(tag, "stroke-dasharray"),
            });
        }
    }
    for tag in tags(markup, "polygon") {
        if let Some(points_str) = attr_str(tag, "points") {
            let points = parse_points(&points_str);
            shapes.push(VectorShape::Polygon {
                points,
                fill: attr_str(tag, "fill"),
                stroke: attr_str(tag, "stroke"),
                stroke_width: attr_f32(tag, "stroke-width"),
                line_join: attr_str(tag, "stroke-linejoin"),
                line_cap: attr_str(tag, "stroke-linecap"),
                dash_array: attr_str(tag, "stroke-dasharray"),
            });
        }
    }
    Ok(ParsedSvg { view_width, view_height, shapes })
}

pub fn resolve_svg_size(view_width: f32, view_height: f32, width: Option<f32>, height: Option<f32>, fit: Option<Fit>) -> Result<(f32, f32)> {
    crate::image::resolve_display_size(view_width as u32, view_height as u32, width, height, fit)
}

fn tags<'a>(markup: &'a str, name: &str) -> Vec<&'a str> {
    let needle = format!("<{name}");
    markup.split('>').filter_map(|part| part.find(&needle).map(|index| &part[index..])).collect()
}

fn parse_points(points_str: &str) -> Vec<(f32, f32)> {
    points_str
        .split(|c: char| c.is_whitespace() || c == ',')
        .filter(|s| !s.is_empty())
        .collect::<Vec<_>>()
        .chunks(2)
        .filter_map(|chunk| {
            if chunk.len() == 2 {
                let x = chunk[0].parse::<f32>().ok()?;
                let y = chunk[1].parse::<f32>().ok()?;
                Some((x, y))
            } else {
                None
            }
        })
        .collect()
}

fn attr_f32(tag: &str, name: &str) -> Option<f32> {
    let needle = format!("{name}=\"");
    let start = tag.find(&needle)? + needle.len();
    let rest = &tag[start..];
    let end = rest.find('"')?;
    rest[..end].parse::<f32>().ok()
}

fn attr_str(tag: &str, name: &str) -> Option<String> {
    let needle = format!("{name}=\"");
    let start = tag.find(&needle)? + needle.len();
    let rest = &tag[start..];
    let end = rest.find('"')?;
    Some(rest[..end].to_string())
}

pub fn parse_path_data(d: &str) -> Result<Vec<PathCommand>> {
    let mut commands = Vec::new();
    let chars: Vec<char> = d.chars().collect();
    let mut pos = 0;
    let mut last_cmd = 'M';

    while pos < chars.len() {
        skip_whitespace(&chars, &mut pos);
        if pos >= chars.len() { break; }

        let cmd = if chars[pos].is_ascii_alphabetic() {
            let c = chars[pos];
            pos += 1;
            last_cmd = c;
            c
        } else {
            match last_cmd {
                'M' => 'L',
                'm' => 'l',
                c => c,
            }
        };

        skip_whitespace(&chars, &mut pos);

        match cmd {
            'M' => {
                let x = parse_number(&chars, &mut pos);
                let y = parse_number(&chars, &mut pos);
                commands.push(PathCommand::MoveTo { x, y });
            }
            'm' => {
                let dx = parse_number(&chars, &mut pos);
                let dy = parse_number(&chars, &mut pos);
                commands.push(PathCommand::RelativeMoveTo { dx, dy });
            }
            'L' => {
                let x = parse_number(&chars, &mut pos);
                let y = parse_number(&chars, &mut pos);
                commands.push(PathCommand::LineTo { x, y });
            }
            'l' => {
                let dx = parse_number(&chars, &mut pos);
                let dy = parse_number(&chars, &mut pos);
                commands.push(PathCommand::RelativeLineTo { dx, dy });
            }
            'H' => {
                let x = parse_number(&chars, &mut pos);
                commands.push(PathCommand::HorizontalLineTo { x });
            }
            'h' => {
                let dx = parse_number(&chars, &mut pos);
                commands.push(PathCommand::RelativeHorizontalLineTo { dx });
            }
            'V' => {
                let y = parse_number(&chars, &mut pos);
                commands.push(PathCommand::VerticalLineTo { y });
            }
            'v' => {
                let dy = parse_number(&chars, &mut pos);
                commands.push(PathCommand::RelativeVerticalLineTo { dy });
            }
            'C' => {
                let x1 = parse_number(&chars, &mut pos);
                let y1 = parse_number(&chars, &mut pos);
                let x2 = parse_number(&chars, &mut pos);
                let y2 = parse_number(&chars, &mut pos);
                let x = parse_number(&chars, &mut pos);
                let y = parse_number(&chars, &mut pos);
                commands.push(PathCommand::CurveTo { x1, y1, x2, y2, x, y });
            }
            'c' => {
                let dx1 = parse_number(&chars, &mut pos);
                let dy1 = parse_number(&chars, &mut pos);
                let dx2 = parse_number(&chars, &mut pos);
                let dy2 = parse_number(&chars, &mut pos);
                let dx = parse_number(&chars, &mut pos);
                let dy = parse_number(&chars, &mut pos);
                commands.push(PathCommand::RelativeCurveTo { dx1, dy1, dx2, dy2, dx, dy });
            }
            'S' => {
                let x2 = parse_number(&chars, &mut pos);
                let y2 = parse_number(&chars, &mut pos);
                let x = parse_number(&chars, &mut pos);
                let y = parse_number(&chars, &mut pos);
                commands.push(PathCommand::SmoothCurveTo { x2, y2, x, y });
            }
            's' => {
                let dx2 = parse_number(&chars, &mut pos);
                let dy2 = parse_number(&chars, &mut pos);
                let dx = parse_number(&chars, &mut pos);
                let dy = parse_number(&chars, &mut pos);
                commands.push(PathCommand::RelativeSmoothCurveTo { dx2, dy2, dx, dy });
            }
            'Q' => {
                let x1 = parse_number(&chars, &mut pos);
                let y1 = parse_number(&chars, &mut pos);
                let x = parse_number(&chars, &mut pos);
                let y = parse_number(&chars, &mut pos);
                commands.push(PathCommand::QuadraticCurveTo { x1, y1, x, y });
            }
            'q' => {
                let dx1 = parse_number(&chars, &mut pos);
                let dy1 = parse_number(&chars, &mut pos);
                let dx = parse_number(&chars, &mut pos);
                let dy = parse_number(&chars, &mut pos);
                commands.push(PathCommand::RelativeQuadraticCurveTo { dx1, dy1, dx, dy });
            }
            'T' => {
                let x = parse_number(&chars, &mut pos);
                let y = parse_number(&chars, &mut pos);
                commands.push(PathCommand::SmoothQuadraticCurveTo { x, y });
            }
            't' => {
                let dx = parse_number(&chars, &mut pos);
                let dy = parse_number(&chars, &mut pos);
                commands.push(PathCommand::RelativeSmoothQuadraticCurveTo { dx, dy });
            }
            'A' => {
                let rx = parse_number(&chars, &mut pos);
                let ry = parse_number(&chars, &mut pos);
                let x_rotation = parse_number(&chars, &mut pos);
                let large_arc: i32 = parse_number(&chars, &mut pos);
                let sweep: i32 = parse_number(&chars, &mut pos);
                let x = parse_number(&chars, &mut pos);
                let y = parse_number(&chars, &mut pos);
                commands.push(PathCommand::Arc { rx, ry, x_rotation, large_arc: large_arc != 0, sweep: sweep != 0, x, y });
            }
            'a' => {
                let rx = parse_number(&chars, &mut pos);
                let ry = parse_number(&chars, &mut pos);
                let x_rotation = parse_number(&chars, &mut pos);
                let large_arc: i32 = parse_number(&chars, &mut pos);
                let sweep: i32 = parse_number(&chars, &mut pos);
                let dx = parse_number(&chars, &mut pos);
                let dy = parse_number(&chars, &mut pos);
                commands.push(PathCommand::RelativeArc { rx, ry, x_rotation, large_arc: large_arc != 0, sweep: sweep != 0, dx, dy });
            }
            'Z' | 'z' => {
                commands.push(PathCommand::ClosePath);
            }
            other => {
                return Err(PdfMakeError::SvgParse(format!("unknown path command '{}'", other)));
            }
        }
    }

    Ok(commands)
}

fn skip_whitespace(chars: &[char], pos: &mut usize) {
    while *pos < chars.len() && (chars[*pos].is_whitespace() || chars[*pos] == ',') {
        *pos += 1;
    }
}

fn parse_number<T: std::str::FromStr>(chars: &[char], pos: &mut usize) -> T
where T::Err: std::fmt::Debug
{
    skip_whitespace(chars, pos);

    let start = *pos;
    if *pos < chars.len() && (chars[*pos] == '-' || chars[*pos] == '+') {
        *pos += 1;
    }
    while *pos < chars.len() && (chars[*pos].is_ascii_digit() || chars[*pos] == '.') {
        *pos += 1;
    }
    if *pos < chars.len() && (chars[*pos] == 'e' || chars[*pos] == 'E') {
        *pos += 1;
        if *pos < chars.len() && (chars[*pos] == '-' || chars[*pos] == '+') {
            *pos += 1;
        }
        while *pos < chars.len() && chars[*pos].is_ascii_digit() {
            *pos += 1;
        }
    }

    let s: String = chars[start..*pos].iter().collect();
    s.parse::<T>().unwrap_or_else(|e| panic!("failed to parse number '{}': {:?}", s, e))
}