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

# copyright 2015 Hamilton Kibbe <ham@hamiltonkib.be> and Paulo Henrique Silva
# <ph.silva@gmail.com>

# 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.

from math import asin
import math

from .primitives import *
from .utils import validate_coordinates, inch, metric, rotate_point



# TODO: Add support for aperture macro variables
__all__ = ['AMPrimitive', 'AMCommentPrimitive', 'AMCirclePrimitive',
           'AMVectorLinePrimitive', 'AMOutlinePrimitive', 'AMPolygonPrimitive',
           'AMMoirePrimitive', 'AMThermalPrimitive', 'AMCenterLinePrimitive',
           'AMLowerLeftLinePrimitive', 'AMUnsupportPrimitive']


class AMPrimitive(object):
    """ Aperture Macro Primitive Base Class

    Parameters
    ----------
    code : int
        primitive shape code

    exposure : str
        on or off Primitives with exposure on create a slid part of
        the macro aperture, and primitives with exposure off erase the
        solid part created previously in the aperture macro definition.
        .. note::
            The erasing effect is limited to the aperture definition in
            which it occurs.

    Returns
    -------
    primitive : :class: `gerber.am_statements.AMPrimitive`

    Raises
    ------
    TypeError, ValueError
    """

    def __init__(self, code, exposure=None):
        VALID_CODES = (0, 1, 2, 4, 5, 6, 7, 20, 21, 22, 9999)
        if not isinstance(code, int):
            raise TypeError('Aperture Macro Primitive code must be an integer')
        elif code not in VALID_CODES:
            raise ValueError('Invalid Code. Valid codes are %s.' %
                             ', '.join(map(str, VALID_CODES)))
        if exposure is not None and exposure.lower() not in ('on', 'off'):
            raise ValueError('Exposure must be either on or off')
        self.code = code
        self.exposure = exposure.lower() if exposure is not None else None

    def to_inch(self):
        raise NotImplementedError('Subclass must implement `to-inch`')

    def to_metric(self):
        raise NotImplementedError('Subclass must implement `to-metric`')

    @property
    def _level_polarity(self):
        if self.exposure == 'off':
            return 'clear'
        return 'dark'

    def to_primitive(self, units):
        """ Return a Primitive instance based on the specified macro params.
        """
        print('Rendering {}s is not supported yet.'.format(str(self.__class__)))

    def __eq__(self, other):
        return self.__dict__ == other.__dict__


class AMCommentPrimitive(AMPrimitive):
    """ Aperture Macro Comment primitive. Code 0

    The comment primitive has no image meaning. It is used to include human-
    readable comments into the AM command.

    .. seealso::
        `The Gerber File Format Specification <http://www.ucamco.com/files/downloads/file/81/the_gerber_file_format_specification.pdf>`_
            **Section 4.12.3.1:** Comment, primitive code 0

    Parameters
    ----------
    code : int
        Aperture Macro primitive code. 0 Indicates an AMCommentPrimitive

    comment : str
        The comment as a string.

    Returns
    -------
    CommentPrimitive : :class:`gerbers.am_statements.AMCommentPrimitive`
        An Initialized AMCommentPrimitive

    Raises
    ------
    ValueError
    """
    @classmethod
    def from_gerber(cls, primitive):
        primitive = primitive.strip()
        code = int(primitive[0])
        comment = primitive[1:]
        return cls(code, comment)

    def __init__(self, code, comment):
        if code != 0:
            raise ValueError('Not a valid Aperture Macro Comment statement')
        super(AMCommentPrimitive, self).__init__(code)
        self.comment = comment.strip(' *')

    def to_inch(self):
        pass

    def to_metric(self):
        pass

    def to_gerber(self, settings=None):
        return '0 %s *' % self.comment

    def to_primitive(self, units):
        """
        Returns None - has not primitive representation
        """
        return None

    def __str__(self):
        return '<Aperture Macro Comment: %s>' % self.comment


class AMCirclePrimitive(AMPrimitive):
    """ Aperture macro Circle primitive. Code 1

    A circle primitive is defined by its center point and diameter.

    .. seealso::
        `The Gerber File Format Specification <http://www.ucamco.com/files/downloads/file/81/the_gerber_file_format_specification.pdf>`_
            **Section 4.12.3.2:** Circle, primitive code 1

    Parameters
    ----------
    code : int
        Circle Primitive code. Must be 1

    exposure : string
        'on' or 'off'

    diameter : float
        Circle diameter

    position : tuple (<float>, <float>)
        Position of the circle relative to the macro origin

    Returns
    -------
    CirclePrimitive : :class:`gerbers.am_statements.AMCirclePrimitive`
        An initialized AMCirclePrimitive

    Raises
    ------
    ValueError, TypeError
    """
    @classmethod
    def from_gerber(cls, primitive):
        modifiers = primitive.strip(' *').split(',')
        code = int(modifiers[0])
        exposure = 'on' if float(modifiers[1]) == 1 else 'off'
        diameter = float(modifiers[2])
        position = (float(modifiers[3]), float(modifiers[4]))
        return cls(code, exposure, diameter, position)

    @classmethod
    def from_primitive(cls, primitive):
        return cls(1, 'on', primitive.diameter, primitive.position)

    def __init__(self, code, exposure, diameter, position):
        validate_coordinates(position)
        if code != 1:
            raise ValueError('CirclePrimitive code is 1')
        super(AMCirclePrimitive, self).__init__(code, exposure)
        self.diameter = diameter
        self.position = position

    def to_inch(self):
        self.diameter = inch(self.diameter)
        self.position = tuple([inch(x) for x in self.position])

    def to_metric(self):
        self.diameter = metric(self.diameter)
        self.position = tuple([metric(x) for x in self.position])

    def to_gerber(self, settings=None):
        data = dict(code=self.code,
                    exposure='1' if self.exposure == 'on' else 0,
                    diameter=self.diameter,
                    x=self.position[0],
                    y=self.position[1])
        return '{code},{exposure},{diameter},{x},{y}*'.format(**data)

    def to_primitive(self, units):
        return Circle((self.position), self.diameter, units=units, level_polarity=self._level_polarity)


class AMVectorLinePrimitive(AMPrimitive):
    """ Aperture Macro Vector Line primitive. Code 2 or 20.

    A vector line is a rectangle defined by its line width, start, and end
    points. The line ends are rectangular.

    .. seealso::
        `The Gerber File Format Specification <http://www.ucamco.com/files/downloads/file/81/the_gerber_file_format_specification.pdf>`_
            **Section 4.12.3.3:** Vector Line, primitive code 2 or 20.

    Parameters
    ----------
    code : int
        Vector Line Primitive code. Must be either 2 or 20.

    exposure : string
        'on' or 'off'

    width : float
        Line width

    start : tuple (<float>, <float>)
        coordinate of line start point

    end : tuple (<float>, <float>)
        coordinate of line end point

    rotation : float
        Line rotation about the origin.

    Returns
    -------
    LinePrimitive : :class:`gerbers.am_statements.AMVectorLinePrimitive`
        An initialized AMVectorLinePrimitive

    Raises
    ------
    ValueError, TypeError
    """

    @classmethod
    def from_primitive(cls, primitive):
        return cls(2, 'on', primitive.aperture.width, primitive.start, primitive.end, 0)

    @classmethod
    def from_gerber(cls, primitive):
        modifiers = primitive.strip(' *').split(',')
        code = int(modifiers[0])
        exposure = 'on' if float(modifiers[1]) == 1 else 'off'
        width = float(modifiers[2])
        start = (float(modifiers[3]), float(modifiers[4]))
        end = (float(modifiers[5]), float(modifiers[6]))
        rotation = float(modifiers[7])
        return cls(code, exposure, width, start, end, rotation)

    def __init__(self, code, exposure, width, start, end, rotation):
        validate_coordinates(start)
        validate_coordinates(end)
        if code not in (2, 20):
            raise ValueError('VectorLinePrimitive codes are 2 or 20')
        super(AMVectorLinePrimitive, self).__init__(code, exposure)
        self.width = width
        self.start = start
        self.end = end
        self.rotation = rotation

    def to_inch(self):
        self.width = inch(self.width)
        self.start = tuple([inch(x) for x in self.start])
        self.end = tuple([inch(x) for x in self.end])

    def to_metric(self):
        self.width = metric(self.width)
        self.start = tuple([metric(x) for x in self.start])
        self.end = tuple([metric(x) for x in self.end])

    def to_gerber(self, settings=None):
        fmtstr = '{code},{exp},{width},{startx},{starty},{endx},{endy},{rotation}*'
        data = dict(code=self.code,
                    exp=1 if self.exposure == 'on' else 0,
                    width=self.width,
                    startx=self.start[0],
                    starty=self.start[1],
                    endx=self.end[0],
                    endy=self.end[1],
                    rotation=self.rotation)
        return fmtstr.format(**data)

    def to_primitive(self, units):
        """
        Convert this to a primitive. We use the Outline to represent this (instead of Line)
        because the behaviour of the end caps is different for aperture macros compared to Lines
        when rotated.
        """

        # Use a line to generate our vertices easily
        line = Line(self.start, self.end, Rectangle(None, self.width, self.width))
        vertices = line.vertices

        aperture = Circle((0, 0), 0)

        lines = []
        prev_point = rotate_point(vertices[-1], self.rotation, (0, 0))
        for point in vertices:
            cur_point = rotate_point(point, self.rotation, (0, 0))

            lines.append(Line(prev_point, cur_point, aperture))

        return Outline(lines, units=units, level_polarity=self._level_polarity)


class AMOutlinePrimitive(AMPrimitive):
    """ Aperture Macro Outline primitive. Code 4.

    An outline primitive is an area enclosed by an n-point polygon defined by
    its start point and n subsequent points. The outline must be closed, i.e.
    the last point must be equal to the start point. Self intersecting
    outlines are not allowed.

    .. seealso::
        `The Gerber File Format Specification <http://www.ucamco.com/files/downloads/file/81/the_gerber_file_format_specification.pdf>`_
            **Section 4.12.3.6:** Outline, primitive code 4.

     Parameters
    ----------
    code : int
        OutlinePrimitive code. Must be 6.

    exposure : string
        'on' or 'off'

    start_point : tuple (<float>, <float>)
        coordinate of outline start point

    points : list of tuples (<float>, <float>)
        coordinates of subsequent points

    rotation : float
        outline rotation about the origin.

    Returns
    -------
    OutlinePrimitive : :class:`gerber.am_statements.AMOutlineinePrimitive`
        An initialized AMOutlinePrimitive

    Raises
    ------
    ValueError, TypeError
    """

    @classmethod
    def from_primitive(cls, primitive):

        start_point = (round(primitive.primitives[0].start[0], 6), round(primitive.primitives[0].start[1], 6))
        points = []
        for prim in primitive.primitives:
            points.append((round(prim.end[0], 6), round(prim.end[1], 6)))

        rotation = 0.0

        return cls(4, 'on', start_point, points, rotation)

    @classmethod
    def from_gerber(cls, primitive):
        modifiers = primitive.strip(' *').split(",")

        code = int(modifiers[0])
        exposure = "on" if float(modifiers[1]) == 1 else "off"
        n = int(float(modifiers[2]))
        start_point = (float(modifiers[3]), float(modifiers[4]))
        points = []
        for i in range(n):
            points.append((float(modifiers[5 + i * 2]),
                           float(modifiers[5 + i * 2 + 1])))
        rotation = float(modifiers[-1])
        return cls(code, exposure, start_point, points, rotation)

    def __init__(self, code, exposure, start_point, points, rotation):
        """ Initialize AMOutlinePrimitive
        """
        validate_coordinates(start_point)
        for point in points:
            validate_coordinates(point)
        if code != 4:
            raise ValueError('OutlinePrimitive code is 4')
        super(AMOutlinePrimitive, self).__init__(code, exposure)
        self.start_point = start_point
        if points[-1] != start_point:
            raise ValueError('OutlinePrimitive must be closed')
        self.points = points
        self.rotation = rotation

    def to_inch(self):
        self.start_point = tuple([inch(x) for x in self.start_point])
        self.points = tuple([(inch(x), inch(y)) for x, y in self.points])

    def to_metric(self):
        self.start_point = tuple([metric(x) for x in self.start_point])
        self.points = tuple([(metric(x), metric(y)) for x, y in self.points])

    def to_gerber(self, settings=None):
        data = dict(
            code=self.code,
            exposure="1" if self.exposure == "on" else "0",
            n_points=len(self.points),
            start_point="%.6g,%.6g" % self.start_point,
            points=",\n".join(["%.6g,%.6g" % point for point in self.points]),
            rotation=str(self.rotation)
        )
        return "{code},{exposure},{n_points},{start_point},{points},{rotation}*".format(**data)

    def to_primitive(self, units):
        """
        Convert this to a drawable primitive. This uses the Outline instead of Line
        primitive to handle differences in end caps when rotated.
        """

        lines = []
        prev_point = rotate_point(self.start_point, self.rotation)
        for point in self.points:
            cur_point = rotate_point(point, self.rotation)

            lines.append(Line(prev_point, cur_point, Circle((0,0), 0)))

            prev_point = cur_point

        if lines[0].start != lines[-1].end:
            raise ValueError('Outline must be closed')

        return Outline(lines, units=units, level_polarity=self._level_polarity)


class AMPolygonPrimitive(AMPrimitive):
    """ Aperture Macro Polygon primitive. Code 5.

    A polygon primitive is a regular polygon defined by the number of
    vertices, the center point, and the diameter of the circumscribed circle.

    .. seealso::
        `The Gerber File Format Specification <http://www.ucamco.com/files/downloads/file/81/the_gerber_file_format_specification.pdf>`_
            **Section 4.12.3.8:** Polygon, primitive code 5.

    Parameters
    ----------
    code : int
        PolygonPrimitive code. Must be 5.

    exposure : string
        'on' or 'off'

    vertices : int, 3 <= vertices <= 12
        Number of vertices

    position : tuple (<float>, <float>)
        X and Y coordinates of polygon center

    diameter : float
        diameter of circumscribed circle.

    rotation : float
        polygon rotation about the origin.

    Returns
    -------
    PolygonPrimitive : :class:`gerbers.am_statements.AMPolygonPrimitive`
        An initialized AMPolygonPrimitive

    Raises
    ------
    ValueError, TypeError
    """

    @classmethod
    def from_primitive(cls, primitive):
        return cls(5, 'on', primitive.sides, primitive.position, primitive.diameter, primitive.rotation)

    @classmethod
    def from_gerber(cls, primitive):
        modifiers = primitive.strip(' *').split(",")
        code = int(modifiers[0])
        exposure = "on" if float(modifiers[1]) == 1 else "off"
        vertices = int(float(modifiers[2]))
        position = (float(modifiers[3]), float(modifiers[4]))
        try:
            diameter = float(modifiers[5])
        except:
            diameter = 0

        rotation = float(modifiers[6])
        return cls(code, exposure, vertices, position, diameter, rotation)

    def __init__(self, code, exposure, vertices, position, diameter, rotation):
        """ Initialize AMPolygonPrimitive
        """
        if code != 5:
            raise ValueError('PolygonPrimitive code is 5')
        super(AMPolygonPrimitive, self).__init__(code, exposure)
        if vertices < 3 or vertices > 12:
            raise ValueError('Number of vertices must be between 3 and 12')
        self.vertices = vertices
        validate_coordinates(position)
        self.position = position
        self.diameter = diameter
        self.rotation = rotation

    def to_inch(self):
        self.position = tuple([inch(x) for x in self.position])
        self.diameter = inch(self.diameter)

    def to_metric(self):
        self.position = tuple([metric(x) for x in self.position])
        self.diameter = metric(self.diameter)

    def to_gerber(self, settings=None):
        data = dict(
            code=self.code,
            exposure="1" if self.exposure == "on" else "0",
            vertices=self.vertices,
            position="%.4g,%.4g" % self.position,
            diameter='%.4g' % self.diameter,
            rotation=str(self.rotation)
        )
        fmt = "{code},{exposure},{vertices},{position},{diameter},{rotation}*"
        return fmt.format(**data)

    def to_primitive(self, units):
        return Polygon(self.position, self.vertices, self.diameter / 2.0, 0, rotation=math.radians(self.rotation), units=units, level_polarity=self._level_polarity)


class AMMoirePrimitive(AMPrimitive):
    """ Aperture Macro Moire primitive. Code 6.

    The moire primitive is a cross hair centered on concentric rings (annuli).
    Exposure is always on.

    .. seealso::
        `The Gerber File Format Specification <http://www.ucamco.com/files/downloads/file/81/the_gerber_file_format_specification.pdf>`_
            **Section 4.12.3.9:** Moire, primitive code 6.

    Parameters
    ----------
    code : int
        Moire Primitive code. Must be 6.

    position : tuple (<float>, <float>)
        X and Y coordinates of moire center

    diameter : float
        outer diameter of outer ring.

    ring_thickness : float
        thickness of concentric rings.

    gap : float
        gap between concentric rings.

    max_rings : float
        maximum number of rings

    crosshair_thickness : float
        thickness of crosshairs

    crosshair_length : float
        length of crosshairs

    rotation : float
        moire rotation about the origin.

    Returns
    -------
    MoirePrimitive : :class:`gerbers.am_statements.AMMoirePrimitive`
        An initialized AMMoirePrimitive

    Raises
    ------
    ValueError, TypeError
    """
    @classmethod
    def from_gerber(cls, primitive):
        modifiers = primitive.strip(' *').split(",")
        code = int(modifiers[0])
        position = (float(modifiers[1]), float(modifiers[2]))
        diameter = float(modifiers[3])
        ring_thickness = float(modifiers[4])
        gap = float(modifiers[5])
        max_rings = int(float(modifiers[6]))
        crosshair_thickness = float(modifiers[7])
        crosshair_length = float(modifiers[8])
        rotation = float(modifiers[9])
        return cls(code, position, diameter, ring_thickness, gap, max_rings, crosshair_thickness, crosshair_length, rotation)

    def __init__(self, code, position, diameter, ring_thickness, gap, max_rings, crosshair_thickness, crosshair_length, rotation):
        """ Initialize AMoirePrimitive
        """
        if code != 6:
            raise ValueError('MoirePrimitive code is 6')
        super(AMMoirePrimitive, self).__init__(code, 'on')
        validate_coordinates(position)
        self.position = position
        self.diameter = diameter
        self.ring_thickness = ring_thickness
        self.gap = gap
        self.max_rings = max_rings
        self.crosshair_thickness = crosshair_thickness
        self.crosshair_length = crosshair_length
        self.rotation = rotation

    def to_inch(self):
        self.position = tuple([inch(x) for x in self.position])
        self.diameter = inch(self.diameter)
        self.ring_thickness = inch(self.ring_thickness)
        self.gap = inch(self.gap)
        self.crosshair_thickness = inch(self.crosshair_thickness)
        self.crosshair_length = inch(self.crosshair_length)

    def to_metric(self):
        self.position = tuple([metric(x) for x in self.position])
        self.diameter = metric(self.diameter)
        self.ring_thickness = metric(self.ring_thickness)
        self.gap = metric(self.gap)
        self.crosshair_thickness = metric(self.crosshair_thickness)
        self.crosshair_length = metric(self.crosshair_length)


    def to_gerber(self, settings=None):
        data = dict(
            code=self.code,
            position="%.4g,%.4g" % self.position,
            diameter=self.diameter,
            ring_thickness=self.ring_thickness,
            gap=self.gap,
            max_rings=self.max_rings,
            crosshair_thickness=self.crosshair_thickness,
            crosshair_length=self.crosshair_length,
            rotation=self.rotation
        )
        fmt = "{code},{position},{diameter},{ring_thickness},{gap},{max_rings},{crosshair_thickness},{crosshair_length},{rotation}*"
        return fmt.format(**data)

    def to_primitive(self, units):
        #raise NotImplementedError()
        return None


class AMThermalPrimitive(AMPrimitive):
    """ Aperture Macro Thermal primitive. Code 7.

    The thermal primitive is a ring (annulus) interrupted by four gaps.
    Exposure is always on.

    .. seealso::
        `The Gerber File Format Specification <http://www.ucamco.com/files/downloads/file/81/the_gerber_file_format_specification.pdf>`_
            **Section 4.12.3.10:** Thermal, primitive code 7.

    Parameters
    ----------
    code : int
        Thermal Primitive code. Must be 7.

    position : tuple (<float>, <float>)
        X and Y coordinates of thermal center

    outer_diameter : float
        outer diameter of thermal.

    inner_diameter : float
        inner diameter of thermal.

    gap : float
        gap thickness

    rotation : float
        thermal rotation about the origin.

    Returns
    -------
    ThermalPrimitive : :class:`gerbers.am_statements.AMThermalPrimitive`
        An initialized AMThermalPrimitive

    Raises
    ------
    ValueError, TypeError
    """
    @classmethod
    def from_gerber(cls, primitive):
        modifiers = primitive.strip(' *').split(",")
        code = int(modifiers[0])
        position = (float(modifiers[1]), float(modifiers[2]))
        outer_diameter = float(modifiers[3])
        inner_diameter = float(modifiers[4])
        gap = float(modifiers[5])
        rotation = float(modifiers[6])
        return cls(code, position, outer_diameter, inner_diameter, gap, rotation)

    def __init__(self, code, position, outer_diameter, inner_diameter, gap, rotation):
        if code != 7:
            raise ValueError('ThermalPrimitive code is 7')
        super(AMThermalPrimitive, self).__init__(code, 'on')
        validate_coordinates(position)
        self.position = position
        self.outer_diameter = outer_diameter
        self.inner_diameter = inner_diameter
        self.gap = gap
        self.rotation = rotation

    def to_inch(self):
        self.position = tuple([inch(x) for x in self.position])
        self.outer_diameter = inch(self.outer_diameter)
        self.inner_diameter = inch(self.inner_diameter)
        self.gap = inch(self.gap)

    def to_metric(self):
        self.position = tuple([metric(x) for x in self.position])
        self.outer_diameter = metric(self.outer_diameter)
        self.inner_diameter = metric(self.inner_diameter)
        self.gap = metric(self.gap)

    def to_gerber(self, settings=None):
        data = dict(
            code=self.code,
            position="%.4g,%.4g" % self.position,
            outer_diameter=self.outer_diameter,
            inner_diameter=self.inner_diameter,
            gap=self.gap,
            rotation=self.rotation
        )
        fmt = "{code},{position},{outer_diameter},{inner_diameter},{gap},{rotation}*"
        return fmt.format(**data)

    def _approximate_arc_cw(self, start_angle, end_angle, radius, center):
        """
        Get an arc as a series of points

        Parameters
        ----------
        start_angle : The start angle in radians
        end_angle : The end angle in radians
        radius`: Radius of the arc
        center : The center point of the arc (x, y) tuple

        Returns
        -------
        array of point tuples
        """

        # The total sweep
        sweep_angle = end_angle - start_angle
        num_steps = 10

        angle_step = sweep_angle / num_steps

        radius = radius
        center = center

        points = []

        for i in range(num_steps + 1):
            current_angle = start_angle + (angle_step * i)

            nextx = (center[0] + math.cos(current_angle) * radius)
            nexty = (center[1] + math.sin(current_angle) * radius)

            points.append((nextx, nexty))

        return points

    def to_primitive(self, units):

        # We start with calculating the top right section, then duplicate it

        inner_radius = self.inner_diameter / 2.0
        outer_radius = self.outer_diameter / 2.0

        # Calculate the start angle relative to the horizontal axis
        inner_offset_angle = asin(self.gap / 2.0 / inner_radius)
        outer_offset_angle = asin(self.gap / 2.0 / outer_radius)

        rotation_rad = math.radians(self.rotation)
        inner_start_angle = inner_offset_angle + rotation_rad
        inner_end_angle =  math.pi / 2 - inner_offset_angle + rotation_rad

        outer_start_angle = outer_offset_angle + rotation_rad
        outer_end_angle = math.pi / 2 - outer_offset_angle + rotation_rad

        outlines = []
        aperture = Circle((0, 0), 0)

        points = (self._approximate_arc_cw(inner_start_angle, inner_end_angle, inner_radius, self.position)
                + list(reversed(self._approximate_arc_cw(outer_start_angle, outer_end_angle, outer_radius, self.position))))
        # Add in the last point since outlines should be closed
        points.append(points[0])

        # There are four outlines at rotated sections
        for rotation in [0, 90.0, 180.0, 270.0]:

            lines = []
            prev_point = rotate_point(points[0], rotation, self.position)
            for point in points[1:]:
                cur_point = rotate_point(point, rotation, self.position)

                lines.append(Line(prev_point, cur_point, aperture))

            prev_point = cur_point

            outlines.append(Outline(lines, units=units, level_polarity=self._level_polarity))

        return outlines


class AMCenterLinePrimitive(AMPrimitive):
    """ Aperture Macro Center Line primitive. Code 21.

    The center line primitive is a rectangle defined by its width, height, and center point.

    .. seealso::
        `The Gerber File Format Specification <http://www.ucamco.com/files/downloads/file/81/the_gerber_file_format_specification.pdf>`_
            **Section 4.12.3.4:** Center Line, primitive code 21.

    Parameters
    ----------
    code : int
        Center Line Primitive code. Must be 21.

    exposure : str
        'on' or 'off'

    width : float
        Width of rectangle

    height : float
        Height of rectangle

    center : tuple (<float>, <float>)
        X and Y coordinates of line center

    rotation : float
        rectangle rotation about its center.

    Returns
    -------
    CenterLinePrimitive : :class:`gerbers.am_statements.AMCenterLinePrimitive`
        An initialized AMCenterLinePrimitive

    Raises
    ------
    ValueError, TypeError
    """

    @classmethod
    def from_primitive(cls, primitive):
        width = primitive.width
        height = primitive.height
        center = primitive.position
        rotation = math.degrees(primitive.rotation)
        return cls(21, 'on', width, height, center, rotation)

    @classmethod
    def from_gerber(cls, primitive):
        modifiers = primitive.strip(' *').split(",")
        code = int(modifiers[0])
        exposure = 'on' if float(modifiers[1]) == 1 else 'off'
        width = float(modifiers[2])
        height = float(modifiers[3])
        center = (float(modifiers[4]), float(modifiers[5]))
        rotation = float(modifiers[6])
        return cls(code, exposure, width, height, center, rotation)

    def __init__(self, code, exposure, width, height, center, rotation):
        if code != 21:
            raise ValueError('CenterLinePrimitive code is 21')
        super(AMCenterLinePrimitive, self).__init__(code, exposure)
        self.width = width
        self.height = height
        validate_coordinates(center)
        self.center = center
        self.rotation = rotation

    def to_inch(self):
        self.center = tuple([inch(x) for x in self.center])
        self.width = inch(self.width)
        self.height = inch(self.height)

    def to_metric(self):
        self.center = tuple([metric(x) for x in self.center])
        self.width = metric(self.width)
        self.height = metric(self.height)

    def to_gerber(self, settings=None):
        data = dict(
            code=self.code,
            exposure = '1' if self.exposure == 'on' else '0',
            width = self.width,
            height = self.height,
            center="%.4g,%.4g" % self.center,
            rotation=self.rotation
        )
        fmt = "{code},{exposure},{width},{height},{center},{rotation}*"
        return fmt.format(**data)

    def to_primitive(self, units):

        x = self.center[0]
        y = self.center[1]
        half_width = self.width / 2.0
        half_height = self.height / 2.0

        points = []
        points.append((x - half_width, y + half_height))
        points.append((x - half_width, y - half_height))
        points.append((x + half_width, y - half_height))
        points.append((x + half_width, y + half_height))

        aperture = Circle((0, 0), 0)

        lines = []
        prev_point = rotate_point(points[3], self.rotation, self.center)
        for point in points:
            cur_point = rotate_point(point, self.rotation, self.center)

            lines.append(Line(prev_point, cur_point, aperture))

        return Outline(lines, units=units, level_polarity=self._level_polarity)


class AMLowerLeftLinePrimitive(AMPrimitive):
    """ Aperture Macro Lower Left Line primitive. Code 22.

    The lower left line primitive is a rectangle defined by its width, height, and the lower left point.

    .. seealso::
        `The Gerber File Format Specification <http://www.ucamco.com/files/downloads/file/81/the_gerber_file_format_specification.pdf>`_
            **Section 4.12.3.5:** Lower Left Line, primitive code 22.

    Parameters
    ----------
    code : int
        Center Line Primitive code. Must be 22.

    exposure : str
        'on' or 'off'

    width : float
        Width of rectangle

    height : float
        Height of rectangle

    lower_left : tuple (<float>, <float>)
        X and Y coordinates of lower left corner

    rotation : float
        rectangle rotation about its origin.

    Returns
    -------
    LowerLeftLinePrimitive : :class:`gerbers.am_statements.AMLowerLeftLinePrimitive`
        An initialized AMLowerLeftLinePrimitive

    Raises
    ------
    ValueError, TypeError
    """
    @classmethod
    def from_gerber(cls, primitive):
        modifiers = primitive.strip(' *').split(",")
        code = int(modifiers[0])
        exposure = 'on' if float(modifiers[1]) == 1 else 'off'
        width = float(modifiers[2])
        height = float(modifiers[3])
        lower_left = (float(modifiers[4]), float(modifiers[5]))
        rotation = float(modifiers[6])
        return cls(code, exposure, width, height, lower_left, rotation)

    def __init__(self, code, exposure, width, height, lower_left, rotation):
        if code != 22:
            raise ValueError('LowerLeftLinePrimitive code is 22')
        super (AMLowerLeftLinePrimitive, self).__init__(code, exposure)
        self.width = width
        self.height = height
        validate_coordinates(lower_left)
        self.lower_left = lower_left
        self.rotation = rotation

    def to_inch(self):
        self.lower_left = tuple([inch(x) for x in self.lower_left])
        self.width = inch(self.width)
        self.height = inch(self.height)

    def to_metric(self):
        self.lower_left = tuple([metric(x) for x in self.lower_left])
        self.width = metric(self.width)
        self.height = metric(self.height)

    def to_gerber(self, settings=None):
        data = dict(
            code=self.code,
            exposure = '1' if self.exposure == 'on' else '0',
            width = self.width,
            height = self.height,
            lower_left="%.4g,%.4g" % self.lower_left,
            rotation=self.rotation
        )
        fmt = "{code},{exposure},{width},{height},{lower_left},{rotation}*"
        return fmt.format(**data)


class AMUnsupportPrimitive(AMPrimitive):
    @classmethod
    def from_gerber(cls, primitive):
        return cls(primitive)

    def __init__(self, primitive):
        super(AMUnsupportPrimitive, self).__init__(9999)
        self.primitive = primitive

    def to_inch(self):
        pass

    def to_metric(self):
        pass

    def to_gerber(self, settings=None):
        return self.primitive

[(798, 'math.asin', 'asin', False, 'from math import asin\n'), (799, 'math.asin', 'asin', False, 'from math import asin\n'), (801, 'math.radians', 'math.radians', False, 'import math\n'), (877, 'math.degrees', 'math.degrees', False, 'import math\n'), (550, 'math.radians', 'math.radians', False, 'import math\n'), (783, 'math.cos', 'math.cos', False, 'import math\n'), (784, 'math.sin', 'math.sin', False, 'import math\n')]