# Author - Nadieh Bremer
# Description - Create a ceiling light construction that rotates from one end to the other

import math
import adsk.core
import adsk.fusion
# import adsk.cam
import traceback

# Remap a value from a range of [in_min, in_max] to [out_min, out_max]
def remap(x, in_min, in_max, out_min, out_max):
    return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min

# Easing function
# Modeled after the parabola y = -x^2 + 2x
# https://gist.github.com/zeffii/c1e14dd6620ad855d81ec2e89a859719
def QuadraticEaseOut(p, start=0, end=1):
    norm = p/end
    value = -(norm * (norm - 2))
    remappedValue = remap(value, 0, 1, start, end)
    return remappedValue

# Create the points for the slices and all the slots
def createDoubleCurves(j, steps, loops, radius, thickness, millRadius, a, sliceDistance, radiusFactor, offsetFactor, slotDepth, paramMaterialThickness, sign):
    # Create an object collection for the points.
    points = adsk.core.ObjectCollection.create()
    ribPoint = 0
    jointPositions = [0, 0]
    jointPositionsRib = [0, 0]
    centerPointsCircles = [0, 0]
    centerPointsCirclesRib = [0, 0]

    for i in range(steps):
        # from 0 to TWOPI radians
        p = (i / steps) * math.pi * 2
        slicePosition = (j+1) * sliceDistance

        offset = math.cos((i % loops) / loops * math.pi * 2 + a * loops) * 1.35
        offset = offset * radiusFactor
        # From circle to hexagon and back
        offset = offset * offsetFactor
        # Inside or outside curve
        if sign == 1:
            offset = offset * (9 + thickness)/9  # 1.16
        else:
            offset = offset * (9 - thickness)/9  # 0.84

        x = (radius + offset + sign * thickness) * math.cos(p)
        y = (radius + offset + sign * thickness) * math.sin(p)

        points.add(adsk.core.Point3D.create(x, y, slicePosition))

        # Points for rectangle to create slots
        if i == 0:
            jointPositions = [adsk.core.Point3D.create(x, y, slicePosition),
                              adsk.core.Point3D.create(x + slotDepth, y + paramMaterialThickness / 2, slicePosition)]
            # Points for rectangle to create slots
            jointPositionsRib = [adsk.core.Point3D.create(slicePosition, x + 2*slotDepth, y),
                                 adsk.core.Point3D.create(slicePosition + paramMaterialThickness / 2, x + slotDepth, y)]

        # Points for t-bone circle
        if i == 0:
            centerPointsCircles = [adsk.core.Point3D.create(x + slotDepth - millRadius, y + paramMaterialThickness / 2, slicePosition),
                                   adsk.core.Point3D.create(x + slotDepth - millRadius, y - paramMaterialThickness / 2, slicePosition)]
            # Points for rectangle to create slots
            centerPointsCirclesRib = [adsk.core.Point3D.create(slicePosition + paramMaterialThickness / 2, x + slotDepth + millRadius, y),
                                      adsk.core.Point3D.create(slicePosition - paramMaterialThickness / 2, x + slotDepth + millRadius, y)]

        # Create rib point
        if i == 0:
            position = 0
            if sign == 1:
                position = -1
            else:
                position = 2
            x = (radius + offset + sign * (thickness + position)) * math.cos(p)
            y = (radius + offset + sign * (thickness + position)) * math.sin(p)
            ribPoint = adsk.core.Point3D.create(slicePosition, x, y)

    # Add the first point to create a closed loop
    points.add(points.item(0))

    return points, ribPoint, jointPositions, jointPositionsRib, centerPointsCircles, centerPointsCirclesRib

def run(context):
    ui = None
    try:
        app = adsk.core.Application.get()
        ui = app.userInterface
        design = app.activeProduct

        # Get the root component of the active design.
        rootComp = design.rootComponent

        # Create a new sketch on the xy plane.
        sketches = rootComp.sketches
        yzPlane = rootComp.yZConstructionPlane
        xzPlane = rootComp.xZConstructionPlane
        sketch = sketches.add(yzPlane)
        sketchRib = sketches.add(xzPlane)

        # Get extrude features
        extrudes = rootComp.features.extrudeFeatures

        # Creating fillets
        fillets = rootComp.features.filletFeatures

        # Create slot rectangle inside the ribs
        lines = sketch.sketchCurves.sketchLines
        linesRib = sketchRib.sketchCurves.sketchLines

        # Create t-bone circle
        circles = sketch.sketchCurves.sketchCircles
        circlesRib = sketchRib.sketchCurves.sketchCircles

        # Get the user parameters
        userParams = design.userParameters
        paramMaterialThickness = userParams.itemByName('material_thickness').value
        paramRingThickness = userParams.itemByName('ring_thickness').value

        # Important Constants
        radiusBase = 9
        thickness = paramRingThickness/2
        slotDepth = 1.5
        millRadius = 0.5/2  #5mm

        loops = 6  # How many points to sample along a sine curve
        steps = 6 * loops  # How many points along a circle are created for each slice
        sliceDistance = 3  # The distance between each slice
        curveItems = 49 #math.round(150/sliceDistance)  # The number of slices, uneven

        #######################################################
        # Create Curved Slices
        #######################################################

        ribPointsInside = adsk.core.ObjectCollection.create()
        ribPointsOutside = adsk.core.ObjectCollection.create()
        ribPointsSlots = adsk.core.ObjectCollection.create()
        ribCirclePoints = adsk.core.ObjectCollection.create()

        # Define the points the spline with fit through
        for j in range(-1, curveItems - 1, 1):
            # Get an adjusted index that runs from 0 to half the number of slices with an easing function
            index = j
            if j > math.floor((curveItems - 1 - 1) / 2):
                index = curveItems - 1 - 1 - 1 - j

            index = QuadraticEaseOut(index, start=0, end=math.floor((curveItems - 2) / 2))

            # Get the radius that slowly increases and decreases again
            radiusFactor = remap(index, 0, math.floor((curveItems - 2) / 2), 0.5, 1.0)
            radius = radiusBase * radiusFactor

            # Let it go from a circle to the full shape and back
            offsetFactor = remap(index, 0, math.floor((curveItems - 2) / 2), 0, 1)

            # Slowly rotate each slice
            a = j / (curveItems-3) * math.pi

            # Create the inner curve
            pointsInside = createDoubleCurves(j, steps, loops, radius, thickness, millRadius, a, sliceDistance, radiusFactor, offsetFactor, slotDepth, paramMaterialThickness, -1)
            # Create the outer curve
            pointsOutside = createDoubleCurves(j, steps, loops, radius, thickness, millRadius, a, sliceDistance, radiusFactor, offsetFactor, slotDepth, paramMaterialThickness, 1)

            # Don't actually add the first and last curve
            if j > -1 and j < curveItems - 2:
                # Create a spline along those points
                spline = sketch.sketchCurves.sketchFittedSplines.add(pointsInside[0])
                spline.isClosed = True

                # Create a spline along those points
                spline = sketch.sketchCurves.sketchFittedSplines.add(pointsOutside[0])
                spline.isClosed = True

                # Create slot rectangle
                lines.addCenterPointRectangle(pointsInside[2][0], pointsInside[2][1])

                # Create circles for t-bones
                circles.addByCenterRadius(pointsInside[4][0], millRadius)
                circles.addByCenterRadius(pointsInside[4][1], millRadius)

                # Add the rectangles for the ribs
                ribPointsSlots.add(pointsInside[3][0])
                ribPointsSlots.add(pointsInside[3][1])

                # Add the circles for the t-bones for the ribs
                ribCirclePoints.add(pointsInside[5][0])
                ribCirclePoints.add(pointsInside[5][1])

            # Add the first item of each outer spline to a "rib"
            ribPointsInside.add(pointsInside[1])
            ribPointsOutside.add(pointsOutside[1])

        #######################################################
        # Extrude Section Between Curves
        #######################################################
        # https://forums.autodesk.com/t5/fusion-360-api-and-scripts/extruding-multiple-profiles-from-one-sketch/td-p/6026535

        # Create an object collection to use an input
        # To be able to extrude all profiles in one go
        profs = adsk.core.ObjectCollection.create()

        # Get the profiles
        # profs.add(sketch.profiles.item(8))
        for j in range(curveItems-2):
            profs.add(sketch.profiles.item(j*8))  # when using offsets - including t-bones
            # profs.add(sketch.profiles.item(j*4))  # when using offsets - no t-bones

        # Define a distance extent
        extrudeDistance = adsk.core.ValueInput.createByString("material_thickness")

        # Create the extrusion
        extrudeInput = extrudes.createInput(profs, adsk.fusion.FeatureOperations.NewBodyFeatureOperation)
        extrudeInput.setSymmetricExtent(extrudeDistance, True)
        extrudeSlices = extrudes.add(extrudeInput)

        # #######################################################
        # # Fillet the Slots
        # #######################################################
        # #Create a collection to hold the edges
        # edgeCollection = adsk.core.ObjectCollection.create()
        #
        # # body = extrudeResult.bodies.item(0)
        #
        # #Loop over all the bodies in the extrusion result
        # for body in extrudeSlices.bodies:
        #     edgeCollection.add(body.edges.item(7))
        #     edgeCollection.add(body.edges.item(9))
        #     #14 items per body
        #
        # # https://forums.autodesk.com/t5/fusion-360-api-and-scripts/extruding-profile-collection-in-single-operation/td-p/7905790
        # # https://help.autodesk.com/view/fusion360/ENU/?guid=GUID-7105bf44-52db-4067-8d7a-afd73c24e954
        # filletRadius = adsk.core.ValueInput.createByReal((.25))
        # filletInput = fillets.createInput()
        # filletInput.addConstantRadiusEdgeSet(edgeCollection, filletRadius, True)
        # filletInput.isG2 = False
        # filletInput.isRollingBallCorner = True
        # fillet = fillets.add(filletInput)

        #######################################################
        # Create Rib Spline
        #######################################################

        # Add the outside rib to the inner side, but in reverse order
        for j in reversed(range(curveItems)):
            ribPointsInside.add(ribPointsOutside.item(j))

        # Turn into a spline
        spline = sketchRib.sketchCurves.sketchFittedSplines.add(ribPointsInside)
        spline.isClosed = True

        for j in range(curveItems - 2):
            linesRib.addCenterPointRectangle(ribPointsSlots.item(j*2), ribPointsSlots.item(j*2 + 1))
            circlesRib.addByCenterRadius(ribCirclePoints.item(j*2), millRadius)
            circlesRib.addByCenterRadius(ribCirclePoints.item(j*2 + 1), millRadius)

        # Get the profile of the rib spline
        profs = adsk.core.ObjectCollection.create()
        # To test:
        # for j in range(0, 30, 1):
        #     profs.add(sketchRib.profiles.item(j))

        # Either the 0th profile or something late in the line
        # Total number = (curveItems-2) * 6 + 1? - for t-bones
        profs.add(sketchRib.profiles.item(6*(curveItems-2) - 0))

        # profs = sketchRib.profiles.item(2*(curveItems-2) - 1)  # number 103 for 54 slices, 105 profiles - no t-bones
        # profs = sketchRib.profiles.item(11)  #number 11 for 8 slices, 13 profiles max - no t-bones
        extrudeDistance = adsk.core.ValueInput.createByString("material_thickness")

        # Extrude the rib, symmetrically
        extrudeInput = extrudes.createInput(profs, adsk.fusion.FeatureOperations.NewBodyFeatureOperation)
        extrudeInput.setSymmetricExtent(extrudeDistance, True)
        extrudeRib = extrudes.add(extrudeInput)

        # #######################################################
        # # Fillet the Rib Slots
        # #######################################################
        #
        # # Create a collection to hold the edges
        # edgeCollection = adsk.core.ObjectCollection.create()
        #
        # # Add very specific edges
        # body = extrudeRib.bodies.item(0)
        # start = 2 + (curveItems-2)*4
        # for j in range(curveItems-2):
        #     edgeCollection.add(body.edges.item(start+1 + j*8))
        #     edgeCollection.add(body.edges.item(start+7 + j*8))
        # # edgeCollection.add(body.edges.item(start+1))
        #
        # filletRadius = adsk.core.ValueInput.createByReal((.125))
        # filletInput = fillets.createInput()
        # filletInput.addConstantRadiusEdgeSet(edgeCollection, filletRadius, True)
        # filletInput.isG2 = False
        # filletInput.isRollingBallCorner = True
        # fillet = fillets.add(filletInput)

    except:
        if ui:
            ui.messageBox('Failed:\n{}'.format(traceback.format_exc()))
