import hashlib


def _json_value(value):
    if value is None or isinstance(value, (bool, int, float, str)):
        return value
    if hasattr(value, "Value") and hasattr(value, "Unit"):
        return {"value": float(value.Value), "unit": str(value.Unit)}
    if hasattr(value, "x") and hasattr(value, "y") and hasattr(value, "z"):
        return [float(value.x), float(value.y), float(value.z)]
    if hasattr(value, "Name") and hasattr(value, "TypeId"):
        return {"name": str(value.Name), "typeId": str(value.TypeId)}
    if isinstance(value, (list, tuple)):
        return [_json_value(item) for item in value]
    return str(value)


def _shape_snapshot(feature):
    shape = feature.Shape
    if shape.isNull():
        raise RuntimeError("{} produced a null Shape".format(feature.Name))
    if not shape.isValid():
        raise RuntimeError("{} produced an invalid Shape".format(feature.Name))
    brep = shape.exportBrepToString().encode("utf-8")
    bounds = shape.BoundBox
    return {
        "brepSha256": hashlib.sha256(brep).hexdigest(),
        "shapeType": str(shape.ShapeType),
        "valid": True,
        "solids": len(shape.Solids),
        "faces": len(shape.Faces),
        "edges": len(shape.Edges),
        "vertices": len(shape.Vertexes),
        "volume": float(shape.Volume),
        "area": float(shape.Area),
        "bounds": {
            "min": [float(bounds.XMin), float(bounds.YMin), float(bounds.ZMin)],
            "max": [float(bounds.XMax), float(bounds.YMax), float(bounds.ZMax)],
        },
    }


def capture_property_mutation(document, feature, property_name, edited_value, target=None, property_path=None):
    target = target or feature
    original_value = getattr(target, property_name)
    before_parameter = _json_value(original_value)

    document.recompute()
    before_shape_object = feature.Shape.copy()
    before_shape = _shape_snapshot(feature)
    try:
        setattr(target, property_name, edited_value)
        document.recompute()
        edited_parameter = _json_value(getattr(target, property_name))
        if before_parameter == edited_parameter:
            raise RuntimeError("{} mutation did not change {}".format(feature.TypeId, property_path or property_name))
        edited_shape = _shape_snapshot(feature)
    finally:
        setattr(target, property_name, original_value)
        document.recompute()
    restored_shape_object = feature.Shape
    restored_shape = _shape_snapshot(feature)
    restored_parameter = _json_value(getattr(target, property_name))

    if restored_parameter != before_parameter:
        raise RuntimeError("{} did not restore {}".format(feature.TypeId, property_path or property_name))
    restored_equivalent = bool(restored_shape_object.isEqual(before_shape_object))
    geometry_equivalent = (restored_shape["shapeType"], restored_shape["solids"], restored_shape["faces"], restored_shape["edges"], restored_shape["vertices"], round(restored_shape["volume"], 7), round(restored_shape["area"], 7)) == (before_shape["shapeType"], before_shape["solids"], before_shape["faces"], before_shape["edges"], before_shape["vertices"], round(before_shape["volume"], 7), round(before_shape["area"], 7))
    if not restored_equivalent and not geometry_equivalent:
        raise RuntimeError("{} did not restore the original geometry".format(feature.TypeId))

    return {
        "schemaVersion": 1,
        "familyId": str(feature.TypeId),
        "featureName": str(feature.Name),
        "propertyPath": property_path or property_name,
        "status": "pass",
        "before": {"parameterSnapshot": before_parameter, "shape": before_shape},
        "edited": {"parameterSnapshot": edited_parameter, "shape": edited_shape},
        "restored": {"parameterSnapshot": restored_parameter, "shape": restored_shape},
        "parameterChanged": True,
        "shapeChanged": edited_shape["brepSha256"] != before_shape["brepSha256"],
        "restoredExactly": restored_equivalent or geometry_equivalent,
        "brepStable": restored_shape["brepSha256"] == before_shape["brepSha256"],
        "restoredGeometrically": True,
    }
