""" 
Padmaster Classes Reference
---------------------------

A padmaster has the following attributes:

* shape:    Geometric shape, e.g. rectangle
* function: A pad function, e.g. 'ViaPad'
* polarity: Positive or negative

Padmasters are used to create pads by replicating them

* at a given point on the 2D plane
* under a given angle


"""

# Copyright:  Karel Tavernier
# Created: 2022 03 24
# Copyright: Karel Tavernier
# License: Apache 2.0 license
# 
# Revision history

from dataclasses import dataclass
from math import sqrt

@dataclass(frozen = True)
class Circle:
    """A pad master with circular shape.
    
    A pad master serves to add pads to the graphics objects list. It defines
    their geometric shape, function and polarity (positive or negative).

    :param float diameter: Diameter of the circular pads
    :param str function: Function of the pads e.g. 'ViaPad'
    :param bool negative: Polarity, negative if True, positive if False (default)
    """
    
    diameter: float
    function: str
    negative: bool = False
    def __post_init__(self):
        if not isinstance(self.diameter, float|int):
            raise TypeError('diameter must be int or float')
        if self.diameter <0 :
            raise ValueError('diameter must be >=0')
        if not isinstance(self.function, str):
            raise TypeError('function must be str')
        if not isinstance(self.negative, bool):
            raise TypeError('function must be bool')
        
@dataclass(frozen = True)
class Rectangle:
    """A pad master with an axis-aligned rectangular shape.
    
    :param float x_size: Size along x-axis
    :param float y_size: Size along y-axis
    :param str function: Function of the pads e.g. 'SMDPad,CuDef''
    :param bool negative: Polarity, negative if True, positive if False (default)    
    """
    
    x_size: float
    y_size: float
    function: str
    negative: bool = False
    def __post_init__(self):
        if not isinstance(self.x_size, float|int):
            raise TypeError('X size must be int or float')
        if self.x_size <= 0:
            raise ValueError('X size must be >=0')
        if not isinstance(self.y_size, float|int):
            raise TypeError('Y size must be int or float')
        if  self.y_size <= 0:
            raise ValueError('Y size must be >=0')
        if not isinstance(self.function, str):
            raise TypeError('Function must be str')
        if not isinstance(self.negative, bool):
            raise TypeError('Function must be bool')
    
@dataclass(frozen=True)
class RoundedRectangle:
    """A pad master, an ax-s-aligned rectangle with rounded corners.
    
    :param float x_size: Size along x-axis
    :param float y_size: Size along y-axis
    :param float radius: Radius of the corner rounding    
    :param str function: Function of the pads e.g. 'SMDPad,CuDef''
    :param bool negative: Polarity, negative if True, positive if False (default)    
    """
    
    x_size: float
    y_size: float
    radius: float
    function: str
    negative: bool = False
    def __post_init__(self):
        if not isinstance(self.x_size, float|int):
            raise TypeError('x_size must be int or float')
        if self.x_size <= 0:
            raise ValueError('x_size must be >0')
        if not isinstance(self.y_size, float|int):
            raise TypeError('y_size must be int or float')
        if  self.y_size <= 0:
            raise ValueError('y_size must be >0')
        if not isinstance(self.radius, float|int):
            raise TypeError('radius must be int or float')
        if not (0 <= self.radius <= 0.5*min(self.x_size, self.y_size)):
            raise ValueError('radius must be: 0 <= radius <= half the smallest side')            
        if not isinstance(self.function, str):
            raise TypeError('function must be str')
        if not isinstance(self.negative, bool):
            raise TypeError('Function must be bool')
        
@dataclass(frozen = True)
class ChamferedRectangle:
    """A pad master, an axis-aligned rectangle with chamfered corner.
    
    :param float x_size: Size along x-axis
    :param float y_size: Size along y-axis
    :param float cutoff: Cutoff distance from corner for the chamfer
    :param str function: Function of the pads e.g. 'SMDPad,CuDef'
    :param bool negative: Polarity, negative if True, positive if False (default)
    """
    
    x_size: float
    y_size: float
    cutoff: float
    function: str
    negative: bool = False
    def __post_init__(self):
        if not isinstance(self.x_size, float|int):
            raise TypeError('X size must be int or float')
        if self.x_size <= 0:
            raise ValueError('X size must be >=0')
        if not isinstance(self.y_size, float|int):
            raise TypeError('Y size must be int or float')
        if  self.y_size <= 0:
            raise ValueError('Y size must be >=0')
        if not isinstance(self.cutoff, float|int):
            raise TypeError('Cutoff must be int or float')
        if not (0 <= self.cutoff <= 0.5*min(self.x_size, self.y_size)):
            raise ValueError('Cutoff must be: 0 <= cutoff <= half the smallest side')            
        if not isinstance(self.function, str):
            raise TypeError('Function must be str')

@dataclass(frozen=True)      
class Thermal:
    """A pad master, circular thermal with four axis-aligned straight gap openings.
    
    :param float outer_diameter: Outer diameter of the thermal
    :param float inner_diameter: Inner diameter of the thermal
    :param float gap: Gap or conductor width beteen thermal barriers along the axis
    :param str function: Function of the pads e.g. 'ThermalReliefPad'
    :param bool negative: Polarity, negative if True, positive if False (default)
    """
    
    outer_diameter: float
    inner_diameter: float
    gap: float
    function: str
    negative: bool = False
    def __post_init__(self):
        if not (isinstance(self.outer_diameter, int|float) and isinstance(self.inner_diameter, int|float) and isinstance(self.gap, int|float)):
            raise TypeError('arguments must be int or float')
        if not (0 < self.inner_diameter < self.outer_diameter):
            raise ValueError('diameter values invalid')
        if not(0 < self.gap < self.outer_diameter/sqrt(2)):
            raise ValueError('gap only valid if 0 < gap <= outer_diameter/math.sqrt(2)')
            
@dataclass(frozen=True)      
class RoundedThermal:
    """A pad master, circular thermal with four axis-aligned rounded gap openings.
    
    :param float outer_diameter: Outer diameter of the thermal
    :param float inner_diameter: Inner diameter of the thermal
    :param float gap: Gap or conductor width beteen thermal barriers along the axis
    :param str function: Function of the pads e.g. 'ThermalReliefPad'
    :param bool negative: Polarity, negative if True, positive if False (default)   
    """
    
    outer_diameter: float
    inner_diameter: float
    gap: float
    function: str
    negative: bool = False
    def __post_init__(self):
        if not (isinstance(self.outer_diameter, int|float) and isinstance(self.inner_diameter, int|float) and isinstance(self.gap, int|float)):
            raise TypeError('arguments must be int or float')
        if not (self.outer_diameter>0 and self.inner_diameter>0 and self.gap>0):
            raise ValueError('parameters must be strictly positive')
        if (self.gap + self.outer_diameter - self.inner_diameter)*2*sqrt(2) >= (self.outer_diameter + self.inner_diameter): # for simpler rounding
#        if self.gap*sqrt(2) >= (self.inner_diameter - (1 + sqrt(2))*(self.outer_diameter - self.inner_diameter)):
            raise ValueError('gap too big in relation to diameters')
            
@dataclass(frozen=True)
class RegularPolygon:
    """A pad master, with shape a regular convex polygon.
    
    One vertex is on the positive x-axis.
    
    :param float outer_diameter: Outer diameter, or diameter of the circumscribed circle
    :param int vertices: Number of vertices
    :param str function: Function of the pads e.g. 'FiducialPad,Local'
    :param bool negative: Polarity, negative if True, positive if False (default)       
    """
    
    outer_diameter: float
    vertices: int
    function: str
    negative: bool = False
    def __post_init__(self):
        if not isinstance(self.outer_diameter, float|int):
            raise TypeError('outer_diameter must be int or float')
        if self.outer_diameter <= 0:
            raise ValueError('outer_diameter must be >=0')
        if not isinstance(self.vertices, int):
            raise TypeError('vertices must be int or float')
        if self.vertices not in range(3,13) :
            raise ValueError('vertices must be from 3 up to 12')         
        if not isinstance(self.function, str):
            raise TypeError('function must be str')
        if not isinstance(self.negative, bool):
            raise TypeError('Function must be bool')
        
@dataclass(frozen=True)
class UserPolygon:
    """A pad master with a user defined shape.
    
    The shape is defined by its polygon outline or contour
    The outline must be closed: the last vertex must coincide with the first one.
    For more information about valied contours see the Gerber Layer Format specification _ section 4.10.3.
    The reference point, this is the point used to put the pad in the plane with add_pad, is the origin
    of the coordinate system used to define the outline
    
    :param int polygon: A tuple with the vertices of the polygon;  each vertex is a tuple (x, y)
    :param str function: Function of the pads e.g. 'TestPad'
    :param bool negative: Polarity, negative if True, positive if False (default)  
    """
    
    polygon: tuple
    function: str
    negative: bool = False
    def __post_init__(self):
        if not isinstance(self.polygon, tuple):
            raise TypeError('polygon must be tuple of points')
        if len(self.polygon) <3:
            raise ValueError('polygon must have at least three vertices')
        if self.polygon[0] != self.polygon[-1]:
            raise ValueError('polygon must be closed')     
        if not isinstance(self.function, str):
            raise TypeError('function must be str')
        if not isinstance(self.negative, bool):
            raise TypeError('Function must be bool')

           
# Run doctest when called as main
if __name__ == "__main__":
    import doctest
    doctest.testmod()