import math
from dataclasses import dataclass, replace
from aperture_macros.parse import GenericMacros

import graphic_primitives as gp

def _flash_hole(self, x, y):
    if self.hole_rect_h is not None:
        return self.primitives(x, y), Rectangle((x, y), (self.hole_dia, self.hole_rect_h), rotation=self.rotation, polarity_dark=False)
    else:
        return self.primitives(x, y), Circle((x, y), self.hole_dia, polarity_dark=False)

class Aperture:
    @property
    def hole_shape(self):
        if self.hole_rect_h is not None:
            return 'rect'
        else:
            return 'circle'

    @property
    def hole_size(self):
        return (self.hole_dia, self.hole_rect_h)

    @property
    def params(self):
        return dataclasses.astuple(self)

    def flash(self, x, y):
        return self.primitives(x, y)

    @parameter
    def equivalent_width(self):
        raise ValueError('Non-circular aperture used in interpolation statement, line width is not properly defined.')

    def to_gerber(self):
        # Hack: The standard aperture shapes C, R, O do not have a rotation parameter. To make this API easier to use,
        # we emulate this parameter. Our circle, rectangle and oblong classes below have a rotation parameter. Only at
        # export time during to_gerber, this parameter is evaluated. 
        actual_inst = self._rotated()
        params = 'X'.join(f'{par:.4}' for par in actual_inst.params)
        return f'{actual_inst.aperture.gerber_shape_code},{params}'

    def __eq__(self, other):
        return hasattr(other, to_gerber) and self.to_gerber() == other.to_gerber()

    def _rotate_hole_90(self):
        if self.hole_rect_h is None:
            return {'hole_dia': self.hole_dia, 'hole_rect_h': None}
        else:
            return {'hole_dia': self.hole_rect_h, 'hole_rect_h': self.hole_dia}


@dataclass(frozen=True)
class CircleAperture(Aperture):
    gerber_shape_code = 'C'
    human_readable_shape = 'circle'
    diameter : float
    hole_dia : float = 0
    hole_rect_h : float = None
    rotation : float = 0 # radians; for rectangular hole; see hack in Aperture.to_gerber

    def primitives(self, x, y, rotation):
        return [ gp.Circle(x, y, self.diameter/2) ]

    def __str__(self):
        return f'<circle aperture d={self.diameter:.3}>'

    flash = _flash_hole

    @parameter
    def equivalent_width(self):
        return self.diameter

    def rotated(self):
        if math.isclose(rotation % (2*math.pi), 0) or self.hole_rect_h is None:
            return self
        else:
            return self.to_macro(self.rotation)

    def to_macro(self):
        return ApertureMacroInstance(GenericMacros.circle, *self.params)


@dataclass(frozen=True)
class RectangleAperture(Aperture):
    gerber_shape_code = 'R'
    human_readable_shape = 'rect'
    w : float
    h : float
    hole_dia : float = 0
    hole_rect_h : float = None
    rotation : float = 0 # radians

    def primitives(self, x, y):
        return [ gp.Rectangle(x, y, self.w, self.h, rotation=self.rotation) ]

    def __str__(self):
        return f'<rect aperture {self.w:.3}x{self.h:.3}>'

    flash = _flash_hole

    @parameter
    def equivalent_width(self):
        return math.sqrt(self.w**2 + self.h**2)

    def _rotated(self):
        if math.isclose(self.rotation % math.pi, 0):
            return self
        elif math.isclose(self.rotation % math.pi, math.pi/2):
            return replace(self, w=self.h, h=self.w, **self._rotate_hole_90())
        else: # odd angle
            return self.to_macro()

    def to_macro(self):
        return ApertureMacroInstance(GenericMacros.rect, *self.params)


@dataclass(frozen=True)
class ObroundAperture(Aperture):
    gerber_shape_code = 'O'
    human_readable_shape = 'obround'
    w : float
    h : float
    hole_dia : float = 0
    hole_rect_h : float = None
    rotation : float = 0

    def primitives(self, x, y):
        return [ gp.Obround(x, y, self.w, self.h, rotation=self.rotation) ]

    def __str__(self):
        return f'<obround aperture {self.w:.3}x{self.h:.3}>'

    flash = _flash_hole

    def _rotated(self):
        if math.isclose(self.rotation % math.pi, 0):
            return self
        elif math.isclose(self.rotation % math.pi, math.pi/2):
            return replace(self, w=self.h, h=self.w, **self._rotate_hole_90())
        else:
            return self.to_macro()

    def to_macro(self, rotation:'radians'=0):
        # generic macro only supports w > h so flip x/y if h > w
        inst = self if self.w > self.h else replace(self, w=self.h, h=self.w, **_rotate_hole_90(self))
        return ApertureMacroInstance(GenericMacros.obround, *inst.params)


@dataclass(frozen=True)
class PolygonAperture(Aperture):
    gerber_shape_code = 'P'
    diameter : float
    n_vertices : int
    rotation : float = 0
    hole_dia : float = 0

    def primitives(self, x, y):
        return [ gp.RegularPolygon(x, y, diameter, n_vertices, rotation=self.rotation) ]

    def __str__(self):
        return f'<{self.n_vertices}-gon aperture d={self.diameter:.3}'

    flash = _flash_hole

    def _rotated(self):
        self.rotation %= (2*math.pi / self.n_vertices)
        return self

    def to_macro(self):
        return ApertureMacroInstance(GenericMacros.polygon, *self.params)


class ApertureMacroInstance(Aperture):
    params : [float]
    rotation : float = 0

    def __init__(self, macro, *parameters):
        self.params = parameters
        self._primitives = macro.to_graphic_primitives(parameters)
        self.macro = macro

    @property
    def gerber_shape_code(self):
        return self.macro.name

    def primitives(self, x, y):
        # FIXME return graphical primitives not macro primitives here
        return [ primitive.with_offset(x, y).rotated(self.rotation, cx=0, cy=0) for primitive in self._primitives ]

    def _rotated(self):
        if math.isclose(self.rotation % (2*math.pi), 0):
            return self
        else:
            return self.to_macro()

    def to_macro(self):
        return type(self)(self.macro.rotated(self.rotation), self.params)

    def __eq__(self, other):
        return hasattr(other, 'macro') and self.macro == other.macro and \
                hasattr(other, 'params') and self.params == other.params and \
                hasattr(other, 'rotation') and self.rotation == other.rotation