# SPDX-License-Identifier: GPL-2.0-or-later

import bpy, math, cmath
from mathutils import Vector, Matrix
from collections import namedtuple

units = [
    ('-', 'None', '1.0', 0),
    ('px', 'Pixel', '1.0', 1),
    ('m', 'Meter', '1.0', 2),
    ('dm', 'Decimeter', '0.1', 3),
    ('cm', 'Centimeter', '0.01', 4),
    ('mm', 'Millimeter', '0.001', 5),
    ('yd', 'Yard', '0.9144', 6),
    ('ft', 'Foot', '0.3048', 7),
    ('in', 'Inch', '0.0254', 8)
]

param_tolerance = 0.0001
AABB = namedtuple('AxisAlignedBoundingBox', 'center dimensions')
Plane = namedtuple('Plane', 'normal distance')
Circle = namedtuple('Circle', 'orientation center radius')

def circleOfTriangle(a, b, c):
    # https://en.wikipedia.org/wiki/Circumscribed_circle#Cartesian_coordinates_from_cross-_and_dot-products
    dirBA = a-b
    dirCB = b-c
    dirAC = c-a
    normal = dirBA.cross(dirCB)
    lengthBA = dirBA.length
    lengthCB = dirCB.length
    lengthAC = dirAC.length
    lengthN = normal.length
    if lengthN == 0:
        return None
    factor = -1/(2*lengthN*lengthN)
    alpha = (dirBA@dirAC)*(lengthCB*lengthCB*factor)
    beta = (dirBA@dirCB)*(lengthAC*lengthAC*factor)
    gamma = (dirAC@dirCB)*(lengthBA*lengthBA*factor)
    center = a*alpha+b*beta+c*gamma
    radius = (lengthBA*lengthCB*lengthAC)/(2*lengthN)
    tangent = (a-center).normalized()
    orientation = Matrix.Identity(3)
    orientation.col[2] = normal/lengthN
    orientation.col[1] = (a-center).normalized()
    orientation.col[0] = orientation.col[1].xyz.cross(orientation.col[2].xyz)
    return Circle(orientation=orientation, center=center, radius=radius)

def circleOfBezier(points, tolerance=0.000001, samples=16):
    circle = circleOfTriangle(points[0], bezierPointAt(points, 0.5), points[3])
    if circle == None:
        return None
    variance = 0
    for t in range(0, samples):
        variance += ((circle.center-bezierPointAt(points, (t+1)/(samples-1))).length/circle.radius-1) ** 2
    variance /= samples
    return None if variance > tolerance else circle

def areaOfPolygon(vertices):
    area = 0
    for index, current in enumerate(vertices):
        prev = vertices[index-1]
        area += (current[0]+prev[0])*(current[1]-prev[1])
    return area*0.5

def linePointDistance(begin, dir, point):
    return (point-begin).cross(dir.normalized()).length

def linePlaneIntersection(origin, dir, plane):
    det = dir@plane.normal
    return float('nan') if det == 0 else (plane.distance-origin@plane.normal)/det

def nearestPointOfLines(originA, dirA, originB, dirB, tolerance=0.0):
    # https://en.wikipedia.org/wiki/Skew_lines#Nearest_Points
    normal = dirA.cross(dirB)
    normalA = dirA.cross(normal)
    normalB = dirB.cross(normal)
    divisorA = dirA@normalB
    divisorB = dirB@normalA
    if abs(divisorA) <= tolerance or abs(divisorB) <= tolerance:
        return (float('nan'), float('nan'), None, None)
    else:
        paramA = (originB-originA)@normalB/divisorA
        paramB = (originA-originB)@normalA/divisorB
        return (paramA, paramB, originA+dirA*paramA, originB+dirB*paramB)

def lineSegmentLineSegmentIntersection(beginA, endA, beginB, endB, tolerance=0.001):
    dirA = endA-beginA
    dirB = endB-beginB
    paramA, paramB, pointA, pointB = nearestPointOfLines(beginA, dirA, beginB, dirB)
    if math.isnan(paramA) or (pointA-pointB).length > tolerance or \
       paramA < 0 or paramA > 1 or paramB < 0 or paramB > 1:
        return None
    return (paramA, paramB, pointA, pointB)

def aabbOfPoints(points):
    min = Vector(points[0])
    max = Vector(points[0])
    for point in points:
        for i in range(0, 3):
            if min[i] > point[i]:
                min[i] = point[i]
            if max[i] < point[i]:
                max[i] = point[i]
    return AABB(center=(max+min)*0.5, dimensions=(max-min)*0.5)

def aabbIntersectionTest(a, b, tolerance=0.0):
    for i in range(0, 3):
        if abs(a.center[i]-b.center[i]) > a.dimensions[i]+b.dimensions[i]+tolerance:
            return False
    return True

def isPointInAABB(point, aabb, tolerance=0.0, ignore_axis=None):
    for i in range(0, 3):
        if i != ignore_axis and (point[i] < aabb.center[i]-aabb.dimensions[i]-tolerance or point[i] > aabb.center[i]+aabb.dimensions[i]+tolerance):
            return False
    return True

def lineAABBIntersection(lineBegin, lineEnd, aabb):
    intersections = []
    for i in range(0, 3):
        normal = [0, 0, 0]
        normal = Vector(normal[0:i] + [1] + normal[i+1:])
        for j in range(-1, 2, 2):
            plane = Plane(normal=normal, distance=aabb.center[i]+j*aabb.dimensions[i])
            param = linePlaneIntersection(lineBegin, lineEnd-lineBegin, plane)
            if param < 0 or param > 1 or math.isnan(param):
                continue
            point = lineBegin+param*(lineEnd-lineBegin)
            if isPointInAABB(point, aabb, 0.0, i):
                intersections.append((param, point))
    return intersections

def bezierPointAt(points, t):
    s = 1-t
    return s*s*s*points[0] + 3*s*s*t*points[1] + 3*s*t*t*points[2] + t*t*t*points[3]

def bezierTangentAt(points, t):
    s = 1-t
    return s*s*(points[1]-points[0])+2*s*t*(points[2]-points[1])+t*t*(points[3]-points[2])
    # return s*s*points[0] + (s*s-2*s*t)*points[1] + (2*s*t-t*t)*points[2] + t*t*points[3]

def bezierLength(points, beginT=0, endT=1, samples=1024):
    # https://en.wikipedia.org/wiki/Arc_length#Finding_arc_lengths_by_integrating
    vec = [points[1]-points[0], points[2]-points[1], points[3]-points[2]]
    dot = [vec[0]@vec[0], vec[0]@vec[1], vec[0]@vec[2], vec[1]@vec[1], vec[1]@vec[2], vec[2]@vec[2]]
    factors = [
        dot[0],
        4*(dot[1]-dot[0]),
        6*dot[0]+4*dot[3]+2*dot[2]-12*dot[1],
        12*dot[1]+4*(dot[4]-dot[0]-dot[2])-8*dot[3],
        dot[0]+dot[5]+2*dot[2]+4*(dot[3]-dot[1]-dot[4])
    ]
    # https://en.wikipedia.org/wiki/Trapezoidal_rule
    length = 0
    prev_value = math.sqrt(factors[4]+factors[3]+factors[2]+factors[1]+factors[0])
    for index in range(0, samples+1):
        t = beginT+(endT-beginT)*index/samples
        # value = math.sqrt(factors[4]*(t**4)+factors[3]*(t**3)+factors[2]*(t**2)+factors[1]*t+factors[0])
        value = math.sqrt((((factors[4]*t+factors[3])*t+factors[2])*t+factors[1])*t+factors[0])
        length += (prev_value+value)*0.5
        prev_value = value
    return length*3/samples

# https://en.wikipedia.org/wiki/Root_of_unity
# cubic_roots_of_unity = [cmath.rect(1, i/3*2*math.pi) for i in range(0, 3)]
cubic_roots_of_unity = [complex(1, 0), complex(-1, math.sqrt(3))*0.5, complex(-1, -math.sqrt(3))*0.5]
def bezierRoots(dists, tolerance=0.0001):
    # https://en.wikipedia.org/wiki/Cubic_function
    # y(t) = a*t^3 +b*t^2 +c*t^1 +d*t^0
    a = 3*(dists[1]-dists[2])+dists[3]-dists[0]
    b = 3*(dists[0]-2*dists[1]+dists[2])
    c = 3*(dists[1]-dists[0])
    d = dists[0]
    if abs(a) > tolerance: # Cubic
        E2 = a*c
        E3 = a*a*d
        A = (2*b*b-9*E2)*b+27*E3
        B = b*b-3*E2
        C = ((A+cmath.sqrt(A*A-4*B*B*B))*0.5) ** (1/3)
        roots = []
        for root in cubic_roots_of_unity:
            root *= C
            root = -1/(3*a)*(b+root+B/root)
            if abs(root.imag) < tolerance and root.real > -param_tolerance and root.real < 1.0+param_tolerance:
                roots.append(max(0.0, min(root.real, 1.0)))
        # Remove doubles
        roots.sort()
        for index in range(len(roots)-1, 0, -1):
            if abs(roots[index-1]-roots[index]) < param_tolerance:
                roots.pop(index)
        return roots
    elif abs(b) > tolerance: # Quadratic
        disc = c*c-4*b*d
        if disc < 0:
            return []
        disc = math.sqrt(disc)
        return [(-c-disc)/(2*b), (-c+disc)/(2*b)]
    elif abs(c) > tolerance: # Linear
        root = -d/c
        return [root] if root >= 0.0 and root <= 1.0 else []
    else: # Constant / Parallel
        return [] if abs(d) > tolerance else float('inf')

def xRaySplineIntersectionTest(spline, origin):
    spline_points = spline.bezier_points if spline.type == 'BEZIER' else spline.points
    cyclic_parallel_fix_flag = False
    intersections = []

    def areIntersectionsAdjacent(index):
        if len(intersections) < 2:
            return
        prev = intersections[index-1]
        current = intersections[index]
        if prev[1] == current[0] and \
           prev[2] > 1.0-param_tolerance and current[2] < param_tolerance and \
           ((prev[3] < 0 and current[3] < 0) or (prev[3] > 0 and current[3] > 0)):
            intersections.pop(index)

    def appendIntersection(index, root, tangentY, intersectionX):
        beginPoint = spline_points[index-1]
        endPoint = spline_points[index]
        if root == float('inf'): # Segment is parallel to ray
            if index == 0 and spline.use_cyclic_u:
                cyclic_parallel_fix_flag = True
            if len(intersections) > 0 and intersections[-1][1] == beginPoint:
                intersections[-1][1] = endPoint # Skip in adjacency test
        elif intersectionX >= origin[0]:
            intersections.append([beginPoint, endPoint, root, tangentY, intersectionX])
            areIntersectionsAdjacent(len(intersections)-1)

    if spline.type == 'BEZIER':
        for index, endPoint in enumerate(spline.bezier_points):
            if index == 0 and not spline.use_cyclic_u:
                continue
            beginPoint = spline_points[index-1]
            points = (beginPoint.co, beginPoint.handle_right, endPoint.handle_left, endPoint.co)
            roots = bezierRoots((points[0][1]-origin[1], points[1][1]-origin[1], points[2][1]-origin[1], points[3][1]-origin[1]))
            if roots == float('inf'): # Intersection
                appendIntersection(index, float('inf'), None, None)
            else:
                for root in roots:
                    appendIntersection(index, root, bezierTangentAt(points, root)[1], bezierPointAt(points, root)[0])
    elif spline.type == 'POLY':
        for index, endPoint in enumerate(spline.points):
            if index == 0 and not spline.use_cyclic_u:
                continue
            beginPoint = spline_points[index-1]
            points = (beginPoint.co, endPoint.co)
            if (points[0][0] < origin[0] and points[1][0] < origin[0]) or \
               (points[0][1] < origin[1] and points[1][1] < origin[1]) or \
               (points[0][1] > origin[1] and points[1][1] > origin[1]):
                continue
            diff = points[1]-points[0]
            height = origin[1]-points[0][1]
            if diff[1] == 0: # Parallel
                if height == 0: # Intersection
                    appendIntersection(index, float('inf'), None, None)
            else: # Not parallel
                root = height/diff[1]
                appendIntersection(index, root, diff[1], points[0][0]+diff[0]*root)

    if cyclic_parallel_fix_flag:
        appendIntersection(0, float('inf'), None, None)
    areIntersectionsAdjacent(0)
    return intersections

def isPointInSpline(point, spline):
    return spline.use_cyclic_u and len(xRaySplineIntersectionTest(spline, point))%2 == 1

def isSegmentLinear(points, tolerance=0.0001):
    return 1.0-(points[1]-points[0]).normalized()@(points[3]-points[2]).normalized() < tolerance

def bezierSegmentPoints(begin, end):
    return [begin.co, begin.handle_right, end.handle_left, end.co]

def grab_cursor(context, event):
    if event.mouse_region_x < 0:
        context.window.cursor_warp(context.region.x+context.region.width, event.mouse_y)
    elif event.mouse_region_x > context.region.width:
        context.window.cursor_warp(context.region.x, event.mouse_y)
    elif event.mouse_region_y < 0:
        context.window.cursor_warp(event.mouse_x, context.region.y+context.region.height)
    elif event.mouse_region_y > context.region.height:
        context.window.cursor_warp(event.mouse_x, context.region.y)

def deleteFromArray(item, array):
    for index, current in enumerate(array):
        if current is item:
            array.pop(index)
            break

def copyAttributes(dst, src):
    for attribute in dir(src):
        try:
            setattr(dst, attribute, getattr(src, attribute))
        except:
            pass

def bezierSliceFromTo(points, minParam, maxParam):
    fromP = bezierPointAt(points, minParam)
    fromT = bezierTangentAt(points, minParam)
    toP = bezierPointAt(points, maxParam)
    toT = bezierTangentAt(points, maxParam)
    paramDiff = maxParam-minParam
    return [fromP, fromP+fromT*paramDiff, toP-toT*paramDiff, toP]

def bezierIntersectionBroadPhase(solutions, pointsA, pointsB, aMin=0.0, aMax=1.0, bMin=0.0, bMax=1.0, depth=8, tolerance=0.001):
    if aabbIntersectionTest(aabbOfPoints(bezierSliceFromTo(pointsA, aMin, aMax)), aabbOfPoints(bezierSliceFromTo(pointsB, bMin, bMax)), tolerance) == False:
        return
    if depth == 0:
        solutions.append([aMin, aMax, bMin, bMax])
        return
    depth -= 1
    aMid = (aMin+aMax)*0.5
    bMid = (bMin+bMax)*0.5
    bezierIntersectionBroadPhase(solutions, pointsA, pointsB, aMin, aMid, bMin, bMid, depth, tolerance)
    bezierIntersectionBroadPhase(solutions, pointsA, pointsB, aMin, aMid, bMid, bMax, depth, tolerance)
    bezierIntersectionBroadPhase(solutions, pointsA, pointsB, aMid, aMax, bMin, bMid, depth, tolerance)
    bezierIntersectionBroadPhase(solutions, pointsA, pointsB, aMid, aMax, bMid, bMax, depth, tolerance)

def bezierIntersectionNarrowPhase(broadPhase, pointsA, pointsB, tolerance=0.000001):
    aMin = broadPhase[0]
    aMax = broadPhase[1]
    bMin = broadPhase[2]
    bMax = broadPhase[3]
    while (aMax-aMin > tolerance) or (bMax-bMin > tolerance):
        aMid = (aMin+aMax)*0.5
        bMid = (bMin+bMax)*0.5
        a1 = bezierPointAt(pointsA, (aMin+aMid)*0.5)
        a2 = bezierPointAt(pointsA, (aMid+aMax)*0.5)
        b1 = bezierPointAt(pointsB, (bMin+bMid)*0.5)
        b2 = bezierPointAt(pointsB, (bMid+bMax)*0.5)
        a1b1Dist = (a1-b1).length
        a2b1Dist = (a2-b1).length
        a1b2Dist = (a1-b2).length
        a2b2Dist = (a2-b2).length
        minDist = min(a1b1Dist, a2b1Dist, a1b2Dist, a2b2Dist)
        if a1b1Dist == minDist:
            aMax = aMid
            bMax = bMid
        elif a2b1Dist == minDist:
            aMin = aMid
            bMax = bMid
        elif a1b2Dist == minDist:
            aMax = aMid
            bMin = bMid
        else:
            aMin = aMid
            bMin = bMid
    return [aMin, bMin, minDist]

def segmentIntersection(segmentA, segmentB, tolerance=0.001):
    pointsA = bezierSegmentPoints(segmentA['beginPoint'], segmentA['endPoint'])
    pointsB = bezierSegmentPoints(segmentB['beginPoint'], segmentB['endPoint'])
    result = []
    def addCut(paramA, paramB):
        cutA = {'param': paramA, 'segment': segmentA}
        cutB = {'param': paramB, 'segment': segmentB}
        cutA['otherCut'] = cutB
        cutB['otherCut'] = cutA
        segmentA['cuts'].append(cutA)
        segmentB['cuts'].append(cutB)
        result.append([cutA, cutB])
    if isSegmentLinear(pointsA) and isSegmentLinear(pointsB):
        intersection = lineSegmentLineSegmentIntersection(pointsA[0], pointsA[3], pointsB[0], pointsB[3])
        if intersection != None:
            addCut(intersection[0], intersection[1])
        return result
    solutions = []
    bezierIntersectionBroadPhase(solutions, pointsA, pointsB)
    for index in range(0, len(solutions)):
        solutions[index] = bezierIntersectionNarrowPhase(solutions[index], pointsA, pointsB)
    for index in range(0, len(solutions)):
        for otherIndex in range(0, len(solutions)):
            if solutions[index][2] == float('inf'):
                break
            if index == otherIndex or solutions[otherIndex][2] == float('inf'):
                continue
            diffA = solutions[index][0]-solutions[otherIndex][0]
            diffB = solutions[index][1]-solutions[otherIndex][1]
            if diffA*diffA+diffB*diffB < 0.01:
                if solutions[index][2] < solutions[otherIndex][2]:
                    solutions[otherIndex][2] = float('inf')
                else:
                    solutions[index][2] = float('inf')
    def areIntersectionsAdjacent(segmentA, segmentB, paramA, paramB):
        return segmentA['endIndex'] == segmentB['beginIndex'] and paramA > 1-param_tolerance and paramB < param_tolerance
    for solution in solutions:
        if (solution[2] > tolerance) or \
          (segmentA['spline'] == segmentB['spline'] and \
          (areIntersectionsAdjacent(segmentA, segmentB, solution[0], solution[1]) or \
           areIntersectionsAdjacent(segmentB, segmentA, solution[1], solution[0]))):
            continue
        addCut(solution[0], solution[1])
    return result

def bezierMultiIntersection(segments):
    for index in range(0, len(segments)):
        for otherIndex in range(index+1, len(segments)):
            segmentIntersection(segments[index], segments[otherIndex])
    prepareSegmentIntersections(segments)
    subdivideBezierSegments(segments)

def bezierProjectHandles(segments):
    insertions = []
    index_offset = 0
    for segment in segments:
        if len(insertions) > 0 and insertions[-1][0] != segment['spline']:
            index_offset = 0
        points = bezierSegmentPoints(segment['beginPoint'], segment['endPoint'])
        paramA, paramB, pointA, pointB = nearestPointOfLines(points[0], points[1]-points[0], points[3], points[2]-points[3])
        if pointA and pointB:
            segment['cuts'].append({'param': 0.5})
            insertions.append((segment['spline'], segment['beginIndex']+1+index_offset, (pointA+pointB)*0.5))
            index_offset += 1
    subdivideBezierSegments(segments)
    for insertion in insertions:
        bezier_point = insertion[0].bezier_points[insertion[1]]
        bezier_point.co = insertion[2]
        bezier_point.handle_left_type = 'VECTOR'
        bezier_point.handle_right_type = 'VECTOR'

def bezierSubivideAt(points, params):
    if len(params) == 0:
        return []
    newPoints = []
    newPoints.append(points[0]+(points[1]-points[0])*params[0])
    for index, param in enumerate(params):
        paramLeft = param
        if index > 0:
            paramLeft -= params[index-1]
        paramRight = -param
        if index == len(params)-1:
            paramRight += 1.0
        else:
            paramRight += params[index+1]
        point = bezierPointAt(points, param)
        tangent = bezierTangentAt(points, param)
        newPoints.append(point-tangent*paramLeft)
        newPoints.append(point)
        newPoints.append(point+tangent*paramRight)
    newPoints.append(points[3]-(points[3]-points[2])*(1.0-params[-1]))
    return newPoints

def subdivideBezierSegment(segment):
    # Blender only allows uniform subdivision. Use this method to subdivide at arbitrary params.
    # NOTE: segment['cuts'] must be sorted by param
    if len(segment['cuts']) == 0:
        return

    segment['beginPoint'] = segment['spline'].bezier_points[segment['beginIndex']]
    segment['endPoint'] = segment['spline'].bezier_points[segment['endIndex']]
    params = [cut['param'] for cut in segment['cuts']]
    newPoints = bezierSubivideAt(bezierSegmentPoints(segment['beginPoint'], segment['endPoint']), params)
    bpy.ops.curve.select_all(action='DESELECT')
    segment['beginPoint'] = segment['spline'].bezier_points[segment['beginIndex']]
    segment['beginPoint'].select_right_handle = True
    segment['beginPoint'].handle_left_type = 'FREE'
    segment['beginPoint'].handle_right_type = 'FREE'
    segment['endPoint'] = segment['spline'].bezier_points[segment['endIndex']]
    segment['endPoint'].select_left_handle = True
    segment['endPoint'].handle_left_type = 'FREE'
    segment['endPoint'].handle_right_type = 'FREE'

    bpy.ops.curve.subdivide(number_cuts=len(params))
    if segment['endIndex'] > 0:
        segment['endIndex'] += len(params)
    segment['beginPoint'] = segment['spline'].bezier_points[segment['beginIndex']]
    segment['endPoint'] = segment['spline'].bezier_points[segment['endIndex']]
    segment['beginPoint'].select_right_handle = False
    segment['beginPoint'].handle_right = newPoints[0]
    segment['endPoint'].select_left_handle = False
    segment['endPoint'].handle_left = newPoints[-1]

    for index, cut in enumerate(segment['cuts']):
        cut['index'] = segment['beginIndex']+1+index
        newPoint = segment['spline'].bezier_points[cut['index']]
        newPoint.handle_left_type = 'FREE'
        newPoint.handle_right_type = 'FREE'
        newPoint.select_left_handle = False
        newPoint.select_control_point = False
        newPoint.select_right_handle = False
        newPoint.handle_left = newPoints[index*3+1]
        newPoint.co = newPoints[index*3+2]
        newPoint.handle_right = newPoints[index*3+3]

def prepareSegmentIntersections(segments):
    def areCutsAdjacent(cutA, cutB):
        return cutA['segment']['beginIndex'] == cutB['segment']['endIndex'] and \
               cutA['param'] < param_tolerance and cutB['param'] > 1.0-param_tolerance
    for segment in segments:
        segment['cuts'].sort(key=(lambda cut: cut['param']))
        for index in range(len(segment['cuts'])-1, 0, -1):
            prev = segment['cuts'][index-1]
            current = segment['cuts'][index]
            if abs(prev['param']-current['param']) < param_tolerance and \
               prev['otherCut']['segment']['spline'] == current['otherCut']['segment']['spline'] and \
               (areCutsAdjacent(prev['otherCut'], current['otherCut']) or \
                areCutsAdjacent(current['otherCut'], prev['otherCut'])):
                deleteFromArray(prev['otherCut'], prev['otherCut']['segment']['cuts'])
                deleteFromArray(current['otherCut'], current['otherCut']['segment']['cuts'])
                segment['cuts'].pop(index-1 if current['otherCut']['param'] < param_tolerance else index)
                current = segment['cuts'][index-1]['otherCut']
                current['segment']['extraCut'] = current

def subdivideBezierSegmentsOfSameSpline(segments):
    # NOTE: segment['cuts'] must be sorted by param
    indexOffset = 0
    for segment in segments:
        segment['beginIndex'] += indexOffset
        if segment['endIndex'] > 0:
            segment['endIndex'] += indexOffset
        subdivideBezierSegment(segment)
        indexOffset += len(segment['cuts'])
    for segment in segments:
        segment['beginPoint'] = segment['spline'].bezier_points[segment['beginIndex']]
        segment['endPoint'] = segment['spline'].bezier_points[segment['endIndex']]

def subdivideBezierSegments(segments):
    # NOTE: segment['cuts'] must be sorted by param
    groups = {}
    for segment in segments:
        spline = segment['spline']
        if (spline in groups) == False:
            groups[spline] = []
        group = groups[spline]
        group.append(segment)
    for spline in groups:
        subdivideBezierSegmentsOfSameSpline(groups[spline])

def curveObject():
    obj = bpy.context.object
    return obj if obj != None and obj.type == 'CURVE' and obj.mode == 'EDIT' else None

def bezierSegments(splines, selection_only):
    segments = []
    for spline in splines:
        if spline.type != 'BEZIER':
            continue
        for index, current in enumerate(spline.bezier_points):
            next = spline.bezier_points[(index+1) % len(spline.bezier_points)]
            if next == spline.bezier_points[0] and not spline.use_cyclic_u:
                continue
            if not selection_only or (current.select_right_handle and next.select_left_handle):
                segments.append({
                    'spline': spline,
                    'beginIndex': index,
                    'endIndex': index+1 if index < len(spline.bezier_points)-1 else 0,
                    'beginPoint': current,
                    'endPoint': next,
                    'cuts': []
                })
    return segments

def getSelectedSplines(include_bezier, include_polygon, allow_partial_selection=False):
    result = []
    for spline in bpy.context.object.data.splines:
        selected = not allow_partial_selection
        if spline.type == 'BEZIER':
            if not include_bezier:
                continue
            for index, point in enumerate(spline.bezier_points):
                if point.select_left_handle == allow_partial_selection or \
                   point.select_control_point == allow_partial_selection or \
                   point.select_right_handle == allow_partial_selection:
                    selected = allow_partial_selection
                    break
        elif spline.type == 'POLY':
            if not include_polygon:
                continue
            for index, point in enumerate(spline.points):
                if point.select == allow_partial_selection:
                    selected = allow_partial_selection
                    break
        else:
            continue
        if selected:
            result.append(spline)
    return result

def addObject(type, name):
    if type == 'CURVE':
        data = bpy.data.curves.new(name=name, type='CURVE')
        data.dimensions = '3D'
    elif type == 'MESH':
        data = bpy.data.meshes.new(name=name, type='MESH')
    obj = bpy.data.objects.new(name, data)
    obj.location = bpy.context.scene.cursor.location
    bpy.context.scene.collection.objects.link(obj)
    obj.select_set(True)
    bpy.context.view_layer.objects.active = obj
    return obj

def addPolygonSpline(obj, cyclic, vertices, weights=None, select=False):
    spline = obj.data.splines.new(type='POLY')
    spline.use_cyclic_u = cyclic
    spline.points.add(len(vertices)-1)
    for index, point in enumerate(spline.points):
        point.co.xyz = vertices[index]
        point.select = select
        if weights:
            point.weight_softbody = weights[index]
    return spline

def addBezierSpline(obj, cyclic, vertices, weights=None, select=False):
    spline = obj.data.splines.new(type='BEZIER')
    spline.use_cyclic_u = cyclic
    spline.bezier_points.add(len(vertices)-1)
    for index, point in enumerate(spline.bezier_points):
        point.handle_left = vertices[index][0]
        point.co = vertices[index][1]
        point.handle_right = vertices[index][2]
        if weights:
            point.weight_softbody = weights[index]
        point.select_left_handle = select
        point.select_control_point = select
        point.select_right_handle = select
        if isSegmentLinear([vertices[index-1][1], vertices[index-1][2], vertices[index][0], vertices[index][1]]):
            spline.bezier_points[index-1].handle_right_type = 'VECTOR'
            point.handle_left_type = 'VECTOR'
    return spline

def mergeEnds(splines, points, is_last_point):
    bpy.ops.curve.select_all(action='DESELECT')
    points[0].handle_left_type = points[0].handle_right_type = 'FREE'
    new_co = (points[0].co+points[1].co)*0.5
    handle = (points[1].handle_left if is_last_point[1] else points[1].handle_right)+new_co-points[1].co
    points[0].select_left_handle = points[0].select_right_handle = True
    if is_last_point[0]:
        points[0].handle_left += new_co-points[0].co
        points[0].handle_right = handle
    else:
        points[0].handle_right += new_co-points[0].co
        points[0].handle_left = handle
    points[0].co = new_co
    points[0].select_control_point = points[1].select_control_point = True
    bpy.ops.curve.make_segment()
    spline = splines[0] if splines[0] in bpy.context.object.data.splines.values() else splines[1]
    point = next(point for point in spline.bezier_points if point.select_left_handle)
    point.select_left_handle = point.select_right_handle = point.select_control_point = False
    bpy.ops.curve.delete()
    return spline

def polygonArcAt(center, radius, begin_angle, angle, step_angle, include_ends):
    vertices = []
    circle_samples = math.ceil(abs(angle)/step_angle)
    for t in (range(0, circle_samples+1) if include_ends else range(1, circle_samples)):
        t = begin_angle+angle*t/circle_samples
        normal = Vector((math.cos(t), math.sin(t), 0))
        vertices.append(center+normal*radius)
    return vertices

def bezierArcAt(tangent, normal, center, radius, angle, tolerance=0.99999):
    transform = Matrix.Identity(4)
    transform.col[0].xyz = tangent.cross(normal)*radius
    transform.col[1].xyz = tangent*radius
    transform.col[2].xyz = normal*radius
    transform.col[3].xyz = center
    segments = []
    segment_count = math.ceil(abs(angle)/(math.pi*0.5)*tolerance)
    angle /= segment_count
    x0 = math.cos(angle*0.5)
    y0 = math.sin(angle*0.5)
    x1 = (4.0-x0)/3.0
    y1 = (1.0-x0)*(3.0-x0)/(3.0*y0)
    points = [
        Vector((x0, -y0, 0)),
        Vector((x1, -y1, 0)),
        Vector((x1, y1, 0)),
        Vector((x0, y0, 0))
    ]
    for i in range(0, segment_count):
        rotation = Matrix.Rotation((i+0.5)*angle, 4, 'Z')
        segments.append(list(map(lambda v: transform@(rotation@v), points)))
    return segments

def iterateSpline(spline, callback):
    spline_points = spline.bezier_points if spline.type == 'BEZIER' else spline.points
    for index, spline_point in enumerate(spline_points):
        prev = spline_points[index-1]
        current = spline_points[index]
        next = spline_points[(index+1)%len(spline_points)]
        if spline.type == 'BEZIER':
            selected = current.select_control_point
            prev_segment_points = bezierSegmentPoints(prev, current)
            next_segment_points = bezierSegmentPoints(current, next)
            prev_tangent = (prev_segment_points[3]-prev_segment_points[2]).normalized()
            current_tangent = (next_segment_points[1]-next_segment_points[0]).normalized()
            next_tangent = (next_segment_points[3]-next_segment_points[2]).normalized()
        else:
            selected = current.select
            prev_segment_points = [prev.co.xyz, None, None, current.co.xyz]
            next_segment_points = [current.co.xyz, None, None, next.co.xyz]
            prev_tangent = (prev_segment_points[3]-prev_segment_points[0]).normalized()
            current_tangent = next_tangent = (next_segment_points[3]-next_segment_points[0]).normalized()
        normal = prev_tangent.cross(current_tangent).normalized()
        angle = prev_tangent@current_tangent
        angle = 0 if abs(angle-1.0) < 0.0001 else math.acos(angle)
        is_first = (index == 0) and not spline.use_cyclic_u
        is_last = (index == len(spline_points)-1) and not spline.use_cyclic_u
        callback(prev_segment_points, next_segment_points, selected, prev_tangent, current_tangent, next_tangent, normal, angle, is_first, is_last)
    return spline_points

def offsetPolygonOfSpline(spline, offset, step_angle, round_line_join, bezier_samples=128, tolerance=0.000001):
    def offsetVertex(position, tangent):
        normal = Vector((-tangent[1], tangent[0], 0))
        return position+normal*offset
    vertices = []
    def handlePoint(prev_segment_points, next_segment_points, selected, prev_tangent, current_tangent, next_tangent, normal, angle, is_first, is_last):
        sign = math.copysign(1, normal[2])
        angle *= sign
        if is_last:
            return
        is_protruding = (abs(angle) > tolerance and abs(offset) > tolerance)
        if is_protruding and not is_first and sign != math.copysign(1, offset): # Convex Corner
            if round_line_join:
                begin_angle = math.atan2(prev_tangent[1], prev_tangent[0])+math.pi*0.5
                vertices.extend(polygonArcAt(next_segment_points[0], offset, begin_angle, angle, step_angle, False))
            else:
                distance = offset*math.tan(angle*0.5)
                vertices.append(offsetVertex(next_segment_points[0], current_tangent)+current_tangent*distance)
        if is_protruding or is_first:
            vertices.append(offsetVertex(next_segment_points[0], current_tangent))
        if spline.type == 'POLY' or isSegmentLinear(next_segment_points):
            vertices.append(offsetVertex(next_segment_points[3], next_tangent))
        else: # Trace Bezier Segment
            prev_tangent = bezierTangentAt(next_segment_points, 0).normalized()
            for t in range(1, bezier_samples+1):
                t /= bezier_samples
                tangent = bezierTangentAt(next_segment_points, t).normalized()
                if t == 1 or math.acos(min(max(-1, prev_tangent@tangent), 1)) >= step_angle:
                    vertices.append(offsetVertex(bezierPointAt(next_segment_points, t), tangent))
                    prev_tangent = tangent
    spline_points = iterateSpline(spline, handlePoint)

    # Solve Self Intersections
    original_area = areaOfPolygon([point.co for point in spline_points])
    sign = -1 if offset < 0 else 1
    i = (0 if spline.use_cyclic_u else 1)
    while i < len(vertices):
        j = i+2
        while j < len(vertices) - (0 if i > 0 else 1):
            intersection = lineSegmentLineSegmentIntersection(vertices[i-1], vertices[i], vertices[j-1], vertices[j])
            if intersection == None:
                j += 1
                continue
            intersection = (intersection[2]+intersection[3])*0.5
            areaInner = sign*areaOfPolygon([intersection, vertices[i], vertices[j-1]])
            areaOuter = sign*areaOfPolygon([intersection, vertices[j], vertices[i-1]])
            if areaInner > areaOuter:
                vertices = vertices[i:j]+[intersection]
                i = (0 if spline.use_cyclic_u else 1)
            else:
                vertices = vertices[:i]+[intersection]+vertices[j:]
            j = i+2
        i += 1
    new_area = areaOfPolygon(vertices)
    return [vertices] if original_area*new_area >= 0 else []

def filletSpline(spline, radius, chamfer_mode, limit_half_way, tolerance=0.0001):
    vertices = []
    distance_limit_factor = 0.5 if limit_half_way else 1.0
    def handlePoint(prev_segment_points, next_segment_points, selected, prev_tangent, current_tangent, next_tangent, normal, angle, is_first, is_last):
        distance = min((prev_segment_points[0]-prev_segment_points[3]).length*distance_limit_factor, (next_segment_points[0]-next_segment_points[3]).length*distance_limit_factor)
        if not selected or is_first or is_last or angle == 0 or distance == 0 or \
           (spline.type == 'BEZIER' and not (isSegmentLinear(prev_segment_points) and isSegmentLinear(next_segment_points))):
            prev_handle = next_segment_points[0] if is_first else prev_segment_points[2] if spline.type == 'BEZIER' else prev_segment_points[0]
            next_handle = next_segment_points[0] if is_last else next_segment_points[1] if spline.type == 'BEZIER' else next_segment_points[3]
            vertices.append([prev_handle, next_segment_points[0], next_handle])
            return
        tan_factor = math.tan(angle*0.5)
        offset = min(radius, distance/tan_factor)
        distance = offset*tan_factor
        circle_center = next_segment_points[0]+normal.cross(prev_tangent)*offset-prev_tangent*distance
        segments = bezierArcAt(prev_tangent, normal, circle_center, offset, angle)
        if chamfer_mode:
            vertices.append([prev_segment_points[0], segments[0][0], segments[-1][3]])
            vertices.append([segments[0][0], segments[-1][3], next_segment_points[3]])
        else:
            for i in range(0, len(segments)+1):
                vertices.append([
                    segments[i-1][2] if i > 0 else prev_segment_points[0],
                    segments[i][0] if i < len(segments) else segments[i-1][3],
                    segments[i][1] if i < len(segments) else next_segment_points[3]
                ])
    iterateSpline(spline, handlePoint)
    i = 0 if spline.use_cyclic_u else 1
    while(i < len(vertices)):
        if (vertices[i-1][1]-vertices[i][1]).length < tolerance:
            vertices[i-1][2] = vertices[i][2]
            del vertices[i]
        else:
            i = i+1
    return addBezierSpline(bpy.context.object, spline.use_cyclic_u, vertices)

def dogBone(spline, radius):
    vertices = []
    def handlePoint(prev_segment_points, next_segment_points, selected, prev_tangent, current_tangent, next_tangent, normal, angle, is_first, is_last):
        if not selected or is_first or is_last or angle == 0 or normal[2] > 0.0 or \
           (spline.type == 'BEZIER' and not (isSegmentLinear(prev_segment_points) and isSegmentLinear(next_segment_points))):
            prev_handle = next_segment_points[0] if is_first else prev_segment_points[2] if spline.type == 'BEZIER' else prev_segment_points[0]
            next_handle = next_segment_points[0] if is_last else next_segment_points[1] if spline.type == 'BEZIER' else next_segment_points[3]
            vertices.append([prev_handle, next_segment_points[0], next_handle])
            return
        tan_factor = math.tan(angle*0.5)
        corner = next_segment_points[0]+normal.cross(prev_tangent)*radius-prev_tangent*radius*tan_factor
        direction = next_segment_points[0]-corner
        distance = direction.length
        corner = next_segment_points[0]+direction/distance*(distance-radius)
        vertices.append([prev_segment_points[0], next_segment_points[0], corner])
        vertices.append([next_segment_points[0], corner, next_segment_points[0]])
        vertices.append([corner, next_segment_points[0], next_segment_points[3]])
    iterateSpline(spline, handlePoint)
    return vertices

def discretizeCurve(spline, step_angle, samples):
    vertices = []
    def handlePoint(prev_segment_points, next_segment_points, selected, prev_tangent, current_tangent, next_tangent, normal, angle, is_first, is_last):
        if is_last:
            return
        if isSegmentLinear(next_segment_points):
            vertices.append(next_segment_points[3])
        else:
            prev_tangent = bezierTangentAt(next_segment_points, 0).normalized()
            for t in range(1, samples+1):
                t /= samples
                tangent = bezierTangentAt(next_segment_points, t).normalized()
                if t == 1 or math.acos(min(max(-1, prev_tangent@tangent), 1)) >= step_angle:
                    vertices.append(bezierPointAt(next_segment_points, t))
                    prev_tangent = tangent
    iterateSpline(spline, handlePoint)
    return vertices

def bezierBooleanGeometry(splineA, splineB, operation):
    if not splineA.use_cyclic_u or not splineB.use_cyclic_u:
        return False
    segmentsA = bezierSegments([splineA], False)
    segmentsB = bezierSegments([splineB], False)

    deletionFlagA = isPointInSpline(splineA.bezier_points[0].co, splineB)
    deletionFlagB = isPointInSpline(splineB.bezier_points[0].co, splineA)
    if operation == 'DIFFERENCE':
        deletionFlagB = not deletionFlagB
    elif operation == 'INTERSECTION':
        deletionFlagA = not deletionFlagA
        deletionFlagB = not deletionFlagB
    elif operation != 'UNION':
        return False

    intersections = []
    for segmentA in segmentsA:
        for segmentB in segmentsB:
            intersections.extend(segmentIntersection(segmentA, segmentB))
    if len(intersections) == 0:
        if deletionFlagA:
            bpy.context.object.data.splines.remove(splineA)
        if deletionFlagB:
            bpy.context.object.data.splines.remove(splineB)
        return True

    prepareSegmentIntersections(segmentsA)
    prepareSegmentIntersections(segmentsB)
    subdivideBezierSegmentsOfSameSpline(segmentsA)
    subdivideBezierSegmentsOfSameSpline(segmentsB)

    def collectCuts(cuts, segments, deletionFlag):
        for segmentIndex, segment in enumerate(segments):
            if 'extraCut' in segment:
                deletionFlag = not deletionFlag
                segment['extraCut']['index'] = segment['beginIndex']
                segment['extraCut']['deletionFlag'] = deletionFlag
                cuts.append(segment['extraCut'])
            else:
                cuts.append(None)
            cuts.extend(segments[segmentIndex]['cuts'])
            segment['deletionFlag'] = deletionFlag
            for cutIndex, cut in enumerate(segment['cuts']):
                deletionFlag = not deletionFlag
                cut['deletionFlag'] = deletionFlag
    cutsA = []
    cutsB = []
    collectCuts(cutsA, segmentsA, deletionFlagA)
    collectCuts(cutsB, segmentsB, deletionFlagB)

    beginIndex = 0
    for segment in segmentsA:
        if segment['deletionFlag'] == False:
            beginIndex = segment['beginIndex']
            break
        for cut in segment['cuts']:
            if cut['deletionFlag'] == False:
                beginIndex = cut['index']
                break

    cuts = cutsA
    spline = splineA
    index = beginIndex
    backward = False
    vertices = []
    while True:
        current = spline.bezier_points[index]
        vertices.append([current.handle_left, current.co, current.handle_right])
        if backward:
            current.handle_left, current.handle_right = current.handle_right.copy(), current.handle_left.copy()
        index += len(spline.bezier_points)-1 if backward else 1
        index %= len(spline.bezier_points)
        if spline == splineA and index == beginIndex:
            break

        cut = cuts[index]
        if cut != None:
            current = spline.bezier_points[index]
            current_handle = current.handle_right if backward else current.handle_left
            spline = splineA if spline == splineB else splineB
            cuts = cutsA if spline == splineA else cutsB
            index = cut['otherCut']['index']
            backward = cut['otherCut']['deletionFlag']
            next = spline.bezier_points[index]
            if backward:
                next.handle_right = current_handle
            else:
                next.handle_left = current_handle
            if spline == splineA and index == beginIndex:
                break

    spline = addBezierSpline(bpy.context.object, True, vertices)
    bpy.context.object.data.splines.remove(splineA)
    bpy.context.object.data.splines.remove(splineB)
    bpy.context.object.data.splines.active = spline
    return True

def truncateToFitBox(transform, spline, aabb):
    spline_points = spline.points
    aux = {
        'traces': [],
        'vertices': [],
        'weights': []
    }
    def terminateTrace(aux):
        if len(aux['vertices']) > 0:
            aux['traces'].append((aux['vertices'], aux['weights']))
        aux['vertices'] = []
        aux['weights'] = []
    for index, point in enumerate(spline_points):
        begin = transform@point.co.xyz
        end = spline_points[(index+1)%len(spline_points)]
        inside = isPointInAABB(begin, aabb)
        if inside:
            aux['vertices'].append(begin)
            aux['weights'].append(point.weight_softbody)
        if index == len(spline_points)-1 and not spline.use_cyclic_u:
            break
        intersections = lineAABBIntersection(begin, transform@end.co.xyz, aabb)
        if len(intersections) == 2:
            terminateTrace(aux)
            aux['traces'].append((
                [intersections[0][1], intersections[1][1]],
                [end.weight_softbody, end.weight_softbody]
            ))
        elif len(intersections) == 1:
            aux['vertices'].append(intersections[0][1])
            aux['weights'].append(end.weight_softbody)
            if inside:
                terminateTrace(aux)
        elif inside and index == len(spline_points)-1 and spline.use_cyclic_u:
            terminateTrace(aux)
            aux['traces'][0] = (aux['traces'][-1][0]+aux['traces'][0][0], aux['traces'][-1][1]+aux['traces'][0][1])
            aux['traces'].pop()
    terminateTrace(aux)
    return aux['traces']

def arrayModifier(splines, offset, count, connect, serpentine):
    if connect:
        for spline in splines:
            if spline.use_cyclic_u:
                spline.use_cyclic_u = False
                points = spline.points if spline.type == 'POLY' else spline.bezier_points
                points.add(1)
                copyAttributes(points[-1], points[0])
    bpy.ops.curve.select_all(action='DESELECT')
    for spline in splines:
        if spline.type == 'BEZIER':
            for point in spline.bezier_points:
                point.select_left_handle = point.select_control_point = point.select_right_handle = True
        elif spline.type == 'POLY':
            for point in spline.points:
                point.select = True
    splines_at_layer = [splines]
    for i in range(1, count):
        bpy.ops.curve.duplicate()
        bpy.ops.transform.translate(value=offset)
        splines_at_layer.append(getSelectedSplines(True, True))
        if serpentine:
            bpy.ops.curve.switch_direction()
    if connect:
        for i in range(1, count):
            prev_layer = splines_at_layer[i-1]
            next_layer = splines_at_layer[i]
            for j in range(0, len(next_layer)):
                bpy.ops.curve.select_all(action='DESELECT')
                if prev_layer[j].type == 'POLY':
                    prev_layer[j].points[-1].select = True
                else:
                    prev_layer[j].bezier_points[-1].select_control_point = True
                if next_layer[j].type == 'POLY':
                    next_layer[j].points[0].select = True
                else:
                    next_layer[j].bezier_points[0].select_control_point = True
                bpy.ops.curve.make_segment()
    bpy.ops.curve.select_all(action='DESELECT')
