from ._skia.core cimport (
    SkArcSize,
    SkPath,
    SkPathBuilder,
    SkPathFillType,
    SkPathIter,
    SkPathVerb,
    SkPoint,
    SkRect,
    SkScalar,
    SkSpan,
    SkLineCap,
    SkLineJoin,
    SkPathDirection,
    SK_ScalarNearlyZero,
    ConvertConicToQuads,
    SkPaint,
    SkPaintStyle,
    sk_sp,
    SkPathEffect,
    SkDashPathEffect,
    FillPathWithPaint,
)
from libcpp.optional cimport optional
from ._skia.pathops cimport (
    Op,
    Simplify,
    AsWinding,
    SkOpBuilder,
    SkPathOp,
    kDifference_SkPathOp,
    kIntersect_SkPathOp,
    kUnion_SkPathOp,
    kXOR_SkPathOp,
    kReverseDifference_SkPathOp,
)
from libc.stdint cimport uint8_t, int32_t, uint32_t
from libc.math cimport fabs, sqrt, isfinite
from libc.stddef cimport size_t
from libc.string cimport memset
cimport cython

# Explicit declarations for Limited API / Stable ABI compatibility
cdef extern from "Python.h":
    void* PyMem_Malloc(size_t)
    void* PyMem_Realloc(void*, size_t)
    void  PyMem_Free(void*)
import itertools
import sys


if sys.version_info[:2] < (3, 10):
    from itertools import tee

    def pairwise(iterable):
        """Return successive overlapping pairs taken from the input iterable.

        Backported from Python 3.10.
        https://docs.python.org/3/library/itertools.html#itertools.pairwise
        """
        a, b = tee(iterable)
        next(b, None)
        return zip(a, b)
else:
    from itertools import pairwise


# Standard Python exception classes (not cdef classes) for Limited API compatibility
class PathOpsError(Exception):
    pass


class UnsupportedVerbError(PathOpsError):
    pass


class OpenPathError(PathOpsError):
    pass


class NumberOfPointsError(PathOpsError):
    pass


# Helpers to convert to/from a float and its bit pattern

cdef inline int32_t _float2bits(float x):
    cdef FloatIntUnion data
    data.Float = x
    return data.SignBitInt


def float2bits(float x):
    """
    >>> hex(float2bits(17.5))
    '0x418c0000'
    >>> hex(float2bits(-10.0))
    '0xc1200000'
    """
    # we use unsigned to match the C printf %x behaviour
    # used by Skia's SkPath::dumpHex
    cdef uint32_t bits = <uint32_t>_float2bits(x)
    return bits


cdef inline float _bits2float(int32_t float_as_bits):
    cdef FloatIntUnion data
    data.SignBitInt = float_as_bits
    return data.Float


def bits2float(long long float_as_bits):
    """
    >>> bits2float(0x418c0000)
    17.5
    >>> bits2float(-0x3ee00000)
    -10.0
    >>> bits2float(0xc1200000)
    -10.0
    """
    return _bits2float(<int32_t>float_as_bits)


cdef float SCALAR_NEARLY_ZERO_SQD = SK_ScalarNearlyZero * SK_ScalarNearlyZero


cdef inline bint can_normalize(SkScalar dx, SkScalar dy):
    return (dx*dx + dy*dy) > SCALAR_NEARLY_ZERO_SQD


cdef inline bint points_almost_equal(const SkPoint& p1, const SkPoint& p2):
    return not can_normalize(p1.x() - p2.x(), p1.y() - p2.y())


cdef inline bint is_middle_point(
    const SkPoint& p1, const SkPoint& p2, const SkPoint& p3
):
    cdef SkScalar midx = (p1.x() + p3.x()) / 2.0
    cdef SkScalar midy = (p1.y() + p3.y()) / 2.0
    return not can_normalize(p2.x() - midx, p2.y() - midy)


cdef inline bint collinear(
    const SkPoint& p1, const SkPoint& p2, const SkPoint& p3
):
    # the area of a triangle is zero iff the three vertices are collinear
    return fabs(
        p1.x() * (p2.y() - p3.y()) +
        p2.x() * (p3.y() - p1.y()) +
        p3.x() * (p1.y() - p2.y())
    ) <= 2 * SK_ScalarNearlyZero


def _format_hex_coords(floats):
    floats = list(floats)
    if not floats:
        return ""
    return "".join(
        "\n    bits2float(%s),  # %g" % (hex(float2bits(f)), f)
        for f in floats
    ) + "\n"


def triplewise(iterable):
    """Return overlapping triplets from an iterable

    E.g. triplewise('ABCDEFG') --> ABC BCD CDE DEF EFG

    From: https://docs.python.org/3/library/itertools.html#itertools-recipes
    """
    for (a, _), (b, c) in pairwise(pairwise(iterable)):
        yield a, b, c


cdef class Path:

    def __init__(self, other=None, fillType=None):
        cdef Path static_path
        if other is not None:
            if isinstance(other, Path):
                static_path = other
                self.path = static_path.path
            else:
                other.draw(self.getPen())
        if fillType is not None:
            self.fillType = fillType

    @staticmethod
    cdef Path create(const SkPathBuilder& path):
        cdef Path self = Path.__new__(Path)
        self.path = path
        return self

    cpdef PathPen getPen(self, object glyphSet=None, bint allow_open_paths=True):
        return PathPen(self, glyphSet=glyphSet, allow_open_paths=allow_open_paths)

    def __iter__(self):
        return RawPathIterator(self)

    def add(self, PathVerb verb, *pts):
        if verb is PathVerb.MOVE:
            self.path.moveTo(pts[0][0], pts[0][1])
        elif verb is PathVerb.LINE:
            self.path.lineTo(pts[0][0], pts[0][1])
        elif verb is PathVerb.QUAD:
            self.path.quadTo(pts[0][0], pts[0][1],
                             pts[1][0], pts[1][1])
        elif verb is PathVerb.CONIC:
            self.path.conicTo(pts[0][0], pts[0][1],
                              pts[1][0], pts[1][1], pts[2])
        elif verb is PathVerb.CUBIC:
            self.path.cubicTo(pts[0][0], pts[0][1],
                              pts[1][0], pts[1][1],
                              pts[2][0], pts[2][1])
        elif verb is PathVerb.CLOSE:
            self.path.close()
        else:
            raise UnsupportedVerbError(verb)

    cpdef void moveTo(self, SkScalar x, SkScalar y):
        self.path.moveTo(x, y)

    cpdef void lineTo(self, SkScalar x, SkScalar y):
        self.path.lineTo(x, y)

    cpdef void quadTo(
        self,
        SkScalar x1,
        SkScalar y1,
        SkScalar x2,
        SkScalar y2
    ):
        self.path.quadTo(x1, y1, x2, y2)

    cpdef void conicTo(
        self,
        SkScalar x1,
        SkScalar y1,
        SkScalar x2,
        SkScalar y2,
        SkScalar w
    ):
        self.path.conicTo(x1, y2, x2, y2, w)

    cpdef void cubicTo(
        self,
        SkScalar x1,
        SkScalar y1,
        SkScalar x2,
        SkScalar y2,
        SkScalar x3,
        SkScalar y3,
    ):
        self.path.cubicTo(x1, y1, x2, y2, x3, y3)

    cpdef void arcTo(
        self,
        SkScalar rx,
        SkScalar ry,
        SkScalar xAxisRotate,
        ArcSize largeArc,
        Direction sweep,
        SkScalar x,
        SkScalar y,
    ):
        self.path.arcTo(
            SkPoint.Make(rx, ry),
            xAxisRotate,
            <SkArcSize>largeArc,
            <SkPathDirection>sweep,
            SkPoint.Make(x, y)
        )


    cpdef void close(self):
        self.path.close()

    cpdef void reset(self):
        self.path.reset()

    cpdef void rewind(self):
        """Deprecated alias for reset(). Use reset() instead."""
        self.path.reset()

    cpdef draw(self, pen):
        cdef str method
        cdef tuple pts

        for method, pts in self.segments:
            getattr(pen, method)(*pts)

    def dump(self, cpp=False, as_hex=False):
        # print a text repesentation to stdout
        if cpp:  # C++
            if as_hex:
                self.path.dump(SkPathBuilder.DumpFormat.kHex)
            else:
                self.path.dump(SkPathBuilder.DumpFormat.kDecimal)
        else:
            print(self._to_string(as_hex=as_hex))  # Python

    def _to_string(self, as_hex=False):
        # return a text repesentation as Python code
        if self.path.isEmpty():
            return ""
        if as_hex:
            coords_to_string = _format_hex_coords
        else:
            coords_to_string = lambda fs: (", ".join("%g" % f for f in fs))
        s = ["path.fillType = %s" % self.fillType]
        for verb, pts in self:
            # if the last pt isn't a pt, such as for conic weight, peel it off
            suffix = ''
            if pts and not isinstance(pts[-1], tuple):
                suffix = "[%s]" % coords_to_string([pts[-1]])
                pts = pts[:-1]
            method = VERB_METHODS[verb]
            coords = itertools.chain(*pts)
            line = "path.%s(%s)%s" % (method, coords_to_string(coords), suffix)
            s.append(line)
        return "\n".join(s)

    def __str__(self):
        return self._to_string()

    def __repr__(self):
        return "<pathops.Path object at %s: %d contours>" % (
            hex(id(self)), self.countContours()
        )

    def __len__(self):
        return self.countContours()

    def __eq__(self, other):
        if not isinstance(other, Path):
            return NotImplemented
        cdef Path static_other = other
        return self.path == static_other.path

    def __ne__(self, other):
        return not self == other

    __hash__ = None  # Path is a mutable object, let's make it unhashable

    cpdef addPath(self, Path path):
        self.path.addPath(path.path.snapshot())

    @property
    def fillType(self):
        return FillType(<uint32_t>self.path.fillType())

    @fillType.setter
    def fillType(self, value):
        cdef uint32_t fill = int(FillType(value))
        self.path.setFillType(<SkPathFillType>fill)

    @property
    def isConvex(self):
        return self.path.snapshot().isConvex()

    def contains(self, tuple pt):
        return self.path.contains(SkPoint.Make(pt[0], pt[1]))

    @property
    def bounds(self):
        cdef optional[SkRect] bounds = self.path.computeTightBounds()
        if not bounds.has_value():
            return None
        cdef SkRect r = bounds.value()
        return (r.left(), r.top(), r.right(), r.bottom())

    @property
    def controlPointBounds(self):
        cdef optional[SkRect] bounds = self.path.computeFiniteBounds()
        if not bounds.has_value():
            return None
        cdef SkRect r = bounds.value()
        return (r.left(), r.top(), r.right(), r.bottom())

    @property
    def area(self):
        return fabs(get_path_area(self.path))

    @property
    def clockwise(self):
        return get_path_area(self.path) < 0

    @clockwise.setter
    def clockwise(self, value):
        if self.clockwise != value:
            self.reverse()

    cpdef reverse(self):
        cdef Path contour
        cdef SkPathBuilder skpath
        skpath.setFillType(self.path.fillType())
        for contour in self.contours:
            reverse_contour(contour.path)
            skpath.addPath(contour.path.snapshot())
        self.path = skpath

    cpdef simplify(
        self,
        bint fix_winding=True,
        bint keep_starting_points=True,
        bint clockwise=False,
    ):
        cdef list first_points
        if keep_starting_points:
            first_points = self.firstPoints
        cdef optional[SkPath] simplified = Simplify(self.path.snapshot())
        if not simplified.has_value():
            raise PathOpsError("simplify operation did not succeed")
        self.path = simplified.value()
        if fix_winding:
            winding_from_even_odd(self, clockwise)
        if keep_starting_points:
            restore_starting_points(self, first_points)


    def _has(self, verb):
        return any(my_verb == verb for my_verb, _ in self)

    cpdef convertConicsToQuads(self, float tolerance=0.25):
        # TODO is 0.25 too delicate? - blindly copies from Skias own use
        if not self._has(SkPathVerb.kConic):
            return

        cdef max_pow2 = 5
        cdef count = 1 + 2 * (1<<max_pow2)
        cdef SkPoint *quad_pts
        cdef num_quads

        # The most points we could possibly need
        quad_pts = <SkPoint *> PyMem_Malloc(count * sizeof(SkPoint))
        if not quad_pts:
            raise MemoryError()
        cdef SkPoint *quad = quad_pts

        cdef SkPathBuilder temp
        cdef SkPathFillType fillType = self.path.fillType()
        temp.setFillType(fillType)

        cdef SkPoint p0
        cdef SkPoint p1
        cdef SkPoint p2
        cdef SkScalar weight
        cdef int pow2

        try:
            prev = (0., 0.)
            for verb, pts in self:
                if verb != SkPathVerb.kConic:
                    if verb != SkPathVerb.kClose:
                        prev_verb = verb
                        prev = pts[-1]

                    # TODO cython got angry when I tried to make this a fn
                    if verb == SkPathVerb.kMove:
                        temp.moveTo(pts[0][0], pts[0][1])
                    elif verb == SkPathVerb.kLine:
                        temp.lineTo(pts[0][0], pts[0][1])
                    elif verb == SkPathVerb.kQuad:
                        temp.quadTo(pts[0][0], pts[0][1],
                                    pts[1][0], pts[1][1])
                    elif verb == SkPathVerb.kCubic:
                        temp.cubicTo(pts[0][0], pts[0][1],
                                     pts[1][0], pts[1][1],
                                     pts[2][0], pts[2][1])
                    elif verb == SkPathVerb.kClose:
                        temp.close()
                    else:
                        raise UnsupportedVerbError(verb)

                    continue

                # Figure out a good value for pow2
                p0 = SkPoint.Make(prev[0], prev[1])
                p1 = SkPoint.Make(pts[0][0], pts[0][1])
                p2 = SkPoint.Make(pts[1][0], pts[1][1])
                weight = pts[2]

                pow2 = compute_conic_to_quad_pow2(p0, p1, p2, weight, tolerance)
                assert pow2 <= max_pow2
                num_quads = ConvertConicToQuads(p0, p1, p2,
                                                weight, quad_pts,
                                                pow2)

                # quad_pts[0] is effectively a moveTo that may be a nop
                if prev != (quad_pts[0].x(), quad_pts[0].y()):
                    temp.moveTo(quad_pts[0].x(), quad_pts[0].y())

                for i in range(num_quads):
                    p1 = quad_pts[2 * i + 1]
                    p2 = quad_pts[2 * i + 2]
                    temp.quadTo(p1.x(), p1.y(), p2.x(), p2.y())

                prev = pts[-2] # -1 is weight

        finally:
            PyMem_Free(quad_pts)

        self.path = temp

    cpdef stroke(
        self,
        SkScalar width,
        LineCap cap,
        LineJoin join,
        SkScalar miter_limit,
        object dash_array=None,
        SkScalar dash_offset=0.0,
    ):
        cdef _SkScalarArray intervals
        cdef sk_sp[SkPathEffect] dash
        cdef SkPaint paint = SkPaint()

        paint.setStyle(SkPaintStyle.kStroke_Style)
        paint.setStrokeWidth(width)
        paint.setStrokeCap(<SkLineCap>cap)
        paint.setStrokeJoin(<SkLineJoin>join)
        paint.setStrokeMiter(miter_limit)

        if dash_array:
            intervals = _SkScalarArray.create(dash_array)
            if intervals.count % 2 != 0:
                raise ValueError("Expected an even number of dash_array entries")
            paint.setPathEffect(
                SkDashPathEffect.Make(<SkSpan[const SkScalar]>intervals.as_span(), dash_offset)
            )

        FillPathWithPaint(self.path.detach(), paint, &self.path)

    cdef list getVerbs(self):
        return [PathVerb(verb) for verb in self.path.verbs()]

    @property
    def verbs(self):
        return self.getVerbs()

    cdef list getPoints(self):
        return [(pt.x(), pt.y()) for pt in self.path.points()]

    @property
    def points(self):
        return self.getPoints()

    cdef int countContours(self) except -1:
        if self.path.isEmpty():
            return 0
        cdef int n = 0
        for verb in self.path.verbs():
            if verb == SkPathVerb.kMove:
                n += 1
        return n

    @property
    def firstPoints(self):
        cdef SkPoint *p = NULL
        cdef int count = 0
        cdef list result = []
        if self.getFirstPoints(&p, &count):
            for i in range(count):
                result.append((p[i].x(), p[i].y()))
            if p is not NULL:
                PyMem_Free(p)
        return result

    cdef int getFirstPoints(self, SkPoint **pp, int *count) except -1:
        cdef int c = self.path.verbs().size()
        if c == 0:
            return 0  # empty

        cdef SkPoint *points = <SkPoint *> PyMem_Malloc(c * sizeof(SkPoint))
        if not points:
            raise MemoryError()

        cdef optional[SkPathIter] iterator = self.path.iter()
        cdef SkPathVerb verb
        cdef optional[SkPathIter.Rec] rec

        cdef int i = 0
        while True:
            rec = iterator.value().next()
            if not rec.has_value():
                break
            verb = rec.value().fVerb
            if verb == SkPathVerb.kMove:
                points[i] = rec.value().fPoints[0]
                i += 1

        points = <SkPoint *> PyMem_Realloc(points, i * sizeof(SkPoint))
        count[0] = i
        pp[0] = points

        return 1

    @property
    def contours(self):
        cdef SkPathBuilder temp
        cdef SkPathFillType fillType = self.path.fillType()

        temp.setFillType(fillType)

        cdef SkPathVerb verb
        cdef SkSpan[const SkPoint] p
        cdef optional[SkPathIter] iterator = self.path.iter()
        cdef optional[SkPathIter.Rec] rec

        while True:
            rec = iterator.value().next()
            if not rec.has_value():
                break
            verb = rec.value().fVerb
            p = rec.value().fPoints
            if verb == SkPathVerb.kMove:
                if not temp.isEmpty():
                    yield Path.create(temp)
                    temp.reset()
                    temp.setFillType(fillType)
                temp.moveTo(p[0])
            elif verb == SkPathVerb.kLine:
                temp.lineTo(p[1])
            elif verb == SkPathVerb.kQuad:
                temp.quadTo(p[1], p[2])
            elif verb == SkPathVerb.kConic:
                temp.conicTo(p[1], p[2], rec.value().conicWeight())
            elif verb == SkPathVerb.kCubic:
                temp.cubicTo(p[1], p[2], p[3])
            elif verb == SkPathVerb.kClose:
                temp.close()
                yield Path.create(temp)
                temp.reset()
                temp.setFillType(fillType)
            else:
                raise AssertionError(verb)
        if not temp.isEmpty():
            yield Path.create(temp)

    @property
    def segments(self):
        # We need to check for TrueType special quadratic closed spline made of
        # off-curve points only so that we can make the move point implied.
        # It's easier to do this in here than inside the SegmentPenIterator, as that
        # yields each segment one by one, whereas we want to sometimes *not* yield a
        # moveTo in very specific circumstances (i.e. the whole contour is a single
        # closed quadratic spline where all the on-curve points are midway between
        # consecutive off-curve points) based on previous and next segments.
        cdef SkPoint p1, p2, p3
        result = []
        it = itertools.chain([None], SegmentPenIterator(self), [None])
        for previous, current, next_ in triplewise(it):
            if (
                previous is not None
                and previous[0] == "moveTo"
                and current[0] == "qCurveTo"
                and next_ is not None
                and next_[0] == "closePath"
                and previous[1][0] == current[1][-1]
            ):
                qpoints = current[1]
                last_off, move_pt, first_off = qpoints[-2], qpoints[-1], qpoints[0]
                p1 = SkPoint.Make(last_off[0], last_off[1])
                p2 = SkPoint.Make(move_pt[0], move_pt[1])
                p3 = SkPoint.Make(first_off[0], first_off[1])
                if is_middle_point(p1, p2, p3):
                    # drop the moveTo and make the last on-curve None
                    del result[-1]
                    result.append((current[0], current[1][:-1] + (None,)))
                    continue
            result.append(current)
        yield from result

    cpdef Path transform(
        self,
        SkScalar scaleX=1,
        SkScalar skewY=0,
        SkScalar skewX=0,
        SkScalar scaleY=1,
        SkScalar translateX=0,
        SkScalar translateY=0,
        SkScalar perspectiveX=0,
        SkScalar perspectiveY=0,
        SkScalar perspectiveBias=1,
    ):
        """Apply 3x3 transformation matrix and return new transformed Path.

        SkMatrix stores the values in row-major order:

        [ scaleX skewX transX
          skewY scaleY transY
          perspX perspY perspBias ]

        However here the first 6 parameters are in column-major order, like
        the affine matrix vectors from SVG transform attribute:

        [ a c e
          b d f    => [a b c d e f]
          0 0 1 ]

        This is so one can easily unpack a 6-tuple as positional arguments
        to this method.

        >>> p1 = Path()
        >>> p1.moveTo(1, 2)
        >>> p1.lineTo(3, 4)
        >>> affine = (2, 0, 0, 2, 0, 0)
        >>> p2 = p1.transform(*affine)
        >>> list(p2.segments) == [
        ...    ('moveTo', ((2.0, 4.0),)),
        ...    ('lineTo', ((6.0, 8.0),)),
        ...    ('endPath', ()),
        ... ]
        True
        """
        cdef SkMatrix matrix = SkMatrix.MakeAll(
            scaleX,
            skewX,
            translateX,
            skewY,
            scaleY,
            translateY,
            perspectiveX,
            perspectiveY,
            perspectiveBias,
        )
        cdef Path result = Path.create(self.path)
        result.path.transform(matrix)
        return result


DEF NUM_VERBS = 7

cdef uint8_t *POINTS_IN_VERB = [
    1,  # MOVE
    1,  # LINE
    2,  # QUAD
    2,  # CONIC
    3,  # CUBIC
    0,  # CLOSE
    0   # DONE
]

cdef dict VERB_METHODS = {
    SkPathVerb.kMove: "moveTo",
    SkPathVerb.kLine: "lineTo",
    SkPathVerb.kQuad: "quadTo",
    SkPathVerb.kConic: "conicTo",
    SkPathVerb.kCubic: "cubicTo",
    SkPathVerb.kClose: "close",
}

cdef dict PEN_METHODS = {
    SkPathVerb.kMove: "moveTo",
    SkPathVerb.kLine: "lineTo",
    SkPathVerb.kQuad: "qCurveTo",
    SkPathVerb.kCubic: "curveTo",
    SkPathVerb.kClose: "closePath",
}


cdef tuple NO_POINTS = ()


cdef class RawPathIterator:

    def __cinit__(self, Path path):
        self.path = path
        self.iterator = self.path.path.iter()

    def __iter__(self):
        return self

    def __next__(self):
        cdef tuple pts
        cdef SkPathVerb verb
        cdef SkSpan[const SkPoint] p

        rec = self.iterator.value().next()
        if not rec.has_value():
            raise StopIteration()
        verb = rec.value().fVerb
        p = rec.value().fPoints

        if verb == SkPathVerb.kMove:
            pts = ((p[0].x(), p[0].y()),)
        elif verb == SkPathVerb.kLine:
            pts = ((p[1].x(), p[1].y()),)
        elif verb == SkPathVerb.kQuad:
            pts = ((p[1].x(), p[1].y()),
                   (p[2].x(), p[2].y()))
        elif verb == SkPathVerb.kConic:
            pts = ((p[1].x(), p[1].y()),
                   (p[2].x(), p[2].y()),
                   rec.value().conicWeight())
        elif verb == SkPathVerb.kCubic:
            pts = ((p[1].x(), p[1].y()),
                   (p[2].x(), p[2].y()),
                   (p[3].x(), p[3].y()))
        elif verb == SkPathVerb.kClose:
            pts = NO_POINTS
        else:
            raise UnsupportedVerbError(verb)

        return (PathVerb(verb), pts)


cdef tuple END_PATH = ("endPath", NO_POINTS)
cdef tuple CLOSE_PATH = ("closePath", NO_POINTS)


cdef class SegmentPenIterator:

    def __cinit__(self, Path path):
        self.path = Path.create(path.path)
        self.pts = self.path.path.points().begin()
        self.verbs = self.path.path.verbs().begin() - 1  # TODO: UB
        self.verb_stop = self.path.path.verbs().end()
        self.move_pt = SkPoint.Make(.0, .0)
        self.closed = True

    def __iter__(self):
        return self

    def __next__(self):
        cdef tuple points
        cdef SkPathVerb verb

        self.verbs += 1
        if self.verbs >= self.verb_stop:
            if not self.closed:
                self.closed = True
                return END_PATH
            else:
                raise StopIteration()
        else:
            verb = self.verbs[0]

        if verb == SkPathVerb.kMove:
            # skia contours are implicitly open, unless they end with "close"
            if not self.closed:
                self.closed = True
                self.verbs -= 1
                return END_PATH
            self.move_pt = self.pts[0]
            self.closed = False
            points = ((self.pts[0].x(), self.pts[0].y()),)
            self.pts += 1
        elif verb == SkPathVerb.kClose:
            self.closed = True
            return CLOSE_PATH
        elif verb == SkPathVerb.kLine:
            if (
                self.nextIsClose()
                and points_almost_equal(self.pts[0], self.move_pt)
            ):
                # skip closing lineTo if contour's last point ~= first
                points = ((self.move_pt.x(), self.move_pt.y()),)
            else:
                points = ((self.pts[0].x(), self.pts[0].y()),)
            self.pts += 1
        elif verb == SkPathVerb.kQuad:
            points = self._join_quadratic_segments()
        elif verb == SkPathVerb.kCubic:
            if (
                self.nextIsClose()
                and points_almost_equal(self.pts[2], self.move_pt)
            ):
                # skip closing lineTo if contour's last point ~= first
                points = (
                    (self.pts[0].x(), self.pts[0].y()),
                    (self.pts[1].x(), self.pts[1].y()),
                    (self.move_pt.x(), self.move_pt.y()),
                )
            else:
                points = (
                    (self.pts[0].x(), self.pts[0].y()),
                    (self.pts[1].x(), self.pts[1].y()),
                    (self.pts[2].x(), self.pts[2].y()),
                )
            self.pts += 3
        else:
            raise UnsupportedVerbError(PathVerb(verb).name)

        cdef str method = PEN_METHODS[verb]
        return (method, points)

    cdef inline bint nextIsClose(self):
        if self.verbs + 1 < self.verb_stop:
            return (self.verbs + 1)[0] == SkPathVerb.kClose
        else:
            return 0

    cdef tuple _join_quadratic_segments(self):
        # must only be called when the current verb is kQuad_Verb
        # assert self.verbs < self.verb_stop and self.verbs[0] == kQuad_Verb

        cdef const SkPathVerb *verbs = self.verbs
        cdef const SkPathVerb *next_verb_ptr
        cdef const SkPoint *pts = self.pts

        cdef list points = []

        while True:
            # always add the current quad's off-curve point
            points.append((pts[0].x(), pts[0].y()))
            # check if the following segments (if any) are also quadratic
            next_verb_ptr = verbs + 1
            if next_verb_ptr != self.verb_stop:
                if next_verb_ptr[0] == SkPathVerb.kQuad:
                    if is_middle_point(pts[0], pts[1], pts[2]):
                        # skip TrueType "implied" on-curve point, and keep
                        # evaluating the next quadratic segment
                        verbs = next_verb_ptr
                        pts += 2
                        continue
                elif (
                    next_verb_ptr[0] == SkPathVerb.kClose
                    and points_almost_equal(pts[1], self.move_pt)
                ):
                    # last segment on a closed contour: make sure there is no
                    # extra closing lineTo when the last point is almost equal
                    # to the moveTo point
                    points.append((self.move_pt.x(), self.move_pt.y()))
                    pts += 2
                    break
            # no more segments, or the next segment isn't quadratic, or it is
            # but the on-curve point doesn't interpolate half-way in between
            # the respective off-curve points; add on-curve and exit the loop
            points.append((pts[1].x(), pts[1].y()))
            pts += 2
            break

        self.verbs = verbs
        self.pts = pts
        return tuple(points)


cdef class PathPen:

    def __cinit__(self, Path path, object glyphSet=None, bint allow_open_paths=True):
        self.path = path
        self.glyphSet = glyphSet
        self.allow_open_paths = allow_open_paths

    cpdef moveTo(self, pt):
        self.path.moveTo(pt[0], pt[1])

    cpdef lineTo(self, pt):
        self.path.lineTo(pt[0], pt[1])

    def curveTo(self, *points):
        num_offcurves = len(points) - 1
        if num_offcurves == 2:
            pt1, pt2, pt3 = points
            self.path.cubicTo(
                pt1[0], pt1[1],
                pt2[0], pt2[1],
                pt3[0], pt3[1])
        elif num_offcurves == 1:
            pt1, pt2 = points
            self.path.quadTo(pt1[0], pt1[1], pt2[0], pt2[1])
        elif num_offcurves == 0:
            pt = points[0]
            self.path.lineTo(pt[0], pt[1])
        else:
            # support BasePen "super-beziers"? Nah.
            raise NumberOfPointsError(
                "curveTo requires between 1 and 3 points; got %d" % len(points)
            )

    def qCurveTo(self, *points):
        num_offcurves = len(points) - 1
        if num_offcurves > 0:
            oncurveless_contour = points[-1] is None
            if oncurveless_contour:
                # Special case for TrueType closed contours without on-curve points.
                # FontTools pens supports this by allowing the last point of qCurveTo
                # to be None, which is translated as an implied on-curve point between
                # the last and the first off-curve points:
                # https://github.com/fonttools/fonttools/blob/02a0636/Lib/fontTools/pens/basePen.py#L332-L344
                # https://github.com/fonttools/skia-pathops/issues/45
                x, y = points[-2]
                nx, ny = points[0]
                implied_pt = (0.5 * (x + nx), 0.5 * (y + ny))
                self.moveTo(implied_pt)
                points = points[:-1] + (implied_pt,)
            for pt1, pt2 in _decompose_quadratic_segment(points):
                self._qCurveToOne(pt1, pt2)
            if oncurveless_contour:
                # oncurve-less contour is closed by definition
                self.closePath()
        elif num_offcurves == 0:
            self.lineTo(points[0])
        else:
            raise NumberOfPointsError("qCurveTo requires at least 1 point; got 0")

    cdef _qCurveToOne(self, pt1, pt2):
        self.path.quadTo(pt1[0], pt1[1], pt2[0], pt2[1])

    cpdef closePath(self):
        self.path.close()

    cpdef endPath(self):
        if not self.allow_open_paths:
            raise OpenPathError()

    cpdef addComponent(self, glyphName, transformation):
        if self.glyphSet is None:
            raise TypeError("Missing required glyphSet; can't decompose components")

        base_glyph = self.glyphSet[glyphName]
        cdef Path base_path = Path()
        base_glyph.draw(base_path.getPen(glyphSet=self.glyphSet))
        cdef Path component_path = base_path.transform(*transformation)

        self.path.addPath(component_path)


cdef double get_path_area(const SkPathBuilder& path) except? -1234567:
    # Adapted from fontTools/pens/areaPen.py
    cdef double value = .0
    cdef SkPathVerb verb
    cdef SkSpan[const SkPoint] p
    cdef SkPoint p0, start_point
    cdef SkScalar x0, y0, x1, y1, x2, y2, x3, y3
    cdef optional[SkPathIter] iterator = path.iter()
    cdef bint need_close = False
    cdef optional[SkPathIter.Rec] rec

    p0 = start_point = SkPoint.Make(.0, .0)
    while True:
        rec = iterator.value().next()
        if not rec.has_value():
            break
        verb = rec.value().fVerb
        p = rec.value().fPoints
        if verb == SkPathVerb.kMove:
            if need_close:
                x0, y0 = p0.x(), p0.y()
                x1, y1 = start_point.x(), start_point.y()
                value -= (x1 - x0) * (y1 + y0) * .5
            p0 = start_point = p[0]
            need_close = True
        elif verb == SkPathVerb.kLine:
            x0, y0 = p0.x(), p0.y()
            x1, y1 = p[1].x(), p[1].y()
            value -= (x1 - x0) * (y1 + y0) * .5
            p0 = p[1]
        elif verb == SkPathVerb.kQuad:
            # https://github.com/Pomax/bezierinfo/issues/44
            x0, y0 = p0.x(), p0.y()
            x1, y1 = p[1].x() - x0, p[1].y() - y0
            x2, y2 = p[2].x() - x0, p[2].y() - y0
            value -= (x2 * y1 - x1 * y2) / 3
            value -= (p[2].x() - x0) * (p[2].y() + y0) * .5
            p0 = p[2]
        elif verb == SkPathVerb.kConic:
            raise UnsupportedVerbError("CONIC")
        elif verb == SkPathVerb.kCubic:
            # https://github.com/Pomax/bezierinfo/issues/44
            x0, y0 = p0.x(), p0.y()
            x1, y1 = p[1].x() - x0, p[1].y() - y0
            x2, y2 = p[2].x() - x0, p[2].y() - y0
            x3, y3 = p[3].x() - x0, p[3].y() - y0
            value -= (
                       x1 * (   -   y2 -   y3) +
                       x2 * (y1        - 2*y3) +
                       x3 * (y1 + 2*y2       )
                     ) * 0.15
            value -= (p[3].x() - x0) * (p[3].y() + y0) * .5
            p0 = p[3]
        elif verb == SkPathVerb.kClose:
            x0, y0 = p0.x(), p0.y()
            x1, y1 = start_point.x(), start_point.y()
            value -= (x1 - x0) * (y1 + y0) * .5
            p0 = start_point = SkPoint.Make(.0, .0)
            need_close = False
        else:
            raise AssertionError(verb)

    if need_close:
        x0, y0 = p0.x(), p0.y()
        x1, y1 = start_point.x(), start_point.y()
        value -= (x1 - x0) * (y1 + y0) * .5

    return value


cdef class _SkScalarArray:

    @staticmethod
    cdef _SkScalarArray create(object values):
        # 'values' must be a sequence (e.g. list or tuple) of floats
        cdef _SkScalarArray self = _SkScalarArray.__new__(_SkScalarArray)
        self.count = len(values)
        self.data = <SkScalar *> PyMem_Malloc(self.count * sizeof(SkScalar))
        if not self.data:
            raise MemoryError()
        for i, v in enumerate(values):
            self.data[i] = v
        return self

    def __dealloc__(self):
        PyMem_Free(self.data)  # no-op if data is NULL

    cdef SkSpan[SkScalar] as_span(self):
        return SkSpan[SkScalar](self.data, self.count)


cdef inline int pts_in_verb(SkPathVerb v) except -1:
    if <uint8_t>v >= NUM_VERBS:
        raise IndexError(v)
    return POINTS_IN_VERB[<uint8_t>v]


cdef bint reverse_contour(SkPathBuilder& path) except False:
    cdef SkPathBuilder temp
    cdef SkPoint lastPt
    cdef optional[SkPoint] maybeLastPt = path.getLastPt()

    if not maybeLastPt.has_value():
        return True  # ignore empty path
    lastPt = maybeLastPt.value()

    cdef SkSpan[const SkPathVerb] va = path.verbs()
    cdef const SkPathVerb *verbsStart = va.begin()  # pointer to the first verb
    cdef const SkPathVerb *verbs = va.end() - 1  # pointer to the last verb

    cdef SkSpan[const SkPoint] pa = path.points()
    cdef const SkPoint *pts = pa.end() - 1  # pointer to the last point

    # the last point becomes the first
    temp.moveTo(lastPt)

    cdef SkPathVerb v
    cdef bint closed = False
    # loop over both arrays in reverse, break before the first verb
    while verbs > verbsStart:
        v = verbs[0]
        verbs -= 1
        pts -= pts_in_verb(v)
        if v == SkPathVerb.kMove:
            # if the path has multiple contours, stop after reversing the last
            break
        elif v == SkPathVerb.kLine:
            temp.lineTo(pts[0])
        elif v == SkPathVerb.kQuad:
            temp.quadTo(pts[1], pts[0])
        elif v == SkPathVerb.kConic:
            raise UnsupportedVerbError("CONIC")
        elif v == SkPathVerb.kCubic:
            temp.cubicTo(pts[2], pts[1], pts[0])
        elif v == SkPathVerb.kClose:
            closed = True
        else:
            raise AssertionError(v)

    if closed:
        temp.close()

    temp.setFillType(path.fillType())
    # assignment to references is allowed in C++ but Cython doesn't support it
    # https://github.com/cython/cython/issues/1863
    # path = temp
    (&path)[0] = temp
    return True


# NOTE This is meant to be used only on simplified paths (i.e. without
# overlapping contours), like the ones returned from Skia's path operations.
# It only tests the bounding boxes and the on-curve points.
cdef int path_is_inside(const SkPathBuilder& self, const SkPathBuilder& other) except -1:
    cdef optional[SkRect] r1, r2
    cdef SkPathVerb verb
    cdef SkSpan[const SkPoint] p
    cdef SkPoint oncurve

    r1 = self.computeTightBounds()
    r2 = other.computeTightBounds()
    if not r1.has_value() or not r2.has_value() or not SkRect.Intersects(r1.value(), r2.value()):
        return 0

    cdef optional[SkPathIter] iterator = other.iter()
    cdef optional[SkPathIter.Rec] rec
    while True:
        rec = iterator.value().next()
        if not rec.has_value():
            break
        verb = rec.value().fVerb
        p = rec.value().fPoints
        if verb == SkPathVerb.kMove:
            oncurve = p[0]
        elif verb == SkPathVerb.kLine:
            oncurve = p[1]
        elif verb == SkPathVerb.kQuad:
            oncurve = p[2]
        elif verb == SkPathVerb.kConic:
            raise UnsupportedVerbError("CONIC")
        elif verb == SkPathVerb.kCubic:
            oncurve = p[3]
        elif verb == SkPathVerb.kClose:
            continue
        else:
            raise AssertionError(verb)
        if not self.contains(oncurve):
            return 0

    return 1


@cython.wraparound(False)
@cython.boundscheck(False)
cpdef int restore_starting_points(Path path, list points) except -1:
    if not points:
        return 0

    cdef list contours = list(path.contours)
    cdef Py_ssize_t n = len(contours)
    cdef Py_ssize_t m = len(points)
    cdef int i, j
    cdef Path this
    cdef bint modified = False

    for i in range(n):
        this = contours[i]
        for j in range(m):
            pt = points[j]
            if set_contour_start_point(this.path, pt[0], pt[1]):
                modified = True
                # we don't retry the same point again on a different contour
                del points[j]
                m -= 1
                break

    if not modified:
        return 0

    path.path.reset()
    for i in range(n):
        this = contours[i]
        path.path.addPath(this.path.detach())

    return 1


DEF DEBUG_WINDING = False


@cython.wraparound(False)
@cython.boundscheck(False)
cpdef bint winding_from_even_odd(Path path, bint clockwise=False) except False:
    """ Take a simplified path (without overlaps) and set the contours
    directions according to the non-zero winding fill type.
    The outermost contours are set to counter-clockwise direction, unless
    'clockwise' is True.
    """
    # TODO re-enable this once the new feature is stabilized in upstream skia
    # https://github.com/fonttools/skia-pathops/issues/10
    # if AsWinding(path.path, &path.path):
    #     if path.clockwise ^ clockwise:
    #         path.reverse()
    #     return True
    #
    # # in the unlikely event the built-in method fails, try our naive approach

    cdef int i, j
    cdef bint inverse = not clockwise
    cdef bint is_clockwise, is_even
    cdef Path contour, other

    # sort contours by area, from largest to smallest
    cdef dict contours_by_area = {}
    cdef object area
    for contour in path.contours:
        area = -fabs(get_path_area(contour.path))
        if area not in contours_by_area:
            contours_by_area[area] = []
        contours_by_area[area].append(contour)
    cdef list group
    cdef list contours = []
    for _, group in sorted(contours_by_area.items()):
        contours.extend(group)
    cdef Py_ssize_t n = len(contours)

    # XXX permature optimization? needs profile
    cdef size_t* nested
    nested = <size_t*>PyMem_Malloc(n * sizeof(size_t))
    if not nested:
        raise MemoryError()
    memset(nested, 0, n * sizeof(size_t))
    try:
        # increment the nesting level when a contour is inside another
        for i in range(n):
            contour = contours[i]
            for j in range(i + 1, n):
                other = contours[j]
                if path_is_inside(contour.path, other.path):
                    nested[j] += 1

        IF DEBUG_WINDING:
            print("nested: ", end="")
            for i in range(n):
                print(nested[i], end=" ")
            print("")

        # reverse a contour when its winding and even-odd number disagree;
        # for TrueType, set the outermost direction to clockwise
        for i in range(n):
            contour = contours[i]
            is_clockwise = get_path_area(contour.path) < .0
            is_even = not (nested[i] & 1)

            IF DEBUG_WINDING:
                print(
                    "%d: inverse=%s is_clockwise=%s is_even=%s"
                    % (i, inverse, is_clockwise, is_even)
                )
            if inverse ^ is_clockwise ^ is_even:
                IF DEBUG_WINDING:
                    print("reverse_contour %d" % i)
                reverse_contour(contour.path)
    finally:
        PyMem_Free(nested)

    path.path.reset()
    for i in range(n):
        contour = contours[i]
        path.path.addPath(contour.path.detach())

    path.path.setFillType(SkPathFillType.kWinding)
    return True


def decompose_quadratic_segment(points):
    return _decompose_quadratic_segment(points)


cdef list _decompose_quadratic_segment(tuple points):
    cdef:
        int i, n = len(points) - 1
        list quad_segments = []
        SkScalar x, y, nx, ny
        tuple implied_pt

    assert n > 0
    for i in range(n - 1):
        x, y = points[i]
        nx, ny = points[i+1]
        implied_pt = (0.5 * (x + nx), 0.5 * (y + ny))
        quad_segments.append((points[i], implied_pt))
    quad_segments.append((points[-2], points[-1]))
    return quad_segments


cdef int find_oncurve_point(
    SkScalar x,
    SkScalar y,
    const SkPoint *pts,
    int pt_count,
    const SkPathVerb *verbs,
    int verb_count,
    int *pt_index,
    int *verb_index,
) except -1:
    cdef SkPoint oncurve
    cdef SkPathVerb v
    cdef int i, j, n
    cdef int seen = 0

    for i in range(verb_count):
        v = verbs[i]
        n = pts_in_verb(v)
        if n == 0:
            continue
        assert seen + n <= pt_count
        j = seen + n - 1
        oncurve = pts[j]
        if oncurve.equals(x, y):
            pt_index[0] = j
            verb_index[0] = i
            return 1
        seen += n

    return 0


cdef int contour_is_closed(SkSpan[const SkPathVerb] verbs) except -1:
    cdef SkPathVerb v
    cdef bint closed = False
    if verbs.empty():
        return closed
    for verb in verbs.subspan(1):
        if verb == SkPathVerb.kMove:
            raise ValueError("expected single contour")
        elif verb == SkPathVerb.kClose:
            closed = True
    return closed


cdef int set_contour_start_point(SkPathBuilder& path, SkScalar x, SkScalar y) except -1:
    cdef SkSpan[const SkPathVerb] va = path.verbs()
    cdef const SkPathVerb *verbs = va.data()
    cdef size_t verb_count = va.size()

    cdef SkSpan[const SkPoint] pa = path.points()
    cdef const SkPoint *pts = pa.data()
    cdef size_t pt_count = pa.size()

    cdef bint closed = contour_is_closed(va)

    cdef int pt_index = -1
    cdef int verb_index = -1
    cdef bint found = find_oncurve_point(
        x, y,
        pts,
        pt_count,
        verbs,
        verb_count,
        &pt_index,
        &verb_index,
    )
    if not found or pt_index == 0 or (
        not closed and pt_index != (pt_count - 1)
    ):
        return 0

    if not closed and pt_index == (pt_count - 1):
        reverse_contour(path)
        return 1

    cdef SkPathBuilder temp
    temp.setFillType(path.fillType())

    cdef SkPathVerb first_verb
    cdef SkPoint first_pt
    cdef int vi, pi

    first_verb = verbs[verb_index]
    vi = (verb_index + 1) % verb_count

    first_pt = pts[pt_index]
    pi = (pt_index + 1) % pt_count

    temp.moveTo(first_pt)

    cdef int i, n
    cdef SkPathVerb v
    cdef const SkPoint *last = &first_pt
    for i in range(1, verb_count):
        v = verbs[vi]
        n = pts_in_verb(v)
        assert pi + n <= pt_count
        if v == SkPathVerb.kMove:
            # the moveTo from the original contour is converted to a lineTo,
            # unless it's equal to the previous point, or collinear between
            # the last oncuve point and the next line segment
            # https://github.com/fonttools/skia-pathops/issues/12
            if (
                points_almost_equal(last[0], pts[pi])
                or (
                    verbs[(vi + 1) % verb_count] == SkPathVerb.kLine
                    and collinear(last[0], pts[pi], pts[(pi + 1) % pt_count])
                )
            ):
                pass
            else:
                temp.lineTo(pts[pi])
                last = pts + pi
        elif v == SkPathVerb.kLine:
            # skip adding lineTo if it's the last segment from the original
            # contour and overlaps with the old moveTo point
            if (
                verbs[(vi + 1) % verb_count] == SkPathVerb.kClose
                and points_almost_equal(pts[pi], pts[(pi + 1) % pt_count])
            ):
                pass
            else:
                temp.lineTo(pts[pi])
                last = pts + pi
        elif v == SkPathVerb.kQuad:
            temp.quadTo(pts[pi], pts[pi + 1])
            last = pts + pi + 1
        elif v == SkPathVerb.kConic:
            raise UnsupportedVerbError("CONIC")
        elif v == SkPathVerb.kCubic:
            temp.cubicTo(pts[pi], pts[pi + 1], pts[pi + 2])
            last = pts + pi + 2
        elif v == SkPathVerb.kClose:
            pass
        else:
            raise AssertionError(v)
        vi = (vi + 1) % verb_count
        pi = (pi + n) % pt_count

    if first_verb == SkPathVerb.kQuad:
        temp.quadTo(pts[pi], pts[pi + 1])
    elif first_verb == SkPathVerb.kCubic:
        temp.cubicTo(pts[pi], pts[pi + 1], pts[pi + 2])

    temp.close()
    (&path)[0] = temp
    return 1


DEF MAX_CONIC_TO_QUAD_POW2 = 5

cdef int compute_conic_to_quad_pow2(
    SkPoint p0, SkPoint p1, SkPoint p2, SkScalar weight, SkScalar tol
) except -1:
    # Return the power-of-2 number of quads needed to approximate this conic
    # with a sequence of quads (will be >= 0). This is used to determine the optimal
    # (within tolerance) 'pow2' parameter when calling SkPath::ConvertConicToQuads.
    # Copied from SkConic::computeQuadPOW2 method in src/core/SkGeometry.cpp:
    # https://github.com/google/skia/blob/52a4379f03f7cd4e1c67eb69a756abc5838a658f/src/core/SkGeometry.cpp#L1198-L1231
    if tol < 0 or not all(
        isfinite(v)
        for v in (tol, weight, p0.x(), p0.y(), p1.x(), p1.y(), p2.x(), p2.y())
    ):
        return 0

    cdef SkScalar a = weight - 1
    cdef SkScalar k = a / (4 * (2 + a))
    cdef SkScalar x = k * (p0.x() - 2 * p1.x() + p2.x())
    cdef SkScalar y = k * (p0.y() - 2 * p1.y() + p2.y())

    cdef SkScalar error = sqrt(x * x + y * y)
    cdef int pow2

    for pow2 in range(MAX_CONIC_TO_QUAD_POW2):
        if error <= tol:
            break
        error *= 0.25
    return pow2


cpdef Path op(
    Path one,
    Path two,
    SkPathOp operator,
    bint fix_winding=True,
    bint keep_starting_points=True,
    bint clockwise=False,
):
    cdef list first_points
    if keep_starting_points:
        first_points = one.firstPoints + two.firstPoints
    cdef optional[SkPath] skresult = Op(one.path.snapshot(), two.path.snapshot(), operator)
    if not skresult.has_value():
        raise PathOpsError("operation did not succeed")
    cdef Path result = Path()
    result.path = skresult.value()
    if fix_winding:
        winding_from_even_odd(result, clockwise)
    if keep_starting_points:
        restore_starting_points(result, first_points)
    return result


cpdef Path simplify(
    Path path,
    bint fix_winding=True,
    bint keep_starting_points=True,
    bint clockwise=False,
):
    cdef list first_points
    if keep_starting_points:
        first_points = path.firstPoints
    cdef optional[SkPath] skresult = Simplify(path.path.snapshot())
    if not skresult.has_value():
        raise PathOpsError("operation did not succeed")
    cdef Path result = Path()
    result.path = skresult.value()
    if fix_winding:
        winding_from_even_odd(result, clockwise)
    if keep_starting_points:
        restore_starting_points(result, first_points)
    return result


cdef class OpBuilder:

    def __init__(
        self,
        bint fix_winding=True,
        bint keep_starting_points=True,
        bint clockwise=False,
    ):
        self.fix_winding = fix_winding
        self.keep_starting_points = keep_starting_points
        self.first_points = []
        self.clockwise = clockwise

    cpdef add(self, Path path, SkPathOp operator):
        self.builder.add(path.path.snapshot(), operator)
        if self.keep_starting_points:
            self.first_points.extend(path.firstPoints)

    cpdef Path resolve(self):
        cdef optional[SkPath] skresult = self.builder.resolve()
        if not skresult.has_value():
            raise PathOpsError("operation did not succeed")
        cdef Path result = Path()
        result.path = skresult.value()
        if self.fix_winding:
            winding_from_even_odd(result, self.clockwise)
        if self.keep_starting_points:
            restore_starting_points(result, self.first_points)
        return result


# Doctests


def test_collinear(p1, p2, p3):
    """
    >>> test_collinear((0.0, 0.0), (1.0, 1.0), (2.0, 2.0001))
    True
    >>> test_collinear((0.0, 0.0), (1.0, 1.0), (2.0, 2.001))
    False
    """
    cdef SkPoint sp1, sp2, sp3
    sp1 = SkPoint.Make(p1[0], p1[1])
    sp2 = SkPoint.Make(p2[0], p2[1])
    sp3 = SkPoint.Make(p3[0], p3[1])
    return collinear(sp1, sp2, sp3)
