import os
import math
import ifcopenshell
import ifcopenshell.util.schema
import ifcopenshell.util.element


class Patcher:
    def __init__(self, src, file, logger, args=None):
        self.src = src
        self.file = file
        self.logger = logger
        self.args = args

    def patch(self):
        # ArchiCAD has the ability to store 2D data with objects like doors for
        # door swings. ArchiCAD's implementation is not 100% correct (using
        # footprint instead of annotation contexts), but otherwise not too
        # shabby.
        #
        # Revit, however, is incapable of understanding this 2D representation.
        # Revit users linking in IFCs produced by ArchiCAD may experience the
        # following symptoms:
        #
        # A. Invisible doors, and difficulty selecting doors
        # B. Invisible door swings, or only visible at particular view ranges
        # C. Weird arc shapes around doors
        # D. Extra lines around doors and walls
        # E. Cannot easily change visibility graphics of 2D vs 3D elements
        # F. Cannot view 2D data in a 3D view
        #
        # This is caused by the perfect storm of Revit IFC bugs, which we will
        # work through methodically.

        # Revit has the ability to switch between 3D representations and 2D
        # representations (e.g. in plan view). It does this by detecting IFC
        # representations that belong to either the Model Body representation
        # for 3D and the "Annotation" representation for 2D. Note: this is not a real
        # "Annotation" representation. It is an invalid representation that is
        # hardcoded in Revit. Nevertheless, if you follow their incorrect
        # context naming convention by renaming ArchiCAD's also incorrect
        # FootPrint context (but less incorrect) to Annotation, Revit will show
        # 2D door representations as desired and switch between 2D and 3D
        # intelligently.
        #
        # See bug https://github.com/Autodesk/revit-ifc/issues/187

        #for context in self.file.by_type("IfcGeometricRepresentationSubContext"):
        #    if context.ContextIdentifier == "FootPrint":
        #        context.ContextIdentifier = None
        #        context.ContextType = "Annotation"
        #        context.ParentContext = [
        #            c
        #            for c in self.file.by_type("IfcGeometricRepresentationContext", include_subtypes=False)
        #            if c.ContextType == "Plan"
        #        ][0]

        # However, we will not use this approach (hence it is commented out).
        # This is because Revit will generate a single object with both a
        # magically switching 2D and 3D representation - you cannot control both
        # representations independently. Furthermore, the 2D representation is
        # always generated at a Z value of 0. This means that the door swings
        # are always invisible on any view range apart from those typically near
        # an elevation of 0 which won't work for most projects. Because they are
        # merged into a single object, you cannot fix the 2D elevation without
        # breaking your 3D elevation. Even if we split it into 2D and 3D
        # objects, the 2D object can only be moved up in 3D or section, but
        # Revit makes the object invisible in 3D or section, so go fish.
        #
        # See symptom B.

        # Purge axis representations from walls. Revit will draw the axis (a
        # line through the wall) but doesn't give you control over its
        # visibilty, resulting in very ugly walls and lines typically cutting
        # across door openings.
        #
        # See bug https://github.com/Autodesk/revit-ifc/issues/360
        #
        # This fixes symptom D.

        for wall in self.file.by_type("IfcWall"):
            reps = list(wall.Representation.Representations)
            reps_excluding_axis = [r for r in reps if r.RepresentationIdentifier != "Axis"]
            wall.Representation.Representations = reps_excluding_axis

        # We're going to split the 3D door body and the 2D door swing into two
        # objects. Before we do that, we're going to cull the 2D representation
        # from door types. If we didn't, this scenario may occur:
        #
        # 1. 3D IfcDoors with IfcDoorType import as Revit doors
        # 2. 2D IfcBuildingElementProxy with no type import as Revit generic model
        # 3. The Revit user turns off the visibilty of the door category.
        # 4. The user is shocked to see that generic models are now invisible,
        #    even though it is clearly a separate category to doors.
        #
        # This should be a warning to all Revit users that unintuitively
        # visibility graphics actually do _not_ affect the categories (e.g.
        # Door, Window) for instances. Instead, they target categories for
        # representation maps types, even if an instance is untyped. Yikes!
        #
        # See bug: https://github.com/Autodesk/revit-ifc/issues/362
        #
        # This fixes symptom E.

        for door in self.file.by_type("IfcDoorType"):
            rep_maps = list(door.RepresentationMaps or [])
            door.RepresentationMaps = [
                r
                for r in rep_maps
                if r.MappedRepresentation
                if r.MappedRepresentation.RepresentationIdentifier != "FootPrint"
            ]

        # Let's now duplicate each door. The original door shall only retain its
        # non 2D representations. The copied door will be changed into a
        # "IfcDiscreteAccessory" so it can be filtered separately in Revit.
        # Then, the copied door will only retain 2D representations. However,
        # we're going to change the 2D representation context into a Model Body
        # 3D context. This tricks Revit into converting the 2D data into 3D
        # model line geometry (or something similar to that).
        #
        # See bug https://github.com/Autodesk/revit-ifc/issues/358
        #
        # This fixes symptom A, B, and F.

        for door in self.file.by_type("IfcDoor"):
            reps = list(door.Representation.Representations)
            reps_excluding_footprint = [r for r in reps if r.RepresentationIdentifier != "FootPrint"]
            footprint_reps = [r for r in reps if r.RepresentationIdentifier == "FootPrint"]
            if not footprint_reps:
                continue
            door_copy = ifcopenshell.util.element.copy(self.file, door)
            door_copy = ifcopenshell.util.schema.reassign_class(self.file, door_copy, "IfcDiscreteAccessory")
            door_copy.Representation = ifcopenshell.util.element.copy(self.file, door.Representation)
            door_copy.Representation.Representations = footprint_reps
            door.Representation.Representations = reps_excluding_footprint
            related_elements = list(door.ContainedInStructure[0].RelatedElements)
            related_elements.append(door_copy)
            door.ContainedInStructure[0].RelatedElements = related_elements

            body_context = [
                c for c in self.file.by_type("IfcGeometricRepresentationSubContext") if c.ContextIdentifier == "Body"
            ][0]
            for subelement in self.file.traverse(footprint_reps[0]):
                if not subelement.is_a("IfcShapeRepresentation"):
                    continue
                subelement.ContextOfItems = body_context
                subelement.RepresentationIdentifier = "Body"
                subelement.RepresentationType = "Curve3D"

        # Revit has bugs with handling arc index, which is relevant in IFC4. Our
        # solution is to facet the arcs and swap out the polycurves with new
        # faceted polycurves.
        #
        # See bug https://github.com/Autodesk/revit-ifc/issues/359
        #
        # This fixes symptom C.

        settings = ifcopenshell.geom.settings()
        settings.set(settings.INCLUDE_CURVES, True)
        unit_scale = ifcopenshell.util.unit.calculate_unit_scale(self.file)
        for curve in self.file.by_type("IfcIndexedPolyCurve"):
            if True in [s.is_a("IfcArcIndex") for s in curve.Segments]:
                shape = ifcopenshell.geom.create_shape(settings, curve)
                e = shape.edges
                v = shape.verts
                is_2d = curve.Points.is_a("IfcCartesianPointList2D")
                if is_2d:
                    vertices = [[v[i] / unit_scale, v[i + 1] / unit_scale] for i in range(0, len(v), 3)]
                else:
                    vertices = [
                        [v[i] / unit_scale, v[i + 1] / unit_scale, v[i + 2] / unit_scale] for i in range(0, len(v), 3)
                    ]
                edges = [[e[i] + 1, e[i + 1] + 1] for i in range(0, len(e), 2)]
                points = []
                curve.Points = f.create_entity(curve.Points.is_a(), vertices)
                curve.Segments = [f.createIfcLineIndex(e) for e in edges]
