import json
import math
import os
import tempfile

import FreeCAD as App
import Part
import Sketcher  # noqa: F401 - registers Sketcher::SketchObject


FREECAD_COMMIT = "0108fd4b4850cc46e625b60e53cea7a7bbe69f8d"
TRANSLATION = App.Vector(1.25, -0.75, 2.0)


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


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 / math.pi),
    }


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


def finite_placement(value):
    return all(math.isfinite(number) for number in value["position"] + value["axis"] + [value["angleDegrees"]])


def add_shape(document, name, shape):
    obj = document.addObject("PartDesign::Feature", name)
    obj.Shape = shape
    return obj


def make_sources(document):
    vector = App.Vector
    sources = {
        "BoxA": add_shape(document, "BoxA", Part.makeBox(4, 5, 6, vector(0, 0, 0))),
        "BoxB": add_shape(document, "BoxB", Part.makeBox(3, 4, 7, vector(12, 3, 2))),
        "BoxC": add_shape(document, "BoxC", Part.makeBox(2, 6, 4, vector(-8, 4, 1))),
        "BoxD": add_shape(document, "BoxD", Part.makeBox(5, 3, 2, vector(3, -9, 5))),
        "PlaneXY": add_shape(document, "PlaneXY", Part.makePlane(20, 20, vector(-10, -10, 0), vector(0, 0, 1))),
        "PlaneYZ": add_shape(document, "PlaneYZ", Part.makePlane(20, 20, vector(0, -10, -10), vector(1, 0, 0))),
        "Circle": add_shape(document, "Circle", Part.Circle(vector(0, 0, 0), vector(0, 0, 1), 5).toShape()),
        "Ellipse": add_shape(document, "Ellipse", Part.Ellipse(vector(0, 0, 0), 6, 3).toShape()),
        "Hyperbola": add_shape(document, "Hyperbola", Part.Hyperbola(vector(0, 0, 0), 5, 3).toShape(-1.0, 1.0)),
        "Parabola": add_shape(document, "Parabola", Part.Parabola().toShape(-2.0, 2.0)),
        "Curve": add_shape(document, "Curve", Part.Arc(vector(-4, 0, 0), vector(0, 4, 2), vector(4, 0, 0)).toShape()),
        "V0": add_shape(document, "V0", Part.Vertex(vector(0, 0, 0))),
        "V1": add_shape(document, "V1", Part.Vertex(vector(5, 0, 0))),
        "V2": add_shape(document, "V2", Part.Vertex(vector(0, 4, 2))),
        "V3": add_shape(document, "V3", Part.Vertex(vector(1, 2, 6))),
        "LX": add_shape(document, "LX", Part.makeLine(vector(0, 0, 0), vector(6, 0, 0))),
        "LY": add_shape(document, "LY", Part.makeLine(vector(0, 0, 0), vector(0, 5, 0))),
        "LZ": add_shape(document, "LZ", Part.makeLine(vector(0, 0, 0), vector(0, 0, 4))),
        "LD": add_shape(document, "LD", Part.makeLine(vector(0, 0, 0), vector(3, 4, 5))),
        "LOff": add_shape(document, "LOff", Part.makeLine(vector(0, 4, 2), vector(3, 5, 6))),
    }
    document.recompute()
    return sources


def source_ref(sources, reference_type, index, combination):
    if reference_type == "Vertex":
        if combination == ["Face", "Vertex"] or combination == ["Vertex", "Face"]:
            name = "V1"
        elif any(value in ("Curve", "Circle", "Edge") for value in combination):
            name = "V1"
        elif combination in (["Line", "Vertex"], ["Vertex", "Line"]):
            name = "V2"
        else:
            name = ["V0", "V1", "V2", "V3"][index % 4]
        return sources[name], "Vertex1"
    if reference_type == "Line":
        if combination == ["Line", "Line"] and index == 1:
            return sources["LOff"], "Edge1"
        return sources[["LX", "LY", "LZ", "LD"][index % 4]], "Edge1"
    if reference_type == "Plane|Placement":
        return sources["PlaneXY" if index == 0 else "PlaneYZ"], ""
    if reference_type in ("Plane", "Face"):
        return sources["PlaneXY" if index == 0 else "PlaneYZ"], "Face1"
    if reference_type == "Edge":
        return sources["Circle"], "Edge1"
    if reference_type == "Curve":
        return sources["Curve"], "Edge1"
    if reference_type == "Circle":
        return sources["Circle"], "Edge1"
    if reference_type == "Conic":
        return sources["Ellipse"], "Edge1"
    if reference_type == "Ellipse":
        return sources["Ellipse"], "Edge1"
    if reference_type == "Hyperbola":
        return sources["Hyperbola"], "Edge1"
    if reference_type in ("Any", "Any|Placement"):
        return sources[["BoxA", "BoxB", "BoxC", "BoxD"][index % 4]], ""
    raise ValueError("Unsupported attachment reference category: " + reference_type)


def supports_for(sources, combination):
    return [source_ref(sources, reference_type, index, combination) for index, reference_type in enumerate(combination)]


def case_state(obj):
    suggestion = obj.Attacher.suggestModes()
    placement = placement_json(obj.Placement)
    return {
        "mapMode": str(obj.MapMode),
        "support": support_json(obj.AttachmentSupport),
        "placement": placement,
        "finitePlacement": finite_placement(placement),
        "state": [str(value) for value in obj.State],
        "status": str(obj.getStatusString()),
        "positionBySupport": bool(obj.positionBySupport()),
        "suggestedModes": list(suggestion["allApplicableModes"]),
        "suggestionMessage": suggestion["message"],
    }


document = App.newDocument("AttachmentCombinationOracle")
sources = make_sources(document)
engine_specs = {
    "plane": ("PartDesign::Plane", document.addObject("PartDesign::Body", "PlaneCases")),
    "line": ("PartDesign::Line", document.addObject("PartDesign::Body", "LineCases")),
    "point": ("PartDesign::Point", document.addObject("PartDesign::Body", "PointCases")),
    "sketch": ("Sketcher::SketchObject", document.addObject("PartDesign::Body", "SketchCases")),
}
seed_objects = {
    name: body.newObject(type_id, "Seed" + name.title())
    for name, (type_id, body) in engine_specs.items()
}
document.recompute()

cases = []
case_objects = {}
for engine_name, seed in seed_objects.items():
    type_id, body = engine_specs[engine_name]
    for mode in list(seed.Attacher.ImplementedModes):
        combinations = seed.Attacher.getModeInfo(mode)["ReferenceCombinations"]
        for combination_index, combination_value in enumerate(combinations):
            combination = [str(value) for value in combination_value]
            case_id = "%s-%s-%02d" % (engine_name, mode, combination_index)
            object_name = "Case%04d" % (len(cases) + 1)
            obj = body.newObject(type_id, object_name)
            obj.Label = case_id
            obj.AttachmentSupport = supports_for(sources, combination)
            obj.MapMode = mode
            cases.append({
                "id": case_id,
                "objectName": object_name,
                "engine": engine_name,
                "typeId": type_id,
                "mode": str(mode),
                "modeIndex": int(seed.Attacher.getModeInfo(mode)["ModeIndex"]),
                "combinationIndex": combination_index,
                "referenceCombination": combination,
            })
            case_objects[object_name] = obj

for seed in seed_objects.values():
    seed.Document.removeObject(seed.Name)
document.recompute()
initial = {name: case_state(obj) for name, obj in case_objects.items()}

for source in sources.values():
    source.Placement.Base = source.Placement.Base + TRANSLATION
document.recompute()
mutated = {name: case_state(obj) for name, obj in case_objects.items()}

with tempfile.TemporaryDirectory(prefix="freecad-attachment-combinations-") as temp_dir:
    path = os.path.join(temp_dir, "AttachmentCombinationOracle.FCStd")
    document.saveAs(path)
    App.closeDocument(document.Name)
    reopened = App.openDocument(path)
    reopened.recompute()
    roundtrip = {case["objectName"]: case_state(reopened.getObject(case["objectName"])) for case in cases}
    App.closeDocument(reopened.Name)

for case in cases:
    name = case["objectName"]
    case["initial"] = initial[name]
    case["mutated"] = mutated[name]
    case["roundtrip"] = roundtrip[name]

report = {
    "schemaVersion": 1,
    "baselineId": "freecad-1.1.1-attachment-combination-oracle",
    "freecadVersion": version_text(),
    "gitCommit": FREECAD_COMMIT,
    "status": "pass",
    "tolerance": 1e-7,
    "translation": [float(TRANSLATION.x), float(TRANSLATION.y), float(TRANSLATION.z)],
    "caseCount": len(cases),
    "engineCaseCounts": {
        engine: len([case for case in cases if case["engine"] == engine])
        for engine in engine_specs
    },
    "cases": cases,
}
print("FREECAD_ATTACHMENT_COMBINATION_RESULT=" + json.dumps(report, sort_keys=True, separators=(",", ":")))
