#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# Copyright 2026 Jan Sebastian Götte <gerbonara@jaseg.de>
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# Based on https://github.com/tracespace/tracespace
#

import math
from contextlib import contextmanager

from PIL import Image
import pytest

from gerbonara.rs274x import GerberFile
from gerbonara.graphic_objects import Line, Arc, Flash, Region
from gerbonara.apertures import CircleAperture, RectangleAperture, ObroundAperture, PolygonAperture
from gerbonara.cam import FileSettings
from gerbonara.utils import MM, Inch

from .image_support import svg_soup
from .utils import *

@contextmanager
def object_test(tmpfile, img_support, epsilon=1e-4):
    gbr = GerberFile()

    yield gbr

    gbr.offset(25.0, 25.0)

    bounds = (0.0, 0.0), (2.0, 2.0) # bottom left, top right

    # The below code is mostly copy-pasted from test_rs274x.py.

    out_svg = tmpfile('SVG Output', '.svg')
    with open(out_svg, 'w') as f:
        # Use inch units here to make sure we and gerbv agree on the exact pixel size of the output since both calculate
        # it from the DPI setting.
        f.write(str(gbr.to_svg(force_bounds=bounds, arg_unit='inch', fg='black', bg='white')))

    # test primitive to_arc_poly
    arc_poly_gbr = GerberFile()
    arc_poly_approx_gbr = GerberFile()
    for obj in gbr.objects:
        for primitive in obj.to_primitives(MM):
            poly = primitive.to_arc_poly()
            region = Region.from_arc_poly(poly)
            arc_poly_gbr.objects.append(region)

            # Regression test for gitlab issue #17
            assert primitive.polarity_dark == poly.polarity_dark
            assert region.polarity_dark == poly.polarity_dark

            # Test for arc poly arc approximation, and regression test for gitlab issue #16
            region = Region.from_arc_poly(poly.approximate_arcs())
            arc_poly_approx_gbr.objects.append(region)

    arc_poly_svg = tmpfile('ArcPoly SVG Output', '.svg')
    with open(arc_poly_svg, 'w') as f:
        f.write(str(arc_poly_gbr.to_svg(force_bounds=bounds, arg_unit='inch', fg='black', bg='white')))

    arc_poly_png = tmpfile('ArcPoly conversion render', '.png')
    img_support.svg_to_png(arc_poly_svg, arc_poly_png, dpi=300, bg='white')

    # Arc approximation test
    arc_poly_approx_svg = tmpfile('ArcPoly Approximation SVG Output', '.svg')
    with open(arc_poly_approx_svg, 'w') as f:
        f.write(str(arc_poly_approx_gbr.to_svg(force_bounds=bounds, arg_unit='inch', fg='black', bg='white')))

    arc_poly_approx_png = tmpfile('ArcPoly Approximation conversion render', '.png')
    img_support.svg_to_png(arc_poly_approx_svg, arc_poly_approx_png, dpi=300, bg='white')

    # Reference export via gerber through GerbV
    out_gbr = tmpfile('GBR Output', '.gbr')
    gbr.save(out_gbr)

    # NOTE: Instead of having gerbv directly export a PNG, we ask gerbv to output SVG which we then rasterize using
    # resvg. We have to do this since gerbv's built-in cairo-based PNG export has severe aliasing issues. In contrast,
    # using resvg for both allows an apples-to-apples comparison of both results.
    ref_svg = tmpfile('Reference export', '.svg')
    ref_png = tmpfile('Reference render', '.png')
    img_support.gerbv_export(out_gbr, ref_svg, origin=bounds[0], size=bounds[1], fg='#000000', bg='#ffffff')
    with svg_soup(ref_svg) as soup:
        img_support.cleanup_gerbv_svg(soup)
    img_support.svg_to_png(ref_svg, ref_png, dpi=300, bg='white')

    out_png = tmpfile('Output render', '.png')
    img_support.svg_to_png(out_svg, out_png, dpi=300, bg='white')

    mean, _max, hist = img_support.image_difference(ref_png, out_png, diff_out=tmpfile('Difference', '.png'))
    assert hist[9] < 1
    assert mean < epsilon
    assert hist[3:].sum() < epsilon*hist.size

    mean, _max, hist = img_support.image_difference(out_png, arc_poly_png, diff_out=tmpfile('ArcPoly Difference', '.png'))
    assert hist[9] < 1
    assert mean < epsilon
    assert hist[3:].sum() < epsilon*hist.size

    mean, _max, hist = img_support.image_difference(out_png, arc_poly_approx_png, diff_out=tmpfile('ArcPoly Approximation Difference', '.png'))
    assert hist[9] < 1
    assert mean < epsilon
    assert hist[3:].sum() < epsilon*hist.size

@pytest.mark.parametrize('angle_deg', [0, 5, -5, 10, -10, 15, -15, 30, -30, 45, -45, 60, -60, 75, -75, 90, -90, 120, -120, 180, 153, 155, 157])
def test_line(angle_deg, tmpfile, img_support):
    with object_test(tmpfile, img_support) as gbr:
        angle = math.radians(angle_deg)
        l = 10
        obj = Line(
            x1=0, y1=0,
            x2=l*math.cos(angle), y2=l*math.sin(angle),
            aperture=CircleAperture(3.0, unit=MM),
            unit=MM
        )

        gbr.objects.append(obj)


def test_zero_length_line(tmpfile, img_support):
    with object_test(tmpfile, img_support) as gbr:
        for x, y in [(0, 0), (0, 10), (10, 15)]:
            obj = Line(
                x1=x, y1=y,
                x2=x, y2=y,
                aperture=CircleAperture(3.0, unit=MM),
                unit=MM
            )
            gbr.objects.append(obj)

# NOTE: We explicitly do not test the sweep_angle = 0 deg case here. In this case, we decided to always draw a full circle.
# IIRC this is in line with some Gerber implementations in the wild, and it enables drawing a clean circle using a single Arc.
# gerbv will not do this, and so this would cause a failing test for no particular reason.
# TODO: Check 360 degree sweep angle case
@pytest.mark.parametrize('start_angle_deg', [0, 5, 10, 15, 30, 45, 60, 75, 90, 120, 180, 153, 155, 157, 190, 200, 210, 240, 250, 255, 270, 280, 290, 340, 350, 355, 358])
@pytest.mark.parametrize('sweep_angle_deg', [1, 5, 10, 15, 30, 45, 60, 75, 90, 120, 180, 153, 155, 157, 190, 200, 210, 240, 250, 255, 270, 280, 290, 340, 350, 355, 358])
@pytest.mark.parametrize('clockwise', [True, False])
def test_arc(start_angle_deg, sweep_angle_deg, clockwise, tmpfile, img_support):
    # Use large epsilon since someone approximates arcs with SVG beziers here, and that approximation really shows up.
    with object_test(tmpfile, img_support, epsilon=1e-2) as gbr:
        start_angle = math.radians(start_angle_deg)
        sweep_angle = math.radians(sweep_angle_deg)
        r = 10

        x1, y1 = r*math.cos(start_angle), r*math.sin(start_angle)
        x2, y2 = r*math.cos(start_angle + sweep_angle), r*math.sin(start_angle + sweep_angle)

        obj = Arc(
            x1=x1, y1=y1,
            x2=x2, y2=y2,
            cx=-x1, cy=-y1,
            clockwise=clockwise,
            aperture=CircleAperture(3.0, unit=MM),
            unit=MM
        )

        gbr.objects.append(obj)


@pytest.mark.parametrize('x,y', [(0, 0), (5, 5), (10, 0), (0, 10), (-5, 5)])
@pytest.mark.parametrize('aperture', [
    CircleAperture(3.0, unit=MM),
    CircleAperture(2.5, hole_dia=1.0, unit=MM),
])
def test_flash_circle(x, y, aperture, tmpfile, img_support):
    with object_test(tmpfile, img_support) as gbr:
        obj = Flash(
            x=x, y=y,
            aperture=aperture,
            unit=MM
        )
        gbr.objects.append(obj)


@pytest.mark.parametrize('aperture_type', [
    lambda: CircleAperture(4.0, unit=MM),
    lambda: CircleAperture(4.0, hole_dia=1.5, unit=MM),
    lambda: RectangleAperture(4.0, 3.0, unit=MM),
    lambda: ObroundAperture(4.0, 2.5, unit=MM),
    lambda: PolygonAperture(4.0, 6, unit=MM),
])
@pytest.mark.parametrize('polarity', [True, False])
def test_flash_aperture_types(aperture_type, polarity, tmpfile, img_support):
    """Test Flash with different aperture types."""

    if aperture_type().hole_dia and not polarity:
        # Our SVG export (intentionally, for simplicity) does not exactly conform to the Gerber spec in aperture
        # rendering. The Gerber spec requires rendering the aperture from its primitives to an off-screen canvas, then
        # compositing that on top of the target surface. The result is that inverted polarity, e.g. for holes, is opaque
        # in our renders, but transparent in spec-compliant renders.
        #
        # We implemented it this way since any other way would be really slow (using lots of SVG masks, filters etc.),
        # and in practical applications this is never an actual problem.
        pytest.skip()

    with object_test(tmpfile, img_support, epsilon=1e-3) as gbr:
        aperture = aperture_type()

        # Create a grid of flashes with this aperture type
        positions = [(0, 0), (8, 0), (0, 8), (8, 8), (4, 4)]
        for x, y in positions:
            obj = Flash(
                x=x, y=y,
                aperture=aperture,
                unit=MM,
                polarity_dark=polarity
            )
            gbr.objects.append(obj)


@pytest.mark.parametrize('w,h', [(5, 5), (8, 4), (4, 8), (10, 3)])
def test_region_rectangle(w, h, tmpfile, img_support):
    """Test Region objects creating rectangles."""
    with object_test(tmpfile, img_support) as gbr:
        x, y = 5, 5
        obj = Region.from_rectangle(x, y, w, h, unit=MM)
        gbr.objects.append(obj)


@pytest.mark.parametrize('start_angle_deg', [0, 45, 90, 135, 180, 225, 270, 315])
@pytest.mark.parametrize('sweep_angle_deg', [90, 180, 270])
def test_region_arc_segments(start_angle_deg, sweep_angle_deg, tmpfile, img_support):
    """Test Region objects with arc segments."""
    with object_test(tmpfile, img_support, epsilon=1e-2) as gbr:
        print(f'{start_angle_deg=} {sweep_angle_deg=}')
        start_angle = math.radians(start_angle_deg)
        sweep_angle = math.radians(sweep_angle_deg)
        r = 15

        # Create a region that looks like a pie slice
        region = Region(unit=MM)

        region.outline.append((0, 0))
        region.outline.append((r * math.cos(start_angle + sweep_angle), r * math.sin(start_angle + sweep_angle)))
        region.outline.append((r * math.cos(start_angle), r * math.sin(start_angle)))
        region.outline.append((0, 0))
        region.arc_centers.append(None)
        region.arc_centers.append((True, (0, 0)))
        region.arc_centers.append(None)

        gbr.objects.append(region)

def test_region_close():
    """ Regression test for gitlab issue #18 """
    go_region = Region([
                (0, 0),
                (100, 0),
                (100, 100),
                (0, 100),
                # (0, 0),  # expected
                ],
            None,
            polarity_dark=False,
            unit=MM)
    assert go_region.outline[-1] == go_region.outline[0]

