# Copyright 2025 Windell H. Oskay, Bantam Tools
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA

"""
digest_svg.py

Routines for digesting an input SVG file into a simplified SVG ("Plob")
formats for easier processing

Part of the NextDraw driver software
http://bantamtools.com

Requires Python 3.9 or newer.
"""

import logging
from math import sqrt

from lxml import etree

from nextdrawcore.nextdraw_options import models
from nextdrawcore.plot_utils_import import from_dependency_import  # plotink
path_objects = from_dependency_import('nextdrawcore.path_objects')
simplepath = from_dependency_import('ink_extensions.simplepath')
simplestyle = from_dependency_import('ink_extensions.simplestyle')
cubicsuperpath = from_dependency_import('ink_extensions.cubicsuperpath')
simpletransform = from_dependency_import('ink_extensions.simpletransform')
inkex = from_dependency_import('ink_extensions.inkex')
message = from_dependency_import('ink_extensions_utils.message')
plot_utils = from_dependency_import('plotink.plot_utils')

logger = logging.getLogger(__name__)


# Standalone utility functions for shape-to-path conversion and transform processing
# These functions can be used independently of the DigestSVG class for modular shape processing

def _convert_rect_to_path(element, doc_width_100=1.0, doc_height_100=1.0):
    """
    Convert rectangle to path data with proper unit handling.

    Args:
        element: Rectangle element to convert
        doc_width_100: Document width scaling factor for unit conversion (default 1.0)
        doc_height_100: Document height scaling factor for unit conversion (default 1.0)

    Returns:
        string - SVG path data, or empty string for invalid rectangles
    """
    x = plot_utils.unitsToUserUnits(element.get('x', '0'), doc_width_100)
    y = plot_utils.unitsToUserUnits(element.get('y', '0'), doc_height_100)
    r_x = plot_utils.unitsToUserUnits(element.get('rx'), doc_width_100)
    width = plot_utils.unitsToUserUnits(element.get('width'), doc_width_100)
    r_y = plot_utils.unitsToUserUnits(element.get('ry'), doc_height_100)
    height = plot_utils.unitsToUserUnits(element.get('height'), doc_height_100)

    # Convert None values to 0 (happens when attributes are missing or invalid)
    r_x = r_x if r_x is not None else 0.0
    width = width if width is not None else 0.0
    r_y = r_y if r_y is not None else 0.0
    height = height if height is not None else 0.0

    def calc_r_attr(attr, other_attr, twice_maximum):
        value = (attr if attr is not None else
                 other_attr if other_attr is not None else
                 0)
        if twice_maximum <= 0:
            return 0
        return min(value, twice_maximum * 0.5)

    r_x = calc_r_attr(r_x, r_y, width)
    r_y = calc_r_attr(r_y, r_x, height)

    instr = []
    if (r_x > 0) or (r_y > 0):
        instr.append(['M ', [x + r_x, y]])
        instr.append([' L ', [x + width - r_x, y]])
        instr.append([' A ', [r_x, r_y, 0, 0, 1, x + width, y + r_y]])
        instr.append([' L ', [x + width, y + height - r_y]])
        instr.append([' A ', [r_x, r_y, 0, 0, 1, x + width - r_x, y + height]])
        instr.append([' L ', [x + r_x, y + height]])
        instr.append([' A ', [r_x, r_y, 0, 0, 1, x, y + height - r_y]])
        instr.append([' L ', [x, y + r_y]])
        instr.append([' A ', [r_x, r_y, 0, 0, 1, x + r_x, y]])
    else:
        instr.append(['M ', [x, y]])
        instr.append([' L ', [x + width, y]])
        instr.append([' L ', [x + width, y + height]])
        instr.append([' L ', [x, y + height]])
        instr.append([' L ', [x, y]])

    return simplepath.formatPath(instr)


def _convert_line_to_path(element, doc_width_100=1.0, doc_height_100=1.0):
    """
    Convert line to path data with proper unit handling.

    Args:
        element: Line element to convert
        doc_width_100: Document width scaling factor for unit conversion (default 1.0)
        doc_height_100: Document height scaling factor for unit conversion (default 1.0)

    Returns:
        string - SVG path data
    """
    x_1, x_2 = [plot_utils.unitsToUserUnits(element.get(attr, '0'),
                doc_width_100) for attr in ['x1', 'x2']]
    y_1, y_2 = [plot_utils.unitsToUserUnits(element.get(attr, '0'),
                doc_height_100) for attr in ['y1', 'y2']]

    # Convert None values to 0 (happens when attributes are invalid)
    x_1 = x_1 if x_1 is not None else 0.0
    x_2 = x_2 if x_2 is not None else 0.0
    y_1 = y_1 if y_1 is not None else 0.0
    y_2 = y_2 if y_2 is not None else 0.0

    path_a = []
    path_a.append(['M ', [x_1, y_1]])
    path_a.append([' L ', [x_2, y_2]])
    return simplepath.formatPath(path_a)


def _convert_polyline_polygon_to_path(element):
    """
    Convert a polyline or polygon element to path data string.
    """
    pl = element.get('points', '').strip()
    if pl == '':
        return ''

    pa = pl.replace(',', ' ').split()  # replace comma with space before splitting
    if not pa:
        return ''

    path_length = len(pa)
    if path_length < 4:  # Minimum of x1,y1 x2,y2 required.
        return ''

    path_d = "M " + pa[0] + " " + pa[1]
    i = 2
    while i < (path_length - 1):
        path_d += " L " + pa[i] + " " + pa[i + 1]
        i += 2

    if element.tag in ('{http://www.w3.org/2000/svg}polygon', 'polygon'):
        path_d += " Z"

    return path_d


def _convert_circle_ellipse_to_path(element, doc_width_100=1.0, doc_height_100=1.0,
                                    diagonal_100=1.0):
    """
    Convert circle/ellipse to path data with proper unit handling.

    Args:
        element: Circle or ellipse element to convert
        doc_width_100: Document width scaling factor for unit conversion (default 1.0)
        doc_height_100: Document height scaling factor for unit conversion (default 1.0)
        diagonal_100: Document diagonal scaling factor for radius conversion (default 1.0)

    Returns:
        string - SVG path data, or empty string for zero radius
    """
    if element.tag in ('{http://www.w3.org/2000/svg}circle', 'circle'):
        r_x = plot_utils.unitsToUserUnits(element.get('r', '0'), diagonal_100)
        r_y = r_x
    else:
        r_x = plot_utils.unitsToUserUnits(element.get('rx', '0'), diagonal_100)
        r_y = plot_utils.unitsToUserUnits(element.get('ry', '0'), diagonal_100)

    # Convert None values to 0 (happens when attributes are invalid)
    r_x = r_x if r_x is not None else 0.0
    r_y = r_y if r_y is not None else 0.0

    if r_x == 0 or r_y == 0:
        return ''   # Return empty string for zero radius

    c_x = plot_utils.unitsToUserUnits(element.get('cx', '0'), doc_width_100)
    c_y = plot_utils.unitsToUserUnits(element.get('cy', '0'), doc_height_100)

    # Convert None values to 0 (happens when attributes are invalid)
    c_x = c_x if c_x is not None else 0.0
    c_y = c_y if c_y is not None else 0.0

    # SVG standard: start at "3 o'clock" position (rightmost point)
    # Use clockwise direction for proper textPath behavior (text on outside of circle)
    start_x = c_x + r_x  # 3 o'clock (rightmost)
    left_x = c_x - r_x   # 9 o'clock (leftmost)

    path_d = f'M {start_x:f},{c_y:f} ' + \
             f'A {r_x:f},{r_y:f} ' + \
             f'0 1 1 {left_x:f},{c_y:f} ' + \
             f'A {r_x:f},{r_y:f} ' + \
             f'0 1 1 {start_x:f},{c_y:f}'

    return path_d


def process_shape_to_path_data(element, parent_transform=None):
    """
    Convert a single SVG shape element to path data string.

    Args:
        element: lxml element (rect, circle, ellipse, line, polyline, polygon, path)
        parent_transform: Optional transform matrix to apply

    Returns:
        dict: {
            'path_data': str,      # SVG path data string
            'success': bool,       # True if conversion succeeded
            'error': str or None   # Error message if conversion failed
        }
    """
    # Initialize result structure
    result = {
        'path_data': '',
        'success': False,
        'error': None
    }

    try:
        # Extract existing logic for each shape type using existing pattern
        if element.tag in ('{http://www.w3.org/2000/svg}rect', 'rect'):
            result['path_data'] = _convert_rect_to_path(element)
        elif element.tag in ('{http://www.w3.org/2000/svg}circle', 'circle',
                             '{http://www.w3.org/2000/svg}ellipse', 'ellipse'):
            result['path_data'] = _convert_circle_ellipse_to_path(element)
        elif element.tag in ('{http://www.w3.org/2000/svg}line', 'line'):
            result['path_data'] = _convert_line_to_path(element)
        elif element.tag in ('{http://www.w3.org/2000/svg}polyline', 'polyline',
                             '{http://www.w3.org/2000/svg}polygon', 'polygon'):
            result['path_data'] = _convert_polyline_polygon_to_path(element)
        elif element.tag in ('{http://www.w3.org/2000/svg}path', 'path'):
            result['path_data'] = element.get('d', '')
        else:
            result['error'] = f"Unsupported shape type: {element.tag}"
            return result

        # Apply transform if provided
        if parent_transform and result['path_data']:
            result['path_data'] = apply_transform_to_path_data(
                result['path_data'], parent_transform
            )

        result['success'] = True

    except Exception as e:
        result['error'] = str(e)
        result['success'] = False

    return result


def apply_transform_to_path_data(path_data, transform_matrix):
    """
    Apply transformation matrix to SVG path data.

    Args:
        path_data: SVG path data string
        transform_matrix: 6-element transformation matrix [a, b, c, d, e, f]
                         or 2x3 matrix [[a, b, c], [d, e, f]]

    Returns:
        str: Transformed path data string
    """
    if not path_data or not path_data.strip():
        return path_data

    try:
        # Handle both 6-element list and 2x3 matrix formats
        if isinstance(transform_matrix, list) and len(transform_matrix) == 6:
            # Convert [a, b, c, d, e, f] to [[a, b, c], [d, e, f]]
            mat = [[transform_matrix[0], transform_matrix[1], transform_matrix[2]],
                   [transform_matrix[3], transform_matrix[4], transform_matrix[5]]]
        else:
            mat = transform_matrix

        # Parse path to cubic superpath format
        csp = cubicsuperpath.parsePath(path_data)

        # Apply transform using existing utilities
        apply_transform_to_path(mat, csp)

        # Convert back to path data string
        return cubicsuperpath.formatPath(csp)

    except Exception:
        return path_data # If transformation fails, return original path data


def extract_element_transform(element):
    """
    Extract and parse transform attribute from SVG element.

    Args:
        element: lxml element with potential transform attribute

    Returns:
        list or None: 6-element transform matrix or None if no transform
    """
    trans = element.get("transform")
    if trans is None:
        return None

    try:
        transform_matrix = simpletransform.parseTransform(trans)
        return transform_matrix
    except Exception:
        return None


def process_single_svg_element(element, parent_transform=None):
    """
    Complete processing of a single SVG element including transforms.

    Args:
        element: lxml element to process
        parent_transform: Optional parent transformation matrix

    Returns:
        dict: Complete processing result including path data and final transform
    """
    result = {
        'path_data': '',
        'success': False,
        'error': None,
        'element_transform': None,
        'combined_transform': None
    }

    try:
        element_transform = extract_element_transform(element)
        result['element_transform'] = element_transform

        combined_transform = parent_transform
        if parent_transform is not None and element_transform is not None:
            combined_transform = simpletransform.composeTransform(
                                    parent_transform, element_transform)
        elif element_transform is not None:
            combined_transform = element_transform

        result['combined_transform'] = combined_transform

        shape_result = process_shape_to_path_data(element, combined_transform)

        result['path_data'] = shape_result['path_data']
        result['success'] = shape_result['success']
        result['error'] = shape_result['error']

    except Exception as e:
        result['error'] = str(e)
        result['success'] = False

    return result


class DigestSVG:  # pylint: disable=pointless-string-statement
    """
    Main class for parsing SVG document and "digesting" it into a path_objects.DocDigest
    object, a heavily simplified representation of the SVG contents.
    """

    logging_attrs = {"default_handler": message.UserMessageHandler()}
    spew_debugdata = False

    def __init__(self, nd_ref, default_logging=True):
        # Create instance variables

        if default_logging:
            logger.setLevel(logging.INFO)
            logger.addHandler(self.logging_attrs["default_handler"])
        if self.spew_debugdata:
            logger.setLevel(logging.DEBUG)  # by default level is INFO

        self.nd_ref = nd_ref
        self.use_tag_nest_level = 0
        self.current_layer = None
        self.current_layer_name = ""
        self.next_id = 0

        self.doc_digest = path_objects.DocDigest()

        # Variables that will be populated in process_svg():
        self.bezier_tolerance = 0.005   # Bezier tolerance - safe default to prevent infinite loops
        self.layer_selection = -2  # All layers; Matches default from plot_status.py

        self.doc_width_100 = 0
        self.doc_height_100 = 0
        self.diagonal_100 = 0

    def process_svg(self, node_list, digest_params, mat_current=None):
        """
        Wrapper around routine to recursively traverse an SVG document.

        This calls the recursive routine and handles building the digest
        structure around it, as well as reporting any necessary errors.

        Inputs:
        node_list, an lxml etree representing an SVG document
        digest_params, a tuple with additional parameters
        mat_current, a transformation matrix

        From the SVG, build and return a path_objects.DocDigest object; a
        python object representing a simplified SVG document. Once flattened,
        the digest can be converted to a Plob file if desired.

        The Plob (Plot Object) format is a highly-restricted subset of SVG.
            Only layers are allowed in the SVG root. Only polylines are allowed
            in layers. There are no other objects, nor object transformations
            or styles.
        """

        [scale_x, scale_y, self.layer_selection] = digest_params

        self.doc_digest.width = self.nd_ref.svg_width
        self.doc_digest.height = self.nd_ref.svg_height

        self.bezier_tolerance = self.nd_ref.params.curve_tolerance/3  # Initialize

        # Store document information in doc_digest
        self.doc_digest.viewbox = f"0 0 {self.doc_digest.width:f} {self.doc_digest.height:f}"

        self.doc_width_100 = self.doc_digest.width / scale_x    # Width of a "100% width" object
        self.doc_height_100 = self.doc_digest.height / scale_y  # height of a "100% height" object
        self.diagonal_100 = sqrt((self.doc_width_100)**2 + (self.doc_height_100)**2)/sqrt(2)

        docname = node_list.get('{http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd}docname')
        if docname:
            self.doc_digest.name = docname

        root_layer = path_objects.LayerItem()
        # Flag to identify layers containing objects from root
        root_layer.name = '__digest-root__'
        root_layer.item_id = str(self.next_id)
        self.next_id += 1
        self.doc_digest.layers.append(root_layer)

        self.current_layer = root_layer  # Layer that graphical elements should be added to
        self.current_layer_name = root_layer.name

        self.traverse(node_list, None, mat_current)
        return self.doc_digest

    def traverse(self, node_list, parent_style, mat_current):
        """
        Recursively traverse the SVG file and process all of the paths. Keep
        track of the composite transformation applied to each path.

        Inputs:
        node_list, an lxml etree representing an SVG document
        parent_style, dict from inherit_style
        mat_current, a transformation matrix

        This function handles path, group, line, rect, polyline, polygon,
        circle, ellipse  and use (clone) elements. Notable elements not handled
        include text. Unhandled elements should be converted to paths in
        Inkscape or another vector graphics editor.
        """

        if mat_current is None:
            mat_current = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0]]

        for node in node_list:
            node_visibility = node.get('visibility')
            element_style = simplestyle.parseStyle(node.get('style'))

            # Presentation attributes, which have lower precedence than the style attribute:
            if 'fill' not in element_style:         # If the style has not been set...
                element_style['fill'] = node.get('fill')
            if 'stroke' not in element_style:       # If the style has not been set...
                element_style['stroke'] = node.get('stroke')
            if 'fill-rule' not in element_style:    # If the style has not been set...
                element_style['fill-rule'] = node.get('fill-rule')
            # Since these are added to the style dictionary, a potential problem is that
            # these are now treated on equal footing to CSS styling information.

            style_dict = inherit_style(parent_style, element_style, node_visibility)

            if style_dict['display'] == 'none':
                continue  # Do not plot this object or its children
            if node.get('display') == 'none':   # Possible SVG attribute as well
                continue  # Do not plot this object or its children

            # Apply the current matrix transform to this node's transform
            trans = node.get("transform")
            if trans is None:
                mat_new = mat_current
            else:
                mat_new = simpletransform.composeTransform(mat_current,
                                                           simpletransform.parseTransform(trans))

            if node.tag in ('{http://www.w3.org/2000/svg}g', 'g'):
                old_layer_name = self.current_layer_name
                if old_layer_name == '__digest-root__' and\
                    node.get(
                        '{http://www.inkscape.org/namespaces/inkscape}groupmode') == 'layer':
                    # Ensure that sublayers are treated like regular groups only

                    str_layer_name = node.get('{http://www.inkscape.org/namespaces/inkscape}label')
                    if str_layer_name is None:
                        str_layer_name = f"Auto-Layer {self.next_id}"

                    new_layer = path_objects.LayerItem()
                    new_layer.name = str_layer_name
                    new_layer.parse_name()

                    if new_layer.props.skip:
                        continue    # Skip Documentation layer and its contents
                    if self.layer_selection >= 0:  # Plotting in layers mode
                        if new_layer.props.number is None:
                            continue
                        if self.layer_selection != new_layer.props.number:
                            continue  # Skip this layer and its contents

                    new_layer.item_id = str(self.next_id)
                    self.next_id += 1

                    if len(self.current_layer.paths) == 0:
                        del self.doc_digest.layers[-1]

                    self.doc_digest.layers.append(new_layer)
                    self.current_layer = new_layer
                    self.current_layer_name = str(str_layer_name)
                    if self.current_layer.props.handling is None:
                        self.bezier_tolerance = self.nd_ref.params.curve_tolerance/3
                    else:
                        tolerance_temp = models.find_curve_tolerance(self.nd_ref,
                                            self.current_layer.props.handling)
                        if tolerance_temp is not None:
                            self.bezier_tolerance = tolerance_temp/3
                        else:
                            self.bezier_tolerance = self.nd_ref.params.curve_tolerance/3

                    self.traverse(node, style_dict, mat_new)

                    # After parsing a layer, add a new "root layer" for any objects
                    # that may appear in root before the next layer:

                    new_layer = path_objects.LayerItem()
                    new_layer.name = '__digest-root__'  # Label this as a "root" layer
                    new_layer.item_id = str(self.next_id)
                    self.next_id += 1

                    self.doc_digest.layers.append(new_layer)
                    self.current_layer = new_layer
                    self.current_layer_name = new_layer.name
                else:  # Regular group or sublayer that we treat as a group.
                    self.traverse(node, style_dict, mat_new)
                continue

            if node.tag in ('{http://www.w3.org/2000/svg}symbol', 'symbol'):
                # A symbol is much like a group, except that it should only
                #       be rendered when called within a "use" tag.
                if self.use_tag_nest_level > 0:
                    self.traverse(node, style_dict, mat_new)
                continue

            if node.tag in ('{http://www.w3.org/2000/svg}a', 'a'):
                # An 'a' is much like a group, in that it is a generic container element.
                self.traverse(node, style_dict, mat_new)
                continue

            if node.tag in ('{http://www.w3.org/2000/svg}switch', 'switch'):
                # A 'switch' is much like a group, in that it is a generic container element.
                # We are not presently evaluating conditions on switch elements, but parsing
                # their contents to the extent possible.
                self.traverse(node, style_dict, mat_new)
                continue

            if node.tag in ('{http://www.w3.org/2000/svg}use', 'use'):
                """
                A <use> element refers to another SVG element via an xlink:href="#blah"
                attribute.  We will handle the element by doing an XPath search through
                the document, looking for the element with the matching id="blah"
                attribute.  We then recursively process that element after applying
                any necessary (x,y) translation.

                Notes:
                 1. We ignore the height and g attributes as they do not apply to
                    path-like elements, and
                 2. Even if the use element has visibility="hidden", SVG still calls
                    for processing the referenced element.  The referenced element is
                    hidden only if its visibility is "inherit" or "hidden".
                 3. We may be able to unlink clones using the code in pathmodifier.py
                """

                refid = node.get('{http://www.w3.org/1999/xlink}href')
                if refid is not None:
                    # [1:] to ignore leading '#' in reference
                    path = f'//*[@id="{refid[1:]}"]'
                    refnode = node.xpath(path)
                    if refnode is not None:
                        x_val = float(node.get('x', '0'))
                        y_val = float(node.get('y', '0'))
                        # Note: the transform has already been applied
                        if x_val != 0 or y_val != 0:
                            mat_new2 = simpletransform.composeTransform(mat_new,
                                simpletransform.parseTransform(
                                    f'translate({x_val:.6E},{y_val:.6E})'))
                        else:
                            mat_new2 = mat_new
                        self.use_tag_nest_level += 1  # Keep track of nested "use" elements.
                        self.traverse(refnode, style_dict, mat_new2)
                        self.use_tag_nest_level -= 1
                continue

            # End container elements; begin graphical elements.

            if self.layer_selection >= 0:
                if self.current_layer_name == '__digest-root__':
                    continue  # Do not print root elements if layer_selection >= 0

            if style_dict['visibility'] in ('hidden', 'collapse'):
                # Not visible; Do not plot. (This comes after the container tags;
                #   visible children of hidden elements can still plot.)
                continue

            if node.tag == '{http://www.w3.org/2000/svg}path':
                path_d = node.get('d')
                self.digest_path(path_d, style_dict, mat_new)
                continue

            if node.tag in ('{http://www.w3.org/2000/svg}rect', 'rect'):
                path_data = _convert_rect_to_path(node, self.doc_width_100, self.doc_height_100)
                if path_data:  # Empty string for invalid shapes
                    self.digest_path(path_data, style_dict, mat_new)
                continue

            if node.tag in ('{http://www.w3.org/2000/svg}line', 'line'):
                path_data = _convert_line_to_path(node, self.doc_width_100, self.doc_height_100)
                if path_data:
                    self.digest_path(path_data, style_dict, mat_new)
                continue

            if node.tag in ('{http://www.w3.org/2000/svg}polyline', 'polyline',
                            '{http://www.w3.org/2000/svg}polygon', 'polygon'):
                path_data = _convert_polyline_polygon_to_path(node)
                if path_data:  # Empty string for invalid/insufficient points
                    self.digest_path(path_data, style_dict, mat_new)
                continue

            if node.tag in ('{http://www.w3.org/2000/svg}ellipse', 'ellipse',
                            '{http://www.w3.org/2000/svg}circle', 'circle'):
                path_data = _convert_circle_ellipse_to_path(node, self.doc_width_100,
                                                            self.doc_height_100, self.diagonal_100)
                if path_data:  # Empty string for zero radius
                    self.digest_path(path_data, style_dict, mat_new)
                continue

            if node.tag in ('{http://www.w3.org/2000/svg}metadata', 'metadata'):
                self.doc_digest.metadata.update(dict(node.attrib))
                continue

            if node.tag in ('{http://www.w3.org/2000/svg}plotdata', 'plotdata',
                            '{https://bantam.tools/nd}plotdata'):
                self.doc_digest.plotdata.update(dict(node.attrib))
                continue

            if node.tag in ['{http://www.w3.org/2000/svg}defs', 'defs',
                            'namedview',
                            '{http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd}namedview',
                            'eggbot', 'WCB', 'MergeData', '{http://www.w3.org/2000/svg}eggbot',
                            '{http://www.w3.org/2000/svg}WCB',
                            '{http://www.w3.org/2000/svg}MergeData',
                            '{http://www.w3.org/2000/svg}title', 'title',
                            '{http://www.w3.org/2000/svg}desc', 'desc',
                            '{http://www.w3.org/2000/svg}pattern', 'pattern',
                            '{http://www.w3.org/2000/svg}radialGradient', 'radialGradient',
                            '{http://www.w3.org/2000/svg}linearGradient', 'linearGradient',
                            '{http://www.w3.org/2000/svg}style', 'style',  # external style sheet
                            '{http://www.w3.org/2000/svg}cursor', 'cursor',
                            '{http://www.w3.org/2000/svg}font', 'font',
                            '{http://www.w3.org/2000/svg}view', 'view',
                            '{http://www.inkscape.org/namespaces/inkscape}templateinfo',
                            '{http://www.w3.org/2000/svg}color-profile', 'color-profile',
                            '{http://www.w3.org/2000/svg}foreignObject', 'foreignObject',
                            '{https://bantam.tools/ndm}data', etree.Comment]:
                continue

            if node.tag in ('{http://www.w3.org/2000/svg}text', 'text',
                            '{http://www.w3.org/2000/svg}flowRoot', 'flowRoot'):
                self.nd_ref.warnings.add_new('text', self.current_layer_name)
                continue

            if node.tag in ('{http://www.w3.org/2000/svg}image', 'image'):
                self.nd_ref.warnings.add_new('image', self.current_layer_name)
                continue
            if not isinstance(node.tag, str):
                # This is likely an XML processing instruction such as an XML
                # comment. lxml uses a function reference for such node tags
                # and as such the node tag is likely not a printable string.
                # Converting it to a printable string likely won't be useful.
                continue
            text = str(node.tag).split('}')
            self.nd_ref.warnings.add_new(str(text[-1]), self.current_layer_name)

    def digest_path(self, path_d, style_dict, mat_transform, return_pathitem=False):
        """
        Parse the path while applying the matrix transformation mat_transform.
        - Input is the "d" string attribute from an SVG path.
        - Turn this path into a cubicsuperpath (list of beziers).
        - Subdivide the cubic path into a list, rendering to straight segments within tolerance.
        - Identify subpaths within each path
        - Build a path_objects.PathItem object and append it to the current layer

        Args:
            path_d: SVG path "d" attribute string
            style_dict: Dictionary of style properties
            mat_transform: Transformation matrix to apply
            return_pathitem: If True, return the PathItem instead of appending to layer

        Returns:
            PathItem object if return_pathitem=True, otherwise None (existing behavior)
        """

        if path_d is None:
            return
        path_d = path_d.strip()
        if path_d == "":
            return

        parsed_path = cubicsuperpath.CubicSuperPath(simplepath.parsePath(path_d))

        if len(parsed_path) == 0:  # path length is zero, will not be plotted
            return

        # Apply the transformation to each point
        apply_transform_to_path(mat_transform, parsed_path)

        subpaths = []

        # p is now a list of lists of cubic beziers [control pt1, control pt2, endpoint]
        # where the start-point is the last point in the previous segment.
        for subpath in parsed_path:  # for subpaths in the path:
            # Divide each path into a set of straight segments:
            plot_utils.subdivideCubicPath(subpath, self.bezier_tolerance)

            if len(subpath) < 2:
                continue        # At least two points required for a path
            # Pick out vertex location information from cubic bezier curve:
            subpaths.append([[vertex[1][0], vertex[1][1]] for vertex in subpath])

        if len(subpaths) == 0:
            return  # At least one sub-path required

        new_path = path_objects.PathItem()
        new_path.fill = style_dict['fill']
        new_path.stroke = style_dict['stroke']
        new_path.fill_rule = style_dict['fill-rule']
        new_path.item_id = str(self.next_id)
        self.next_id += 1

        new_path.subpaths = subpaths

        ok_to_fill = False
        for subpath in subpaths:
            if len(subpath) != 2:
                ok_to_fill = True   # As long as at least one path has more than two vertices
                break
        if not ok_to_fill:
            new_path.fill = None  # Strip fill, if path has only 2-vertex subpaths

        # Return the PathItem if requested
        if return_pathitem:
            return new_path
        else:  # Add new list of subpaths to the current "LayerItem" element:
            self.current_layer.paths.append(new_path)


def apply_transform_to_path(mat, path):
    '''
    A very slightly faster version of simpletransform.applyTransformToPath()
    Possibly move this function to plotink in the future.
    '''
    [mt00, mt01, mt02], [mt10, mt11, mt12] = mat
    for comp in path:
        for ctl in comp:
            for point in ctl:  # apply transform to each point:
                pt_x = point[0]
                pt_y = point[1]
                point[0] = mt00*pt_x + mt01*pt_y + mt02
                point[1] = mt10*pt_x + mt11*pt_y + mt12


def inherit_style(parent_style, node_style, visibility):
    '''
    Parse style dict of node for fill and stroke information only.
    Inherit style from parent, but supersede it when a local style is defined.
    Also handle precedence of SVG "visibility" attribute, separate from the style.
    Note that children of hidden parents may be plotted if they assert visibility.
    '''
    default_style = {}
    default_style['fill'] = None
    default_style['stroke'] = None
    default_style['fill-rule'] = None
    default_style['visibility'] = 'visible'
    default_style['display'] = None  # A null value; not "display:none".

    if parent_style is None:  # Use default values when there is no parent
        parent_style = default_style

    # Use copy, not assignment, so that new_style represents an independent dict:
    new_style = parent_style.copy()

    if visibility:  # Update first, allowing it to be overruled by style attributes
        new_style['visibility'] = visibility

    if node_style is None:  # No additional new style information provided.
        return new_style

    for attrib in ['fill', 'stroke', 'fill-rule', 'visibility', 'display',]:
        # Valid for "string" attributes that DO NOT have units that need scaling;
        # Do not extend this to other style attributes without accounting for that.
        value = node_style.get(attrib)  # Defaults to None, preventing KeyError
        if value:
            if value in ['inherit']:
                new_style[attrib] = parent_style[attrib]
            else:
                new_style[attrib] = value

    return new_style


def verify_plob(svg, model, bounds, svg_width, svg_height):
    """
    Check to see if the provided SVG is a valid plob that can be automatically converted
    to a plot digest object. Also check that the plob version and hardware model match.
    Styles are *presently allowed* in the plob, for the sake of verification. (They can
        increase the file size, but do not otherwise cause harm.)

    Returns True or False.

    We may wish to also check for an application name match in the
    future. At present, that check is not yet necessary.
    """

    data_node = None
    # First try to find plotdata element using the current namespace (NextDraw v1.4+)
    nodes = svg.findall('{https://bantam.tools/nd}plotdata')
    if not nodes:
        # Fall back to legacy plotdata element formats for backward compatibility
        # This supports plob files created by older versions of the software
        # that stored plotdata in different namespaces or without namespaces
        nodes = svg.xpath("//*[self::svg:plotdata|self::plotdata]", namespaces=inkex.NSS)
    if nodes:
        data_node = nodes[0]
    if data_node is not None:
        try:
            svg_model = data_node.get('model')
            svg_plob_version = data_node.get('plob_version')
        except TypeError:
            return False
    else:
        return False  # No plot data; Plob cannot be verified.
    if not svg_model:
        return False
    if svg_plob_version:
        if svg_plob_version != path_objects.PLOB_VERSION:
            return False
    else:
        return False

    # Add viewBox format validation
    viewbox = svg.get('viewBox')
    if viewbox is not None:  # Check if attribute exists (including empty string)
        if not viewbox.strip():  # Empty or whitespace-only string
            return False  # Empty viewBox is invalid
        try:
            coords = [float(x) for x in viewbox.split()]
            if len(coords) != 4 or coords[2] <= 0 or coords[3] <= 0:
                return False  # Invalid viewBox: must have 4 coords with positive width/height
        except (ValueError, TypeError):
            return False  # Malformed viewBox attribute

    # inkex.errormsg( "Passed plotdata checks") # Optional halfwaypoint check
    for node in svg:
        if node.tag in ['g', '{http://www.w3.org/2000/svg}g']:
            name_temp = node.get('{http://www.inkscape.org/namespaces/inkscape}label')
            if name_temp is None:
                # Allow empty groups with no plottable content per PLOB policy
                # Recursively check for any graphical elements that could affect plotting
                def has_plottable_elements(element):
                    # Check if this element itself is plottable
                    if element.tag in [
                        'polyline', '{http://www.w3.org/2000/svg}polyline',
                        'path', '{http://www.w3.org/2000/svg}path',
                        'line', '{http://www.w3.org/2000/svg}line',
                        'rect', '{http://www.w3.org/2000/svg}rect',
                        'circle', '{http://www.w3.org/2000/svg}circle',
                        'ellipse', '{http://www.w3.org/2000/svg}ellipse',
                        'polygon', '{http://www.w3.org/2000/svg}polygon'
                    ]:
                        return True
                    # Recursively check children
                    for child in element:
                        if has_plottable_elements(child):
                            return True
                    return False

                if has_plottable_elements(node):
                    return False  # Groups with plottable content must be named
                continue  # Skip empty groups (they can't plot anyway)
            if len(str(name_temp)) > 0:
                if str(name_temp)[0] == '%':
                    continue  # Skip Documentation layers and their contents
            if node.get("transform"):  # No transforms are allowed on plottable layers
                return False
            for subnode in node:
                if subnode.get("transform"):  # No transforms are allowed on objects
                    return False
                if subnode.tag in ['polyline', '{http://www.w3.org/2000/svg}polyline']:
                    continue
                return False
        elif node.tag in ['{http://www.w3.org/2000/svg}defs', 'defs', 'metadata',
                          '{http://www.w3.org/2000/svg}metadata',
                          '{http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd}namedview',
                          'plotdata', '{http://www.w3.org/2000/svg}plotdata',
                          '{https://bantam.tools/ndm}data', '{https://bantam.tools/nd}plotdata']:
            continue
        else:
            return False

    if int(svg_model) == model:
        return True
    else:
        # We have a special case at this point: This *is* a valid plob, but does not have an
        # exact match to the specified plotter model. At this point, check to see if the SVG's
        # width and height fit within the selected model's travel bounds. If so, approve this
        # as a valid plob for the purposes of plotting.

        [[_xzero, _yzero], [x_travel, y_travel]] = bounds
        if (svg_width <= x_travel) and (svg_height <= y_travel):
            return True

    return False
