# coding=utf-8
#
# Copyright (C) 2018 Martin Owens <doctormo@gmail.com>
#
# 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.
#
"""
functions for digesting paths into a simple list structure
"""

import re
import copy

from math import atan2, cos, pi, sin, sqrt, acos, tan

from .transforms import Transform, BoundingBox, Vector2d
from .utils import classproperty, strargs

try:  # pylint: disable=using-constant-test
    from typing import overload, Any, Type, Dict, Optional, Union, Tuple, List, Iterator, Generator  # pylint: disable=unused-import
    from typing import TypeVar
    Pathlike = TypeVar('Pathlike', bound="PathCommand")
    AbsolutePathlike = TypeVar('AbsolutePathlike', bound="AbsolutePathCommand")
except ImportError:
    overload = lambda x: x

# All the names that get added to the inkex API itself.
__all__ = (
    'Path', 'CubicSuperPath',
    # Path commands:
    'Line', 'line',
    'Move', 'move',
    'ZoneClose', 'zoneClose',
    'Horz', 'horz',
    'Vert', 'vert',
    'Curve', 'curve',
    'Smooth', 'smooth',
    'Quadratic', 'quadratic',
    'TepidQuadratic', 'tepidQuadratic',
    'Arc', 'arc',
    # errors
    'InvalidPath'
)

LEX_REX = re.compile(r'([MLHVCSQTAZmlhvcsqtaz])([^MLHVCSQTAZmlhvcsqtaz]*)')
NONE = lambda obj: obj is not None


class InvalidPath(ValueError):
    """Raised when given an invalid path string"""


class PathCommand(object):
    """
    Base class of all path commands
    """

    # Number of arguments that follow this path commands letter
    nargs = -1

    # The full name of the segment (i.e. Line, Arc, etc)
    name = classproperty(lambda cls: cls.__name__)

    # The single letter representation of this command (i.e. L, A, etc)
    letter = classproperty(lambda cls: cls.name[0])

    # The implicit next command. This is for automatic chains where the next command
    # isn't given, just a bunch on numbers which we automatically parse.
    @classproperty
    def next_command(self):
        return self

    @property
    def is_relative(self):  # type: () -> bool
        raise NotImplementedError

    @property
    def is_absolute(self):  # type: () -> bool
        raise NotImplementedError

    def to_relative(self, prev):  # type: (Vector2d) -> RelativePathCommand
        """Return absolute counterpart for absolute commands or copy for relative"""
        raise NotImplementedError

    def to_absolute(self, prev):  # type: (Vector2d) -> AbsolutePathCommand
        """Return relative counterpart for relative commands or copy for absolute"""
        raise NotImplementedError

    # The precision of the numbers when converting to string
    number_template = "{:.6g}"

    # Maps single letter path command to corresponding class
    # (filled at the bottom of file, when all classes already defined)
    _letter_to_class = {} # type: Dict[str, Type[Any]]

    @staticmethod
    def letter_to_class(letter):
        """Returns class for given path command letter"""
        return PathCommand._letter_to_class[letter]

    @property
    def args(self):  # type: () -> List[float]
        """Returns path command arguments as tuple of floats"""
        raise NotImplementedError()

    def control_points(self, first, prev, prev_prev):
        # type: (Vector2d, Vector2d, Vector2d) -> Union[List[Vector2d], Generator[Vector2d, None, None]]
        """Returns list of path command control points"""
        raise NotImplementedError

    @classmethod
    def _argt(cls, sep):
        return sep.join([cls.number_template] * cls.nargs)

    def __str__(self):
        return "{} {}".format(self.letter, self._argt(" ").format(*self.args)).strip()

    def __repr__(self):
        return "{{}}({})".format(self._argt(", ")).format(self.name, *self.args)

    def __eq__(self, other):
        previous = Vector2d()
        if type(self) == type(other):  # pylint: disable=unidiomatic-typecheck
            return self.args == other.args
        if isinstance(other, tuple):
            return self.args == other
        if not isinstance(other, PathCommand):
            raise ValueError("Can't compare types")
        try:
            if self.is_relative == other.is_relative:
                return self.to_curve(previous) == other.to_curve(previous)
        except ValueError:
            pass
        return False

    def end_point(self, first, prev):  # type: (Vector2d, Vector2d) -> Vector2d
        """Returns last control point of path command"""
        raise NotImplementedError()

    def update_bounding_box(self, first, last_two_points, bbox):
        # type: (Vector2d, List[Vector2d], BoundingBox) -> None
        # pylint: disable=unused-argument
        """
        Enlarges given bbox to contain path element.

        :param (tuple of float) first: first point of path. Required to calculate Z segment
        :param (list of tuple) last_two_points: list with last two control points in abs coords.
        :param (BoundingBox) bbox: bounding box to update
        """
        raise NotImplementedError("Bounding box is not implemented for {}".format(self.name))

    def to_curve(self, prev, prev_prev=Vector2d()):
        # type: (Vector2d, Vector2d) -> Curve
        """Convert command to :py:class:`Curve`
        Curve().to_curve() returns a copy
        """
        raise NotImplementedError("To curve not supported for {}".format(self.name))

    def to_curves(self, prev, prev_prev=Vector2d()):
        # type: (Vector2d, Vector2d) -> List[Curve]
        """Convert command to list of :py:class:`Curve` commands """
        return [self.to_curve(prev, prev_prev)]

    def to_line(self, prev):
        # type: (Vector2d) -> Line
        """Converts this segment to a line (copies if already a line)"""
        return Line(*self.end_point(Vector2d(), prev))


class RelativePathCommand(PathCommand):
    """
    Abstract base class for relative path commands.

    Implements most of methods of :py:class:`PathCommand` through
    conversion to :py:class:`AbsolutePathCommand`
    """

    @property
    def is_relative(self):
        return True

    @property
    def is_absolute(self):
        return False

    def control_points(self, first, prev, prev_prev):
        # type: (Vector2d, Vector2d, Vector2d) -> Union[List[Vector2d], Generator[Vector2d, None, None]]
        return self.to_absolute(prev).control_points(first, prev, prev_prev)

    def to_relative(self, prev):
        # type: (Pathlike, Vector2d) -> Pathlike
        return self.__class__(*self.args)

    def update_bounding_box(self, first, last_two_points, bbox):
        self.to_absolute(last_two_points[-1]).update_bounding_box(first, last_two_points, bbox)

    def end_point(self, first, prev):
        # type: (Vector2d, Vector2d) -> Vector2d
        return self.to_absolute(prev).end_point(first, prev)

    def to_curve(self, prev, prev_prev=Vector2d()):
        # type: (Vector2d, Vector2d) -> Curve
        return self.to_absolute(prev).to_curve(prev, prev_prev)

    def to_curves(self, prev, prev_prev=Vector2d()):
        # type: (Vector2d, Vector2d) -> List[Curve]
        return self.to_absolute(prev).to_curves(prev, prev_prev)


class AbsolutePathCommand(PathCommand):
    """
    Absolute path command. Unlike :py:class:`RelativePathCommand` can be transformed directly.
    """

    @property
    def is_relative(self):
        return False

    @property
    def is_absolute(self):
        return True

    def to_absolute(self, previous):  # type: (AbsolutePathlike, Vector2d) -> AbsolutePathlike
        return self.__class__(*self.args)

    def transform(self, transform):  # type: (AbsolutePathlike, Transform) -> AbsolutePathlike
        """Returns new transformed segment

        :param transform: a transformation to apply
        """
        raise NotImplementedError()

    def rotate(self, degrees, center):  # type: (AbsolutePathlike, float, Vector2d) -> AbsolutePathlike
        """
        Returns new transformed segment

        :param degrees: rotation angle in degrees
        :param center: invariant point of rotation
        """
        return self.transform(Transform(rotate=(degrees, center[0], center[1])))

    def translate(self, dr):  # type: (AbsolutePathlike, Vector2d) -> AbsolutePathlike
        """Translate or scale this path command by dr"""
        return self.transform(Transform(translate=dr))

    def scale(self, factor):  # type: (AbsolutePathlike, Union[float, Tuple[float,float]]) -> AbsolutePathlike
        """Returns new transformed segment

        :param factor: scale or (scale_x, scale_y)
        """
        return self.transform(Transform(scale=factor))


class Line(AbsolutePathCommand):
    """Line segment"""

    nargs = 2

    @property
    def args(self):
        return self.x, self.y

    def __init__(self, x, y):
        self.x = x
        self.y = y

    def update_bounding_box(self, first, last_two_points, bbox):
        bbox += BoundingBox((last_two_points[-1].x, self.x), (last_two_points[-1].y, self.y))

    def control_points(self, first, prev, prev_prev):
        # type: (Vector2d, Vector2d, Vector2d) -> Generator[Vector2d, None, None]
        yield Vector2d(self.x, self.y)

    def to_relative(self, prev):
        # type: (Vector2d) -> line
        return line(self.x - prev.x, self.y - prev.y)

    def transform(self, transform):
        # type: (Line, Transform) -> Line
        return Line(*transform.apply_to_point((self.x, self.y)))

    def end_point(self, first, prev):
        # type: (Vector2d, Vector2d) -> Vector2d
        return Vector2d(self.x, self.y)

    def to_curve(self, prev, prev_prev=Vector2d()):
        # type: (Vector2d, Optional[Vector2d]) -> Curve
        return Curve(prev.x, prev.y, self.x, self.y, self.x, self.y)



class line(RelativePathCommand):  # pylint: disable=invalid-name
    """Relative line segment"""

    nargs = 2

    @property
    def args(self):
        return self.dx, self.dy

    def __init__(self, dx, dy):
        self.dx = dx
        self.dy = dy

    def to_absolute(self, prev):  # type: (Vector2d) -> Line
        return Line(prev.x + self.dx, prev.y + self.dy)


class Move(AbsolutePathCommand):
    """Move pen segment without a line"""

    nargs = 2
    next_command = Line

    @property
    def args(self):
        return self.x, self.y

    def __init__(self, x, y):
        self.x = x
        self.y = y

    def update_bounding_box(self, first, last_two_points, bbox):
        bbox += BoundingBox(self.x, self.y)

    def control_points(self, first, prev, prev_prev):
        # type: (Vector2d, Vector2d, Vector2d) -> Generator[Vector2d, None, None]
        yield Vector2d(self.x, self.y)

    def to_relative(self, prev):
        # type: (Vector2d) -> move
        return move(self.x - prev.x, self.y - prev.y)

    def transform(self, transform):
        # type: (Transform) -> Move
        return Move(*transform.apply_to_point((self.x, self.y)))

    def end_point(self, first, prev):
        # type: (Vector2d, Vector2d) -> Vector2d
        return Vector2d(self.x, self.y)

    def to_curve(self, prev, prev_prev=Vector2d()):
        # type: (Vector2d, Optional[Vector2d]) -> Curve
        raise ValueError("Move segments can not be changed into curves.")


class move(RelativePathCommand):  # pylint: disable=invalid-name
    """Relative move segment"""

    nargs = 2
    next_command = line

    @property
    def args(self):
        return self.dx, self.dy

    def __init__(self, dx, dy):
        self.dx = dx
        self.dy = dy

    def to_absolute(self, prev):  # type: (Vector2d) -> Move
        return Move(prev.x + self.dx, prev.y + self.dy)


class ZoneClose(AbsolutePathCommand):
    """Close segment to finish a path"""
    nargs = 0
    next_command = Move

    @property
    def args(self):
        return ()

    def update_bounding_box(self, first, last_two_points, bbox):
        pass

    def transform(self, transform):
        # type: (Transform) -> ZoneClose
        return ZoneClose()

    def control_points(self, first, prev, prev_prev):
        # type: (Vector2d, Vector2d, Vector2d) -> Generator[Vector2d, None, None]
        yield first

    def to_relative(self, prev):
        # type: (Vector2d) -> zoneClose
        return zoneClose()

    def end_point(self, first, prev):
        # type: (Vector2d, Vector2d) -> Vector2d
        return first

    def to_curve(self, prev, prev_prev=Vector2d()):
        # type: (Vector2d, Optional[Vector2d]) -> Curve
        raise ValueError("ZoneClose segments can not be changed into curves.")


class zoneClose(RelativePathCommand):  # pylint: disable=invalid-name
    """Same as above (svg says no difference)"""

    nargs = 0
    next_command = Move

    @property
    def args(self):
        return ()

    def to_absolute(self, prev):
        return ZoneClose()


class Horz(AbsolutePathCommand):
    """Horizontal Line segment"""
    nargs = 1

    @property
    def args(self):
        return self.x,

    def __init__(self, x):
        self.x = x

    def update_bounding_box(self, first, last_two_points, bbox):
        bbox += BoundingBox((last_two_points[-1].x, self.x), last_two_points[-1].y)

    def control_points(self, first, prev, prev_prev):
        # type: (Vector2d, Vector2d, Vector2d) -> Generator[Vector2d, None, None]
        yield Vector2d(self.x, prev.y)

    def to_relative(self, prev):
        # type: (Vector2d) -> horz
        return horz(self.x - prev.x)

    def transform(self, transformation):
        # type: (Pathlike, Transform) -> Pathlike
        raise ValueError("Horizontal lines can't be transformed directly.")

    def end_point(self, first, prev):
        # type: (Vector2d, Vector2d) -> Vector2d
        return Vector2d(self.x, prev.y)

    def to_curve(self, prev, prev_prev=Vector2d()):
        # type: (Vector2d, Optional[Vector2d]) -> Curve
        """Convert a horizontal line into a curve"""
        return self.to_line(prev).to_curve(prev)

    def to_line(self, prev):
        # type: (Vector2d) -> Line
        """Return this path command as a Line instead"""
        return Line(self.x, prev.y)


class horz(RelativePathCommand):  # pylint: disable=invalid-name
    """Relative horz line segment"""

    nargs = 1

    @property
    def args(self):
        return self.dx,

    def __init__(self, dx):
        self.dx = dx

    def to_absolute(self, prev):  # type: (Vector2d) -> Horz
        return Horz(prev.x + self.dx)

    def to_line(self, prev):  # type: (Vector2d) -> Line
        """Return this path command as a Line instead"""
        return Line(prev.x + self.dx, prev.y)


class Vert(AbsolutePathCommand):
    """Vertical Line segment"""

    nargs = 1

    @property
    def args(self):
        return self.y,

    def __init__(self, y):
        self.y = y

    def update_bounding_box(self, first, last_two_points, bbox):
        bbox += BoundingBox(last_two_points[-1].x, (last_two_points[-1].y, self.y))

    def transform(self, transform):  # type: (Pathlike, Transform) -> Pathlike
        raise ValueError("Vertical lines can't be transformed directly.")

    def control_points(self, first, prev, prev_prev):
        # type: (Vector2d, Vector2d, Vector2d) -> Generator[Vector2d, None, None]
        yield Vector2d(prev.x, self.y)

    def to_relative(self, prev):
        # type: (Vector2d) -> vert
        return vert(self.y - prev.y)

    def end_point(self, first, prev):
        # type: (Vector2d, Vector2d) -> Vector2d
        return Vector2d(prev.x, self.y)

    def to_line(self, prev):
        # type: (Vector2d) -> Line
        """Return this path command as a line instead"""
        return Line(prev.x, self.y)

    def to_curve(self, prev, prev_prev=Vector2d()):  # type: (Vector2d, Optional[Vector2d]) -> Curve
        """Convert a horizontal line into a curve"""
        return self.to_line(prev).to_curve(prev)


class vert(RelativePathCommand):  # pylint: disable=invalid-name
    """Relative vertical line segment"""

    nargs = 1

    @property
    def args(self):
        return self.dy,

    def __init__(self, dy):
        self.dy = dy

    def to_absolute(self, prev):  # type: (Vector2d) -> Vert
        return Vert(prev.y + self.dy)

    def to_line(self, prev): # type: (Vector2d) -> Line
        """Return this path command as a line instead"""
        return Line(prev.x, prev.y + self.dy)


class Curve(AbsolutePathCommand):
    """Absolute Curved Line segment"""
    nargs = 6

    @property
    def args(self):
        return self.x2, self.y2, self.x3, self.y3, self.x4, self.y4

    def __init__(self, x2, y2, x3, y3, x4, y4):
        self.x2 = x2
        self.y2 = y2

        self.x3 = x3
        self.y3 = y3

        self.x4 = x4
        self.y4 = y4

    def update_bounding_box(self, first, last_two_points, bbox):
        from .transforms import cubic_extrema

        x1, x2, x3, x4 = last_two_points[-1].x, self.x2, self.x3, self.x4
        y1, y2, y3, y4 = last_two_points[-1].y, self.y2, self.y3, self.y4

        if not (x1 in bbox.x and
                x2 in bbox.x and
                x3 in bbox.x and
                x4 in bbox.x):
            bbox.x += cubic_extrema(x1, x2, x3, x4)

        if not (y1 in bbox.y and
                y2 in bbox.y and
                y3 in bbox.y and
                y4 in bbox.y):
            bbox.y += cubic_extrema(y1, y2, y3, y4)

    def transform(self, transform):
        # type: (Transform) -> Curve
        x2, y2 = transform.apply_to_point((self.x2, self.y2))
        x3, y3 = transform.apply_to_point((self.x3, self.y3))
        x4, y4 = transform.apply_to_point((self.x4, self.y4))
        return Curve(x2, y2, x3, y3, x4, y4)

    def control_points(self, first, prev, prev_prev):
        # type: (Vector2d, Vector2d, Vector2d) -> Generator[Vector2d, None, None]
        yield Vector2d(self.x2, self.y2)
        yield Vector2d(self.x3, self.y3)
        yield Vector2d(self.x4, self.y4)

    def to_relative(self, prev):  # type: (Vector2d) -> curve
        return curve(
            self.x2 - prev.x, self.y2 - prev.y,
            self.x3 - prev.x, self.y3 - prev.y,
            self.x4 - prev.x, self.y4 - prev.y
        )

    def end_point(self, first, prev):
        return Vector2d(self.x4, self.y4)

    def to_curve(self, prev, prev_prev=Vector2d()):  # type: (Vector2d, Optional[Vector2d]) -> Curve
        """No conversion needed, pass-through, returns self"""
        return Curve(*self.args)

    def to_bez(self):
        """Returns the list of coords for SuperPath"""
        return [list(self.args[:2]), list(self.args[2:4]), list(self.args[4:6])]

class curve(RelativePathCommand):  # pylint: disable=invalid-name
    """Relative curved line segment"""
    nargs = 6

    @property
    def args(self):
        return self.dx2, self.dy2, self.dx3, self.dy3, self.dx4, self.dy4

    def __init__(self, dx2, dy2, dx3, dy3, dx4, dy4):
        self.dx2 = dx2
        self.dy2 = dy2

        self.dx3 = dx3
        self.dy3 = dy3

        self.dx4 = dx4
        self.dy4 = dy4

    def to_absolute(self, prev):  # type: (Vector2d) -> Curve
        return Curve(
            self.dx2 + prev.x, self.dy2 + prev.y,
            self.dx3 + prev.x, self.dy3 + prev.y,
            self.dx4 + prev.x, self.dy4 + prev.y
        )


class Smooth(AbsolutePathCommand):
    """Absolute Smoothed Curved Line segment"""
    nargs = 4

    @property
    def args(self):
        return self.x3, self.y3, self.x4, self.y4

    def __init__(self, x3, y3, x4, y4):

        self.x3 = x3
        self.y3 = y3

        self.x4 = x4
        self.y4 = y4

    def update_bounding_box(self, first, last_two_points, bbox):
        self.to_curve(last_two_points[-1], last_two_points[-2]).update_bounding_box(first, last_two_points, bbox)

    def control_points(self, first, prev, prev_prev):
        # type: (Vector2d, Vector2d, Vector2d) -> Generator[Vector2d, None, None]

        x1, x2, x3, x4 = prev_prev.x, prev.x, self.x3, self.x4
        y1, y2, y3, y4 = prev_prev.y, prev.y, self.y3, self.y4

        # infer reflected point
        x2 = 2 * x2 - x1
        y2 = 2 * y2 - y1

        yield Vector2d(x2, y2)
        yield Vector2d(x3, y3)
        yield Vector2d(x4, y4)

    def to_relative(self, prev):  # type: (Vector2d) -> smooth
        return smooth(
            self.x3 - prev.x, self.y3 - prev.y,
            self.x4 - prev.x, self.y4 - prev.y
        )

    def transform(self, transform):
        # type: (Transform) -> Smooth
        x3, y3 = transform.apply_to_point((self.x3, self.y3))
        x4, y4 = transform.apply_to_point((self.x4, self.y4))
        return Smooth(x3, y3, x4, y4)

    def end_point(self, first, prev):
        return Vector2d(self.x4, self.y4)

    def to_curve(self, prev, prev_prev=Vector2d()):
        # type: (Vector2d, Vector2d) -> Curve
        """
        Convert this Smooth curve to a regular curve by creating a mirror
        set of nodes based on the previous node. Previous should be a curve.
        """
        (x2, y2), (x3, y3), (x4, y4) = self.control_points(prev, prev, prev_prev)
        return Curve(x2, y2, x3, y3, x4, y4)


class smooth(RelativePathCommand):  # pylint: disable=invalid-name
    """Relative smoothed curved line segment"""
    nargs = 4

    @property
    def args(self):
        return self.dx3, self.dy3, self.dx4, self.dy4

    def __init__(self, dx3, dy3, dx4, dy4):
        self.dx3 = dx3
        self.dy3 = dy3

        self.dx4 = dx4
        self.dy4 = dy4

    def to_absolute(self, prev):  # type: (Vector2d) -> Smooth
        return Smooth(
            self.dx3 + prev.x, self.dy3 + prev.y,
            self.dx4 + prev.x, self.dy4 + prev.y
        )


class Quadratic(AbsolutePathCommand):
    """Absolute Quadratic Curved Line segment"""
    nargs = 4

    @property
    def args(self):
        return self.x2, self.y2, self.x3, self.y3

    def __init__(self, x2, y2, x3, y3):

        self.x2 = x2
        self.y2 = y2

        self.x3 = x3
        self.y3 = y3

    def update_bounding_box(self, first, last_two_points, bbox):
        from .transforms import quadratic_extrema

        x1, x2, x3 = last_two_points[-1].x, self.x2, self.x3
        y1, y2, y3 = last_two_points[-1].y, self.y2, self.y3

        if not (x1 in bbox.x and
                x2 in bbox.x and
                x3 in bbox.x):
            bbox.x += quadratic_extrema(x1, x2, x3)

        if not (y1 in bbox.y and
                y2 in bbox.y and
                y3 in bbox.y):
            bbox.y += quadratic_extrema(y1, y2, y3)

    def control_points(self, first, prev, prev_prev):
        # type: (Vector2d, Vector2d, Vector2d) -> Generator[Vector2d, None, None]
        yield Vector2d(self.x2, self.y2)
        yield Vector2d(self.x3, self.y3)

    def to_relative(self, prev):
        # type: (Vector2d) -> quadratic
        return quadratic(
            self.x2 - prev.x, self.y2 - prev.y,
            self.x3 - prev.x, self.y3 - prev.y
        )

    def transform(self, transform):
        # type: (Transform) -> Quadratic
        x2, y2 = transform.apply_to_point((self.x2, self.y2))
        x3, y3 = transform.apply_to_point((self.x3, self.y3))
        return Quadratic(x2, y2, x3, y3)

    def end_point(self, first, prev):
        # type: (Vector2d, Vector2d) -> Vector2d
        return Vector2d(self.x3, self.y3)

    def to_curve(self, prev, prev_prev=Vector2d()):
        # type: (Vector2d, Vector2d) -> Curve
        """Attempt to convert a quadratic to a curve"""
        prev = Vector2d(prev)
        x1 = 1. / 3 * prev.x + 2. / 3 * self.x2
        x2 = 2. / 3 * self.x2 + 1. / 3 * self.x3
        y1 = 1. / 3 * prev.y + 2. / 3 * self.y2
        y2 = 2. / 3 * self.y2 + 1. / 3 * self.y3
        return Curve(x1, y1, x2, y2, self.x3, self.y3)


class quadratic(RelativePathCommand):  # pylint: disable=invalid-name
    """Relative quadratic line segment"""
    nargs = 4

    @property
    def args(self):
        return self.dx2, self.dy2, self.dx3, self.dy3

    def __init__(self, dx2, dy2, dx3, dy3):
        self.dx2 = dx2
        self.dx3 = dx3
        self.dy2 = dy2
        self.dy3 = dy3

    def to_absolute(self, prev):  # type: (Vector2d) -> Quadratic
        return Quadratic(
            self.dx2 + prev.x, self.dy2 + prev.y,
            self.dx3 + prev.x, self.dy3 + prev.y
        )


class TepidQuadratic(AbsolutePathCommand):
    """Continued Quadratic Line segment"""
    nargs = 2

    @property
    def args(self):
        return self.x3, self.y3

    def __init__(self, x3, y3):
        self.x3 = x3
        self.y3 = y3

    def update_bounding_box(self, first, last_two_points, bbox):
        self.to_quadratic(last_two_points[-1], last_two_points[-2]).update_bounding_box(first, last_two_points, bbox)

    def control_points(self, first, prev, prev_prev):
        # type: (Vector2d, Vector2d, Vector2d) -> Generator[Vector2d, None, None]

        x1, x2, x3 = prev_prev.x, prev.x, self.x3
        y1, y2, y3 = prev_prev.y, prev.y, self.y3

        # infer reflected point
        x2 = 2 * x2 - x1
        y2 = 2 * y2 - y1

        yield Vector2d(x2, y2)
        yield Vector2d(x3, y3)

    def to_relative(self, prev):  # type: (Vector2d) -> tepidQuadratic
        return tepidQuadratic(
            self.x3 - prev.x, self.y3 - prev.y
        )

    def transform(self, transform):
        # type: (Transform) -> TepidQuadratic
        x3, y3 = transform.apply_to_point((self.x3, self.y3))
        return TepidQuadratic(x3, y3)

    def end_point(self, first, prev):
        # type: (Vector2d, Vector2d) -> Vector2d
        return Vector2d(self.x3, self.y3)

    def to_curve(self, prev, prev_prev=Vector2d()):
        # type: (Vector2d, Vector2d) -> Curve
        return self.to_quadratic(prev, prev_prev).to_curve(prev)

    def to_quadratic(self, prev, prev_prev):
        # type: (Vector2d, Vector2d) -> Quadratic
        """
        Convert this continued quadratic into a full quadratic
        """
        (x2, y2), (x3, y3) = self.control_points(prev, prev, prev_prev)
        return Quadratic(x2, y2, x3, y3)


class tepidQuadratic(RelativePathCommand):  # pylint: disable=invalid-name
    """Relative continued quadratic line segment"""
    nargs = 2

    @property
    def args(self):
        return self.dx3, self.dy3

    def __init__(self, dx3, dy3):
        self.dx3 = dx3
        self.dy3 = dy3

    def to_absolute(self, prev):
        # type: (Vector2d) -> TepidQuadratic
        return TepidQuadratic(
            self.dx3 + prev.x, self.dy3 + prev.y
        )


class Arc(AbsolutePathCommand):
    """Special Arc segment"""
    nargs = 7

    @property
    def args(self):
        return self.rx, self.ry, self.x_axis_rotation, self.large_arc, self.sweep, self.x, self.y

    def __init__(self, rx, ry, x_axis_rotation, large_arc, sweep, x, y):
        self.rx = rx
        self.ry = ry
        self.x_axis_rotation = x_axis_rotation
        self.large_arc = large_arc
        self.sweep = sweep
        self.x = x
        self.y = y

    def update_bounding_box(self, first, last_two_points, bbox):
        prev = last_two_points[-1]
        for seg in self.to_curves(prev=prev):
            seg.update_bounding_box(first, [None, prev], bbox)
            prev = seg.end_point(first, prev)

    def control_points(self, first, prev, prev_prev):
        # type: (Vector2d, Vector2d, Vector2d) -> Generator[Vector2d, None, None]
        yield Vector2d(self.x, self.y)

    def to_curves(self, prev, prev_prev=Vector2d()):
        # type: (Vector2d, Vector2d) -> List[Curve]
        """Convert this arc into bezier curves"""
        path = CubicSuperPath([arc_to_path(list(prev), self.args)]).to_path(curves_only=True)
        # Ignore the first move command from to_path()
        return list(path)[1:]

    def transform(self, transform):
        # type: (Transform) -> Arc
        x_, y_ = transform.apply_to_point((self.x, self.y))

        T = transform  # type: Transform
        if self.x_axis_rotation != 0:
            T = T * Transform(rotate=self.x_axis_rotation)
        a, c, b, d, _, _ = list(T.to_hexad())
        # T = | a b |
        #     | c d |

        detT = a * d - b * c
        detT2 = detT ** 2

        rx = float(self.rx)
        ry = float(self.ry)

        if rx == 0.0 or ry == 0.0 or detT2 == 0.0:
            # invalid Arc parameters
            # transform only last point
            return Arc(self.rx, self.ry, self.x_axis_rotation, self.large_arc, self.sweep, x_, y_)

        A = (d ** 2 / rx ** 2 + c ** 2 / ry ** 2) / detT2
        B = - (d * b / rx ** 2 + c * a / ry ** 2) / detT2
        D = (b ** 2 / rx ** 2 + a ** 2 / ry ** 2) / detT2

        theta = atan2(-2 * B, D - A) / 2
        theta_deg = theta * 180.0 / pi
        DA = (D - A)
        l2 = 4 * B ** 2 + DA ** 2

        if l2 == 0:
            delta = 0.0
        else:
            delta = 0.5 * (-DA ** 2 - 4 * B ** 2) / sqrt(l2)

        half = (A + D) / 2

        rx_ = 1.0 / sqrt(half + delta)
        ry_ = 1.0 / sqrt(half - delta)

        x_, y_ = transform.apply_to_point((self.x, self.y))

        if detT > 0:
            sweep = self.sweep
        else:
            sweep = 0 if self.sweep>0 else 1

        return Arc(rx_, ry_, theta_deg, self.large_arc, sweep, x_, y_)

    def to_relative(self, prev):
        # type: (Vector2d) -> arc
        return arc(self.rx, self.ry, self.x_axis_rotation, self.large_arc, self.sweep, self.x - prev.x, self.y - prev.y)

    def end_point(self, first, prev):
        # type: (Vector2d, Vector2d) -> Vector2d
        return Vector2d(self.x, self.y)


class arc(RelativePathCommand):  # pylint: disable=invalid-name
    """Relative Arc line segment"""

    nargs = 7

    @property
    def args(self):
        return self.rx, self.ry, self.x_axis_rotation, self.large_arc, self.sweep, self.dx, self.dy

    def __init__(self, rx, ry, x_axis_rotation, large_arc, sweep, dx, dy):
        self.rx = rx
        self.ry = ry
        self.x_axis_rotation = x_axis_rotation
        self.large_arc = large_arc
        self.sweep = sweep
        self.dx = dx
        self.dy = dy

    def to_absolute(self, prev):  # type: (Vector2d) -> "Arc"
        x1, y1 = prev
        return Arc(self.rx, self.ry, self.x_axis_rotation, self.large_arc, self.sweep, self.dx + x1, self.dy + y1)


PathCommand._letter_to_class = {
    "M": Move,
    "L": Line,
    "V": Vert,
    "H": Horz,
    "A": Arc,
    "C": Curve,
    "S": Smooth,
    "Z": ZoneClose,
    "Q": Quadratic,
    "T": TepidQuadratic,
    "m": move,
    "l": line,
    "v": vert,
    "h": horz,
    "a": arc,
    "c": curve,
    "s": smooth,
    "z": zoneClose,
    "q": quadratic,
    "t": tepidQuadratic
}


class Path(list):
    """A list of segment commands which combine to draw a shape"""

    class PathCommandProxy(object):
        """
        A handy class for Path traverse and coordinate access

        Reduces number of arguments in user code compared to bare :py:class:`PathCommand` methods
        """

        def __init__(self, command, first_point, previous_end_point, prev2_control_point):
            self.command = command  # type: PathCommand
            self.first_point = first_point  # type: Vector2d
            self.previous_end_point = previous_end_point  # type: Vector2d
            self.prev2_control_point = prev2_control_point  # type: Vector2d

        @property
        def name(self):
            return self.command.name

        @property
        def letter(self):
            return self.command.letter

        @property
        def next_command(self):
            return self.command.next_command

        @property
        def is_relative(self):
            return self.command.is_relative

        @property
        def is_absolute(self):
            return self.command.is_absolute

        @property
        def args(self):
            return self.command.args

        @property
        def control_points(self):
            return self.command.control_points(self.first_point, self.previous_end_point, self.prev2_control_point)

        @property
        def end_point(self):
            return self.command.end_point(self.first_point, self.previous_end_point)

        def to_curve(self):
            return self.command.to_curve(self.previous_end_point, self.prev2_control_point)

        def to_curves(self):
            return self.command.to_curves(self.previous_end_point, self.prev2_control_point)

        def __str__(self):
            return str(self.command)

        def __repr__(self):
            return "<" + self.__class__.__name__ + ">" + repr(self.command)

    def __init__(self, path_d=None):
        super(Path, self).__init__()
        if isinstance(path_d, str):
            # Returns a generator returning PathCommand objects
            path_d = self.parse_string(path_d)
        elif isinstance(path_d, CubicSuperPath):
            path_d = path_d.to_path()

        for item in (path_d or ()):
            if isinstance(item, PathCommand):
                self.append(item)
            elif isinstance(item, (list, tuple)) and len(item) == 2:
                if isinstance(item[1], (list, tuple)):
                    self.append(PathCommand.letter_to_class(item[0])(*item[1]))
                else:
                    self.append(Line(*item))
            else:
                raise TypeError("Bad path type: {}({}, ...): {}".format(
                    type(path_d).__name__, type(item).__name__, item))

    @classmethod
    def parse_string(cls, path_d):
        """Parse a path string and generate segment objects"""
        for cmd, numbers in LEX_REX.findall(path_d):
            args = list(strargs(numbers))
            cmd = PathCommand.letter_to_class(cmd)
            i = 0
            while i < len(args) or cmd.nargs == 0:
                seg = cmd(*args[i:i + cmd.nargs])
                i += cmd.nargs
                cmd = seg.next_command
                yield seg

    def bounding_box(self):
        # type: () -> Optional[BoundingBox]
        """Return bounding box of the Path"""
        if not self:
            return None
        iterator = self.proxy_iterator()
        proxy = next(iterator)
        bbox = BoundingBox(proxy.first_point.x, proxy.first_point.y)
        try:
            while True:
                proxy = next(iterator)
                proxy.command.update_bounding_box(proxy.first_point, [
                    proxy.prev2_control_point,
                    proxy.previous_end_point,
                ], bbox)
        except StopIteration:
            return bbox

    def append(self, cmd):
        """Append a command to this path including any chained commands"""
        if isinstance(cmd, list):
            self.extend(cmd)
        elif isinstance(cmd, PathCommand):
            super(Path, self).append(cmd)

    def translate(self, x, y, inplace=False):  # pylint: disable=invalid-name
        """Move all coords in this path by the given amount"""
        return self.transform(Transform(translate=(x, y)), inplace=inplace)

    def scale(self, x, y, inplace=False):  # pylint: disable=invalid-name
        """Scale all coords in this path by the given amounts"""
        return self.transform(Transform(scale=(x, y)), inplace=inplace)

    def rotate(self, deg, center=None, inplace=False):
        """Rotate the path around the given point"""
        if center is None:
            # Default center is center of bbox
            bbox = self.bounding_box()
            if bbox:
                center = bbox.center
            else:
                center = Vector2d()
        center = Vector2d(center)
        return self.transform(Transform(rotate=(deg, center.x, center.y)), inplace=inplace)

    @property
    def control_points(self):

        prev = Vector2d()
        prev_prev = Vector2d()
        first = Vector2d()

        for i, seg in enumerate(self):  # type: PathCommand
            if i == 0:
                first = seg.end_point(first, prev)
            for cp in seg.control_points(first, prev, prev_prev):
                prev_prev = prev
                prev = cp
                yield cp

    @property
    def end_points(self):
        prev = Vector2d()
        first = Vector2d()

        for i, seg in enumerate(self):  # type: PathCommand
            if i == 0:
                first = seg.end_point(first, prev)
            end_point = seg.end_point(first, prev)
            prev = end_point
            yield end_point

    def transform(self, transform, inplace=False):
        """Convert to new path"""
        result = Path()
        previous = Vector2d()
        previous_new = Vector2d()
        first = Vector2d()
        first_new = Vector2d()

        for i, seg in enumerate(self):  # type: PathCommand
            if i == 0:
                first = seg.end_point(first, previous)

            if isinstance(seg, (horz, Horz, Vert, vert)):
                seg = seg.to_line(previous)

            if seg.is_relative:
                new_seg = seg.to_absolute(previous).transform(transform).to_relative(previous_new)
            else:
                new_seg = seg.transform(transform)

            if i == 0:
                first_new = new_seg.end_point(first_new, previous_new)

            if inplace:
                self[i] = new_seg
            else:
                result.append(new_seg)
            previous = seg.end_point(first, previous)
            previous_new = new_seg.end_point(first_new, previous_new)
        if inplace:
            return self
        return result

    def reverse(self):
        """Returns a reversed path"""
        pass

    def close(self):
        """Attempt to close the last path segment"""
        if self and not isinstance(self[-1], (zoneClose, ZoneClose)):
            self.append(ZoneClose())

    def proxy_iterator(self):
        """
        Yields :py:class:`AugmentedPathIterator`

        :rtype: Iterator[ Path.PathCommandProxy ]
        """

        previous = Vector2d()
        prev_prev = Vector2d()
        first = Vector2d()

        for i, seg in enumerate(self):  # type: PathCommand
            if i == 0:
                prev_prev = previous = first = seg.end_point(first, previous)
            yield Path.PathCommandProxy(seg, first, previous, prev_prev)
            if isinstance(seg, (curve, tepidQuadratic, quadratic, smooth,
                                Curve, TepidQuadratic, Quadratic, Smooth)):
                prev_prev = list(seg.control_points(first, previous, prev_prev))[-2]
            previous = seg.end_point(first, previous)

    def to_absolute(self):
        """Convert this path to use only absolute coordinates"""
        abspath = Path()

        previous = Vector2d()
        first = Vector2d()

        for seg in self:  # type: PathCommand
            if isinstance(seg, (move, Move)):
                first = seg.end_point(first, previous)

            abspath.append(seg.to_absolute(previous))
            previous = seg.end_point(first, previous)

        return abspath

    def to_relative(self):
        """Convert this path to use only relative coordinates"""
        abspath = Path()

        previous = Vector2d()
        first = Vector2d()

        for seg in self:  # type: PathCommand
            if isinstance(seg, (move, Move)):
                first = seg.end_point(first, previous)

            abspath.append(seg.to_relative(previous))
            previous = seg.end_point(first, previous)

        return abspath

    def __str__(self):
        return " ".join([str(seg) for seg in self])

    def __add__(self, other):
        acopy = copy.deepcopy(self)
        if isinstance(other, str):
            other = Path(other)
        if isinstance(other, list):
            acopy.extend(other)
        return acopy

    def to_arrays(self):
        """Returns path in format of parsePath output, returning arrays of absolute command data

        .. deprecated:: 1.0
            This is compatibility function for older API. Should not be used in new code

        """
        return [[seg.letter, list(seg.args)] for seg in self.to_absolute()]

    def to_superpath(self):
        """Convert this path into a cubic super path"""
        return CubicSuperPath(self)

    def copy(self):
        """Make a copy"""
        return copy.deepcopy(self)


class CubicSuperPath(list):
    """
    A conversion of a path into a predictable list of cubic curves which
    can be operated on as a list of simplified instructions.

    When converting back into a path, all lines, arcs etc will be converted
    to curve instructions.

    Structure is held as [SubPath[(point_a, bezier, point_b), ...]], ...]
    """

    def __init__(self, items):
        super(CubicSuperPath, self).__init__()
        self._closed = True
        self._prev = Vector2d()
        self._prev_prev = Vector2d()


        if isinstance(items, str):
            items = Path(items)

        if isinstance(items, Path):
            items = items.to_absolute()

        for item in items:
            self.append(item)

    def __str__(self):
        return str(self.to_path())

    def append(self, item):
        """Accept multiple different formats for the data"""
        if isinstance(item, list) and len(item) == 2 and isinstance(item[0], str):
            item = PathCommand.letter_to_class(item[0])(*item[1])
        is_quadratic = False
        if isinstance(item, PathCommand):
            if isinstance(item, Move):
                if self._closed is False:
                    super(CubicSuperPath, self).append([])
                item = [list(item.args), list(item.args), list(item.args)]
            elif isinstance(item, ZoneClose) and self and self[-1]:
                # This duplicates the first segment to 'close' the path, it's appended directly
                # because we don't want to last coord to change for the final segment.
                self[-1].append([self[-1][0][0][:], self[-1][0][1][:], self[-1][0][2][:]])
                # Then adds a new subpath for the next shape (if any)
                self._closed = True
                self._prev.assign(self._first)
                return
            elif isinstance(item, Arc):
                # Arcs are made up of three curves (approximated)
                for arc_curve in item.to_curves(self._prev, self._prev_prev):
                    x2, y2, x3, y3, x4, y4 = arc_curve.args
                    self.append([[x2, y2], [x3, y3], [x4, y4]])
                    self._prev_prev.assign(x3, y3)
                return
            else:
                is_quadratic = isinstance(item, (Quadratic, TepidQuadratic, quadratic, tepidQuadratic))
                if isinstance(item, (Horz, Vert)):
                    item = item.to_line(self._prev)
                pp = self._prev_prev
                if is_quadratic:
                    self._prev_prev = list(item.control_points(self._first, self._prev, pp))[-2:-1][0]
                item = item.to_curve(self._prev, pp)

        if isinstance(item, Curve):
            # Curves are cut into three tuples for the super path.
            item = item.to_bez()

        if not isinstance(item, list):
            raise ValueError("Unknown super curve item type: {}".format(item))

        if len(item) != 3 or not all([len(bit) == 2 for bit in item]):
            # The item is already a subpath (usually from some other process)
            if len(item[0]) == 3 and all([len(bit) == 2 for bit in item[0]]):
                super(CubicSuperPath, self).append(self._clean(item))
                self._prev_prev = Vector2d(self[-1][-1][0])
                self._prev = Vector2d(self[-1][-1][1])
                return
            raise ValueError("Unknown super curve list format: {}".format(item))

        if self._closed:
            # Closed means that the previous segment is closed so we need a new one
            # We always append to the last open segment. CSP starts out closed.
            self._closed = False
            super(CubicSuperPath, self).append([])

        if self[-1]:
            # The last tuple is replaced, it's the coords of where the next segment will land.
            self[-1][-1][-1] = item[0][:]
        # The last coord is duplicated, but is expected to be replaced
        self[-1].append(item[1:] + copy.deepcopy(item)[-1:])

        self._prev = Vector2d(self[-1][-1][1])
        if not is_quadratic:
            self._prev_prev = Vector2d(self[-1][-1][0])

    def _clean(self, lst):
        """Recursively clean lists so they have the same type"""
        if isinstance(lst, (tuple, list)):
            return [self._clean(child) for child in lst]
        return lst

    @property
    def _first(self):
        try:
            return Vector2d(self[-1][0][0])
        except IndexError:
            return Vector2d()

    def to_path(self, curves_only=False):
        """Convert the super path back to an svg path"""
        return Path(list(self.to_segments(curves_only)))

    def to_segments(self, curves_only=False):
        """Generate a set of segments for this cubic super path"""
        for subpath in self:
            previous = []
            for segment in subpath:
                if not previous:
                    yield Move(*segment[1][:])
                elif self.is_line(previous, segment) and not curves_only:
                    if segment is subpath[-1] and Vector2d(segment[1]).is_close(subpath[0][1]):
                        yield ZoneClose()
                    else:
                        yield Line(*segment[1][:])
                else:
                    yield Curve(*(previous[2][:] + segment[0][:] + segment[1][:]))
                previous = segment

    def transform(self, transform):
        """Apply a transformation matrix to this super path"""
        return self.to_path().transform(transform).to_superpath()

    @staticmethod
    def is_line(previous, segment):
        """Check whether csp segment (two points) has retracted handles."""
        return Vector2d(previous[1]).is_close(previous[2]) and \
               Vector2d(segment[0]).is_close(segment[1])

def arc_to_path(point, params):
    """Approximates an arc with cubic bezier segments.

    Arguments:
    point:  Starting point (absolute coords)
    params: Arcs parameters as per
              https://www.w3.org/TR/SVG/paths.html#PathDataEllipticalArcCommands

    Returns a list of triplets of points : [control_point_before, node, control_point_after]
    (first and last returned triplets are [p1, p1, *] and [*, p2, p2])
    """
    A = point[:]
    rx, ry, teta, longflag, sweepflag, x2, y2 = params[:]
    teta = teta * pi / 180.0
    B = [x2, y2]
    # Degenerate ellipse
    if rx == 0 or ry == 0 or A == B:
        return [[A[:], A[:], A[:]], [B[:], B[:], B[:]]]

    # turn coordinates so that the ellipse morph into a *unit circle* (not 0-centered)
    mat = matprod((rotmat(teta), [[1.0 / rx, 0.0], [0.0, 1.0 / ry]], rotmat(-teta)))
    applymat(mat, A)
    applymat(mat, B)

    k = [-(B[1] - A[1]), B[0] - A[0]]
    d = k[0] * k[0] + k[1] * k[1]
    k[0] /= sqrt(d)
    k[1] /= sqrt(d)
    d = sqrt(max(0, 1 - d / 4.0))
    # k is the unit normal to AB vector, pointing to center O
    # d is distance from center to AB segment (distance from O to the midpoint of AB)
    # for the last line, remember this is a unit circle, and kd vector is ortogonal to AB (Pythagorean thm)

    if longflag == sweepflag:  # top-right ellipse in SVG example https://www.w3.org/TR/SVG/images/paths/arcs02.svg
        d *= -1

    O = [(B[0] + A[0]) / 2.0 + d * k[0], (B[1] + A[1]) / 2.0 + d * k[1]]
    OA = [A[0] - O[0], A[1] - O[1]]
    OB = [B[0] - O[0], B[1] - O[1]]
    start = acos(OA[0] / norm(OA))
    if OA[1] < 0:
        start *= -1
    end = acos(OB[0] / norm(OB))
    if OB[1] < 0:
        end *= -1
    # start and end are the angles from center of the circle to A and to B respectively

    if sweepflag and start > end:
        end += 2 * pi
    if (not sweepflag) and start < end:
        end -= 2 * pi

    NbSectors = int(abs(start - end) * 2 / pi) + 1
    dTeta = (end - start) / NbSectors
    v = 4 * tan(dTeta / 4.) / 3.
    # I would use v = tan(dTeta/2)*4*(sqrt(2)-1)/3 ?
    p = []
    for i in range(0, NbSectors + 1, 1):
        angle = start + i * dTeta
        v1 = [O[0] + cos(angle) - (-v) * sin(angle), O[1] + sin(angle) + (-v) * cos(angle)]
        pt = [O[0] + cos(angle), O[1] + sin(angle)]
        v2 = [O[0] + cos(angle) - v * sin(angle), O[1] + sin(angle) + v * cos(angle)]
        p.append([v1, pt, v2])
    p[0][0] = p[0][1][:]
    p[-1][2] = p[-1][1][:]

    # go back to the original coordinate system
    mat = matprod((rotmat(teta), [[rx, 0], [0, ry]], rotmat(-teta)))
    for pts in p:
        applymat(mat, pts[0])
        applymat(mat, pts[1])
        applymat(mat, pts[2])
    return p


def matprod(mlist):
    """Get the product of the mat"""
    prod = mlist[0]
    for mat in mlist[1:]:
        a00 = prod[0][0] * mat[0][0] + prod[0][1] * mat[1][0]
        a01 = prod[0][0] * mat[0][1] + prod[0][1] * mat[1][1]
        a10 = prod[1][0] * mat[0][0] + prod[1][1] * mat[1][0]
        a11 = prod[1][0] * mat[0][1] + prod[1][1] * mat[1][1]
        prod = [[a00, a01], [a10, a11]]
    return prod


def rotmat(teta):
    """Rotate the mat"""
    return [[cos(teta), -sin(teta)], [sin(teta), cos(teta)]]


def applymat(mat, point):
    """Apply the given mat"""
    x = mat[0][0] * point[0] + mat[0][1] * point[1]
    y = mat[1][0] * point[0] + mat[1][1] * point[1]
    point[0] = x
    point[1] = y


def norm(point):
    """Normalise"""
    return sqrt(point[0] * point[0] + point[1] * point[1])
