"""
given a stl of the users face, align the joint of the right eye in the scan to the molds eye. The

"""

from logging import Logger
import adsk.core, adsk.fusion, traceback, math, time
import glob
import os
import json
import base64
import zlib
import signal

debug = False

class UiLogger:
    def __init__(self, forceUpdate):
        app = adsk.core.Application.get()
        ui = app.userInterface
        palettes = ui.palettes
        self.textPalette = palettes.itemById("TextCommands")
        self.forceUpdate = forceUpdate
        self.textPalette.isVisible = True

    def print(self, text):
        self.textPalette.writeText(text)
        if (self.forceUpdate):
            adsk.doEvents()


app = adsk.core.Application.get()
if not app:
    app = adsk.core.Application.create()
ui = app.userInterface

logger = UiLogger(True)

FUSION_JOB_PATH = os.getenv('FUSION_JOB_PATH')
OUTPUT_NC_NAME = 'toolpath'


def addJointAtOrigin(component):
    sketches = component.sketches
    sketch = sketches.add(component.xZConstructionPlane)
    sketchPts = sketch.sketchPoints
    point = adsk.core.Point3D.create(0, 0, 0)
    sketchPt = sketchPts.add(point)
    jointGeometry = adsk.fusion.JointGeometry.createByPoint(sketchPt)
    # Create the JointOriginInput
    jointOrigins_ = component.jointOrigins
    jointOriginInput = jointOrigins_.createInput(jointGeometry)
    # Create the JointOrigin
    jointOrigin = jointOrigins_.add(jointOriginInput)
    return jointOrigin


def getDataFile(projectName, fileName, versionNumber):
    project = adsk.core.DataProject.cast(None)
    # find the project
    for p in app.data.dataProjects:
        if p.name == projectName:
            project = p
    root = adsk.core.DataFolder.cast(project.rootFolder)
    # find the file
    file = adsk.core.DataFile.cast(None)
    for f in root.dataFiles:
        if f.name == fileName:
            for v in f.versions:
                if v.versionNumber == versionNumber:
                    file = v

    return file


def MeshPlaneCutCommand(mesh: adsk.fusion.MeshBody,
        plane: adsk.fusion.ConstructionPlane):
    app: adsk.core.Application = adsk.core.Application.get()
    ui: adsk.core.UserInterface = app.userInterface
    sels: adsk.core.Selections = ui.activeSelections
    sels.clear()
    app.executeTextCommand(u'Commands.Start ParaMeshPlaneCutCommand')
    app.executeTextCommand(u'UI.EnableCommandInput infoBodyToModify')
    sels.add(mesh)
    app.executeTextCommand(u'UI.EnableCommandInput planeSelectionInfo')
    sels.add(plane)
    app.executeTextCommand(u'Commands.SetString infoCutType infoCutTypeTrim')
    app.executeTextCommand(
        u'Commands.SetString infoFillType infoFillTypeMinimal')
    app.executeTextCommand(u'NuCommands.CommitCmd')


def create_toolpath(scanpath, ipd):
    # Load the Cushion mold file
    logger.print(f"Load the Cushion mold file 'Cushion/CushionMoldV2'.")
    df = getDataFile('Cushion', 'CushionMoldV2', 93)
    cushionDoc = app.documents.open(df, True)
    renderWS = ui.workspaces.itemById("FusionSolidEnvironment")
    renderWS.activate()
    product = app.activeProduct
    design = adsk.fusion.Design.cast(product)
    root = design.rootComponent
    # Get the pupil joint
    logger.print(f"Find files joint origin.")
    eyeRelif = [i for i in root.allJointOrigins if i.name == 'pupil'][0]
    logger.print(f"Found the molds eye relif joint origin.")
    logger.print(f"Find component for mesh 'FaceMesh'.")
    occ = root.occurrences.itemByName("FaceMesh:1")

    # Load users STL.
    logger.print(f"Load users STL. " + scanpath)
    # design = adsk.fusion.Design.cast(app.activeProduct)
    body = occ.component.meshBodies.add(scanpath,
                                        adsk.fusion.MeshUnits.CentimeterMeshUnit)[0]

    logger.print(f"Setting IPD to {64}mm. As the preprocessor now handles this.")
    design.userParameters.itemByName('IPD').value = 64 / 10  # units are cm
    # create offset Construction Plane
    planeXY = root.xZConstructionPlane
    conPlanes = occ.component.constructionPlanes
    planeIpt = conPlanes.createInput()
    planeIpt.setByOffset(planeXY, adsk.core.ValueInput.createByReal(3.2))
    plane = conPlanes.add(planeIpt)

    # trim nose
    MeshPlaneCutCommand(body.createForAssemblyContext(occ),
                        plane.createForAssemblyContext(occ))

    # Add joint to mesh.
    logger.print(f"Add joint to mesh.")
    origin = addJointAtOrigin(occ.component)
    # Mate joint and joint origin.
    logger.print(f"Mate joint and joint origin.")
    jointInput = root.joints.createInput(origin.geometry, eyeRelif)
    jointInput.geometryOrOriginOne = origin
    jointInput.geometryOrOriginTwo = eyeRelif
    jointInput.setAsRigidJointMotion()
    joint = root.joints.add(jointInput)

    # switch to CAM view
    doc = app.activeDocument
    products = doc.products
    renderWS = ui.workspaces.itemById("CAMEnvironment")
    renderWS.activate()

    logger.print(f"Generate all the toolpaths.")
    cam = adsk.cam.CAM.cast(products.itemByProductType("CAMProductType"))

    #add face's mesh to setup
    setup = cam.setups.item(0)
    obj_coll = adsk.core.ObjectCollection.create()
    obj_coll.add(body.createForAssemblyContext(occ))
    setup.models = obj_coll

    # Helper: turn a body into a list of faces (API sample does this for groups)
    def _all_faces_of(brep_body: adsk.fusion.BRepBody):
        faces = []
        for f in brep_body.faces:
            faces.append(f)
        return faces

    # NOTE: Avoid/Machine selection expects *entities list* (faces/bodies/etc), not an ObjectCollection.
    #       For best results pass faces. (This mirrors Autodesk’s sample.) :contentReference[oaicite:2]{index=2}

    # 2) Update the *existing* keepout/avoid group geometry for two ops, then apply
    for op_index in (7, 8):  # "Contour2" and "Contour2(2)" in your setup
        op = setup.operations.item(op_index)

        # Get the parameter that holds Avoid/Machine groups
        surf_param = op.parameters.itemByName('checkSurfaceSelectionSets')
        if not surf_param:
            logger.print(
                f"[WARN] Operation {op.name}: no 'checkSurfaceSelectionSets' parameter found.")
            continue

        # This is a CadMachineAvoidGroupsParameterValue
        mag_param_val = surf_param.value

        # Pull the editable collection of groups
        groups = mag_param_val.getMachineAvoidGroups()

        # IMPORTANT: If you changed Setup.models above, sync to refresh defaults (Model/Fixture/etc)
        # before you modify anything, then *do not* call sync again until after apply. :contentReference[oaicite:3]{index=3}
        groups.sync()

        # Find the existing "Avoid" (keepout) group.
        # (You can also inspect default groups via groups.defaultGroup(...).)
        target_group = None
        for i in range(groups.count):
            g = groups.item(i)
            # g.machineMode is one of adsk.cam.MachiningMode.*
            if hasattr(g,
                       'machineMode') and g.machineMode == adsk.cam.MachiningMode.Avoid_MachiningMode:
                target_group = g
                break

        if not target_group:
            logger.print(
                f"[WARN] Operation {op.name}: no Avoid group to replace; skipping.")
            continue

        # Build the new geometry list. Prefer faces to match the API sample.
        # If you only have a MeshBody, you may need to pass the body itself (as a Base),
        # but BRepFaces are most reliable. (The sample uses faces.) :contentReference[oaicite:4]{index=4}
        new_entities = []

        # If your imported scan is a BRepBody, use faces:
        if isinstance(body, adsk.fusion.BRepBody):
            new_entities = _all_faces_of(body)
        else:
            # Fall back: try assigning the body (in assembly context) directly.
            # API accepts an array of Base; bodies are valid inputs for some modes. :contentReference[oaicite:5]{index=5}
            new_entities = [body.createForAssemblyContext(occ)]

        # Replace the existing group's geometry (do NOT delete the group)
        target_group.inputGeometry = new_entities

        # Push changes back into the operation
        mag_param_val.applyMachineAvoidGroups(
            groups)  # crucial commit step :contentReference[oaicite:6]{index=6}

        # Regenerate just this operation's toolpath
        cam.generateToolpath(
            op)  # or op.generateToolpath() depending on your preference :contentReference[oaicite:7]{index=7}

        logger.print(f"[OK] Replaced Avoid group geometry in op '{op.name}'.")

    # Exit before performing lengthy tool path gen
    if debug:
        return

    # Generate all the toolpaths
    future = cam.generateAllToolpaths(False)

    #  create and show the progress dialog while the toolpaths are being generated.
    # progress = ui.createProgressDialog()
    # progress.isCancelButtonShown = False
    # progress.show('Toolpath Generation Progress', 'Generating Toolpaths', 0, 10)
    # # Enter a loop to wait while the toolpaths are being generated and update
    # # the progress dialog.
    # while not future.isGenerationCompleted:
    #     # since toolpaths are calculated in parallel, loop the progress bar while the toolpaths
    #     # are being generated but none are yet complete.
    #     n = 0
    #     start = time.time()
    #     while future.numberOfCompleted == 0:
    #         if time.time() - start > .125:  # increment the progess value every .125 seconds.
    #             start = time.time()
    #             n += 1
    #             progress.progressValue = n
    #             adsk.doEvents()
    #         if n > 10:
    #             n = 0
    #     # The first toolpath has finished computing so now display better information in the progress dialog.
    #     # set the progress bar value to the number of completed toolpaths
    #     progress.progressValue = future.numberOfCompleted
    #     # set the progress bar max to the number of operations to be completed.
    #     progress.maximumValue = numOps
    #     # set the message for the progress dialog to track the progress value and the total number of operations to be completed.
    #     progress.message = 'Generating %v of %m' + ' Toolpaths'

    while not future.isGenerationCompleted:
        adsk.doEvents()

    n_operations = future.numberOfOperations
    n_tasks = future.numberOfTasks
    n_completed = future.numberOfCompleted
    n_completed_tasks = future.numberOfCompletedTasks

    if n_operations != n_completed:
        raise RuntimeError(f"Failed to generate all toolpaths, n_operations missmatch. {n_operations} != {n_completed}")

    if n_tasks != n_completed_tasks:
        raise RuntimeError(f"Failed to generate all toolpaths, n_tasks missmatch. {n_tasks} != {n_completed_tasks}")



    logger.print(f"Saving nc file to {os.path.join(FUSION_JOB_PATH, OUTPUT_NC_NAME)}")
    postConfig = cam.genericPostFolder + '/' + 'grbl.cps'
    units = adsk.cam.PostOutputUnitOptions.DocumentUnitsOutput
    postInput = adsk.cam.PostProcessInput.create(OUTPUT_NC_NAME, postConfig,
                                                 FUSION_JOB_PATH, units)
    postInput.isOpenInEditor = False
    # create the post properties
    postProperties = adsk.core.NamedValues.create()
    # create the disable sequence number property
    disableSequenceNumbers = adsk.core.ValueInput.createByBoolean(False)
    postProperties.add("showSequenceNumbers", disableSequenceNumbers)
    # add the post properties to the post process input
    postInput.postProperties = postProperties
    cam.postProcessAll(postInput)
    cushionDoc.close(False)


def run(context):
    try:
        logger.print('Starting')
        with open(os.path.join(FUSION_JOB_PATH, 'open_success.lock'), 'w') as f:
            f.write('open_success')
        with open(os.path.join(FUSION_JOB_PATH, 'manifest.json'), 'r') as f:
            manifest = json.load(f)
        mesh_data = None
        if len(manifest.keys()):
            ipd = int(manifest["ipd"])
            scanpath = FUSION_JOB_PATH+'/mesh.stl'
            if "meshData" in manifest:
                decoded = base64.b64decode(manifest["meshData"])
                mesh_data = zlib.decompress(decoded)
            elif "mesh" in manifest:
                mesh_data = base64.b64decode(manifest["mesh"])
            # else:
            #     #load from mesh file
            #     with open(PROCESS_DIR_PATH + 'mesh.stl', 'rb') as f:
            #         mesh_data = f.read()
            with open(scanpath, 'wb') as f:
                f.write(mesh_data)
            create_toolpath(scanpath, ipd)
    except:
        # if ui:
        with open(os.path.join(FUSION_JOB_PATH, 'error.txt'), 'w') as f:
            f.write("Fusion job Path:{}\n".format(FUSION_JOB_PATH))
            f.write('Failed:\n{}'.format(traceback.format_exc()))
        logger.print('Failed:\n{}'.format(traceback.format_exc()))
    finally:
        if debug:
            return
        pid = os.getpid()
        os.kill(pid, signal.SIGTERM)