// Copyright 2019 the Resvg Authors
// SPDX-License-Identifier: Apache-2.0 OR MIT

use std::sync::Arc;

use kurbo::{ParamCurve, ParamCurveArclen};
use svgtypes::{FontFamily, Length, LengthUnit, parse_font_families};

use super::svgtree::{AId, EId, FromValue, SvgNode};
use super::{OptionLog, converter, style};
use crate::*;

impl<'a, 'input: 'a> FromValue<'a, 'input> for TextAnchor {
    fn parse(_: SvgNode, _: AId, value: &str) -> Option<Self> {
        match value {
            "start" => Some(TextAnchor::Start),
            "middle" => Some(TextAnchor::Middle),
            "end" => Some(TextAnchor::End),
            _ => None,
        }
    }
}

impl<'a, 'input: 'a> FromValue<'a, 'input> for AlignmentBaseline {
    fn parse(_: SvgNode, _: AId, value: &str) -> Option<Self> {
        match value {
            "auto" => Some(AlignmentBaseline::Auto),
            "baseline" => Some(AlignmentBaseline::Baseline),
            "before-edge" => Some(AlignmentBaseline::BeforeEdge),
            "text-before-edge" => Some(AlignmentBaseline::TextBeforeEdge),
            "middle" => Some(AlignmentBaseline::Middle),
            "central" => Some(AlignmentBaseline::Central),
            "after-edge" => Some(AlignmentBaseline::AfterEdge),
            "text-after-edge" => Some(AlignmentBaseline::TextAfterEdge),
            "ideographic" => Some(AlignmentBaseline::Ideographic),
            "alphabetic" => Some(AlignmentBaseline::Alphabetic),
            "hanging" => Some(AlignmentBaseline::Hanging),
            "mathematical" => Some(AlignmentBaseline::Mathematical),
            _ => None,
        }
    }
}

impl<'a, 'input: 'a> FromValue<'a, 'input> for DominantBaseline {
    fn parse(_: SvgNode, _: AId, value: &str) -> Option<Self> {
        match value {
            "auto" => Some(DominantBaseline::Auto),
            "use-script" => Some(DominantBaseline::UseScript),
            "no-change" => Some(DominantBaseline::NoChange),
            "reset-size" => Some(DominantBaseline::ResetSize),
            "ideographic" => Some(DominantBaseline::Ideographic),
            "alphabetic" => Some(DominantBaseline::Alphabetic),
            "hanging" => Some(DominantBaseline::Hanging),
            "mathematical" => Some(DominantBaseline::Mathematical),
            "central" => Some(DominantBaseline::Central),
            "middle" => Some(DominantBaseline::Middle),
            "text-after-edge" => Some(DominantBaseline::TextAfterEdge),
            "text-before-edge" => Some(DominantBaseline::TextBeforeEdge),
            _ => None,
        }
    }
}

impl<'a, 'input: 'a> FromValue<'a, 'input> for LengthAdjust {
    fn parse(_: SvgNode, _: AId, value: &str) -> Option<Self> {
        match value {
            "spacing" => Some(LengthAdjust::Spacing),
            "spacingAndGlyphs" => Some(LengthAdjust::SpacingAndGlyphs),
            _ => None,
        }
    }
}

impl<'a, 'input: 'a> FromValue<'a, 'input> for FontStyle {
    fn parse(_: SvgNode, _: AId, value: &str) -> Option<Self> {
        match value {
            "normal" => Some(FontStyle::Normal),
            "italic" => Some(FontStyle::Italic),
            "oblique" => Some(FontStyle::Oblique),
            _ => None,
        }
    }
}

/// A text character position.
///
/// _Character_ is a Unicode codepoint.
#[derive(Clone, Copy, Debug)]
struct CharacterPosition {
    /// An absolute X axis position.
    x: Option<f32>,
    /// An absolute Y axis position.
    y: Option<f32>,
    /// A relative X axis offset.
    dx: Option<f32>,
    /// A relative Y axis offset.
    dy: Option<f32>,
}

pub(crate) fn convert(
    text_node: SvgNode,
    state: &converter::State,
    cache: &mut converter::Cache,
    parent: &mut Group,
) {
    let pos_list = resolve_positions_list(text_node, state);
    let rotate_list = resolve_rotate_list(text_node);
    let writing_mode = convert_writing_mode(text_node);

    let chunks = collect_text_chunks(text_node, &pos_list, state, cache);

    let rendering_mode: TextRendering = text_node
        .find_attribute(AId::TextRendering)
        .unwrap_or(state.opt.text_rendering);

    // Nodes generated by markers must not have an ID. Otherwise we would have duplicates.
    let id = if state.parent_markers.is_empty() {
        text_node.element_id().to_string()
    } else {
        String::new()
    };

    let dummy = Rect::from_xywh(0.0, 0.0, 0.0, 0.0).unwrap();

    let mut text = Text {
        id,
        rendering_mode,
        dx: pos_list.iter().map(|v| v.dx.unwrap_or(0.0)).collect(),
        dy: pos_list.iter().map(|v| v.dy.unwrap_or(0.0)).collect(),
        rotate: rotate_list,
        writing_mode,
        chunks,
        abs_transform: parent.abs_transform,
        // All fields below will be reset by `text_to_paths`.
        bounding_box: dummy,
        abs_bounding_box: dummy,
        stroke_bounding_box: dummy,
        abs_stroke_bounding_box: dummy,
        flattened: Box::new(Group::empty()),
        layouted: vec![],
    };

    if text::convert(&mut text, &state.opt.font_resolver, cache).is_none() {
        return;
    }

    parent.children.push(Node::Text(Box::new(text)));
}

struct IterState {
    chars_count: usize,
    chunk_bytes_count: usize,
    split_chunk: bool,
    text_flow: TextFlow,
    chunks: Vec<TextChunk>,
}

fn collect_text_chunks(
    text_node: SvgNode,
    pos_list: &[CharacterPosition],
    state: &converter::State,
    cache: &mut converter::Cache,
) -> Vec<TextChunk> {
    let mut iter_state = IterState {
        chars_count: 0,
        chunk_bytes_count: 0,
        split_chunk: false,
        text_flow: TextFlow::Linear,
        chunks: Vec::new(),
    };

    collect_text_chunks_impl(text_node, pos_list, state, cache, &mut iter_state);

    iter_state.chunks
}

fn collect_text_chunks_impl(
    parent: SvgNode,
    pos_list: &[CharacterPosition],
    state: &converter::State,
    cache: &mut converter::Cache,
    iter_state: &mut IterState,
) {
    for child in parent.children() {
        if child.is_element() {
            if child.tag_name() == Some(EId::TextPath) {
                if parent.tag_name() != Some(EId::Text) {
                    // `textPath` can be set only as a direct `text` element child.
                    iter_state.chars_count += count_chars(child);
                    continue;
                }

                match resolve_text_flow(child, state) {
                    Some(v) => {
                        iter_state.text_flow = v;
                    }
                    None => {
                        // Skip an invalid text path and all it's children.
                        // We have to update the chars count,
                        // because `pos_list` was calculated including this text path.
                        iter_state.chars_count += count_chars(child);
                        continue;
                    }
                }

                iter_state.split_chunk = true;
            }

            collect_text_chunks_impl(child, pos_list, state, cache, iter_state);

            iter_state.text_flow = TextFlow::Linear;

            // Next char after `textPath` should be split too.
            if child.tag_name() == Some(EId::TextPath) {
                iter_state.split_chunk = true;
            }

            continue;
        }

        if !parent.is_visible_element(state.opt) {
            iter_state.chars_count += child.text().chars().count();
            continue;
        }

        let anchor = parent.find_attribute(AId::TextAnchor).unwrap_or_default();

        // TODO: what to do when <= 0? UB?
        let font_size = super::units::resolve_font_size(parent, state);
        let font_size = match NonZeroPositiveF32::new(font_size) {
            Some(n) => n,
            None => {
                // Skip this span.
                iter_state.chars_count += child.text().chars().count();
                continue;
            }
        };

        let font = convert_font(parent, state);

        let raw_paint_order: svgtypes::PaintOrder =
            parent.find_attribute(AId::PaintOrder).unwrap_or_default();
        let paint_order = super::converter::svg_paint_order_to_usvg(raw_paint_order);

        let mut dominant_baseline = parent
            .find_attribute(AId::DominantBaseline)
            .unwrap_or_default();

        // `no-change` means "use parent".
        if dominant_baseline == DominantBaseline::NoChange {
            dominant_baseline = parent
                .parent_element()
                .unwrap()
                .find_attribute(AId::DominantBaseline)
                .unwrap_or_default();
        }

        let mut apply_kerning = true;
        #[allow(clippy::if_same_then_else)]
        if parent.resolve_length(AId::Kerning, state, -1.0) == 0.0 {
            apply_kerning = false;
        } else if parent.find_attribute::<&str>(AId::FontKerning) == Some("none") {
            apply_kerning = false;
        }

        let font_optical_sizing = match parent.find_attribute::<&str>(AId::FontOpticalSizing) {
            Some("none") => crate::FontOpticalSizing::None,
            _ => crate::FontOpticalSizing::Auto, // "auto" or missing (browser default)
        };

        let mut text_length =
            parent.try_convert_length(AId::TextLength, Units::UserSpaceOnUse, state);
        // Negative values should be ignored.
        if let Some(n) = text_length {
            if n < 0.0 {
                text_length = None;
            }
        }

        let visibility: Visibility = parent.find_attribute(AId::Visibility).unwrap_or_default();

        let span = TextSpan {
            start: 0,
            end: 0,
            fill: style::resolve_fill(parent, true, state, cache),
            stroke: style::resolve_stroke(parent, true, state, cache),
            paint_order,
            font,
            font_size,
            small_caps: parent.find_attribute::<&str>(AId::FontVariant) == Some("small-caps"),
            apply_kerning,
            font_optical_sizing,
            decoration: resolve_decoration(parent, state, cache),
            visible: visibility == Visibility::Visible,
            dominant_baseline,
            alignment_baseline: parent
                .find_attribute(AId::AlignmentBaseline)
                .unwrap_or_default(),
            baseline_shift: convert_baseline_shift(parent, state),
            letter_spacing: parent.resolve_length(AId::LetterSpacing, state, 0.0),
            word_spacing: parent.resolve_length(AId::WordSpacing, state, 0.0),
            text_length,
            length_adjust: parent.find_attribute(AId::LengthAdjust).unwrap_or_default(),
        };

        let mut is_new_span = true;
        for c in child.text().chars() {
            let char_len = c.len_utf8();

            // Create a new chunk if:
            // - this is the first span (yes, position can be None)
            // - text character has an absolute coordinate assigned to it (via x/y attribute)
            // - `c` is the first char of the `textPath`
            // - `c` is the first char after `textPath`
            let is_new_chunk = pos_list[iter_state.chars_count].x.is_some()
                || pos_list[iter_state.chars_count].y.is_some()
                || iter_state.split_chunk
                || iter_state.chunks.is_empty();

            iter_state.split_chunk = false;

            if is_new_chunk {
                iter_state.chunk_bytes_count = 0;

                let mut span2 = span.clone();
                span2.start = 0;
                span2.end = char_len;

                iter_state.chunks.push(TextChunk {
                    x: pos_list[iter_state.chars_count].x,
                    y: pos_list[iter_state.chars_count].y,
                    anchor,
                    spans: vec![span2],
                    text_flow: iter_state.text_flow.clone(),
                    text: c.to_string(),
                });
            } else if is_new_span {
                // Add this span to the last text chunk.
                let mut span2 = span.clone();
                span2.start = iter_state.chunk_bytes_count;
                span2.end = iter_state.chunk_bytes_count + char_len;

                if let Some(chunk) = iter_state.chunks.last_mut() {
                    chunk.text.push(c);
                    chunk.spans.push(span2);
                }
            } else {
                // Extend the last span.
                if let Some(chunk) = iter_state.chunks.last_mut() {
                    chunk.text.push(c);
                    if let Some(span) = chunk.spans.last_mut() {
                        debug_assert_ne!(span.end, 0);
                        span.end += char_len;
                    }
                }
            }

            is_new_span = false;
            iter_state.chars_count += 1;
            iter_state.chunk_bytes_count += char_len;
        }
    }
}

fn resolve_text_flow(node: SvgNode, state: &converter::State) -> Option<TextFlow> {
    let linked_node = node.attribute::<SvgNode>(AId::Href)?;
    let path = super::shapes::convert(linked_node, state)?;

    // The reference path's transform needs to be applied
    let transform = linked_node.resolve_transform(AId::Transform, state);
    let path = if !transform.is_identity() {
        let mut path_copy = path.as_ref().clone();
        path_copy = path_copy.transform(transform)?;
        Arc::new(path_copy)
    } else {
        path
    };

    let start_offset: Length = node.attribute(AId::StartOffset).unwrap_or_default();
    let start_offset = if start_offset.unit == LengthUnit::Percent {
        // 'If a percentage is given, then the `startOffset` represents
        // a percentage distance along the entire path.'
        let path_len = path_length(&path);
        (path_len * (start_offset.number / 100.0)) as f32
    } else {
        node.resolve_length(AId::StartOffset, state, 0.0)
    };

    let id = NonEmptyString::new(linked_node.element_id().to_string())?;
    Some(TextFlow::Path(Arc::new(TextPath {
        id,
        start_offset,
        path,
    })))
}

fn convert_font(node: SvgNode, state: &converter::State) -> Font {
    let style: FontStyle = node.find_attribute(AId::FontStyle).unwrap_or_default();
    let stretch = conv_font_stretch(node);
    let weight = resolve_font_weight(node);
    let mut variations = parse_font_variation_settings(node);

    // Auto-map standard font properties to variation axes if not explicitly set.
    // This allows variable fonts to work with regular font-weight/font-stretch properties.
    let has_wght = variations.iter().any(|v| &v.tag == b"wght");
    let has_wdth = variations.iter().any(|v| &v.tag == b"wdth");
    let has_ital = variations.iter().any(|v| &v.tag == b"ital");
    let has_slnt = variations.iter().any(|v| &v.tag == b"slnt");

    // Map font-weight to wght axis (if not already set)
    if !has_wght && weight != 400 {
        variations.push(FontVariation::new(*b"wght", weight as f32));
    }

    // Map font-stretch to wdth axis (if not already set)
    // CSS font-stretch percentages: ultra-condensed=50%, condensed=75%, normal=100%, expanded=125%, ultra-expanded=200%
    if !has_wdth {
        let wdth = match stretch {
            FontStretch::UltraCondensed => 50.0,
            FontStretch::ExtraCondensed => 62.5,
            FontStretch::Condensed => 75.0,
            FontStretch::SemiCondensed => 87.5,
            FontStretch::Normal => 100.0,
            FontStretch::SemiExpanded => 112.5,
            FontStretch::Expanded => 125.0,
            FontStretch::ExtraExpanded => 150.0,
            FontStretch::UltraExpanded => 200.0,
        };
        if wdth != 100.0 {
            variations.push(FontVariation::new(*b"wdth", wdth));
        }
    }

    // Map font-style: italic to ital axis (if not already set)
    if !has_ital && style == FontStyle::Italic {
        variations.push(FontVariation::new(*b"ital", 1.0));
    }

    // Map font-style: oblique to slnt axis (if not already set)
    // Default oblique angle is typically 12-14 degrees
    if !has_slnt && style == FontStyle::Oblique {
        variations.push(FontVariation::new(*b"slnt", -12.0));
    }

    let font_families = if let Some(n) = node.ancestors().find(|n| n.has_attribute(AId::FontFamily))
    {
        n.attribute(AId::FontFamily).unwrap_or("")
    } else {
        ""
    };

    let mut families = parse_font_families(font_families)
        .ok()
        .log_none(|| {
            log::warn!(
                "Failed to parse {} value: '{}'. Falling back to {}.",
                AId::FontFamily,
                font_families,
                state.opt.font_family
            );
        })
        .unwrap_or_default();

    if families.is_empty() {
        families.push(FontFamily::Named(state.opt.font_family.clone()));
    }

    Font {
        families,
        style,
        stretch,
        weight,
        variations,
    }
}

/// Parses the `font-variation-settings` CSS property.
///
/// Syntax: `normal | [ <string> <number> ]#`
/// Example: `"wght" 700, "wdth" 50`
fn parse_font_variation_settings(node: SvgNode) -> Vec<FontVariation> {
    let value = if let Some(n) = node
        .ancestors()
        .find(|n| n.has_attribute(AId::FontVariationSettings))
    {
        let v = n.attribute(AId::FontVariationSettings).unwrap_or("");
        log::debug!("Found font-variation-settings: '{}'", v);
        v
    } else {
        return Vec::new();
    };

    // "normal" means no variations
    if value.eq_ignore_ascii_case("normal") || value.is_empty() {
        return Vec::new();
    }

    let mut variations = Vec::new();

    // Parse comma-separated list of "tag" value pairs
    for part in value.split(',') {
        let part = part.trim();
        if part.is_empty() {
            continue;
        }

        // Find the tag (quoted string) and value
        // Format: "wght" 700 or 'wght' 700
        let mut chars = part.chars().peekable();

        // Skip whitespace
        while chars.peek().map_or(false, |c| c.is_whitespace()) {
            chars.next();
        }

        // Parse quoted tag
        let quote = match chars.next() {
            Some('"') => '"',
            Some('\'') => '\'',
            _ => continue, // Invalid format
        };

        let mut tag_str = String::new();
        for c in chars.by_ref() {
            if c == quote {
                break;
            }
            tag_str.push(c);
        }

        // Tag must be exactly 4 characters
        if tag_str.len() != 4 {
            log::warn!(
                "Invalid font-variation-settings tag: '{}' (must be 4 characters)",
                tag_str
            );
            continue;
        }

        // Skip whitespace before value
        while chars.peek().map_or(false, |c| c.is_whitespace()) {
            chars.next();
        }

        // Parse the numeric value
        let value_str: String = chars.collect();
        let value_str = value_str.trim();

        let value = match value_str.parse::<f32>() {
            Ok(v) => v,
            Err(_) => {
                log::warn!("Invalid font-variation-settings value: '{}'", value_str);
                continue;
            }
        };

        let tag_bytes = tag_str.as_bytes();
        let tag = [tag_bytes[0], tag_bytes[1], tag_bytes[2], tag_bytes[3]];

        variations.push(FontVariation::new(tag, value));
    }

    variations
}

// TODO: properly resolve narrower/wider
fn conv_font_stretch(node: SvgNode) -> FontStretch {
    if let Some(n) = node.ancestors().find(|n| n.has_attribute(AId::FontStretch)) {
        match n.attribute(AId::FontStretch).unwrap_or("") {
            "narrower" | "condensed" => FontStretch::Condensed,
            "ultra-condensed" => FontStretch::UltraCondensed,
            "extra-condensed" => FontStretch::ExtraCondensed,
            "semi-condensed" => FontStretch::SemiCondensed,
            "semi-expanded" => FontStretch::SemiExpanded,
            "wider" | "expanded" => FontStretch::Expanded,
            "extra-expanded" => FontStretch::ExtraExpanded,
            "ultra-expanded" => FontStretch::UltraExpanded,
            _ => FontStretch::Normal,
        }
    } else {
        FontStretch::Normal
    }
}

fn resolve_font_weight(node: SvgNode) -> u16 {
    fn bound(min: usize, val: usize, max: usize) -> usize {
        std::cmp::max(min, std::cmp::min(max, val))
    }

    let nodes: Vec<_> = node.ancestors().collect();
    let mut weight = 400;
    for n in nodes.iter().rev().skip(1) {
        // skip Root
        weight = match n.attribute(AId::FontWeight).unwrap_or("") {
            "normal" => 400,
            "bold" => 700,
            "100" => 100,
            "200" => 200,
            "300" => 300,
            "400" => 400,
            "500" => 500,
            "600" => 600,
            "700" => 700,
            "800" => 800,
            "900" => 900,
            "bolder" => {
                // By the CSS2 spec the default value should be 400
                // so `bolder` will result in 500.
                // But Chrome and Inkscape will give us 700.
                // Have no idea is it a bug or something, but
                // we will follow such behavior for now.
                let step = if weight == 400 { 300 } else { 100 };

                bound(100, weight + step, 900)
            }
            "lighter" => {
                // By the CSS2 spec the default value should be 400
                // so `lighter` will result in 300.
                // But Chrome and Inkscape will give us 200.
                // Have no idea is it a bug or something, but
                // we will follow such behavior for now.
                let step = if weight == 400 { 200 } else { 100 };

                bound(100, weight - step, 900)
            }
            _ => weight,
        };
    }

    weight as u16
}

/// Resolves text's character positions.
///
/// This includes: x, y, dx, dy.
///
/// # The character
///
/// The first problem with this task is that the *character* itself
/// is basically undefined in the SVG spec. Sometimes it's an *XML character*,
/// sometimes a *glyph*, and sometimes just a *character*.
///
/// There is an ongoing [discussion](https://github.com/w3c/svgwg/issues/537)
/// on the SVG working group that addresses this by stating that a character
/// is a Unicode code point. But it's not final.
///
/// Also, according to the SVG 2 spec, *character* is *a Unicode code point*.
///
/// Anyway, we treat a character as a Unicode code point.
///
/// # Algorithm
///
/// To resolve positions, we have to iterate over descendant nodes and
/// if the current node is a `tspan` and has x/y/dx/dy attribute,
/// than the positions from this attribute should be assigned to the characters
/// of this `tspan` and it's descendants.
///
/// Positions list can have more values than characters in the `tspan`,
/// so we have to clamp it, because values should not overlap, e.g.:
///
/// (we ignore whitespaces for example purposes,
/// so the `text` content is `Text` and not `T ex t`)
///
/// ```text
/// <text>
///   a
///   <tspan x="10 20 30">
///     bc
///   </tspan>
///   d
/// </text>
/// ```
///
/// In this example, the `d` position should not be set to `30`.
/// And the result should be: `[None, 10, 20, None]`
///
/// Another example:
///
/// ```text
/// <text>
///   <tspan x="100 110 120 130">
///     a
///     <tspan x="50">
///       bc
///     </tspan>
///   </tspan>
///   d
/// </text>
/// ```
///
/// The result should be: `[100, 50, 120, None]`
fn resolve_positions_list(text_node: SvgNode, state: &converter::State) -> Vec<CharacterPosition> {
    // Allocate a list that has all characters positions set to `None`.
    let total_chars = count_chars(text_node);
    let mut list = vec![
        CharacterPosition {
            x: None,
            y: None,
            dx: None,
            dy: None,
        };
        total_chars
    ];

    let mut offset = 0;
    for child in text_node.descendants() {
        if child.is_element() {
            // We must ignore text positions on `textPath`.
            if !matches!(child.tag_name(), Some(EId::Text) | Some(EId::Tspan)) {
                continue;
            }

            let child_chars = count_chars(child);
            macro_rules! push_list {
                ($aid:expr, $field:ident) => {
                    if let Some(num_list) = super::units::convert_list(child, $aid, state) {
                        // Note that we are using not the total count,
                        // but the amount of characters in the current `tspan` (with children).
                        let len = std::cmp::min(num_list.len(), child_chars);
                        for i in 0..len {
                            list[offset + i].$field = Some(num_list[i]);
                        }
                    }
                };
            }

            push_list!(AId::X, x);
            push_list!(AId::Y, y);
            push_list!(AId::Dx, dx);
            push_list!(AId::Dy, dy);
        } else if child.is_text() {
            // Advance the offset.
            offset += child.text().chars().count();
        }
    }

    list
}

/// Resolves characters rotation.
///
/// The algorithm is well explained
/// [in the SVG spec](https://www.w3.org/TR/SVG11/text.html#TSpanElement) (scroll down a bit).
///
/// ![](https://www.w3.org/TR/SVG11/images/text/tspan05-diagram.png)
///
/// Note: this algorithm differs from the position resolving one.
fn resolve_rotate_list(text_node: SvgNode) -> Vec<f32> {
    // Allocate a list that has all characters angles set to `0.0`.
    let mut list = vec![0.0; count_chars(text_node)];
    let mut last = 0.0;
    let mut offset = 0;
    for child in text_node.descendants() {
        if child.is_element() {
            if let Some(rotate) = child.attribute::<Vec<f32>>(AId::Rotate) {
                for i in 0..count_chars(child) {
                    if let Some(a) = rotate.get(i).cloned() {
                        list[offset + i] = a;
                        last = a;
                    } else {
                        // If the rotate list doesn't specify the rotation for
                        // this character - use the last one.
                        list[offset + i] = last;
                    }
                }
            }
        } else if child.is_text() {
            // Advance the offset.
            offset += child.text().chars().count();
        }
    }

    list
}

/// Resolves node's `text-decoration` property.
fn resolve_decoration(
    tspan: SvgNode,
    state: &converter::State,
    cache: &mut converter::Cache,
) -> TextDecoration {
    // Checks if a decoration is present in a single node.
    fn find_decoration(node: SvgNode, value: &str) -> bool {
        if let Some(str_value) = node.attribute::<&str>(AId::TextDecoration) {
            str_value.split(' ').any(|v| v == value)
        } else {
            false
        }
    }

    // The algorithm is as follows: First, we check whether the given text decoration appears in ANY
    // ancestor, i.e. it can also appear in ancestors outside of the <text> element. If the text
    // decoration is declared somewhere, it means that this tspan will have it. However, we still
    // need to find the corresponding fill/stroke for it. To do this, we iterate through all
    // ancestors (i.e. tspans) until we find the text decoration declared. If not, we will
    // stop at latest at the text node, and use its fill/stroke.
    let mut gen_style = |text_decoration: &str| {
        if !tspan
            .ancestors()
            .any(|n| find_decoration(n, text_decoration))
        {
            return None;
        }

        let mut fill_node = None;
        let mut stroke_node = None;

        for node in tspan.ancestors() {
            if find_decoration(node, text_decoration) || node.tag_name() == Some(EId::Text) {
                fill_node = fill_node.map_or(Some(node), Some);
                stroke_node = stroke_node.map_or(Some(node), Some);
                break;
            }
        }

        Some(TextDecorationStyle {
            fill: fill_node.and_then(|node| style::resolve_fill(node, true, state, cache)),
            stroke: stroke_node.and_then(|node| style::resolve_stroke(node, true, state, cache)),
        })
    };

    TextDecoration {
        underline: gen_style("underline"),
        overline: gen_style("overline"),
        line_through: gen_style("line-through"),
    }
}

fn convert_baseline_shift(node: SvgNode, state: &converter::State) -> Vec<BaselineShift> {
    let mut shift = Vec::new();
    let nodes: Vec<_> = node
        .ancestors()
        .take_while(|n| n.tag_name() != Some(EId::Text))
        .collect();
    for n in nodes {
        if let Some(len) = n.try_attribute::<Length>(AId::BaselineShift) {
            if len.unit == LengthUnit::Percent {
                let n = super::units::resolve_font_size(n, state) * (len.number as f32 / 100.0);
                shift.push(BaselineShift::Number(n));
            } else {
                let n = super::units::convert_length(
                    len,
                    n,
                    AId::BaselineShift,
                    Units::ObjectBoundingBox,
                    state,
                );
                shift.push(BaselineShift::Number(n));
            }
        } else if let Some(s) = n.attribute(AId::BaselineShift) {
            match s {
                "sub" => shift.push(BaselineShift::Subscript),
                "super" => shift.push(BaselineShift::Superscript),
                _ => shift.push(BaselineShift::Baseline),
            }
        }
    }

    if shift
        .iter()
        .all(|base| matches!(base, BaselineShift::Baseline))
    {
        shift.clear();
    }

    shift
}

fn count_chars(node: SvgNode) -> usize {
    node.descendants()
        .filter(|n| n.is_text())
        .fold(0, |w, n| w + n.text().chars().count())
}

/// Converts the writing mode.
///
/// [SVG 2] references [CSS Writing Modes Level 3] for the definition of the
/// 'writing-mode' property, there are only two writing modes:
/// horizontal left-to-right and vertical right-to-left.
///
/// That specification introduces new values for the property. The SVG 1.1
/// values are obsolete but must still be supported by converting the specified
/// values to computed values as follows:
///
/// - `lr`, `lr-tb`, `rl`, `rl-tb` => `horizontal-tb`
/// - `tb`, `tb-rl` => `vertical-rl`
///
/// The current `vertical-lr` behaves exactly the same as `vertical-rl`.
///
/// Also, looks like no one really supports the `rl` and `rl-tb`, except `Batik`.
/// And I'm not sure if its behaviour is correct.
///
/// So we will ignore it as well, mainly because I have no idea how exactly
/// it should affect the rendering.
///
/// [SVG 2]: https://www.w3.org/TR/SVG2/text.html#WritingModeProperty
/// [CSS Writing Modes Level 3]: https://www.w3.org/TR/css-writing-modes-3/#svg-writing-mode-css
fn convert_writing_mode(text_node: SvgNode) -> WritingMode {
    if let Some(n) = text_node
        .ancestors()
        .find(|n| n.has_attribute(AId::WritingMode))
    {
        match n.attribute(AId::WritingMode).unwrap_or("lr-tb") {
            "tb" | "tb-rl" | "vertical-rl" | "vertical-lr" => WritingMode::TopToBottom,
            _ => WritingMode::LeftToRight,
        }
    } else {
        WritingMode::LeftToRight
    }
}

fn path_length(path: &tiny_skia_path::Path) -> f64 {
    let mut prev_mx = path.points()[0].x;
    let mut prev_my = path.points()[0].y;
    let mut prev_x = prev_mx;
    let mut prev_y = prev_my;

    fn create_curve_from_line(px: f32, py: f32, x: f32, y: f32) -> kurbo::CubicBez {
        let line = kurbo::Line::new(
            kurbo::Point::new(px as f64, py as f64),
            kurbo::Point::new(x as f64, y as f64),
        );
        let p1 = line.eval(0.33);
        let p2 = line.eval(0.66);
        kurbo::CubicBez::new(line.p0, p1, p2, line.p1)
    }

    let mut length = 0.0;
    for seg in path.segments() {
        let curve = match seg {
            tiny_skia_path::PathSegment::MoveTo(p) => {
                prev_mx = p.x;
                prev_my = p.y;
                prev_x = p.x;
                prev_y = p.y;
                continue;
            }
            tiny_skia_path::PathSegment::LineTo(p) => {
                create_curve_from_line(prev_x, prev_y, p.x, p.y)
            }
            tiny_skia_path::PathSegment::QuadTo(p1, p) => kurbo::QuadBez::new(
                kurbo::Point::new(prev_x as f64, prev_y as f64),
                kurbo::Point::new(p1.x as f64, p1.y as f64),
                kurbo::Point::new(p.x as f64, p.y as f64),
            )
            .raise(),
            tiny_skia_path::PathSegment::CubicTo(p1, p2, p) => kurbo::CubicBez::new(
                kurbo::Point::new(prev_x as f64, prev_y as f64),
                kurbo::Point::new(p1.x as f64, p1.y as f64),
                kurbo::Point::new(p2.x as f64, p2.y as f64),
                kurbo::Point::new(p.x as f64, p.y as f64),
            ),
            tiny_skia_path::PathSegment::Close => {
                create_curve_from_line(prev_x, prev_y, prev_mx, prev_my)
            }
        };

        length += curve.arclen(0.5);
        prev_x = curve.p3.x as f32;
        prev_y = curve.p3.y as f32;
    }

    length
}
