# -*- coding: utf-8 -*-

"""
scenegraph.element._path

Low level path utility functions suitable for optimizations based on typing.
"""


# imports ####################################################################

from math import hypot, sqrt, pi, cos, sin, atan2, radians


# constants ##################################################################

INF = float("inf")


# geometry ###################################################################


def _line(p0, p1):
    """equation of (p0, p1) in the ax+by+c=0 form."""
    (x0, y0), (x1, y1) = p0, p1
    dx, dy = x1 - x0, y1 - y0
    return dy, -dx, y0 * dx - x0 * dy


def _intersection(l0, l1, e=1e-6):
    """intersection of lines."""
    a0, b0, c0 = l0
    a1, b1, c1 = l1
    w = a0 * b1 - a1 * b0
    x = b0 * c1 - b1 * c0
    y = c0 * a1 - c1 * a0
    if abs(w) < e:
        raise ZeroDivisionError
    return x / w, y / w


def _parallel(l, p):
    """parallel to l passing by p."""
    a, b, c = l  # @UnusedVariable
    x, y = p
    return a, b, -(a * x + b * y)


def _h(p0, p1):
    """distance between two points."""
    (x0, y0), (x1, y1) = p0, p1
    return hypot(x1 - x0, y1 - y0)


def _lerp(p0, p1, t=.5):
    (x0, y0), (x1, y1) = p0, p1
    return x0 + t * (x1 - x0), y0 + t * (y1 - y0)


# flattening #################################################################

# Bézier splines

_L2_RATIO = 4  # trade-off precision for polygons


def _casteljau(p0, p1, p2, p3, t=.5):
    """de Casteljau subdivision of cubic Bézier curve."""
    p01, p12, p23 = _lerp(p0, p1, t), _lerp(p1, p2, t), _lerp(p2, p3, t)
    p012, p123 = _lerp(p01, p12, t), _lerp(p12, p23, t)
    p0123 = _lerp(p012, p123, t)
    return p01, p12, p23, p012, p123, p0123


def _cubic(p0, p1, p2, p3, du2):
    """cubic Bézier spline flattenization."""
    if (p0, p2) == (p1, p3):
        return [p3]
    
    (x0, y0), (x1, y1), (x2, y2), (x3, y3) = p0, p1, p2, p3
    d1 = (x3 - x0) * (y1 - y0) - (y3 - y0) * (x1 - x0)
    d2 = (x3 - x0) * (y2 - y0) - (y3 - y0) * (x2 - x0)
    dd03 = (x3 - x0) * (x3 - x0) + (y3 - y0) * (y3 - y0)
    if (d1 * d1 + d2 * d2) * du2 < dd03 * _L2_RATIO:
        return [_lerp(p1, p2), p3]
    else:
        p01, p12, p23, p012, p123, p0123 = _casteljau(p0, p1, p2, p3)  # @UnusedVariable
        return _cubic(p0, p01, p012, p0123, du2) + \
            _cubic(p0123, p123, p23, p3, du2)


def _quadric(p0, p1, p2, du2):
    """quadric Bézier spline flattenization by transforming it to cubic."""
    return _cubic(p0, _lerp(p0, p1, 2 / 3.), _lerp(p1, p2, 1 / 3.), p2, du2)


# arc

def _arc(p0, rs, phi, flags, p1, du2):
    """arc flatenization.
    
    implementation derived from
    <http://www.w3.org/TR/SVG/implnote.html#ArcImplementationNotes>
    """
    if p0 == p1:
        return []
    
    rx, ry = rs
    if rx == 0 or ry == 0:
        return [p1]
    rx, ry = abs(rx), abs(ry)
    
    phi = radians(phi) % pi
    c, s = cos(phi), sin(phi)

    large_arc, sweep = map(bool, flags)
    
    (x0, y0), (x1, y1) = p0, p1
    
    ux, uy = .5 * (x0 - x1), .5 * (y0 - y1)
    X, Y = c * ux + s * uy, -s * ux + c * uy
    
    X2, Y2, r2x, r2y = X * X, Y * Y, rx * rx, ry * ry
    L2 = X2 / r2x + Y2 / r2y
    
    if L2 > 1.:
        L = sqrt(L2)
        rx, ry = L * rx, L * ry
        r2x, r2y = L2 * r2x, L2 * r2y

    K = sqrt(max(0., (r2x * r2y - r2x * Y2 - r2y * X2) / (r2x * Y2 + r2y * X2)))
    if large_arc == sweep:
        K = -K
    Xc, Yc = K * Y * rx / ry, -K * X * ry / rx
    
    a0 = atan2(-(Yc - Y) / ry, -(Xc - X) / rx)
    da = atan2(-(Yc + Y) / ry, -(Xc + X) / rx) - a0
    if sweep:
        if da < 0:
            da += 2 * pi
    else:
        if da > 0:
            da -= 2 * pi

    path = []
    xc, yc = c * Xc - s * Yc + ux + x1, s * Xc + c * Yc + uy + y1
    N = int((((r2x + r2y) * du2) ** .25) * abs(da))
    for i in range(N - 1):
        a = a0 + da * (i + 1) / N
        X, Y = rx * cos(a), ry * sin(a)
        path.append((c * X - s * Y + xc, s * X + c * Y + yc))
    path.append(p1)  # i in range(N) introduce numerical errors for p1
    
    return path


# stroking ###################################################################

# caps

def _offset(p0, p1, hw):
    if p0 == p1:
        return 0., hw
    (x0, y0), (x1, y1) = p0, p1
    dx, dy = x1 - x0, y1 - y0
    w = hw / hypot(dx, dy)
    return dy * w, -dx * w


def _caps_butt(p0, p1, hw, du=1, start=True):
    """compute butt cap of width 2*hw for [p0,p1]."""
    aw, bw = _offset(p0, p1, hw)
    if start:
        x, y = p0
    else:
        x, y = p1
    return [(x + aw, y + bw), (x - aw, y - bw)]


def _caps_square(p0, p1, hw, du=1, start=True):
    """compute square cap of width 2*hw for [p0,p1]."""
    aw, bw = _offset(p0, p1, hw)
    if start:
        x, y = p0
        return [(x + aw + bw, y + bw - aw), (x - aw + bw, y - bw - aw),
                (x + aw,      y + bw),      (x - aw,      y - bw)]
    else:
        x, y = p1
        return [(x + aw,      y + bw),      (x - aw,      y - bw),
                (x + aw - bw, y + bw + aw), (x - aw - bw, y - bw + aw)]


def _caps_round(p0, p1, hw, du=1, start=True):
    """compute round cap of width 2*hw for [p0,p1]."""
    aw, bw = _offset(p0, p1, hw)
    n = int(sqrt(hw * du)) + 1  # 1/(du*hw) ~ 1 - cos(da/2) ~ daˆ2/8 at first order
    da = pi / (2 * n + 1)
    if start:
        x, y = p0
        n0 = n
    else:
        x, y = p1
        n0 = 0
    r = []
    for i in range(n + 1):
        a = (n0 - i) * da
        c, s = cos(a), sin(a)
        r += [(x + c * aw + s * bw, y + c * bw - s * aw), (x - c * aw + s * bw, y - c * bw - s * aw)]
    return r

# join


def _join_miter(p0, p1, p2, hw, du, miterlimit):
    p0a, p0b, p1a, p1b = _join_bevel(p0, p1, p2, hw, du, miterlimit)
    l0, l1 = _line(p0, p1), _line(p1, p2)
    try:
        pa = _intersection(_parallel(l0, p0a), _parallel(l1, p1a))
        pb = _intersection(_parallel(l0, p0b), _parallel(l1, p1b))
    except ZeroDivisionError:
        return [p1a, p1b]
    r = miterlimit * hw / _h(p1a, pa)
    if r < 1.:
        return [_lerp(p0a, pa, r), _lerp(p0b, pb, r),
                _lerp(p1a, pa, r), _lerp(p1b, pb, r)]
#        return [p0a, p0b, p1a, p1b]
    else:
        return [pa, pb]


def _join_bevel(p0, p1, p2, hw, du, miterlimit):
    return _caps_butt(p0, p1, hw, start=False) + \
        _caps_butt(p1, p2, hw)


def _join_round(p0, p1, p2, hw, du, miterlimit):
    return _caps_butt(p0, p1, hw, du, start=False) + \
        _caps_round(p1, p2, hw, du)

# stroke

_caps = {
    'butt': _caps_butt,
    'square': _caps_square,
    'round': _caps_round,
}

_joins = {
    'miter': _join_miter,
    'bevel': _join_bevel,
    'round': _join_round,
}


def _enumerate_unique(path):
    previous = None
    for i, p in enumerate(path):
        if p != previous:
            yield i, p
        previous = p


def _stroke(path, closed, joins, width, du=1.,
            cap='butt', join='miter', miterlimit=4.):
    """compute a stroke from discretized path."""
    hw = width / 2.
    _cap = _caps[cap]
    _join = _joins[join]
    stroke = []
    
    path_points = _enumerate_unique(path)
    (i0, p0) = next(path_points)
    (i1, p1) = next(path_points, (i0, p0))
    p0i, p1i = p0, p1
    
    join_indices = iter(joins)
    next_join = next(join_indices)
    while next_join < i1:
        next_join = next(join_indices)
    
    for i2, p2 in path_points:
        if i1 == next_join:
            j = _join(p0, p1, p2, hw, du, miterlimit)
            next_join = next(join_indices, 0)
        else:
            j = _join_miter(p0, p1, p2, hw, du, 1.)
        stroke += j
        i1 = i2
        p0, p1 = p1, p2
    
    if closed:
        b = e = _join(p0, p1, p1i, hw, du, miterlimit)
    else:
        b = _cap(p0i, p1i, hw, du)
        e = _cap(p0, p1, hw, du, start=False)
    
    return b + stroke + e


# filling ####################################################################

def _triangle_strip_hits(strip, x, y):
    """yields hits and signs in triangles stored as strip."""
    strip_iter = iter(strip)
    p0, p1 = next(strip_iter), next(strip_iter)
    a0, b0, c0 = _line(p0, p1)
    s0, s = a0 * x + b0 * y + c0, 1
    for p2 in strip_iter:
        a1, b1, c1 = _line(p1, p2)
        s1 = a1 * x + b1 * y + c1
        a2, b2, c2 = _line(p2, p0)
        s2 = a2 * x + b2 * y + c2
        yield (s0 * s1 > 0) and (s1 * s2 > 0), s0 * s > 0
        p0, p1 = p1, p2
        s0, s = s1, -s


def _evenodd_hit(x, y, fills):
    """even/odd hit test on interior of a path."""
    # print( '_evenodd_hit : coucou')
    in_count = 0
    for hit, _ in _triangle_strip_hits(fills, x, y):
        if hit:
            in_count += 1
            # print( '_evenodd_hit : in_count=%d' % in_count )
    return (in_count % 2) == 1


def _nonzero_hit(x, y, fills):
    """non-zero hit test on interior of a path."""
    # print( '_nonzero_hit : coucou')
    in_count = 0
    for hit, positive in _triangle_strip_hits(fills, x, y):
        if hit:
            if positive:
                in_count += 1
                # print( '_nonzero_hit : positive hit : in_count=%d' % in_count )
            else:
                in_count -= 1
                # print( '_nonzero_hit : negative hit : in_count=%d' % in_count )
    # print( '_nonzero_hit : in_count=%d' % in_count )
    return in_count != 0


def _stroke_hit(x, y, strokes):
    """hit test on stroke of a path."""
    for hit, _ in _triangle_strip_hits(strokes, x, y):
        if hit:
            return True
    return False


def _bbox(paths):
    """bounding box of a path."""
    x_min = y_min = +INF
    x_max = y_max = -INF
    for path in paths:
        xs, ys = zip(*path)
        x_min, x_max = min(x_min, min(xs)), max(x_max, max(xs))
        y_min, y_max = min(y_min, min(ys)), max(y_max, max(ys))
    return (x_min, y_min), (x_max, y_max)
