# -*- coding: utf-8 -*-
"""SVGPATH2MPL
~~~~~~~~~~~

Parse SVG path definition strings into matplotlib
Path objects. A path in SVG is defined by a 'path' element which contains a
``d="(path data)"`` attribute that contains moveto, line, curve (both cubic
and quadratic Béziers), arc and closepath instructions. See the SVG Path
specification at <https://www.w3.org/TR/SVG/paths.html>. :copyright: (c)
2016, Nezar Abdennur.

:license: BSD.
"""

from math import acos
from math import cos
from math import degrees
from math import radians
from math import sin
from math import sqrt
import re

from matplotlib.path import Path
import matplotlib.transforms as transforms
import numpy as np


__version__ = '1.0.0'
__all__ = ['parse_path']

COMMANDS = set('MmZzLlHhVvCcSsQqTtAa')
UPPERCASE = set('MZLHVCSQTA')

COMMAND_RE = re.compile(r'([MmZzLlHhVvCcSsQqTtAa])')
FLOAT_RE = re.compile(r'[-+]?[0-9]*\.?[0-9]+(?:[eE][-+]?[0-9]+)?')

COMMAND_CODES = {
    'M': (Path.MOVETO,),  # moveto
    'L': (Path.LINETO,),  # line
    'H': (Path.LINETO,),  # shorthand for horizontal line
    'V': (Path.LINETO,),  # shorthand for vertical line
    'Q': (Path.CURVE3,) * 2,  # quadratic bezier
    'T': (Path.CURVE3,) * 2,  # shorthand for smooth quadratic bezier
    'C': (Path.CURVE4,) * 3,  # cubic bezier
    'S': (Path.CURVE4,) * 3,  # shorthand for smooth cubic bezier
    'Z': (Path.CLOSEPOLY,),  # closepath
    'A': None  # arc
}

PARAMS = {
    'M': 2,  # moveto
    'L': 2,  # line
    'H': 1,  # shorthand for horizontal line
    'V': 1,  # shorthand for vertical line
    'Q': 4,  # quadratic bezier
    'T': 4,  # shorthand for smooth quadratic bezier
    'C': 6,  # cubic bezier
    'S': 6,  # shorthand for smooth cubic bezier
    'Z': 0,  # closepath
    'A': 7  # arc
}


def endpoint_to_center(start, radius, rotation, large, sweep, end):
  """
    Translates the "endpoint" parameterization of an elliptical arc used by
    the SVG spec to the "center" parameterization.
    Parameters
    ----------
    start : complex
        Starting point (x1, y1).
    radius : complex
        Two elliptical radii (rx, ry).
    rotation : float
        Angle from the x-axis of the current coordinate system to the x-axis of
        the ellipse.
    large : bool
        False if an arc spanning < 180 degrees is to be drawn, True if an arc
        spanning >= 180 degrees is to be drawn.
    sweep : bool
        If sweep-flag is True, then the arc will be drawn in a "positive-angle"
        direction from start to end.
    end : complex
        End point (x2, y2).
    Returns
    -------
    radius : complex
        Radii of the ellipse (rx, ry), potentially corrected if out-of-range.
    center : complex
        Center of the ellipse (xc, yc).
    theta1, theta2 : float
        Start and end angles of an arc on the unit circle prior to being
        stretched and rotated into an elliptical arc.
    Notes
    -----
    See [1]_, `svg.path <https://github.com/regebro/svg.path>`_, and this
    `SO question <https://stackoverflow.com/q/197649>`_.
    References
    ----------
    .. [1] http://www.w3.org/TR/SVG/implnote.html#ArcImplementationNotes
    """
  # Step 1: compute x1prim, y1prim
  cosr = cos(radians(rotation))
  sinr = sin(radians(rotation))
  dx = (start.real - end.real) / 2
  dy = (start.imag - end.imag) / 2
  x1prim = cosr * dx + sinr * dy
  y1prim = -sinr * dx + cosr * dy
  x1prim_sq = x1prim * x1prim
  y1prim_sq = y1prim * y1prim

  rx = abs(radius.real)
  ry = abs(radius.imag)
  rx_sq = rx * rx
  ry_sq = ry * ry

  # Correct out of range radii
  radius_scale = (x1prim_sq / rx_sq) + (y1prim_sq / ry_sq)
  if radius_scale > 1:
    radius_scale = sqrt(radius_scale)
    rx *= radius_scale
    ry *= radius_scale
    rx_sq = rx * rx
    ry_sq = ry * ry
  else:
    # SVG spec only scales UP
    radius_scale = 1
  radius = rx + ry * 1j

  # Step 2: compute cxprim, cyprim
  t1 = rx_sq * y1prim_sq
  t2 = ry_sq * x1prim_sq
  c = sqrt(abs((rx_sq * ry_sq - t1 - t2) / (t1 + t2)))
  if large == sweep:
    c = -c
  cxprim = c * rx * y1prim / ry
  cyprim = -c * ry * x1prim / rx

  # Step 3: compute the center from cxprim, cyprim
  center = complex(
      (cosr * cxprim - sinr * cyprim) + ((start.real + end.real) / 2),
      (sinr * cxprim + cosr * cyprim) + ((start.imag + end.imag) / 2),
  )

  # Step 4: compute theta and delta_theta
  ux = (x1prim - cxprim) / rx
  uy = (y1prim - cyprim) / ry
  vx = (-x1prim - cxprim) / rx
  vy = (-y1prim - cyprim) / ry
  n = sqrt(ux * ux + uy * uy)
  p = ux
  theta = degrees(acos(p / n))
  if uy < 0:
    theta = -theta
  theta = theta % 360

  n = sqrt((ux * ux + uy * uy) * (vx * vx + vy * vy))
  p = ux * vx + uy * vy
  d = p / n
  # In certain cases the above calculation can through inaccuracies
  # become just slightly out of range, f ex -1.0000000000000002.
  d = np.clip(d, -1.0, 1.0)

  delta = degrees(acos(d))
  if (ux * vy - uy * vx) < 0:
    delta = -delta
  delta = delta % 360
  if not sweep:
    if delta > 0:
      delta -= 360

  return radius, center, theta, theta + delta


def arc_path(start, radius, rotation, large, sweep, end):
  """
    Generate an elliptical arc path given an endpoint parameterization.
    Uses matplotlib to draw the arc using quadratic Bezier curves.
    Parameters
    ----------
    start : complex
        Starting point (x1, y1).
    radius : complex
        Two elliptical radii (rx, ry).
    rotation : float
        Angle from the x-axis of the current coordinate system to the x-axis of
        the ellipse.
    large : bool
        False if an arc spanning < 180 degrees is to be drawn, True if an arc
        spanning >= 180 degrees is to be drawn.
    sweep : bool
        If sweep-flag is True, then the arc will be drawn in a "positive-angle"
        direction from start to end.
    end : complex
        End point (x2, y2).
    Returns
    -------
    codes : array (n,)
        Command codes
    verts : array (n,2)
        Vertices
    Notes
    -----
    We first perform a conversion to center parameterization and generate
    a circular arc at the origin. Then we apply scaling, translation and
    rotation.
    One can think of an ellipse as a circle that has been stretched and then
    rotated. Start by making an arc along the unit circle from `theta1` to
    `theta2`, centered at `center`. Then scale the circle along the x and y
    axes according to the given radii. Finally, rotate the arc around the
    center through the given angle `rotation`.
    """
  radius, center, theta1, theta2 = endpoint_to_center(start, radius, rotation,
                                                      large, sweep, end)

  # Create an arc on the unit circle
  # Matplotlib does this using CURVE4 operations
  # https://matplotlib.org/stable/_modules/matplotlib/path.html#Path.arc
  if theta2 > theta1:
    arc = Path.arc(theta1=theta1, theta2=theta2)
    reverse_path = False
  else:
    arc = Path.arc(theta1=theta2, theta2=theta1)
    reverse_path = True

  # Transform it into an elliptical arc:
  # * scale the minor and major axes
  # * translate it to the center
  # * rotate the x-axis of the ellipse from the x-axis of the current
  #   coordinate system
  trans = (
      transforms.Affine2D().scale(radius.real, radius.imag).translate(
          center.real, center.imag).rotate_deg_around(center.real, center.imag,
                                                      rotation))
  arc = trans.transform_path(arc)
  codes = np.array(arc.codes)
  verts = np.array(arc.vertices)

  # Make sure we are drawing from start to end
  if reverse_path:
    verts = verts[::-1, :]

  # Change the initial MOVETO operation into a LINETO to connect to the
  # previous path
  codes[0] = Path.LINETO

  return codes, verts


def _tokenize_path(pathdef):
  for x in COMMAND_RE.split(pathdef):
    if x in COMMANDS:
      yield x
    for token in FLOAT_RE.findall(x):
      yield token


def _next_pos(elements):
  return float(elements.pop()) + float(elements.pop()) * 1j


def _parse_path(pathdef, current_pos):
  # In the SVG specs, initial movetos are absolute, even if specified as 'm'.
  # This is the default behavior here as well. But if you pass in a
  # current_pos variable, the initial moveto will be relative to that
  # current_pos. This is useful.
  elements = list(_tokenize_path(pathdef))
  # Reverse for easy use of .pop()
  elements.reverse()

  start_pos = None
  command = None

  while elements:

    # 1. Determine the current command

    if elements[-1] in COMMANDS:
      # New command.
      last_command = command  # Used by S and T
      command = elements.pop()
      absolute = command in UPPERCASE
      command = command.upper()
    else:
      # Implicit command.
      # If this element starts with numbers, it is an implicit command
      # and we don't change the command. Check that it's allowed:
      if command is None:
        raise ValueError('Unallowed implicit command in {}, position {}'.format(
            pathdef,
            len(pathdef.split()) - len(elements)))
      last_command = command  # Used by S and T

    # 2. Parse the current command

    # MOVETO
    if command == 'M':
      pos = _next_pos(elements)
      if absolute:
        current_pos = pos
      else:
        current_pos += pos

      # when M is called, reset start_pos
      # This behavior of Z is defined in svg spec:
      # http://www.w3.org/TR/SVG/paths.html#PathDataClosePathCommand
      start_pos = current_pos

      yield COMMAND_CODES['M'], [(current_pos.real, current_pos.imag)]

      # Implicit moveto commands are treated as lineto commands.
      # So we set command to lineto here, in case there are
      # further implicit commands after this moveto.
      command = 'L'

    # CLOSEPATH
    elif command == 'Z':
      # path closure
      if current_pos != start_pos:
        verts = [(start_pos.real, start_pos.imag)]
        yield COMMAND_CODES['L'], verts

      # mpl.Path: a point is required but ignored
      verts = [(start_pos.real, start_pos.imag)]
      yield COMMAND_CODES['Z'], verts

      current_pos = start_pos
      start_pos = None
      command = None  # You can't have implicit commands after closing.

    # LINETO
    elif command == 'L':
      pos = _next_pos(elements)
      if not absolute:
        pos += current_pos
      verts = [(pos.real, pos.imag)]
      yield COMMAND_CODES['L'], verts
      current_pos = pos

    # HORIZONTAL_PATHTO
    elif command == 'H':
      x = elements.pop()
      pos = float(x) + current_pos.imag * 1j
      if not absolute:
        pos += current_pos.real
      verts = [(pos.real, pos.imag)]
      yield COMMAND_CODES['H'], verts
      current_pos = pos

    # VERTICAL_PATHTO
    elif command == 'V':
      y = elements.pop()
      pos = current_pos.real + float(y) * 1j
      if not absolute:
        pos += current_pos.imag * 1j
      verts = [(pos.real, pos.imag)]
      yield COMMAND_CODES['V'], verts
      current_pos = pos

    # CUBIC_BEZIER
    elif command == 'C':
      control1 = _next_pos(elements)
      control2 = _next_pos(elements)
      end = _next_pos(elements)
      if not absolute:
        control1 += current_pos
        control2 += current_pos
        end += current_pos
      verts = [(control1.real, control1.imag), (control2.real, control2.imag),
               (end.real, end.imag)]
      yield COMMAND_CODES['C'], verts
      current_pos = end

    # SMOOTH_CUBIC_BEZIER
    elif command == 'S':
      # Smooth curve. First control point is the "reflection" of
      # the second control point in the previous path.
      if last_command not in 'CS':
        # If there is no previous command or if the previous command
        # was not an C, c, S or s, assume the first control point is
        # coincident with the current point.
        control1 = current_pos
      else:
        # The first control point is assumed to be the reflection of
        # the second control point on the previous command relative
        # to the current point.
        last_control = control2
        control1 = current_pos + current_pos - last_control
      control2 = _next_pos(elements)
      end = _next_pos(elements)
      if not absolute:
        control2 += current_pos
        end += current_pos
      verts = [(control1.real, control1.imag), (control2.real, control2.imag),
               (end.real, end.imag)]
      yield COMMAND_CODES['S'], verts
      current_pos = end

    # QUADRATIC_BEZIER
    elif command == 'Q':
      control = _next_pos(elements)
      end = _next_pos(elements)
      if not absolute:
        control += current_pos
        end += current_pos
      verts = [(control.real, control.imag), (end.real, end.imag)]
      yield COMMAND_CODES['Q'], verts
      current_pos = end

    # SMOOTH_QUADRATIC_BEZIER
    elif command == 'T':
      # Smooth curve. Control point is the "reflection" of
      # the second control point in the previous path.
      if last_command not in 'QT':
        # If there is no previous command or if the previous command
        # was not an Q, q, T or t, assume the first control point is
        # coincident with the current point.
        control = current_pos
      else:
        # The control point is assumed to be the reflection of
        # the control point on the previous command relative
        # to the current point.
        last_control = control
        control = current_pos + current_pos - last_control
      end = _next_pos(elements)
      if not absolute:
        end += current_pos
      verts = [(control.real, control.imag), (end.real, end.imag)]
      yield COMMAND_CODES['T'], verts
      current_pos = end

    # ELLIPTICAL_ARC
    elif command == 'A':
      radius = _next_pos(elements)
      rotation = float(elements.pop())
      large = float(elements.pop())
      sweep = float(elements.pop())
      end = _next_pos(elements)

      if not absolute:
        end += current_pos

      if current_pos == end:
        # This is equivalent of omitting the segment, so do nothing
        continue
      elif radius.real == 0 or radius.imag == 0:
        # This should be treated as a straight line
        verts = [(end.real, end.imag)]
        yield COMMAND_CODES['L'], verts
      else:
        codes, verts = arc_path(current_pos, radius, rotation, large, sweep,
                                end)
        yield codes, verts

      current_pos = end


def parse_path(pathdef, current_pos=0 + 0j):
  """
    Parse an SVG path definition string into a matplotlib Path object.
    Parameters
    ----------
    pathdef : str
        SVG path 'd' attribute, e.g. 'M 100 100 L 300 100 L 200 300 z'.
    current_pos : complex, optional
        Coordinates of the starting position of the path, given as a complex
        number. When provided, an initial moveto operation will be intepreted
        as relative to this position even if given as M.
    Returns
    -------
    :class:`matplotlib.path.Path` instance
    See also
    --------
    matplotlib.path.Path
    matplotlib.patches.PathPatch
    matplotlib.collections.PathCollection
    matplotlib.transforms
    """
  codes = []
  verts = []
  for c, v in _parse_path(pathdef, current_pos):
    codes.extend(c)
    verts.extend(v)
  return Path(verts, codes)