#  This program is free software; you can redistribute it and/or modify
#  it under the terms of the GNU General Public License as published by
#  the Free Software Foundation; either version 2 of the License, or
#  (at your option) any later version.
#
#  This program is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.
#
#  You should have received a copy of the GNU General Public License
#  along with this program; if not, write to the Free Software
#  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
#  MA 02110-1301, USA.
#

from __future__ import division

import pcbnew
import math


class FootprintWizardDrawingAids:
    """
    Collection of handy functions to simplify drawing shapes from within
    footprint wizards

    A "drawing context" is provided which can be used to set and retain
    settings such as line tickness and layer
    """

    # directions (in degrees, compass-like)
    dirN = 0
    dirNE = 45
    dirE = 90
    dirSE = 135
    dirS = 180
    dirSW = 225
    dirW = 270
    dirNW = 315

    # flip constants
    flipNone = 0
    flipX = 1  # flip X values, i.e. about Y
    flipY = 2  # flip Y valuersabout X
    flipBoth = 3

    xfrmIDENTITY = [1, 0, 0, 0, 1, 0]  # no transform

    # these values come from our KiCad Library Convention 0.11
    defaultLineThickness = pcbnew.FromMM(0.15)

    def DefaultGraphicLayer(self):
        return pcbnew.F_SilkS

    def DefaultTextValueLayer(self):
        return pcbnew.F_Fab

    def __init__(self, module):
        self.module = module
        # drawing context defaults
        self.dc = {
            'layer': self.DefaultGraphicLayer(),
            'lineThickness': self.defaultLineThickness,
            'transforms': [],
            'transform': self.xfrmIDENTITY
        }

    def PushTransform(self, mat):
        """
        Add a transform to the top of the stack and recompute the
        overall transform
        """
        self.dc['transforms'].append(mat)
        self.RecomputeTransforms()

    def PopTransform(self, num=1):
        """
        Remove a transform from the top of the stack and recompute the
        overall transform
        """

        for i in range(num):
            mat = self.dc['transforms'].pop()
        self.RecomputeTransforms()
        return mat

    def ResetTransform(self):
        """
        Reset the transform stack to the identity matrix
        """
        self.dc['transforms'] = []
        self.RecomputeTransforms()

    def _ComposeMatricesWithIdentity(self, mats):
        """
        Compose a sequence of matrices together by sequential
        pre-mutiplciation with the identity matrix
        """

        x = self.xfrmIDENTITY

        for mat in mats:
            #precompose with each transform in turn
            x = [
                x[0] * mat[0] + x[1] * mat[3],
                x[0] * mat[1] + x[1] * mat[4],
                x[0] * mat[2] + x[1] * mat[5] + x[2],
                x[3] * mat[0] + x[4] * mat[3],
                x[3] * mat[1] + x[4] * mat[4],
                x[3] * mat[2] + x[4] * mat[5] + x[5]]

        return x

    def RecomputeTransforms(self):
        """
        Re-compute the transform stack into a single transform and
        store in the DC
        """
        self.dc['transform'] = self._ComposeMatricesWithIdentity(
            self.dc['transforms'])

    def TransformTranslate(self, x, y, push=True):
        """
        Set up and return a transform matrix representing a translartion
        optionally pushing onto the stack

        (   1  0   x  )
        (   0  1   y  )
        """
        mat = [1, 0, x, 0, 1, y]

        if push:
            self.PushTransform(mat)
        return mat

    def TransformFlipOrigin(self, flip, push=True):
        """
        Set up and return a transform matrix representing a horizontal,
        vertical or both flip about the origin
        """
        mat = None
        if flip == self.flipX:
            mat = [-1, 0, 0, 0, 1, 0]
        elif flip == self.flipY:
            mat = [1, 0, 0, 0, -1, 0]
        elif flip == self.flipBoth:
            mat = [-1, 0, 0, 0, -1, 0]
        elif flip == self.flipNone:
            mat = self.xfrmIDENTITY
        else:
            raise ValueError

        if push:
            self.PushTransform(mat)
        return mat

    def TransformFlip(self, x, y, flip=flipNone, push=True):
        """
        Set up and return a transform matrix representing a horizontal,
        vertical or both flip about a point (x,y)

        This is performed by a translate-to-origin, flip, translate-
        back sequence
        """
        mats = [self.TransformTranslate(x, y, push=False),
                self.TransformFlipOrigin(flip, push=False),
                self.TransformTranslate(-x, -y, push=False)]

        #distill into a single matrix
        mat = self._ComposeMatricesWithIdentity(mats)

        if push:
            self.PushTransform(mat)
        return mat

    def TransformRotationOrigin(self, rot, push=True):
        """
        Set up and return a transform matrix representing a rotation
        about the origin, and optionally push onto the stack

        (   cos(t)  -sin(t)   0  )
        (   sin(t)   cos(t)   0  )
        """
        rads = rot * math.pi / 180
        mat = [math.cos(rads), -math.sin(rads), 0,
               math.sin(rads), math.cos(rads), 0]

        if push:
            self.PushTransform(mat)
        return mat

    def TransformRotation(self, x, y, rot, push=True):
        """
        Set up and return a transform matrix representing a rotation
        about the point (x,y), and optionally push onto the stack

        This is performed by a translate-to-origin, rotate, translate-
        back sequence
        """

        mats = [self.TransformTranslate(x, y, push=False),
                self.TransformRotationOrigin(rot, push=False),
                self.TransformTranslate(-x, -y, push=False)]

        #distill into a single matrix
        mat = self._ComposeMatricesWithIdentity(mats)

        if push:
            self.PushTransform(mat)
        return mat

    def TransformScaleOrigin(self, sx, sy=None, push=True):
        """
        Set up and return a transform matrix representing a scale about
        the origin, and optionally push onto the stack

        (   sx   0   0  )
        (    0  sy   0  )
        """

        if sy is None:
            sy = sx

        mat = [sx, 0, 0, 0, sy, 0]

        if push:
            self.PushTransform(mat)
        return mat

    def TransformPoint(self, x, y, mat=None):
        """
        Return a point (x, y) transformed by the given matrix, or if
        that is not given, the drawing context transform
        """

        if not mat:
            mat = self.dc['transform']

        return pcbnew.wxPoint(x * mat[0] + y * mat[1] + mat[2],
                              x * mat[3] + y * mat[4] + mat[5])

    def SetLineTickness(self, lineThickness):
        """
        Set the current pen lineThickness used for subsequent drawing
        operations
        """
        self.dc['lineThickness'] = lineThickness

    def GetLineTickness(self):
        """
        Get the current drawing context line tickness
        """
        return self.dc['lineThickness']

    def SetLayer(self, layer):
        """
        Set the current drawing layer, used for subsequent drawing
        operations
        """
        self.dc['layer'] = layer

    def GetLayer(self):
        """
        return the current drawing layer, used drawing operations
        """
        return self.dc['layer']

    def Line(self, x1, y1, x2, y2):
        """
        Draw a line from (x1, y1) to (x2, y2)
        """
        outline = pcbnew.EDGE_MODULE(self.module)
        outline.SetWidth(self.GetLineTickness())
        outline.SetLayer(self.GetLayer())
        outline.SetShape(pcbnew.S_SEGMENT)
        start = self.TransformPoint(x1, y1)
        end = self.TransformPoint(x2, y2)
        outline.SetStartEnd(start, end)
        self.module.Add(outline)

    def Circle(self, x, y, r, filled=False):
        """
        Draw a circle at (x,y) of radius r
        If filled is true, the thickness and radius of the line will be set
        such that the circle appears filled
        """
        circle = pcbnew.EDGE_MODULE(self.module)
        start = self.TransformPoint(x, y)

        if filled:
            circle.SetWidth(r)
            end = self.TransformPoint(x, y + r/2)
        else:
            circle.SetWidth(self.dc['lineThickness'])
            end = self.TransformPoint(x, y + r)

        circle.SetLayer(self.dc['layer'])
        circle.SetShape(pcbnew.S_CIRCLE)
        circle.SetStartEnd(start, end)
        self.module.Add(circle)

    def Arc(self, cx, cy, sx, sy, a):
        """
        Draw an arc based on centre, start and angle

        The transform matrix is applied

        Note that this won't work properly if the result is not a
        circular arc (eg a horzontal scale)
        """
        circle = pcbnew.EDGE_MODULE(self.module)
        circle.SetWidth(self.dc['lineThickness'])

        center = self.TransformPoint(cx, cy)
        start = self.TransformPoint(sx, sy)

        circle.SetLayer(self.dc['layer'])
        circle.SetShape(pcbnew.S_ARC)

        # check if the angle needs to be reverse (a flip scaling)
        if cmp(self.dc['transform'][0], 0) != cmp(self.dc['transform'][4], 0):
            a = -a

        circle.SetAngle(a)
        circle.SetStartEnd(center, start)
        self.module.Add(circle)

    # extends from (x1,y1) right
    def HLine(self, x, y, l):
        """
        Draw a horizontal line from (x,y), rightwards
        """
        self.Line(x, y, x + l, y)

    def VLine(self, x, y, l):
        """
        Draw a vertical line from (x1,y1), downwards
        """
        self.Line(x, y, x, y + l)

    def Polyline(self, pts, mirrorX=None, mirrorY=None):
        """
        Draw a polyline, optinally mirroring around the given points
        """

        def _PolyLineInternal(pts):
            if len(pts) < 2:
                return

            for i in range(0, len(pts) - 1):
                self.Line(pts[i][0], pts[i][1], pts[i+1][0], pts[i+1][1])

        _PolyLineInternal(pts)  # original

        if mirrorX is not None:
            self.TransformFlip(mirrorX, 0, self.flipX)
            _PolyLineInternal(pts)
            self.PopTransform()

        if mirrorY is not None:
            self.TransformFlipOrigin(0, mirrorY, self.flipY)
            _PolyLineInternal(pts)
            self.PopTransform()

        if mirrorX is not None and mirrorY is not None:
            self.TransformFlip(mirrorX, mirrorY, self.flipBoth)  # both
            _PolyLineInternal(pts)
            self.PopTransform()

    def Reference(self, x, y, size, orientation_degree = 0):
        """
        Draw the module's reference as the given point.

        The actual setting of the reference is not done in this drawing
        aid - that is up to the wizard
        """

        text_size = pcbnew.wxSize(size, size)

        self.module.Reference().SetPos0(self.TransformPoint(x, y))
        self.module.Reference().SetTextPosition(
            self.module.Reference().GetPos0())
        self.module.Reference().SetSize(text_size)
        self.module.Reference().SetOrientation(orientation_degree*10)   # internal angles are in 0.1 deg

    def Value(self, x, y, size, orientation_degree = 0):
        """
        As for references, draw the module's value
        """
        text_size = pcbnew.wxSize(size, size)

        self.module.Value().SetPos0(self.TransformPoint(x, y))
        self.module.Value().SetTextPosition(self.module.Value().GetPos0())
        self.module.Value().SetSize(text_size)
        self.module.Value().SetLayer(self.DefaultTextValueLayer())
        self.module.Value().SetOrientation(orientation_degree*10)   # internal angles are in 0.1 deg

    def Box(self, x, y, w, h):
        """
        Draw a rectangular box, centred at (x,y), with given width and
        height
        """

        pts = [[x - w/2, y - h/2],  # left
               [x + w/2, y - h/2],  # right
               [x + w/2, y + h/2],  # bottom
               [x - w/2, y + h/2],  # top
               [x - w/2, y - h/2]]  # close

        self.Polyline(pts)

    def NotchedCircle(self, x, y, r, notch_w, notch_h, rotate=0):
        """
        Circle radus r centred at (x, y) with a raised or depressed notch
        at the top
        Notch height is measured from the top of the circle radius
        """

        self.TransformRotation(x, y, rotate)

        # find the angle where the notch vertical meets the circle
        angle_intercept = math.asin(notch_w/(2 * r))

        # and find the co-ords of this point
        sx = math.sin(angle_intercept) * r
        sy = -math.cos(angle_intercept) * r

        # NOTE: this may be out by a factor of ten one day
        arc_angle = (math.pi * 2 - angle_intercept * 2) * (1800/math.pi)

        self.Arc(x,y, sx, sy, arc_angle)

        pts = [[sx,  sy],
               [sx,  -r - notch_h],
               [-sx, -r - notch_h],
               [-sx, sy]]

        self.Polyline(pts)
        self.PopTransform()

    def NotchedBox(self, x, y, w, h, notchW, notchH, rotate=0):
        """
        Draw a box with a notch in the top edge
        """

        self.TransformRotation(x, y, rotate)

        # limit to half the overall width
        notchW = min(x + w/2, notchW)

        # draw notch
        self.Polyline([  # three sides of box
            (x - w/2, y - h/2),
            (x - w/2, y + h/2),
            (x + w/2, y + h/2),
            (x + w/2, y - h/2),
            # the notch
            (notchW/2, y - h/2),
            (notchW/2, y - h/2 + notchH),
            (-notchW/2, y - h/2 + notchH),
            (-notchW/2, y - h/2),
            (x - w/2, y - h/2)
        ])

        self.PopTransform()

    def BoxWithDiagonalAtCorner(self, x, y, w, h,
                                setback=pcbnew.FromMM(1.27), flip=flipNone):
        """
        Draw a box with a diagonal at the top left corner
        """

        self.TransformFlip(x, y, flip, push=True)

        pts = [[x - w/2 + setback, y - h/2],
               [x - w/2,           y - h/2 + setback],
               [x - w/2,           y + h/2],
               [x + w/2,           y + h/2],
               [x + w/2,           y - h/2],
               [x - w/2 + setback, y - h/2]]

        self.Polyline(pts)

        self.PopTransform()

    def BoxWithOpenCorner(self, x, y, w, h,
                          setback=pcbnew.FromMM(1.27), flip=flipNone):
        """
        Draw a box with an opening at the top left corner
        """

        self.TransformTranslate(x, y)
        self.TransformFlipOrigin(flip)

        pts = [[- w/2,           - h/2 + setback],
               [- w/2,           + h/2],
               [+ w/2,           + h/2],
               [+ w/2,           - h/2],
               [- w/2 + setback, - h/2]]

        self.Polyline(pts)

        self.PopTransform(num=2)

    def MarkerArrow(self, x, y, direction=dirN, width=pcbnew.FromMM(1)):
        """
        Draw a marker arrow facing in the given direction, with the
        point at (x,y)

        Direction of 0 is north
        """

        self.TransformTranslate(x, y)
        self.TransformRotationOrigin(direction)

        pts = [[0,          0],
               [width / 2,  width / 2],
               [-width / 2, width / 2],
               [0,          0]]

        self.Polyline(pts)
        self.PopTransform(2)
