import json

import FreeCAD as App
import Part
import Sketcher

from freecad_mutation_evidence import capture_property_mutation


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


def shape_result(case_id, document, feature):
    document.recompute()
    shape = feature.Shape
    return {
        "id": case_id,
        "typeId": feature.TypeId,
        "passed": not shape.isNull() and shape.isValid() and len(shape.Solids) == 1,
        "status": feature.getStatusString(),
        "solids": len(shape.Solids) if not shape.isNull() else 0,
        "faces": len(shape.Faces) if not shape.isNull() else 0,
        "edges": len(shape.Edges) if not shape.isNull() else 0,
        "volume": float(shape.Volume) if not shape.isNull() else 0.0,
    }


def add_rectangle(sketch, x_min, y_min, x_max, y_max):
    points = [(x_min, y_min), (x_max, y_min), (x_max, y_max), (x_min, y_max)]
    for index, start in enumerate(points):
        end = points[(index + 1) % len(points)]
        sketch.addGeometry(Part.LineSegment(App.Vector(start[0], start[1], 0), App.Vector(end[0], end[1], 0)), False)


def feature_linear_pattern():
    document = App.newDocument("FeatureLinearPatternOracle")
    body = document.addObject("PartDesign::Body", "Body")
    base = body.newObject("PartDesign::AdditiveBox", "Base")
    base.Length = 20
    base.Width = 10
    base.Height = 2
    boss = body.newObject("PartDesign::AdditiveBox", "Boss")
    boss.Length = 2
    boss.Width = 2
    boss.Height = 2
    boss.Placement.Base = App.Vector(2, 4, 2)
    document.recompute()
    pattern = body.newObject("PartDesign::LinearPattern", "LinearPattern")
    pattern.TransformMode = "Features"
    pattern.Originals = [boss]
    pattern.Direction = (document.X_Axis, [""])
    pattern.Occurrences = 3
    pattern.Length = 12
    mutation = capture_property_mutation(document, pattern, "Occurrences", 4)
    result = shape_result("linear-feature-list", document, pattern)
    result["mutation"] = mutation
    result.update({
        "transformMode": pattern.TransformMode,
        "originals": [item.Name for item in pattern.Originals],
        "direction": pattern.Direction[0].Name,
        "occurrences": int(pattern.Occurrences),
        "length": float(pattern.Length),
    })
    App.closeDocument(document.Name)
    return result


def feature_mirrored():
    document = App.newDocument("FeatureMirroredOracle")
    body = document.addObject("PartDesign::Body", "Body")
    base = body.newObject("PartDesign::AdditiveBox", "Base")
    base.Length = 20
    base.Width = 10
    base.Height = 2
    boss = body.newObject("PartDesign::AdditiveBox", "Boss")
    boss.Length = 2
    boss.Width = 2
    boss.Height = 2
    boss.Placement.Base = App.Vector(0, 4, 2)
    document.recompute()
    mirrored = body.newObject("PartDesign::Mirrored", "Mirrored")
    mirrored.TransformMode = "Features"
    mirrored.Originals = [boss]
    mirrored.MirrorPlane = (document.YZ_Plane, [""])
    mutation = capture_property_mutation(document, mirrored, "Refine", False)
    result = shape_result("mirrored-feature-list", document, mirrored)
    result["mutation"] = mutation
    result.update({
        "transformMode": mirrored.TransformMode,
        "originals": [item.Name for item in mirrored.Originals],
        "plane": mirrored.MirrorPlane[0].Name,
    })
    App.closeDocument(document.Name)
    return result


def feature_polar_pattern():
    document = App.newDocument("FeaturePolarPatternOracle")
    body = document.addObject("PartDesign::Body", "Body")
    box = document.addObject("PartDesign::AdditiveBox", "Box")
    body.addObject(box)
    box.Length = 10
    box.Width = 10
    box.Height = 10
    document.recompute()
    pattern = document.addObject("PartDesign::PolarPattern", "PolarPattern")
    pattern.Originals = [box]
    pattern.Axis = (document.X_Axis, [""])
    pattern.Angle = 360
    pattern.Occurrences = 4
    pattern.Refine = True
    body.addObject(pattern)
    mutation = capture_property_mutation(document, pattern, "Occurrences", 3)
    result = shape_result("polar-feature-list", document, pattern)
    result["mutation"] = mutation
    result.update({
        "transformMode": pattern.TransformMode,
        "originals": [item.Name for item in pattern.Originals],
        "axis": pattern.Axis[0].Name,
        "angle": float(pattern.Angle),
        "occurrences": int(pattern.Occurrences),
    })
    App.closeDocument(document.Name)
    return result


def feature_multi_transform():
    document = App.newDocument("FeatureMultiTransformOracle")
    body = document.addObject("PartDesign::Body", "Body")
    sketch = document.addObject("Sketcher::SketchObject", "SketchPad")
    body.addObject(sketch)
    add_rectangle(sketch, 0, 0, 10, 10)
    document.recompute()
    pad = document.addObject("PartDesign::Pad", "Pad")
    body.addObject(pad)
    pad.Profile = sketch
    pad.Length = 10
    document.recompute()
    multi = document.addObject("PartDesign::MultiTransform", "MultiTransform")
    document.recompute()
    multi.Originals = [pad]
    multi.Shape = pad.Shape
    body.addObject(multi)
    document.recompute()
    mirrored = document.addObject("PartDesign::Mirrored", "Mirrored")
    mirrored.MirrorPlane = (sketch, ["H_Axis"])
    body.addObject(mirrored)
    linear = document.addObject("PartDesign::LinearPattern", "LinearPattern")
    linear.Direction = (sketch, ["H_Axis"])
    linear.Length = 20
    linear.Occurrences = 3
    body.addObject(linear)
    polar = document.addObject("PartDesign::PolarPattern", "PolarPattern")
    polar.Axis = (sketch, ["N_Axis"])
    polar.Angle = 360
    polar.Occurrences = 4
    body.addObject(polar)
    multi.Transformations = [mirrored, linear, polar]
    mutation = capture_property_mutation(document, multi, "Length", 15.0, target=linear, property_path="Transformations[1].Length")
    result = shape_result("multi-transform-feature-list", document, multi)
    result["mutation"] = mutation
    result.update({
        "transformMode": multi.TransformMode,
        "originals": [item.Name for item in multi.Originals],
        "transformations": [item.TypeId for item in multi.Transformations],
    })
    App.closeDocument(document.Name)
    return result


def transform_failure(type_id):
    suffix = type_id.split("::", 1)[1]
    document = App.newDocument("Feature{}FailureOracle".format(suffix))
    try:
        body = document.addObject("PartDesign::Body", "Body")
        base = body.newObject("PartDesign::AdditiveBox", "Base")
        base.Length = 20
        base.Width = 10
        base.Height = 2
        boss = body.newObject("PartDesign::AdditiveBox", "Boss")
        boss.Length = 2
        boss.Width = 2
        boss.Height = 2
        boss.Placement.Base = App.Vector(2, 4, 2)
        document.recompute()
        feature = document.addObject(type_id, suffix)
        feature.Originals = [boss]
        if type_id == "PartDesign::MultiTransform":
            feature.Shape = boss.Shape
        body.addObject(feature)
        if type_id == "PartDesign::LinearPattern":
            feature.Direction = (base, ["Edge999"])
        if type_id == "PartDesign::MultiTransform":
            child = document.addObject("PartDesign::LinearPattern", "InvalidLinearChild")
            child.Length = 12
            child.Occurrences = 3
            child.Direction = (base, ["Edge999"])
            body.addObject(child)
            feature.Transformations = [child]
        document.recompute()
        shape = feature.Shape
        shape_null = bool(shape.isNull())
        state = [str(value) for value in feature.State]
        status_string = str(feature.getStatusString())
        invalid = "Invalid" in state or status_string != "Valid"
        observed = "rejected" if invalid else "accepted-empty" if shape_null else "accepted"
        return {
            "id": "{}-missing-reference".format(suffix.lower()),
            "typeId": type_id,
            "inputClass": "valid-original-missing-transform-reference",
            "expected": "rejected",
            "observed": observed,
            "passed": observed == "rejected",
            "shapeNull": shape_null,
            "staleShapePreserved": bool(invalid and not shape_null),
            "state": state,
            "status": status_string,
            "originals": [item.Name for item in feature.Originals],
            "transformations": [item.TypeId for item in feature.Transformations] if type_id == "PartDesign::MultiTransform" else [],
        }
    except Exception as error:
        return {
            "id": "{}-missing-reference".format(suffix.lower()),
            "typeId": type_id,
            "inputClass": "valid-original-missing-transform-reference",
            "expected": "rejected",
            "observed": "rejected",
            "passed": True,
            "errorType": type(error).__name__,
            "error": str(error),
        }
    finally:
        App.closeDocument(document.Name)


def hole_document(name):
    document = App.newDocument(name)
    body = document.addObject("PartDesign::Body", "Body")
    box = body.newObject("PartDesign::AdditiveBox", "Box")
    box.Length = 20
    box.Width = 20
    box.Height = 10
    document.recompute()
    top_face = max(range(len(box.Shape.Faces)), key=lambda index: box.Shape.Faces[index].CenterOfMass.z)
    sketch = body.newObject("Sketcher::SketchObject", "HoleSketch")
    sketch.AttachmentSupport = (box, ["Face{}".format(top_face + 1)])
    sketch.MapMode = "FlatFace"
    sketch.addGeometry(Part.Circle(App.Vector(10, 10, 0), App.Vector(0, 0, 1), 3), False)
    hole = body.newObject("PartDesign::Hole", "Hole")
    hole.Profile = sketch
    hole.DepthType = "Dimension"
    hole.Depth = 8
    hole.DrillPoint = "Flat"
    return document, body, hole


def iso_hole(modeled):
    case_id = "iso-m6-modeled-thread" if modeled else "iso-m6-tap-drill"
    document, body, hole = hole_document("ModeledThreadOracle" if modeled else "TapDrillOracle")
    hole.ThreadType = "ISOMetricProfile"
    hole.ThreadSize = "M6x1.0"
    hole.Threaded = True
    hole.ModelThread = modeled
    hole.ThreadDirection = "Left" if modeled else "Right"
    hole.ThreadDepthType = "Dimension"
    hole.ThreadDepth = 6
    mutation = capture_property_mutation(document, hole, "Diameter", 6.5)
    result = shape_result(case_id, document, hole)
    result["mutation"] = mutation
    result.update({
        "threaded": bool(hole.Threaded),
        "modeled": bool(hole.ModelThread),
        "threadType": hole.ThreadType,
        "threadSize": hole.ThreadSize,
        "threadClass": hole.ThreadClass,
        "threadFit": hole.ThreadFit,
        "threadDirection": hole.ThreadDirection,
        "threadDepthType": hole.ThreadDepthType,
        "threadDepth": float(hole.ThreadDepth),
        "diameter": float(hole.Diameter),
        "threadDiameter": float(hole.ThreadDiameter),
        "threadPitch": float(hole.ThreadPitch) if "ThreadPitch" in hole.PropertiesList else 1.0,
        "properties": list(hole.PropertiesList),
    })
    App.closeDocument(document.Name)
    return result


success_cases = [feature_linear_pattern(), feature_mirrored(), feature_polar_pattern(), feature_multi_transform(), iso_hole(False), iso_hole(True)]
failure_cases = [transform_failure(type_id) for type_id in [
    "PartDesign::Mirrored",
    "PartDesign::MultiTransform",
    "PartDesign::LinearPattern",
    "PartDesign::PolarPattern",
]]
cases = success_cases + failure_cases
report = {
    "schemaVersion": 1,
    "baselineId": "freecad-1.1.1-partdesign-transform-oracle",
    "freecadVersion": version_text(),
    "gitCommit": "0108fd4b4850cc46e625b60e53cea7a7bbe69f8d",
    "status": "pass" if all(case["passed"] for case in cases) else "failed",
    "tolerance": 1e-7,
    "cases": cases,
    "summary": {
        "cases": len(cases),
        "passed": sum(1 for case in cases if case["passed"]),
        "successCases": len(success_cases),
        "successPassed": sum(1 for case in success_cases if case["passed"]),
        "failureCases": len(failure_cases),
        "failurePassed": sum(1 for case in failure_cases if case["passed"]),
        "rejected": sum(1 for case in failure_cases if case.get("observed") == "rejected"),
        "acceptedEmpty": sum(1 for case in failure_cases if case.get("observed") == "accepted-empty"),
    },
}
print("FREECAD_PARTDESIGN_TRANSFORM_RESULT=" + json.dumps(report, sort_keys=True, separators=(",", ":")))
