#! /usr/bin/env python3

import inkex
from lxml import etree
from math import sin, cos, pi

class Path:
    """ Class that defines an svg stroke to be drawn in Inkscape

    Attributes
    ---------
    points: tuple or list of tuples
        Points defining stroke lines.
    style: str
        Single character defining style of stroke. Default values are:
        'm' for mountain creases
        'v' for valley creases
        'e' for edge borders
        Extra possible values:
        'u' for universal creases
        's' for semicreases
        'c' for kirigami cuts
    closed: bool
        Tells if desired path should contain a last stroke from the last point to the first point, closing the path
    radius: float
        If only one point is given, it's assumed to be a circle and radius sets the radius


    Methods
    ---------
    invert(self)
        Inverts path

    Overloaded Operators
    ---------
    __add__(self, offsets)
        Adding a tuple to a Path returns a new path with all points having an offset defined by the tuple

    __mul__(self, transform)
        Define multiplication of a Path to a vector in complex exponential representation


    Static Methods
    ---------
    draw_paths_recursively(path_tree, group, styles_dict)
        Draws strokes defined on "path_tree" to "group". Styles dict maps style of path_tree element to the definition
        of the style. Ex.:
        if path_tree[i].style = 'm', styles_dict must have an element 'm'.

    generate_hgrid(cls, xlims, ylims, nb_of_divisions, style, include_edge=False)
        Generate list of Path instances, in which each Path is a stroke defining a horizontal grid dividing the space
        xlims * ylims nb_of_divisions times.

    generate_vgrid(cls, xlims, ylims, nb_of_divisions, style, include_edge=False)
        Generate list of Path instances, in which each Path is a stroke defining a vertical grid dividing the space
        xlims * ylims nb_of_divisions times.

    generate_separated_paths(cls, points, styles, closed=False)
        Generate list of Path instances, in which each Path is the stroke between each two point tuples, in case each
        stroke must be handled separately

    reflect(cls, path, p1, p2)
        Reflects each point of path on line defined by two points and return new Path instance with new reflected points

    list_reflect(cls, paths, p1, p2)
        Generate list of new Path instances, rotation each path by transform

    list_rotate(cls, paths, theta, translation=(0, 0))
        Generate list of new Path instances, rotation each path by transform

    list_add(cls, paths, offsets)
        Generate list of new Path instances, adding a different tuple for each list


    """

    def __init__(self, points, style, closed=False, invert=False, radius=0.1, separated=False):
        """ Constructor

        Parameters
        ---------
        points: list of 2D tuples
            stroke will connect all points
        style: str
            Single character defining style of stroke. For use with the OrigamiPatterns class (probably the only
            project that will ever use this file) the default values are:
            'm' for mountain creases
            'v' for valley creases
            'e' for edge borders
        closed: bool 
            if true, last point will be connected to first point at the end
        invert: bool
            if true, stroke will start at the last point and go all the way to the first one
        """
        if type(points) == list and len(points) != 1:
            self.type = 'linear'
            if invert:
                self.points = points[::-1]
            else:
                self.points = points

        elif (type(points) == list and len(points) == 1):
            self.type = 'circular'
            self.points = points
            self.radius = radius

        elif (type(points) == tuple and len(points) == 2):
            self.type = 'circular'
            self.points = [points]
            self.radius = radius

        else:
            raise TypeError("Points must be tuple of length 2 (for a circle) or a list of tuples of length 2 each")

        self.style = style
        self.closed = closed

    def invert(self):
        """ Inverts path
        """
        self.points = self.points[::-1]

    """ 
        Draw path recursively
        - Static method
        - Draws strokes defined on "path_tree" to "group"
        - Inputs:
        -- path_tree [nested list] of Path instances
        -- group [etree.SubElement]
        -- styles_dict [dict] containing all styles for path_tree
        """
    @staticmethod
    def draw_paths_recursively(path_tree, group, styles_dict):
        """ Static method, draw list of Path instances recursively
        """
        for subpath in path_tree:
            if type(subpath) == list:
                if len(subpath) == 1:
                    subgroup = group
                else:
                    subgroup = etree.SubElement(group, 'g')
                Path.draw_paths_recursively(subpath, subgroup, styles_dict)
            else:
                if styles_dict[subpath.style]['draw']:
                    if subpath.type == 'linear':

                        points = subpath.points
                        path = 'M{},{} '.format(*points[0])
                        for i in range(1, len(points)):
                            path = path + 'L{},{} '.format(*points[i])
                        if subpath.closed:
                            path = path + 'L{},{} Z'.format(*points[0])

                        attribs = {'style': str(inkex.Style(styles_dict[subpath.style])), 'd': path}
                        etree.SubElement(group, inkex.addNS('path', 'svg'), attribs)
                    else:
                        attribs = {'style': str(inkex.Style(styles_dict[subpath.style])),
                                   'cx': str(subpath.points[0][0]), 'cy': str(subpath.points[0][1]),
                                   'r': str(subpath.radius)}
                        etree.SubElement(group, inkex.addNS('circle', 'svg'), attribs)

    @classmethod
    def generate_hgrid(cls, xlims, ylims, nb_of_divisions, style, include_edge=False):
        """ Generate list of Path instances, in which each Path is a stroke defining
        a horizontal grid dividing the space xlims * ylims nb_of_divisions times.

        All lines are alternated, to minimize Laser Cutter unnecessary movements

        Parameters
        ---------
        xlims: tuple
            Defines x_min and x_max for space that must be divided.
        ylims: tuple
            Defines y_min and y_max for space that must be divided.
        nb_of_divisions: int
            Defines how many times it should be divided.
        style: str
            Single character defining style of stroke.
        include_edge: bool 
            Defines if edge should be drawn or not.

        Returns
        ---------
        paths: list of Path instances
        """
        rect_len = (ylims[1] - ylims[0])/nb_of_divisions
        hgrid = []
        for i in range(1 - include_edge, nb_of_divisions + include_edge):
            hgrid.append(cls([(xlims[0], ylims[0]+i*rect_len),
                              (xlims[1], ylims[0]+i*rect_len)],
                             style=style, invert=i % 2 == 0))
        return hgrid

    @classmethod
    def generate_vgrid(cls, xlims, ylims, nb_of_divisions, style, include_edge=False):
        """ Generate list of Path instances, in which each Path is a stroke defining
        a vertical grid dividing the space xlims * ylims nb_of_divisions times.

        All lines are alternated, to minimize Laser Cutter unnecessary movements

        Parameters
        ---------
        -> refer to generate_hgrid

        Returns
        ---------
        paths: list of Path instances
        """
        rect_len = (xlims[1] - xlims[0])/nb_of_divisions
        vgrid = []
        for i in range(1 - include_edge, nb_of_divisions + include_edge):
            vgrid.append(cls([(xlims[0]+i*rect_len, ylims[0]),
                              (xlims[0]+i*rect_len, ylims[1])],
                             style=style, invert=i % 2 == 0))
        return vgrid

    @classmethod
    def generate_polygon(cls, sides, radius, style, center=(0, 0)):
        points = []
        for i in range(sides):
            points.append((radius * cos((1 + i * 2) * pi / sides),
                           radius * sin((1 + i * 2) * pi / sides)))
        return Path(points, style, closed=True)

    @classmethod
    def generate_separated_paths(cls, points, styles, closed=False):
        """ Generate list of Path instances, in which each Path is the stroke
        between each two point tuples, in case each stroke must be handled separately.

        Returns
        ---------
        paths: list
            list of Path instances
        """
        paths = []
        if type(styles) == str:
            styles = [styles] * (len(points) - 1 + int(closed))
        elif len(styles) != len(points) - 1 + int(closed):
            raise TypeError("Number of paths and styles don't match")
        for i in range(len(points) - 1 + int(closed)):
            j = (i+1)%len(points)
            paths.append(cls([points[i], points[j]],
                             styles[i]))
        return paths

    def __add__(self, offsets):
        """ " + " operator overload.
        Adding a tuple to a Path returns a new path with all points having an offset
        defined by the tuple
        """
        if type(offsets) == list:
            if len(offsets) != 1 or len(offsets) != len(self.points):
                raise TypeError("Paths can only be added by a tuple of a list of N tuples, "
                                "where N is the same number of points")

        elif type(offsets) != tuple:
            raise TypeError("Paths can only be added by tuples")
        else:
            offsets = [offsets] * len(self.points)

        # if type(self.points) == list:
        points_new = []
        for point, offset in zip(self.points, offsets):
            points_new.append((point[0]+offset[0],
                               point[1]+offset[1]))

        if self.type == 'circular':
            radius = self.radius
        else:
            radius = 0.2

         # if self.type == 'circular' else 0.1

        return Path(points_new, self.style, self.closed, radius=radius)

    @classmethod
    def list_add(cls, paths, offsets):
        """ Generate list of new Path instances, adding a different tuple for each list

        Parameters
        ---------
        paths: Path or list
            list of N Path instances
        offsets: tuple or list
            list of N tuples

        Returns
        ---------
        paths_new: list
            list of N Path instances
        """
        if type(paths) == Path and type(offsets) == tuple:
            paths = [paths]
            offsets = [offsets]
        elif type(paths) == list and type(offsets) == tuple:
            offsets = [offsets] * len(paths)
        elif type(paths) == Path and type(offsets) == list:
            paths = [paths] * len(offsets)
        elif type(paths) == list and type(offsets) == list:
            if len(paths) == 1:
                paths = [paths[0]] * len(offsets)
            elif len(offsets) == 1:
                offsets = [offsets[0]] * len(paths)
            elif len(offsets) != len(paths):
                raise TypeError("List of paths and list of tuples must have same length. {} paths and {} offsets "
                                " where given".format(len(paths), len(offsets)))
            else:
                pass

        paths_new = []
        for path, offset in zip(paths, offsets):
            paths_new.append(path+offset)

        return paths_new

    def __mul__(self, transform):
        """ " * " operator overload.
        Define multiplication of a Path to a vector in complex exponential representation

        Parameters
        ---------
        transform: float of tuple of length 2 or 4
            if float, transform represents magnitude
                Example: path * 3
            if tuple length 2, transform[0] represents magnitude and transform[1] represents angle of rotation
                Example: path * (3, pi)
            if tuple length 4, transform[2],transform[3] define a different axis of rotation
                Example: path * (3, pi, 1, 1)
        """
        points_new = []

        # "temporary" (probably permanent) compatibility hack
        try:
            long_ = long
        except:
            long_ = int
            
        if isinstance(transform, (int, long_, float)):
            for p in self.points:
                points_new.append((transform * p[0],
                                   transform * p[1]))

        elif isinstance(transform, (list, tuple)):
            if len(transform) == 2:
                u = transform[0]*cos(transform[1])
                v = transform[0]*sin(transform[1])
                x_, y_ = 0, 0
            elif len(transform) == 4:
                u = transform[0]*cos(transform[1])
                v = transform[0]*sin(transform[1])
                x_, y_ = transform[2:]
            else:
                raise IndexError('Paths can only be multiplied by a number or a tuple/list of length 2 or 4')

            for p in self.points:
                x, y = p[0]-x_, p[1]-y_
                points_new.append((x_ + x * u - y * v,
                                   y_ + x * v + y * u))
        else:
            raise TypeError('Paths can only be multiplied by a number or a tuple/list of length 2 or 4')

        if self.type == 'circular':
            radius = self.radius
        else:
            radius = 0.2

        return Path(points_new, self.style, self.closed, radius=radius)

    @classmethod
    def list_rotate(cls, paths, theta, translation=(0, 0)):
        """ Generate list of new Path instances, rotation each path by transform

        Parameters
        ---------
        paths: Path or list
            list of N Path instances
        theta: float (radians)
            angle of rotation
        translation: tuple or list 2
            axis of rotation

        Returns
        ---------
        paths_new: list
            list of N Path instances
        """
        if len(translation) != 2:
            TypeError("Translation must have length 2")

        if type(paths) != list:
            paths = [paths]

        paths_new = []
        for path in paths:
            paths_new.append(path*(1, theta, translation[0], translation[1]))

        if len(paths_new) == 1:
            paths_new = paths_new[0]
        return paths_new

    # TODO:
    # Apparently it's not working properly, must be debugged and tested
    @classmethod
    def reflect(cls, path, p1, p2):
        """ Reflects each point of path on line defined by two points and return new Path instance with new reflected points

        Parameters
        ---------
        path: Path
        p1: tuple or list of size 2
        p2: tuple or list of size 2

        Returns
        ---------
        path_reflected: Path
        """

        (x1, y1) = p1
        (x2, y2) = p2

        if x1 == x2 and y1 == y2:
            ValueError("Duplicate points don't define a line")
        elif x1 == x2:
            t_x = [-1, 0, 2*x1, 1]
            t_y = [0, 1, 0, 1]
        else:
            m = (y2 - y1)/(x2 - x1)
            t = y1 - m*x1
            t_x = [1 - m**2, 2*m, -2*m*t, m**2 + 1]
            t_y = [2*m, m**2 - 1, +2*t, m**2 + 1]

        points_new = []
        for p in path.points:
            x_ = (t_x[0]*p[0] + t_x[1]*p[1] + t_x[2]) / t_x[3]
            y_ = (t_y[0]*p[0] + t_y[1]*p[1] + t_y[2]) / t_y[3]
            points_new.append((x_, y_))

        return Path(points_new, path.style, path.closed)

    # TODO:
    # Apparently it's not working properly, must be debugged and tested
    @classmethod
    def list_reflect(cls, paths, p1, p2):
        """ Generate list of new Path instances, rotation each path by transform

        Parameters
        ---------
        paths: Path or list
            list of N Path instances
        p1: tuple or list of size 2
        p2: tuple or list of size 2

        Returns
        ---------
        paths_new: list
            list of N Path instances
        """

        if type(paths) == Path:
            paths = [paths]

        paths_new = []
        for path in paths:
            paths_new.append(Path.reflect(path, p1, p2))

        return paths_new