import json
import os
import tempfile

import FreeCAD as App


def version_text():
    return ".".join(str(value) for value in App.Version()[:3])


def names(objects):
    return [obj.Name for obj in objects]


def link_sub_values(value):
    output = []
    for obj, sub_elements in value:
        if isinstance(sub_elements, str):
            subs = [sub_elements] if sub_elements else []
        else:
            subs = [str(sub) for sub in sub_elements if str(sub)]
        output.append({"object": obj.Name, "subElements": subs})
    return output


def placement_json(placement):
    axis = placement.Rotation.Axis
    base = placement.Base
    return {
        "position": [float(base.x), float(base.y), float(base.z)],
        "axis": [float(axis.x), float(axis.y), float(axis.z)],
        "angleDegrees": float(placement.Rotation.Angle * 180.0 / 3.141592653589793),
    }


document = App.newDocument("PartDesignStructureOracle")
source_body = document.addObject("PartDesign::Body", "SourceBody")
source_box = source_body.newObject("PartDesign::AdditiveBox", "SourceBox")
source_box.Length = 10
source_box.Width = 8
source_box.Height = 6

datum_body = document.addObject("PartDesign::Body", "DatumBody")
datum_plane = datum_body.newObject("PartDesign::Plane", "DatumPlane")
datum_plane.AttachmentSupport = [(document.XY_Plane, "")]
datum_plane.MapMode = "FlatFace"
datum_plane.AttachmentOffset = App.Placement(App.Vector(0, 0, 5), App.Rotation(App.Vector(0, 0, 1), 30))
datum_line = datum_body.newObject("PartDesign::Line", "DatumLine")
datum_line.AttachmentSupport = [(document.XY_Plane, "")]
datum_line.MapMode = "ObjectX"
datum_point = datum_body.newObject("PartDesign::Point", "DatumPoint")
datum_point.AttachmentSupport = [(document.XY_Plane, "")]
datum_point.MapMode = "ObjectOrigin"
datum_point.AttachmentOffset = App.Placement(App.Vector(1, 2, 3), App.Rotation())

binder_body = document.addObject("PartDesign::Body", "BinderBody")
shape_binder = binder_body.newObject("PartDesign::ShapeBinder", "ShapeBinder")
shape_binder.Support = [(source_box, "")]
shape_binder.TraceSupport = True

sub_binder_body = document.addObject("PartDesign::Body", "SubBinderBody")
sub_binder = sub_binder_body.newObject("PartDesign::SubShapeBinder", "SubShapeBinder")
sub_binder.Support = [(source_box, ("Edge1", "Edge2", "Edge3", "Edge4"))]

document.recompute()
initial = {
    "sourceVolume": float(source_box.Shape.Volume),
    "shapeBinderVolume": float(shape_binder.Shape.Volume),
    "subShapeBinderLength": float(sub_binder.Shape.Length),
    "bodyTips": {body.Name: body.Tip.Name if body.Tip else None for body in [source_body, datum_body, binder_body, sub_binder_body]},
    "bodyGroups": {body.Name: names(body.Group) for body in [source_body, datum_body, binder_body, sub_binder_body]},
    "datumPlane": {"mapMode": datum_plane.MapMode, "support": link_sub_values(datum_plane.AttachmentSupport), "attachmentOffset": placement_json(datum_plane.AttachmentOffset), "placement": placement_json(datum_plane.Placement), "state": [str(value) for value in datum_plane.State]},
    "datumLine": {"mapMode": datum_line.MapMode, "support": link_sub_values(datum_line.AttachmentSupport), "state": [str(value) for value in datum_line.State]},
    "datumPoint": {"mapMode": datum_point.MapMode, "support": link_sub_values(datum_point.AttachmentSupport), "attachmentOffset": placement_json(datum_point.AttachmentOffset), "placement": placement_json(datum_point.Placement), "state": [str(value) for value in datum_point.State]},
    "shapeBinder": {"support": link_sub_values(shape_binder.Support), "traceSupport": bool(shape_binder.TraceSupport), "state": [str(value) for value in shape_binder.State]},
    "subShapeBinder": {"support": link_sub_values(sub_binder.Support), "state": [str(value) for value in sub_binder.State]},
}

source_box.Length = 14
datum_plane.AttachmentOffset = App.Placement(App.Vector(0, 0, 7), App.Rotation(App.Vector(0, 0, 1), 45))
document.recompute()
edited = {
    "sourceVolume": float(source_box.Shape.Volume),
    "shapeBinderVolume": float(shape_binder.Shape.Volume),
    "subShapeBinderLength": float(sub_binder.Shape.Length),
    "datumPlaneAttachmentOffset": placement_json(datum_plane.AttachmentOffset),
    "datumPlanePlacement": placement_json(datum_plane.Placement),
    "bodyTips": {body.Name: body.Tip.Name if body.Tip else None for body in [source_body, datum_body, binder_body, sub_binder_body]},
}

invalid_plane = datum_body.newObject("PartDesign::Plane", "InvalidDatumPlane")
invalid_plane.AttachmentSupport = [(source_box, "Face999")]
invalid_plane.MapMode = "FlatFace"
document.recompute()
invalid_support = {
    "typeId": invalid_plane.TypeId,
    "support": link_sub_values(invalid_plane.AttachmentSupport),
    "state": [str(value) for value in invalid_plane.State],
    "status": str(invalid_plane.getStatusString()),
}

with tempfile.TemporaryDirectory(prefix="freecad-partdesign-structure-") as temp_dir:
    path = os.path.join(temp_dir, "PartDesignStructureOracle.FCStd")
    document.recompute()
    document.saveAs(path)
    App.closeDocument(document.Name)
    reopened_document = App.openDocument(path)
    reopened_document.recompute()
    reopened_source = reopened_document.getObject("SourceBox")
    reopened_plane = reopened_document.getObject("DatumPlane")
    reopened_shape_binder = reopened_document.getObject("ShapeBinder")
    reopened_sub_binder = reopened_document.getObject("SubShapeBinder")
    reopened_invalid = reopened_document.getObject("InvalidDatumPlane")
    reopened_bodies = [reopened_document.getObject(name) for name in ["SourceBody", "DatumBody", "BinderBody", "SubBinderBody"]]
    roundtrip = {
        "sourceVolume": float(reopened_source.Shape.Volume),
        "shapeBinderVolume": float(reopened_shape_binder.Shape.Volume),
        "subShapeBinderLength": float(reopened_sub_binder.Shape.Length),
        "shapeBinderSupport": link_sub_values(reopened_shape_binder.Support),
        "subShapeBinderSupport": link_sub_values(reopened_sub_binder.Support),
        "datumPlaneSupport": link_sub_values(reopened_plane.AttachmentSupport),
        "datumPlaneMapMode": reopened_plane.MapMode,
        "datumPlaneAttachmentOffset": placement_json(reopened_plane.AttachmentOffset),
        "bodyTips": {body.Name: body.Tip.Name if body.Tip else None for body in reopened_bodies},
        "bodyGroups": {body.Name: names(body.Group) for body in reopened_bodies},
        "invalidDatumState": [str(value) for value in reopened_invalid.State],
        "invalidDatumStatus": str(reopened_invalid.getStatusString()),
    }
    App.closeDocument(reopened_document.Name)

with tempfile.TemporaryDirectory(prefix="freecad-partdesign-cross-document-") as temp_dir:
    source_path = os.path.join(temp_dir, "PartDesignExternalSource.FCStd")
    consumer_path = os.path.join(temp_dir, "PartDesignExternalConsumer.FCStd")
    external_source = App.newDocument("PartDesignExternalSource")
    external_source_body = external_source.addObject("PartDesign::Body", "ExternalSourceBody")
    external_box = external_source_body.newObject("PartDesign::AdditiveBox", "ExternalBox")
    external_box.Length = 4
    external_box.Width = 5
    external_box.Height = 6
    external_source.recompute()
    external_source.saveAs(source_path)
    external_consumer = App.newDocument("PartDesignExternalConsumer")
    external_consumer.saveAs(consumer_path)
    external_shape_body = external_consumer.addObject("PartDesign::Body", "ExternalShapeBody")
    external_shape_binder = external_shape_body.newObject("PartDesign::ShapeBinder", "ExternalShapeBinder")
    try:
        external_shape_binder.Support = [(external_box, "")]
        shape_binder_external_link = {"accepted": True, "errorType": None, "error": None}
    except Exception as error:
        shape_binder_external_link = {"accepted": False, "errorType": type(error).__name__, "error": str(error)}
    external_sub_body = external_consumer.addObject("PartDesign::Body", "ExternalSubBody")
    external_sub_binder = external_sub_body.newObject("PartDesign::SubShapeBinder", "ExternalSubShapeBinder")
    external_sub_binder.Support = [(external_box, ("Face1",))]
    external_consumer.recompute()
    cross_document_initial = {
        "sourceVolume": float(external_box.Shape.Volume),
        "shapeBinderExternalLink": shape_binder_external_link,
        "shapeBinderSupportType": external_shape_binder.getTypeIdOfProperty("Support"),
        "subShapeBinderSupportType": external_sub_binder.getTypeIdOfProperty("Support"),
        "shapeBinderShapeNull": bool(external_shape_binder.Shape.isNull()),
        "subShapeBinderArea": float(external_sub_binder.Shape.Area),
        "sourceFaceArea": float(external_box.Shape.Face1.Area),
        "shapeBinderSupport": link_sub_values(external_shape_binder.Support),
        "subShapeBinderSupport": link_sub_values(external_sub_binder.Support),
        "subShapeBinderSourceDocument": external_sub_binder.Support[0][0].Document.Name,
        "shapeBodyTip": external_shape_body.Tip.Name if external_shape_body.Tip else None,
        "subBodyTip": external_sub_body.Tip.Name if external_sub_body.Tip else None,
    }
    external_source.save()
    external_consumer.save()
    App.closeDocument(external_consumer.Name)
    App.closeDocument(external_source.Name)
    reopened_source = App.openDocument(source_path)
    reopened_consumer = App.openDocument(consumer_path)
    reopened_source.recompute()
    reopened_consumer.recompute()
    reopened_box = reopened_source.getObject("ExternalBox")
    reopened_shape_body = reopened_consumer.getObject("ExternalShapeBody")
    reopened_sub_body = reopened_consumer.getObject("ExternalSubBody")
    reopened_shape_binder = reopened_consumer.getObject("ExternalShapeBinder")
    reopened_sub_binder = reopened_consumer.getObject("ExternalSubShapeBinder")
    cross_document_roundtrip = {
        "sourceVolume": float(reopened_box.Shape.Volume),
        "shapeBinderShapeNull": bool(reopened_shape_binder.Shape.isNull()),
        "subShapeBinderArea": float(reopened_sub_binder.Shape.Area),
        "sourceFaceArea": float(reopened_box.Shape.Face1.Area),
        "shapeBinderSupport": link_sub_values(reopened_shape_binder.Support),
        "subShapeBinderSupport": link_sub_values(reopened_sub_binder.Support),
        "subShapeBinderSourceDocument": reopened_sub_binder.Support[0][0].Document.Name,
        "shapeBodyTip": reopened_shape_body.Tip.Name if reopened_shape_body.Tip else None,
        "subBodyTip": reopened_sub_body.Tip.Name if reopened_sub_body.Tip else None,
    }
    reopened_box.Width = 8
    reopened_source.recompute()
    reopened_consumer.recompute()
    cross_document_edited = {
        "sourceVolume": float(reopened_box.Shape.Volume),
        "shapeBinderShapeNull": bool(reopened_shape_binder.Shape.isNull()),
        "subShapeBinderArea": float(reopened_sub_binder.Shape.Area),
        "sourceFaceArea": float(reopened_box.Shape.Face1.Area),
        "shapeBinderState": [str(value) for value in reopened_shape_binder.State],
        "subShapeBinderState": [str(value) for value in reopened_sub_binder.State],
    }
    reopened_source.removeObject("ExternalBox")
    reopened_source.recompute()
    reopened_consumer.recompute()
    cross_document_deleted = {
        "shapeBinderSupport": link_sub_values(reopened_shape_binder.Support),
        "subShapeBinderSupport": link_sub_values(reopened_sub_binder.Support),
        "shapeBinderShapeNull": bool(reopened_shape_binder.Shape.isNull()),
        "subShapeBinderShapeNull": bool(reopened_sub_binder.Shape.isNull()),
        "subShapeBinderCachedArea": float(reopened_sub_binder.Shape.Area),
        "shapeBinderState": [str(value) for value in reopened_shape_binder.State],
        "subShapeBinderState": [str(value) for value in reopened_sub_binder.State],
        "shapeBinderStatus": str(reopened_shape_binder.getStatusString()),
        "subShapeBinderStatus": str(reopened_sub_binder.getStatusString()),
    }
    App.closeDocument(reopened_consumer.Name)
    App.closeDocument(reopened_source.Name)

report = {
    "schemaVersion": 1,
    "baselineId": "freecad-1.1.1-partdesign-structure-oracle",
    "freecadVersion": version_text(),
    "gitCommit": "0108fd4b4850cc46e625b60e53cea7a7bbe69f8d",
    "status": "pass",
    "tolerance": 1e-7,
    "initial": initial,
    "edited": edited,
    "invalidSupport": invalid_support,
    "roundtrip": roundtrip,
    "crossDocument": {
        "initial": cross_document_initial,
        "roundtrip": cross_document_roundtrip,
        "edited": cross_document_edited,
        "deleted": cross_document_deleted,
    },
}
print("FREECAD_PARTDESIGN_STRUCTURE_RESULT=" + json.dumps(report, sort_keys=True, separators=(",", ":")))
