import json
import os
import tempfile

import FreeCAD as App
import Part  # noqa: F401 - registers Part and PartDesign shapes


FREECAD_COMMIT = "0108fd4b4850cc46e625b60e53cea7a7bbe69f8d"


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


def document_open(name):
    return name in App.listDocuments()


def object_ref(obj):
    if obj is None:
        return None
    return {"document": obj.Document.Name, "object": obj.Name}


def xlink_ref(value):
    return object_ref(value)


def xlink_sub_ref(value):
    if not value or value[0] is None:
        return None
    obj, sub_elements = value
    subs = [sub_elements] if isinstance(sub_elements, str) else list(sub_elements)
    return {**object_ref(obj), "subElements": [str(value) for value in subs if str(value)]}


def xlink_list_ref(value):
    return [object_ref(obj) for obj in value if obj is not None]


def xlink_sub_list_ref(value):
    output = []
    for obj, sub_elements in value:
        if obj is None:
            continue
        subs = [sub_elements] if isinstance(sub_elements, str) else list(sub_elements)
        output.append({**object_ref(obj), "subElements": [str(value) for value in subs if str(value)]})
    return output


def property_status(holder, name):
    return [str(value) for value in holder.getPropertyStatus(name)]


def holder_state(holder):
    return {
        "xLink": xlink_ref(holder.XLink),
        "xLinkSub": xlink_sub_ref(holder.XLinkSub),
        "xLinkList": xlink_list_ref(holder.XLinkList),
        "xLinkSubList": xlink_sub_list_ref(holder.XLinkSubList),
        "propertyStatus": {
            name: property_status(holder, name)
            for name in ["XLink", "XLinkSub", "XLinkList", "XLinkSubList"]
        },
        "state": [str(value) for value in holder.State],
        "status": str(holder.getStatusString()),
    }


def binder_state(binder):
    support = []
    for obj, sub_elements in binder.Support:
        subs = [sub_elements] if isinstance(sub_elements, str) else list(sub_elements)
        support.append({**object_ref(obj), "subElements": [str(value) for value in subs if str(value)]})
    return {
        "support": support,
        "area": float(binder.Shape.Area),
        "shapeNull": bool(binder.Shape.isNull()),
        "state": [str(value) for value in binder.State],
        "status": str(binder.getStatusString()),
    }


def linked_dimensions(holder):
    return {
        "xLinkLength": float(holder.XLink.Length) if holder.XLink else None,
        "xLinkSubWidth": float(holder.XLinkSub[0].Width) if holder.XLinkSub and holder.XLinkSub[0] else None,
        "xLinkListHeights": [float(obj.Height) for obj in holder.XLinkList if obj is not None],
        "xLinkSubListAreas": [float(obj.getSubObject(sub).Area) for obj, subs in holder.XLinkSubList for sub in subs],
    }


with tempfile.TemporaryDirectory(prefix="freecad-xlink-relink-") as temp_dir:
    source_path = os.path.join(temp_dir, "XLinkSource.FCStd")
    consumer_path = os.path.join(temp_dir, "XLinkConsumer.FCStd")
    missing_path = os.path.join(temp_dir, "XLinkSource.missing.FCStd")

    source = App.newDocument("XLinkSource")
    source_body = source.addObject("PartDesign::Body", "SourceBody")
    source_box = source_body.newObject("PartDesign::AdditiveBox", "SourceBox")
    source_box.Length = 4
    source_box.Width = 5
    source_box.Height = 6
    second_body = source.addObject("PartDesign::Body", "SecondBody")
    second_box = second_body.newObject("PartDesign::AdditiveBox", "SecondBox")
    second_box.Length = 3
    second_box.Width = 7
    second_box.Height = 2
    source.recompute()
    source.saveAs(source_path)

    consumer = App.newDocument("XLinkConsumer")
    consumer.saveAs(consumer_path)
    holder = consumer.addObject("App::FeaturePython", "XLinkHolder")
    holder.addProperty("App::PropertyXLink", "XLink", "External")
    holder.addProperty("App::PropertyXLinkSub", "XLinkSub", "External")
    holder.addProperty("App::PropertyXLinkList", "XLinkList", "External")
    holder.addProperty("App::PropertyXLinkSubList", "XLinkSubList", "External")
    holder.XLink = source_box
    holder.XLinkSub = (source_box, ["Face1"])
    holder.XLinkList = [source_box, second_box]
    holder.XLinkSubList = [(source_box, ["Face1"]), (second_box, ["Face1", "Face2"])]
    binder_body = consumer.addObject("PartDesign::Body", "BinderBody")
    binder = binder_body.newObject("PartDesign::SubShapeBinder", "ExternalBinder")
    binder.Support = [(source_box, ("Face1",))]
    consumer.recompute()
    consumer.saveAs(consumer_path)
    source.save()
    consumer.save()
    initial = {
        "holder": holder_state(holder),
        "dimensions": linked_dimensions(holder),
        "binder": binder_state(binder),
        "sourceDocuments": sorted(document.Name for document in App.listDocuments().values()),
    }

    App.closeDocument(source.Name)
    consumer.recompute()
    source_closed = {
        "holder": holder_state(holder),
        "binder": binder_state(binder),
        "sourceOpen": document_open("XLinkSource"),
    }

    reopened_source = App.openDocument(source_path)
    reopened_source.recompute()
    consumer.recompute()
    relink_after_close = {
        "holder": holder_state(holder),
        "dimensions": linked_dimensions(holder),
        "binder": binder_state(binder),
        "sourceOpen": document_open("XLinkSource") and App.listDocuments()["XLinkSource"] is reopened_source,
    }

    App.closeDocument(consumer.Name)
    App.closeDocument(reopened_source.Name)
    os.rename(source_path, missing_path)
    missing_consumer = App.openDocument(consumer_path)
    missing_consumer.recompute()
    missing_holder = missing_consumer.getObject("XLinkHolder")
    missing_binder = missing_consumer.getObject("ExternalBinder")
    missing_source = {
        "holder": holder_state(missing_holder),
        "binder": binder_state(missing_binder),
        "sourceOpen": document_open("XLinkSource"),
    }

    os.rename(missing_path, source_path)
    restored_source = App.openDocument(source_path)
    restored_source.recompute()
    missing_consumer.recompute()
    restored_holder = missing_consumer.getObject("XLinkHolder")
    restored_binder = missing_consumer.getObject("ExternalBinder")
    relink_after_missing = {
        "holder": holder_state(restored_holder),
        "dimensions": linked_dimensions(restored_holder),
        "binder": binder_state(restored_binder),
        "sourceOpen": document_open("XLinkSource") and App.listDocuments()["XLinkSource"] is restored_source,
    }

    restored_box = restored_source.getObject("SourceBox")
    restored_second = restored_source.getObject("SecondBox")
    restored_box.Length = 8
    restored_box.Width = 9
    restored_second.Height = 4
    restored_source.recompute()
    missing_consumer.recompute()
    edited = {
        "holder": holder_state(restored_holder),
        "dimensions": linked_dimensions(restored_holder),
        "binder": binder_state(restored_binder),
        "sourceFaceArea": float(restored_box.Shape.Face1.Area),
    }

    restored_source.save()
    missing_consumer.save()
    App.closeDocument(missing_consumer.Name)
    App.closeDocument(restored_source.Name)
    final_source = App.openDocument(source_path)
    final_consumer = App.openDocument(consumer_path)
    final_source.recompute()
    final_consumer.recompute()
    final_holder = final_consumer.getObject("XLinkHolder")
    final_binder = final_consumer.getObject("ExternalBinder")
    final_roundtrip = {
        "holder": holder_state(final_holder),
        "dimensions": linked_dimensions(final_holder),
        "binder": binder_state(final_binder),
        "sourceFaceArea": float(final_source.getObject("SourceBox").Shape.Face1.Area),
    }
    App.closeDocument(final_consumer.Name)
    App.closeDocument(final_source.Name)

report = {
    "schemaVersion": 1,
    "baselineId": "freecad-1.1.1-xlink-relink-oracle",
    "freecadVersion": version_text(),
    "gitCommit": FREECAD_COMMIT,
    "status": "pass",
    "tolerance": 1e-7,
    "propertyTypes": ["App::PropertyXLink", "App::PropertyXLinkSub", "App::PropertyXLinkList", "App::PropertyXLinkSubList"],
    "initial": initial,
    "sourceClosed": source_closed,
    "relinkAfterClose": relink_after_close,
    "missingSource": missing_source,
    "relinkAfterMissing": relink_after_missing,
    "edited": edited,
    "finalRoundtrip": final_roundtrip,
}
print("FREECAD_XLINK_RELINK_RESULT=" + json.dumps(report, sort_keys=True, separators=(",", ":")))
