# SPDX-License-Identifier: LGPL-2.1-or-later

# ***************************************************************************
# *                                                                         *
# *   Copyright (c) 2022 Yorik van Havre <yorik@uncreated.net>              *
# *                                                                         *
# *   This file is part of FreeCAD.                                         *
# *                                                                         *
# *   FreeCAD is free software: you can redistribute it and/or modify it    *
# *   under the terms of the GNU Lesser General Public License as           *
# *   published by the Free Software Foundation, either version 2.1 of the  *
# *   License, or (at your option) any later version.                       *
# *                                                                         *
# *   FreeCAD is distributed in the hope that it will be useful, but        *
# *   WITHOUT ANY WARRANTY; without even the implied warranty of            *
# *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU      *
# *   Lesser General Public License for more details.                       *
# *                                                                         *
# *   You should have received a copy of the GNU Lesser General Public      *
# *   License along with FreeCAD. If not, see                               *
# *   <https://www.gnu.org/licenses/>.                                      *
# *                                                                         *
# ***************************************************************************

"""This module contains IFC object definitions"""

import FreeCAD
import FreeCADGui

translate = FreeCAD.Qt.translate


# the property groups below should not be treated as psets
NON_PSETS = [
    "Base",
    "IFC",
    "",
    "Geometry",
    "Dimension",
    "Linear/radial dimension",
    "SectionPlane",
    "Axis",
    "PhysicalProperties",
    "BuildingPart",
    "IFC Attributes",
]


class ifc_object:
    """Base class for all IFC-based objects"""

    def __init__(self, otype=None):
        self.cached = True  # this marks that the object is freshly created and its shape should be taken from cache
        self.virgin_placement = True  # this allows one to set the initial placement without triggering any placement change
        if otype:
            self.Type = otype[0].upper() + otype[1:]  # capitalize to match Draft standard
        else:
            self.Type = "IfcObject"

    def onBeforeChange(self, obj, prop):
        if prop == "Schema":
            self.old_schema = obj.Schema
        elif prop == "Placement":
            self.old_placement = obj.Placement

    def onChanged(self, obj, prop):
        # link class property to its hidder IfcClass counterpart
        if prop == "IfcClass" and hasattr(obj, "Class") and obj.Class != obj.IfcClass:
            obj.Class = obj.IfcClass
            self.rebuild_classlist(obj, setprops=True)
        elif prop == "Class" and hasattr(obj, "IfcClass") and obj.Class != obj.IfcClass:
            obj.IfcClass = obj.Class
            self.rebuild_classlist(obj, setprops=True)
        elif prop == "Schema":
            self.edit_schema(obj, obj.Schema)
        elif prop == "Type":
            self.edit_type(obj)
            self.assign_classification(obj)
        elif prop == "Classification":
            self.edit_classification(obj)
        elif prop == "Group":
            self.edit_group(obj)
        elif hasattr(obj, prop) and obj.getGroupOfProperty(prop) == "IFC":
            if prop not in ["StepId"]:
                self.edit_attribute(obj, prop)
        elif prop == "Label":
            self.edit_attribute(obj, "Name", obj.Label)
        elif prop == "Text":
            self.edit_annotation(obj, "Text", "\n".join(obj.Text))
        elif prop in ["Start", "End"]:
            self.edit_annotation(obj, prop)
        elif prop in ["DisplayLength", "DisplayHeight", "Depth"]:
            self.edit_annotation(obj, prop)
        elif prop == "Placement":
            if getattr(self, "virgin_placement", False):
                self.virgin_placement = False
            elif obj.Placement != getattr(self, "old_placement", None):
                # print("placement changed for",obj.Label,"to",obj.Placement)
                self.edit_placement(obj)
        elif prop == "Modified":
            if obj.ViewObject:
                obj.ViewObject.signalChangeIcon()
        elif hasattr(obj, prop) and obj.getGroupOfProperty(prop) == "Geometry":
            self.edit_geometry(obj, prop)
        elif hasattr(obj, prop) and obj.getGroupOfProperty(prop) == "Quantities":
            self.edit_quantity(obj, prop)
        elif hasattr(obj, prop) and obj.getGroupOfProperty(prop) not in NON_PSETS:
            # Treat all property groups outside the default ones as Psets
            # print("DEBUG: editinog pset prop",prop)
            self.edit_pset(obj, prop)

    def onDocumentRestored(self, obj):
        self.rebuild_classlist(obj)
        if hasattr(obj, "IfcFilePath"):
            # once we have loaded the project, recalculate child coin nodes
            from PySide import QtCore  # lazy loading

            if obj.OutListRecursive:
                for child in obj.OutListRecursive:
                    if getattr(child, "ShapeMode", None) == "Coin":
                        child.Proxy.cached = True
                        child.touch()
            else:
                obj.Proxy.cached = True
                QtCore.QTimer.singleShot(100, obj.touch)
            QtCore.QTimer.singleShot(100, obj.Document.recompute)
            QtCore.QTimer.singleShot(100, self.fit_all)

    def assign_classification(self, obj):
        """
        Assigns Classification to an IFC object in a case where
        the object references a Type that has a Classification property,
        so we move copy the Type's property to our actual object.
        """

        if not getattr(obj, "Type", None):
            return

        type_obj = obj.Type
        if getattr(type_obj, "Classification", None):
            # Check if there is Classification already, since user can just change
            # the IFC type, but there could be one previously assigned which had
            # Classification
            if getattr(obj, "Classification", None) is None:
                obj.addProperty("App::PropertyString", "Classification", "IFC")
            obj.Classification = type_obj.Classification
            obj.recompute()
        elif getattr(obj, "Classification", None):
            # This means user has assigned type that has no classification, so clear
            # the one that they have currently selected
            obj.Classification = ""
            obj.recompute()

    def fit_all(self):
        """Fits the view"""

        if FreeCAD.GuiUp:
            FreeCADGui.SendMsgToActiveView("ViewFit")

    def rebuild_classlist(self, obj, setprops=False):
        """rebuilds the list of Class enum property according to current class"""

        from . import ifc_tools  # lazy import

        obj.Class = [obj.IfcClass]
        obj.Class = ifc_tools.get_ifc_classes(obj, obj.IfcClass)
        obj.Class = obj.IfcClass
        if setprops:
            ifc_tools.remove_unused_properties(obj)
            ifc_tools.add_properties(obj)

    def __getstate__(self):
        return getattr(self, "Type", None)

    def __setstate__(self, state):
        self.loads(state)

    def dumps(self):
        return getattr(self, "Type", None)

    def loads(self, state):
        if state:
            self.Type = state

    def execute(self, obj):
        from . import ifc_generator  # lazy import

        if obj.isDerivedFrom("Part::Feature"):
            cached = getattr(self, "cached", False)
            ifc_generator.generate_geometry(obj, cached=cached)
            self.cached = False
            self.rebuild_classlist(obj)

    def addObject(self, obj, child):
        if child not in obj.Group:
            g = obj.Group
            g.append(child)
            obj.Group = g

    def removeObject(self, obj, child):
        if child in obj.Group:
            g = obj.Group
            g.remove(child)
            obj.Group = g

    def edit_attribute(self, obj, attribute, value=None):
        """Edits an attribute of an underlying IFC object"""

        from . import ifc_tools  # lazy import

        if not value:
            value = obj.getPropertyByName(attribute)
        ifcfile = ifc_tools.get_ifcfile(obj)
        elt = ifc_tools.get_ifc_element(obj, ifcfile)
        if elt:
            result = ifc_tools.set_attribute(ifcfile, elt, attribute, value)
            if result:
                if hasattr(result, "id") and (result.id() != obj.StepId):
                    obj.StepId = result.id()

    def edit_annotation(self, obj, attribute, value=None):
        """Edits an attribute of an underlying IFC annotation"""

        from . import ifc_tools  # lazy import
        from . import ifc_export

        if not value:
            if hasattr(obj, attribute):
                value = obj.getPropertyByName(attribute)
        ifcfile = ifc_tools.get_ifcfile(obj)
        elt = ifc_tools.get_ifc_element(obj, ifcfile)
        if elt:
            if attribute == "Text":
                text = ifc_export.get_text(elt)
                if text:
                    ifc_tools.set_attribute(ifcfile, text, "Literal", value)
            elif attribute in ["Start", "End"]:
                dim = ifc_export.get_dimension(elt)
                if dim:
                    rep = dim[0]
                    for curve in rep.Items:
                        if not hasattr(curve, "Elements"):
                            # this is a TextLiteral for the dimension text - skip it
                            continue
                        for sub in curve.Elements:
                            if sub.is_a("IfcIndexedPolyCurve"):
                                points = sub.Points
                                value = list(points.CoordList)
                                is2d = "2D" in points.is_a()
                                if attribute == "Start":
                                    value[0] = ifc_export.get_scaled_point(obj.Start, ifcfile, is2d)
                                else:
                                    value[-1] = ifc_export.get_scaled_point(obj.End, ifcfile, is2d)
                                ifc_tools.set_attribute(ifcfile, points, "CoordList", value)
                            else:
                                print("DEBUG: unknown dimension curve type:", sub)
            elif attribute in ["DisplayLength", "DisplayHeight", "Depth"]:
                l = w = h = 1000.0
                if obj.ViewObject:
                    if obj.ViewObject.DisplayLength.Value:
                        l = ifc_export.get_scaled_value(obj.ViewObject.DisplayLength.Value, ifcfile)
                    if obj.ViewObject.DisplayHeight.Value:
                        w = ifc_export.get_scaled_value(obj.ViewObject.DisplayHeight.Value, ifcfile)
                if obj.Depth.Value:
                    h = ifc_export.get_scaled_value(obj.Depth.Value, ifcfile)
                if elt.Representation.Representations:
                    for rep in elt.Representation.Representations:
                        for item in rep.Items:
                            if item.is_a("IfcCsgSolid"):
                                if item.TreeRootExpression.is_a("IfcBlock"):
                                    block = item.TreeRootExpression
                                    loc = block.Position.Location
                                    ifc_tools.set_attribute(ifcfile, block, "XLength", l)
                                    ifc_tools.set_attribute(ifcfile, block, "YLength", w)
                                    ifc_tools.set_attribute(ifcfile, block, "ZLength", h)
                                    ifc_tools.set_attribute(
                                        ifcfile, loc, "Coordinates", (-l / 2, -h / 2, -h)
                                    )

    def edit_geometry(self, obj, prop):
        """Edits a geometry property of an object"""

        from . import ifc_geometry  # lazy loading
        from . import ifc_tools  # lazy import

        result = ifc_geometry.set_geom_property(obj, prop)
        if result:
            obj.touch()

    def edit_schema(self, obj, schema):
        """Changes the schema of an IFC document"""

        from . import ifc_tools  # lazy import

        ifcfile = ifc_tools.get_ifcfile(obj)
        if not ifcfile:
            return
        if not getattr(self, "old_schema", None):
            return
        if schema != ifcfile.wrapped_data.schema_name():
            # set obj.Proxy.silent = True to disable the schema change warning
            if obj.ViewObject and not getattr(self, "silent", False):
                if not obj.ViewObject.Proxy.schema_warning():
                    return
            ifcfile, migration_table = ifc_tools.migrate_schema(ifcfile, schema)
            self.ifcfile = ifcfile
            for old_id, new_id in migration_table.items():
                child = [o for o in obj.OutListRecursive if getattr(o, "StepId", None) == old_id]
                if len(child) == 1:
                    child[0].StepId = new_id

    def edit_placement(self, obj):
        """Syncs the internal IFC placement"""

        from . import ifc_tools  # lazy import

        ifc_tools.set_placement(obj)

    def edit_pset(self, obj, prop):
        """Edits a Pset value"""

        from . import ifc_psets  # lazy import

        ifc_psets.edit_pset(obj, prop)

    def edit_group(self, obj):
        """Edits the children list"""

        from . import ifc_tools  # lazy import
        from . import ifc_layers

        if obj.Class in [
            "IfcPresentationLayerAssignment",
            "IfcPresentationLayerWithStyle",
        ]:
            ifcfile = ifc_tools.get_ifcfile(obj)
            if not ifcfile:
                return
            newlist = []
            for child in obj.Group:
                if not getattr(child, "StepId", None) or ifc_tools.get_ifcfile(child) != ifcfile:
                    print(
                        "DEBUG: Not an IFC object. Removing",
                        child.Label,
                        "from layer",
                        obj.Label,
                    )
                else:
                    # print("DEBUG: adding", child.Label, "to layer", obj.Label)
                    newlist.append(child)
                    ifc_layers.add_to_layer(child, obj)
            if newlist != obj.Group:
                obj.Group = newlist

    def edit_type(self, obj):
        """Edits the type of this object"""

        from . import ifc_types  # lazy import

        ifc_types.edit_type(obj)

    def edit_quantity(self, obj, prop):
        """Edits the given quantity"""
        pass  # TODO implement

    def get_section_data(self, obj):
        """Returns two things: a list of objects and a cut plane"""

        from . import ifc_tools  # lazy import
        import Part

        if not obj.IfcClass == "IfcAnnotation":
            return [], None
        if obj.ObjectType != "DRAWING":
            return [], None
        objs = getattr(obj, "Objects", [])
        if not objs:
            # no object defined, we automatically use the project
            objs = []
            proj = ifc_tools.get_project(obj)
            if isinstance(proj, FreeCAD.DocumentObject):
                objs.append(proj)
            objs.extend(ifc_tools.get_freecad_children(proj))
        if objs:
            s = []
            for o in objs:
                # TODO print a better message
                if o.ShapeMode != "Shape":
                    s.append(o)
            if s:
                FreeCAD.Console.PrintLog("DEBUG: Generating shapes. This might take some time...\n")
                for o in s:
                    o.ShapeMode = "Shape"
                    o.recompute()
            l = 1
            h = 1
            if obj.ViewObject:
                if hasattr(obj.ViewObject, "DisplayLength"):
                    l = obj.ViewObject.DisplayLength.Value
                    h = obj.ViewObject.DisplayHeight.Value
            plane = Part.makePlane(l, h, FreeCAD.Vector(l / 2, -h / 2, 0), FreeCAD.Vector(0, 0, 1))
            plane.Placement = obj.Placement
            return objs, plane
        else:
            print("DEBUG: Section plane returned no objects")
            return [], None

    def edit_classification(self, obj):
        """Edits the classification of this object"""

        from . import ifc_classification  # lazy loading

        ifc_classification.edit_classification(obj)


class document_object:
    """Holder for the document's IFC objects"""

    def __init__(self):
        pass
