#!/usr/bin/env python3

import xml.etree.ElementTree as ET
import re
import math

def tokenize_path_data(d_attribute):
    """
    Splits an SVG path 'd' attribute string into a list of tokens:
      - Single-letter commands (M, m, L, l, C, c, etc.)
      - Float values (including possible scientific notation)
    Returns a list of strings/float, e.g. ['M', 10.0, 20.0, 'L', 30.5, 40.7, 'Z'].
    """
    # Regex explanation:
    #   ([MmZzLlHhVvCcSsQqTtAa])  => one of the valid path command letters
    #   |                        => OR
    #   ([+-]?\d+(\.\d+)?([eE][+-]?\d+)?) => a number that may include optional +/-,
    #                                       decimals, and scientific notation
    token_pattern = re.compile(r'([MmZzLlHhVvCcSsQqTtAa])|([+-]?\d+(\.\d+)?([eE][+-]?\d+)?)')
    raw_tokens = token_pattern.findall(d_attribute)

    # raw_tokens is a list of tuples from the capturing groups. We only need one
    # of those fields from each match. We'll filter out empty strings and parse floats.
    tokens = []
    for match in raw_tokens:
        # match is a tuple: (command_letter, number_string, ..., ...)
        command_letter = match[0]
        number_string = match[1]
        if command_letter:
            # It's a path command like 'M', 'L', etc.
            tokens.append(command_letter)
        else:
            # It's a numeric value
            tokens.append(float(number_string))

    return tokens

def extract_paths_from_svg(svg_data):
    """
    Parses an SVG file and extracts the 'd' attribute from
    all <path> elements in order. Returns a list of token lists.
    """
    root = ET.fromstring(svg_data)

    # Inkscape / Illustrator SVGs often have a default namespace.
    # You may need to adjust the namespace if the <path> tags are qualified.
    # For example:
    #   namespace = {'svg': 'http://www.w3.org/2000/svg'}
    #   for path_elem in root.findall('.//svg:path', namespace):
    #       ...
    # If your SVGs do not use a default namespace, the below should work.

    all_token_lists = []
    for path_elem in root.findall('.//{http://www.w3.org/2000/svg}path'):
        d_attribute = path_elem.get('d')
        if d_attribute:
            tokens = tokenize_path_data(d_attribute)
            all_token_lists.append(tokens)

    # If your SVG has no default namespace or if you removed it, you might do:
    #   for path_elem in root.findall('.//path'):
    #       d_attribute = path_elem.get('d')
    #       ...
    #
    # Adjust as appropriate depending on your actual SVG structure/namespaces.

    return all_token_lists

def normalize_tokens(tokens):
    """
    Normalize path tokens for structural comparison.
    Currently this drops degenerate linear segments:
      - L/l where endpoint equals current point
      - H/h where x stays unchanged
      - V/v where y stays unchanged
    """
    out = []
    i = 0
    cmd = None
    cx = 0.0
    cy = 0.0
    sx = 0.0
    sy = 0.0
    have_current = False

    def gather_numbers(start):
        j = start
        vals = []
        while j < len(tokens) and not isinstance(tokens[j], str):
            vals.append(tokens[j])
            j += 1
        return vals, j

    while i < len(tokens):
        token = tokens[i]
        if isinstance(token, str):
            cmd = token
            i += 1
        elif cmd is None:
            # Malformed path: no command to apply.
            return tokens

        if cmd in ('Z', 'z'):
            out.append(cmd)
            if have_current:
                cx, cy = sx, sy
            continue

        values, i = gather_numbers(i)

        if cmd in ('M', 'm'):
            if len(values) < 2:
                return tokens

            # First pair is moveto and cannot be dropped.
            out.extend([cmd, values[0], values[1]])
            if cmd == 'M':
                cx, cy = values[0], values[1]
            else:
                if have_current:
                    cx += values[0]
                    cy += values[1]
                else:
                    cx, cy = values[0], values[1]
            sx, sy = cx, cy
            have_current = True

            # Remaining pairs are implicit lineto.
            kept = []
            k = 2
            while k + 1 < len(values):
                x = values[k]
                y = values[k + 1]
                if cmd == 'M':
                    nx, ny = x, y
                else:
                    nx, ny = cx + x, cy + y

                if nx != cx or ny != cy:
                    kept.extend([x, y])
                    cx, cy = nx, ny
                k += 2

            out.extend(kept)
            continue

        if cmd in ('L', 'l'):
            kept = []
            k = 0
            while k + 1 < len(values):
                x = values[k]
                y = values[k + 1]
                if cmd == 'L':
                    nx, ny = x, y
                else:
                    nx, ny = cx + x, cy + y

                if nx != cx or ny != cy:
                    kept.extend([x, y])
                    cx, cy = nx, ny
                k += 2

            if kept:
                out.append(cmd)
                out.extend(kept)
            continue

        if cmd in ('H', 'h'):
            kept = []
            for x in values:
                nx = x if cmd == 'H' else cx + x
                if nx != cx:
                    kept.append(x)
                    cx = nx
            if kept:
                out.append(cmd)
                out.extend(kept)
            continue

        if cmd in ('V', 'v'):
            kept = []
            for y in values:
                ny = y if cmd == 'V' else cy + y
                if ny != cy:
                    kept.append(y)
                    cy = ny
            if kept:
                out.append(cmd)
                out.extend(kept)
            continue

        # For other commands, keep original parameters and just track current point.
        if cmd in ('C', 'c'):
            if values:
                out.append(cmd)
                out.extend(values)
            k = 0
            while k + 5 < len(values):
                if cmd == 'C':
                    cx, cy = values[k + 4], values[k + 5]
                else:
                    cx += values[k + 4]
                    cy += values[k + 5]
                k += 6
            have_current = True
            continue

        if cmd in ('S', 's'):
            if values:
                out.append(cmd)
                out.extend(values)
            k = 0
            while k + 3 < len(values):
                if cmd == 'S':
                    cx, cy = values[k + 2], values[k + 3]
                else:
                    cx += values[k + 2]
                    cy += values[k + 3]
                k += 4
            have_current = True
            continue

        if cmd in ('Q', 'q'):
            if values:
                out.append(cmd)
                out.extend(values)
            k = 0
            while k + 3 < len(values):
                if cmd == 'Q':
                    cx, cy = values[k + 2], values[k + 3]
                else:
                    cx += values[k + 2]
                    cy += values[k + 3]
                k += 4
            have_current = True
            continue

        if cmd in ('T', 't'):
            if values:
                out.append(cmd)
                out.extend(values)
            k = 0
            while k + 1 < len(values):
                if cmd == 'T':
                    cx, cy = values[k], values[k + 1]
                else:
                    cx += values[k]
                    cy += values[k + 1]
                k += 2
            have_current = True
            continue

        if cmd in ('A', 'a'):
            if values:
                out.append(cmd)
                out.extend(values)
            k = 0
            while k + 6 < len(values):
                if cmd == 'A':
                    cx, cy = values[k + 5], values[k + 6]
                else:
                    cx += values[k + 5]
                    cy += values[k + 6]
                k += 7
            have_current = True
            continue

        # Unknown command: keep as-is.
        out.append(cmd)
        out.extend(values)

    return out

def compare_token_lists_exact(tokens_a, tokens_b):
    """
    Compares two lists of path tokens (commands and floats).
    Returns True if they match (same commands in same positions,
    numeric values within given tolerance), otherwise False.
    """
    if len(tokens_a) != len(tokens_b):
        return None  # Different lengths => not a match

    max_diff = 0
    for a, b in zip(tokens_a, tokens_b):
        if isinstance(a, str) and isinstance(b, str):
            # Must match exactly the same command letter
            if a != b:
                return None
        elif isinstance(a, float) and isinstance(b, float):
            # Compare numeric values
            diff = abs(a - b)
            max_diff = max(max_diff, diff)
        else:
            # One is command, the other is float => mismatch
            return None

    return max_diff

def compare_token_lists(tokens_a, tokens_b):
    """
    Compare path token streams with a fallback that ignores degenerate
    linear segments (no-op L/l/H/h/V/v) when needed to resolve
    structural mismatches.
    """
    ret = compare_token_lists_exact(tokens_a, tokens_b)
    if ret is not None:
        return ret

    return compare_token_lists_exact(normalize_tokens(tokens_a),
                                     normalize_tokens(tokens_b))

def compare_svg_files(svg_file_1, svg_file_2):
    """
    Compares two SVG files to check if they have the same number of <path> elements,
    and each corresponding path has the same structure of commands.
    Return max difference between respective numeric values in the paths.
    """

    svg_data_1 = open(svg_file_1).read()
    svg_data_2 = open(svg_file_2).read()

    # If contents match exactly, return 0
    if svg_data_1 == svg_data_2:
        return 0

    paths1 = extract_paths_from_svg(svg_data_1)
    paths2 = extract_paths_from_svg(svg_data_2)

    # Check that we have the same number of <path> elements
    if len(paths1) != len(paths2):
        return None

    # Compare each path token list
    max_diff = 0
    for tokens1, tokens2 in zip(paths1, paths2):
        ret = compare_token_lists(tokens1, tokens2)
        if ret is None:
            return ret
        max_diff = max(max_diff, ret)

    return max_diff


if __name__ == "__main__":
    import sys

    if '--help' in sys.argv[1:]:
        print("Usage: hb-svg-compare TOLERANCE < file_with_svg_pairs.txt")
        sys.exit(1)

    tolerance = 0
    if len(sys.argv) > 1:
        tolerance = float(sys.argv[1])

    # Read all lines of two SVG file paths from stdin and compare
    for line in sys.stdin:
        svg1, svg2 = line.strip().split()
        diff = compare_svg_files(svg1, svg2)

        if diff is None:
            diff = "DIFF"
        elif diff <= tolerance:
            continue
        else:
            diff = f"{diff:g}"

        print(f"{diff}\t{svg1}\t{svg2}")
        # Flush stdout to make sure output is immediate
        sys.stdout.flush()
