
import math
from itertools import zip_longest
import subprocess
import re

import bs4

from .utils import tmpfile, print_on_error
from .image_support import run_cargo_cmd, svg_soup

from gerbonara import graphic_objects as go
from gerbonara.utils import MM, arc_bounds, sum_bounds
from gerbonara.layers import LayerStack
from gerbonara.cad.kicad.sexp import build_sexp, Atom
from gerbonara.cad.kicad.sexp_mapper import sexp
from gerbonara.cad.kicad.footprints import Footprint, FootprintInstance, LAYER_MAP_G2K
from gerbonara.cad.kicad.layer_colors import KICAD_LAYER_COLORS, KICAD_DRILL_COLORS


def test_parse(kicad_mod_file):
    Footprint.open_mod(kicad_mod_file)


def test_round_trip(kicad_mod_file, tmpfile):
    print('========== Stage 1 load ==========')
    orig_fp = Footprint.open_mod(kicad_mod_file)
    print('========== Stage 1 save ==========')
    stage1_sexp = build_sexp(orig_fp.sexp())
    tmp_fp_gen1 = tmpfile('First generation output', '.kicad_mod')
    tmp_fp_gen1.write_text(stage1_sexp)

    print('========== Stage 2 load ==========')
    reparsed_fp = Footprint.parse(stage1_sexp)
    print('========== Stage 2 save ==========')
    stage2_sexp = build_sexp(reparsed_fp.sexp())
    tmp_fp_gen2 = tmpfile('Second generation output', '.kicad_mod')
    tmp_fp_gen2.write_text(stage2_sexp)
    print('========== Checks ==========')

    for stage1, stage2 in zip_longest(stage1_sexp.splitlines(), stage2_sexp.splitlines()):
        assert stage1 == stage2

    return

    original = re.sub(r'\(', '\n(', re.sub(r'\s+', ' ', kicad_mod_file.read_text()))
    original = re.sub(r'\) \)', '))', original)
    original = re.sub(r'\) \)', '))', original)
    original = re.sub(r'\) \)', '))', original)
    original = re.sub(r'\) \)', '))', original)
    stage1 = re.sub(r'\(', '\n(', re.sub(r'\s+', ' ', stage1_sexp))
    for original, stage1 in zip_longest(original.splitlines(), stage1.splitlines()):
        if original.startswith('(version'):
            continue

        original, stage1 = original.strip(), stage1.strip()
        if original != stage1:
            if any(original.startswith(f'({foo}') for foo in ['arc', 'circle', 'rectangle', 'polyline', 'text']):
                # These files have symbols with graphic primitives in non-standard order
                return

            if original.startswith('(symbol') and stage1.startswith('(symbol'):
                # Re-export can change symbol order. This is ok.
                return

            if original.startswith('(at') and stage1.startswith('(at'):
                # There is some disagreement as to whether rotation angles are ints or floats, and the spec doesn't say.
                return

            assert original == stage1
    

# Regrettably, we have to re-implement a significant part of the SVG spec to fix up the SVGs that kicad-cli produces.

def _compute_style(elem):
    current_style = {}
    for elem in [*reversed(list(elem.parents)), elem]:
        attrs = dict(elem.attrs)
        for match in re.finditer(r'([^:;]+):([^:;]+)', attrs.pop('style', '')):
            k, v = match.groups()
            current_style[k.strip().lower()] = v.strip()

        for k, v in elem.attrs.items():
            current_style[k.lower()] = v
    return current_style


def _parse_path_d(path):
    path_d = path.get('d')
    if not path_d:
        return

    style = _compute_style(path)
    if style.get('stroke', 'none') != 'none':
        sr = float(style.get('stroke-width', 0)) / 2
    else:
        sr = 0

    if 'C' in path_d:
        raise ValueError('Path contains cubic beziers')

    last_x, last_y = None, None
    # NOTE: kicad-cli exports oddly broken svgs. One of  the weirder issues is that in some paths, the "L" command is
    # simply ommitted.
    for match in re.finditer(r'([ML]?) ?([0-9.]+) *,? *([0-9.]+)|(A) ?([0-9.]+) *,? *([0-9.]+) *,? *([0-9.]+) *,? * ([01]) *,? *([01]) *,? *([0-9.]+) *,? *([0-9.]+)', path_d):
        ml, x, y, a, rx, ry, angle, large_arc, sweep, ax, ay = match.groups()

        if ml or not a:
            x, y = float(x), float(y)
            last_x, last_y = x, y
            yield x-sr, y-sr
            yield x-sr, y+sr
            yield x+sr, y-sr
            yield x+sr, y+sr

        else: # a
            rx, ry = float(rx), float(ry)
            ax, ay = float(ax), float(ay)
            angle = float(angle)
            large_arc = bool(int(large_arc))
            sweep = bool(int(sweep))

            if not math.isclose(rx, ry, abs_tol=1e-6):
                raise ValueError("Elliptical arcs not supported. How did that end up here? KiCad can't do those either!")

            mx = (last_x + ax)/2
            my = (last_y + ay)/2
            dx = ax - last_x
            dy = ay - last_y
            l = math.hypot(dx, dy)

            # clockwise normal
            nx = -dy/l
            ny = dx/l
            arg = rx**2 - (l/2)**2
            if arg < 0 or math.isclose(arg, 0, abs_tol=1e-6):
                cx, cy = mx, my
            else:
                nl = math.sqrt(arg)
                if sweep != large_arc:
                    cx = mx + nx*nl
                    cy = my + ny*nl
                else:
                    cx = mx - nx*nl
                    cy = my - ny*nl

            (min_x, min_y), (max_x, max_y) = arc_bounds(last_x, last_y, ax, ay, cx, cy, clockwise=(not sweep))
            min_x -= sr
            min_y -= sr
            max_x += sr
            max_y += sr
            # dbg_i += 1
            # with open(f'/tmp/dbg-arc-{dbg_i}.svg', 'w') as f:
            #     vbx, vby = min(last_x, ax), min(last_y, ay)
            #     vbw, vbh = max(last_x, ax), max(last_y, ay)
            #     vbw -= vbx
            #     vbh -= vby
            #     k = 2
            #     vbx -= vbw*k
            #     vby -= vbh*k
            #     vbw *= 2*k+1
            #     vbh *= 2*k+1
            #     sw = min(vbw, vbh)*1e-3
            #     mr = 3*sw
            #     f.write('<?xml version="1.0" standalone="no"?>\n')
            #     f.write('<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">\n')
            #     f.write(f'<svg version="1.1" width="200mm" height="200mm" viewBox="{vbx} {vby} {vbw} {vbh}" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">>\n')
            #     f.write(f'<path fill="none" stroke="#ff00ff" stroke-width="{sw}" d="{path_d}"/>\n')
            #     f.write(f'<rect fill="none" stroke="#404040" stroke-width="{sw}" x="{min_x}" y="{min_y}" width="{max_x-min_x}" height="{max_y-min_y}"/>\n')
            #     f.write(f'<circle fill="none" r="{mr}" stroke="blue" stroke-width="{sw}" cx="{last_x}" cy="{last_y}"/>\n')
            #     f.write(f'<circle fill="none" r="{mr}" stroke="blue" stroke-width="{sw}" cx="{ax}" cy="{ay}"/>\n')
            #     f.write(f'<circle fill="none" r="{mr}" stroke="red" stroke-width="{sw}" cx="{mx}" cy="{my}"/>\n')
            #     f.write(f'<circle fill="none" r="{mr}" stroke="red" stroke-width="{sw}" cx="{cx}" cy="{cy}"/>\n')
            #     f.write('</svg>\n')
            yield min_x, min_y
            yield min_x, max_y
            yield max_x, min_y
            yield max_x, max_y
            last_x, last_y = ax, ay

def test_render(kicad_mod_file, tmpfile, print_on_error, img_support):
    # These files have a large, mask-only pad that has a large solder mask margin set. Kicad doesn't render the margin
    # at all, which I think it should. We render things exactly as I'd expect.
    if kicad_mod_file.name in ['Fiducial_classic_big_CopperBottom_Type2.kicad_mod',
                               'Fiducial_classic_big_CopperTop_Type2.kicad_mod',
                               'Fiducial_classic_big_SilkscreenTop_Type2.kicad_mod']:
        return

    # Hide text and remove text from KiCad's renders. Our text rendering is alright, but KiCad has some weird issue
    # where it seems to mis-calculate the bounding box of stroke font text, leading to a wonky viewport not matching the
    # actual content, and text that is slightly off from where it should be. The difference is only a few hundred
    # micrometers, but it's enough to really throw off our error calculation, so we just ignore text.
    fp = FootprintInstance(0, 0, sexp=Footprint.open_mod(kicad_mod_file), hide_text=True)

    stack = LayerStack(courtyard=True, fabrication=True, adhesive=True)
    stack.add_layer('mechanical drawings')
    stack.add_layer('mechanical comments')
    fp.render(stack)
    color_map = {f'{side} {use}': KICAD_LAYER_COLORS[kicad_id] for (side, use), kicad_id in LAYER_MAP_G2K.items()}
    color_map['drill pth'] = (255, 255, 255, 1)
    color_map['drill npth'] = (255, 255, 255, 1)
    # Remove alpha since overlaid shapes won't work correctly with non-1 alpha without complicated svg filter hacks
    color_map = {key: (f'#{r:02x}{g:02x}{b:02x}', '1') for key, (r, g, b, _a) in color_map.items()}

    margin = 10 # mm

    layer = stack[('top', 'courtyard')]
    bounds = []
    #print('===== BOUNDS =====')
    for obj in layer.objects:
        if isinstance(obj, (go.Line, go.Arc)):
            bbox = (min_x, min_y), (max_x, max_y) = obj.bounding_box(unit=MM)
            #import textwrap
            #print(f'{min_x: 3.6f} {min_y: 3.6f} {max_x: 3.6f} {max_y: 3.6f}', '\n'.join(textwrap.wrap(str(obj), width=80, subsequent_indent=' '*(3+4*(3+1+6)))))
            bounds.append(bbox)
    #print('===== END =====')

    if not bounds:
        print('Footprint has no paths on courtyard layer')
        return

    bounds = sum_bounds(bounds)
    (min_x, min_y), (max_x, max_y) = bounds
    w, h = max_x-min_x, max_y-min_y
    print_on_error('Gerbonara bounds:', bounds, f'w={w:.6f}', f'h={h:.6f}')

    out_svg = tmpfile('Output', '.svg')
    out_svg.write_text(str(stack.to_svg(colors=color_map, force_bounds=bounds, margin=margin)))

    print_on_error('Input footprint:', kicad_mod_file)
    ref_svg = tmpfile('Reference render', '.svg')
    img_support.kicad_fp_export(kicad_mod_file, ref_svg)

    # KiCad's bounding box calculation for SVG output looks broken, and the resulting files have viewports that are too
    # large. We align our output and KiCad's output using the footprint's courtyard layer.
    points = []
    with svg_soup(ref_svg) as soup:
        for group in soup.find_all('g'):
            style = group.get('style', '').lower().replace(' ', '')
            if 'fill:#ff26e2' not in style or 'stroke:#ff26e2' not in style:
                continue

            # This group contains courtyard layer items.
            for path in group.find_all('path'):
                points += _parse_path_d(path)

        if not points:
            print('Footprint has no paths on courtyard layer')
            return

        min_x = min(x for x, y in points)
        min_y = min(y for x, y in points)
        max_x = max(x for x, y in points)
        max_y = max(y for x, y in points)
        print_on_error('KiCad bounds:', ((min_x, min_y), (max_x, max_y)), f'w={max_x-min_x:.6f}', f'h={max_y-min_y:.6f}')
        min_x -= margin
        min_y -= margin
        max_x += margin
        max_y += margin
        w, h = max_x-min_x, max_y-min_y

        root = soup.find('svg')
        root_w = root['width'] = f'{w:.6f}mm'
        root_h = root['height'] = f'{h:.6f}mm'
        root['viewBox'] = f'{min_x:.6f} {min_y:.6f} {w:.6f} {h:.6f}'

        # nuke text since kicad-cli's text positioning looks sligthly wonky and we failed to replicate that wonkyness
        # exactly. 
        for elem in soup.find_all('g', attrs={'class': 'stroked-text'}):
            elem.decompose()

        for elem in soup.find_all('text'):
            elem.decompose()

    # Currently, there is a bug in resvg leading to mis-rendering. On the file below from the KiCad standard lib, resvg
    # renders all round pads in a wrong color (?). Interestingly, passing the file through usvg before rendering fixes
    # this.
    # Sample footprint: Connector_PinSocket_2.00mm.pretty/PinSocket_2x11_P2.00mm_Vertical.kicad_mod
    run_cargo_cmd('usvg', [str(ref_svg), str(ref_svg)])

    with svg_soup(ref_svg) as soup:
        # fix up usvg width/height
        root = soup.find('svg')
        root['width'] = root_w
        root['height'] = root_h

        #for elem in root.find_all('path'):
        #    if elem.attrs.get('fill', '').lower() == '#d864ff' and math.isclose(float(elem.attrs.get('fill-opacity', 0)), 0.4): 
        #        elem.decompose()

        # remove alpha to avoid complicated filter hacks
        for elem in root.descendants:
            if not isinstance(elem, bs4.Tag):
                continue

            if elem.has_attr('opacity'):
                elem['opacity'] = '1'

            if elem.has_attr('fill-opacity'):
                elem['fill-opacity'] = '1'

            if elem.has_attr('stroke-opacity'):
                elem['stroke-opacity'] = '1'

        # kicad-cli incorrectly fills arcs
        for elem in root.find_all('path'):
            if ' C ' in elem.get('d', '') and elem.get('stroke', 'none') != 'none':
                elem['fill'] = 'none'

    # Move fabrication layers above drills because kicad-cli's svg rendering is wonky.
    with svg_soup(out_svg) as soup:
        root = soup.find('svg')
        root.append(soup.find('g', id='l-bottom-fabrication').extract())
        root.append(soup.find('g', id='l-top-fabrication').extract())

    img_support.svg_to_png(ref_svg, tmpfile('Reference render', '.png'), bg=None, dpi=600)
    img_support.svg_to_png(out_svg, tmpfile('Output render', '.png'), bg=None, dpi=600)
    mean, _max, hist = img_support.svg_difference(ref_svg, out_svg, dpi=600, diff_out=tmpfile('Difference', '.png'))

    # compensate for circular pads aliasing badly
    aliasing_artifacts =  1e-3 * len(fp.sexp.pads)/10
    assert mean < 3e-3 + aliasing_artifacts
    assert hist[9] < 100
    assert hist[3:].sum() < (1e-3 + 10*aliasing_artifacts)*hist.size
    
