#!/usr/bin/env python
# -*- coding: utf-8 -*-

# Copyright 2019 Hiroshi Murayama <opiopan@gmail.com>
# Copyright 2022 Jan Götte <gerbonara@jaseg.de>

import warnings
import contextlib
import math

from .expression import Expression, UnitExpression, ConstantExpression, expr

from .. import graphic_primitives as gp


def point_distance(a, b):
    x1, y1 = a
    x2, y2 = b
    return math.sqrt((x2 - x1)**2 + (y2 - y1)**2)

def deg_to_rad(a):
    return (a / 180) * math.pi

class Primitive:
    def __init__(self, unit, args):
        self.unit = unit

        if len(args) > len(type(self).__annotations__):
            raise ValueError(f'Too many arguments ({len(args)}) for aperture macro primitive {self.code} ({type(self)})')

        for arg, (name, fieldtype) in zip(args, type(self).__annotations__.items()):
            arg = expr(arg) # convert int/float to Expression object

            if fieldtype == UnitExpression:
                setattr(self, name, UnitExpression(arg, unit))
            else:
                setattr(self, name, arg)

        for name in type(self).__annotations__:
            if not hasattr(self, name):
                raise ValueError(f'Too few arguments ({len(args)}) for aperture macro primitive {self.code} ({type(self)})')

    def to_gerber(self, unit=None):
        return f'{self.code},' + ','.join(
                getattr(self, name).to_gerber(unit) for name in type(self).__annotations__)

    def __str__(self):
        attrs = ','.join(str(getattr(self, name)).strip('<>') for name in type(self).__annotations__)
        return f'<{type(self).__name__} {attrs}>'

    def __repr__(self):
        return str(self)

    class Calculator:
        def __init__(self, instance, variable_binding={}, unit=None):
            self.instance = instance
            self.variable_binding = variable_binding
            self.unit = unit

        def __enter__(self):
            return self

        def __exit__(self, _type, _value, _traceback):
            pass

        def __getattr__(self, name):
            return getattr(self.instance, name).calculate(self.variable_binding, self.unit)

        def __call__(self, expr):
            return expr.calculate(self.variable_binding, self.unit)


class Circle(Primitive):
    code = 1
    exposure : Expression
    diameter : UnitExpression
    # center x/y
    x : UnitExpression
    y : UnitExpression
    rotation : Expression = None

    def __init__(self, unit, args):
        super().__init__(unit, args)
        if self.rotation is None:
            self.rotation = ConstantExpression(0)

    def to_graphic_primitives(self, offset, rotation, variable_binding={}, unit=None, polarity_dark=True):
        with self.Calculator(self, variable_binding, unit) as calc:
            x, y = gp.rotate_point(calc.x, calc.y, deg_to_rad(calc.rotation) + rotation, 0, 0)
            x, y = x+offset[0], y+offset[1]
            return [ gp.Circle(x, y, calc.diameter/2, polarity_dark=(bool(calc.exposure) == polarity_dark)) ]

    def dilate(self, offset, unit):
        self.diameter += UnitExpression(offset, unit)

class VectorLine(Primitive):
    code = 20
    exposure : Expression
    width : UnitExpression
    start_x : UnitExpression
    start_y : UnitExpression
    end_x : UnitExpression
    end_y : UnitExpression
    rotation : Expression

    def to_graphic_primitives(self, offset, rotation, variable_binding={}, unit=None, polarity_dark=True):
        with self.Calculator(self, variable_binding, unit) as calc:
            center_x = (calc.end_x + calc.start_x) / 2
            center_y = (calc.end_y + calc.start_y) / 2
            delta_x = calc.end_x - calc.start_x
            delta_y = calc.end_y - calc.start_y
            length = point_distance((calc.start_x, calc.start_y), (calc.end_x, calc.end_y))

            center_x, center_y = center_x+offset[0], center_y+offset[1]
            rotation += deg_to_rad(calc.rotation) + math.atan2(delta_y, delta_x)

            return [ gp.Rectangle(center_x, center_y, length, calc.width, rotation=rotation,
                        polarity_dark=(bool(calc.exposure) == polarity_dark)) ]

    def dilate(self, offset, unit):
        self.width += UnitExpression(2*offset, unit)


class CenterLine(Primitive):
    code = 21
    exposure : Expression
    width : UnitExpression
    height : UnitExpression
    # center x/y
    x : UnitExpression
    y : UnitExpression
    rotation : Expression

    def to_graphic_primitives(self, offset, rotation, variable_binding={}, unit=None, polarity_dark=True):
        with self.Calculator(self, variable_binding, unit) as calc:
            rotation += deg_to_rad(calc.rotation)
            x, y = gp.rotate_point(calc.x, calc.y, rotation, 0, 0)
            x, y = x+offset[0], y+offset[1]
            w, h = calc.width, calc.height

            return [ gp.Rectangle(x, y, w, h, rotation, polarity_dark=(bool(calc.exposure) == polarity_dark)) ]

    def dilate(self, offset, unit):
        self.width += UnitExpression(2*offset, unit)
            

class Polygon(Primitive):
    code = 5
    exposure : Expression
    n_vertices : Expression
    # center x/y
    x : UnitExpression
    y : UnitExpression
    diameter : UnitExpression
    rotation : Expression

    def to_graphic_primitives(self, offset, rotation, variable_binding={}, unit=None, polarity_dark=True):
        with self.Calculator(self, variable_binding, unit) as calc:
            rotation += deg_to_rad(calc.rotation)
            x, y = gp.rotate_point(calc.x, calc.y, rotation, 0, 0)
            x, y = x+offset[0], y+offset[1]
            return [ gp.ArcPoly.from_regular_polygon(calc.x, calc.y, calc.diameter/2, calc.n_vertices, rotation,
                        polarity_dark=(bool(calc.exposure) == polarity_dark)) ]

    def dilate(self, offset, unit):
        self.diameter += UnitExpression(2*offset, unit)


class Thermal(Primitive):
    code = 7
    exposure : Expression
    # center x/y
    x : UnitExpression
    y : UnitExpression
    d_outer : UnitExpression
    d_inner : UnitExpression
    gap_w : UnitExpression
    rotation : Expression

    def to_graphic_primitives(self, offset, rotation, variable_binding={}, unit=None, polarity_dark=True):
        with self.Calculator(self, variable_binding, unit) as calc:
            rotation += deg_to_rad(calc.rotation)
            x, y = gp.rotate_point(calc.x, calc.y, rotation, 0, 0)
            x, y = x+offset[0], y+offset[1]

            dark = (bool(calc.exposure) == polarity_dark)

            return [
                    gp.Circle(x, y, calc.d_outer/2, polarity_dark=dark),
                    gp.Circle(x, y, calc.d_inner/2, polarity_dark=not dark),
                    gp.Rectangle(x, y, d_outer, gap_w, rotation=rotation, polarity_dark=not dark),
                    gp.Rectangle(x, y, gap_w, d_outer, rotation=rotation, polarity_dark=not dark),
                    ]

    def dilate(self, offset, unit):
        # I'd rather print a warning and produce graphically slightly incorrect output in these few cases here than
        # producing macros that may evaluate to primitives with negative values.
        warnings.warn('Attempted dilation of macro aperture thermal primitive. This is not supported.')


class Outline(Primitive):
    code = 4

    def __init__(self, unit, args):
        if len(args) < 11:
            raise ValueError(f'Invalid aperture macro outline primitive, not enough parameters ({len(args)}).')
        if len(args) > 5004:
            raise ValueError(f'Invalid aperture macro outline primitive, too many points ({len(args)//2-2}).')

        self.exposure = args.pop(0)

        # length arg must not contain variables (that would not make sense)
        length_arg = args.pop(0).calculate()

        if length_arg != len(args)//2-1:
            raise ValueError(f'Invalid aperture macro outline primitive, given size {length_arg} does not match length of coordinate list({len(args)//2-1}).')

        if len(args) % 2 == 1:
            self.rotation = args.pop()
        else:
            self.rotation = ConstantExpression(0.0)

        if args[0] != args[-2] or args[1] != args[-1]:
            raise ValueError(f'Invalid aperture macro outline primitive, polygon is not closed {args[2:4], args[-3:-1]}')

        self.coords = [(UnitExpression(x, unit), UnitExpression(y, unit)) for x, y in zip(args[0::2], args[1::2])]

    def __str__(self):
        return f'<Outline {len(self.coords)} points>'

    def to_gerber(self, unit=None):
        coords = ','.join(coord.to_gerber(unit) for xy in self.coords for coord in xy)
        return f'{self.code},{self.exposure.to_gerber()},{len(self.coords)-1},{coords},{self.rotation.to_gerber()}'

    def to_graphic_primitives(self, offset, rotation, variable_binding={}, unit=None, polarity_dark=True):
        with self.Calculator(self, variable_binding, unit) as calc:
            rotation += deg_to_rad(calc.rotation)
            bound_coords = [ gp.rotate_point(calc(x), calc(y), rotation, 0, 0) for x, y in self.coords ]
            bound_coords = [ (x+offset[0], y+offset[1]) for x, y in bound_coords ]
            bound_radii = [None] * len(bound_coords)
            return [gp.ArcPoly(bound_coords, bound_radii, polarity_dark=(bool(calc.exposure) == polarity_dark))]

    def dilate(self, offset, unit):
        # we would need a whole polygon offset/clipping library here
        warnings.warn('Attempted dilation of macro aperture outline primitive. This is not supported.')


class Comment:
    code = 0

    def __init__(self, comment):
        self.comment = comment

    def to_gerber(self, unit=None):
        return f'0 {self.comment}'

PRIMITIVE_CLASSES = {
    **{cls.code: cls for cls in [
        Comment,
        Circle,
        VectorLine,
        CenterLine,
        Outline,
        Polygon,
        Thermal,
    ]},
    # alternative codes
    2: VectorLine,
}

