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

# ***************************************************************************
# *                                                                         *
# *   Copyright (c) 2018 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/>.                                      *
# *                                                                         *
# ***************************************************************************

__title__ = "FreeCAD Arch BuildingPart"
__author__ = "Yorik van Havre"
__url__ = "https://www.freecad.org"

## @package ArchBuildingPart
#  \ingroup ARCH
#  \brief The BuildingPart object and tools
#
#  This module provides tools to build BuildingPart objects.
#  BuildingParts are used to group different Arch objects

import os
import tempfile

import FreeCAD
import Arch
import ArchCommands
import ArchIFC
import Draft
import DraftVecUtils

from draftutils import params
from draftutils import utils

if FreeCAD.GuiUp:
    from PySide.QtCore import QT_TRANSLATE_NOOP
    import FreeCADGui
    from draftutils.translate import translate
    import draftutils.units as units
else:
    # \cond
    def translate(ctxt, txt):
        return txt

    def QT_TRANSLATE_NOOP(ctxt, txt):
        return txt

    # \endcond
unicode = str


# fmt: off
BuildingTypes = ['Undefined',
'Agricultural - Barn',
'Agricultural - Chicken coop or chickenhouse',
'Agricultural - Cow-shed',
'Agricultural - Farmhouse',
'Agricultural - Granary',
'Agricultural - Greenhouse',
'Agricultural - Hayloft',
'Agricultural - Pigpen or sty',
'Agricultural - Root cellar',
'Agricultural - Shed',
'Agricultural - Silo',
'Agricultural - Stable',
'Agricultural - Storm cellar',
'Agricultural - Well house',
'Agricultural - Underground pit',

'Commercial - Automobile repair shop',
'Commercial - Bank',
'Commercial - Car wash',
'Commercial - Convention center',
'Commercial - Forum',
'Commercial - Gas station',
'Commercial - Hotel',
'Commercial - Market',
'Commercial - Market house',
'Commercial - Skyscraper',
'Commercial - Shop',
'Commercial - Shopping mall',
'Commercial - Supermarket',
'Commercial - Warehouse',
'Commercial - Restaurant',

'Residential - Apartment block',
'Residential - Asylum',
'Residential - Condominium',
'Residential - Dormitory',
'Residential - Duplex',
'Residential - House',
'Residential - Nursing home',
'Residential - Townhouse',
'Residential - Villa',
'Residential - Bungalow',

'Educational - Archive',
'Educational - College classroom building',
'Educational - College gymnasium',
'Educational - College students union',
'Educational - School',
'Educational - Library',
'Educational - Museum',
'Educational - Art gallery',
'Educational - Theater',
'Educational - Amphitheater',
'Educational - Concert hall',
'Educational - Cinema',
'Educational - Opera house',
'Educational - Boarding school',

'Government - Capitol',
'Government - City hall',
'Government - Consulate',
'Government - Courthouse',
'Government - Embassy',
'Government - Fire station',
'Government - Meeting house',
'Government - Moot hall',
'Government - Palace',
'Government - Parliament',
'Government - Police station',
'Government - Post office',
'Government - Prison',

'Industrial - Brewery',
'Industrial - Factory',
'Industrial - Foundry',
'Industrial - Power plant',
'Industrial - Mill',

'Military - Arsenal',
'Military -Barracks',

'Parking - Boathouse',
'Parking - Garage',
'Parking - Hangar',

'Storage - Silo',
'Storage - Hangar',

'Religious - Church',
'Religious - Basilica',
'Religious - Cathedral',
'Religious - Chapel',
'Religious - Oratory',
'Religious - Martyrium',
'Religious - Mosque',
'Religious - Mihrab',
'Religious - Surau',
'Religious - Imambargah',
'Religious - Monastery',
'Religious - Mithraeum',
'Religious - Fire temple',
'Religious - Shrine',
'Religious - Synagogue',
'Religious - Temple',
'Religious - Pagoda',
'Religious - Gurdwara',
'Religious - Hindu temple',

'Transport - Airport terminal',
'Transport - Bus station',
'Transport - Metro station',
'Transport - Taxi station',
'Transport - Railway station',
'Transport - Signal box',
'Transport - Lighthouse',

'Infrastructure - Data centre',

'Power station - Fossil-fuel power station',
'Power station - Nuclear power plant',
'Power station - Geothermal power',
'Power station - Biomass-fuelled power plant',
'Power station - Waste heat power plant',
'Power station - Renewable energy power station',
'Power station - Atomic energy plant',

'Other - Apartment',
'Other - Clinic',
'Other - Community hall',
'Other - Eatery',
'Other - Folly',
'Other - Food court',
'Other - Hospice',
'Other - Hospital',
'Other - Hut',
'Other - Bathhouse',
'Other - Workshop',
'Other - World trade centre'
]
# fmt: on


class BuildingPart(ArchIFC.IfcProduct):
    "The BuildingPart object"

    def __init__(self, obj):

        obj.Proxy = self
        self.Type = "BuildingPart"
        obj.addExtension("App::GroupExtensionPython")
        # obj.addExtension('App::OriginGroupExtensionPython')
        self.setProperties(obj)

    def setProperties(self, obj):
        ArchIFC.IfcProduct.setProperties(self, obj)

        pl = obj.PropertiesList
        if not "Height" in pl:
            obj.addProperty(
                "App::PropertyLength",
                "Height",
                "BuildingPart",
                QT_TRANSLATE_NOOP("App::Property", "The height of this object"),
                locked=True,
            )
        if not "HeightPropagate" in pl:
            obj.addProperty(
                "App::PropertyBool",
                "HeightPropagate",
                "Children",
                QT_TRANSLATE_NOOP(
                    "App::Property",
                    "If true, the height value propagates to contained objects if the height of those objects is set to 0",
                ),
                locked=True,
            )
            obj.HeightPropagate = True
        if not "LevelOffset" in pl:
            obj.addProperty(
                "App::PropertyDistance",
                "LevelOffset",
                "BuildingPart",
                QT_TRANSLATE_NOOP("App::Property", "The level of the (0,0,0) point of this level"),
                locked=True,
            )
        if not "Area" in pl:
            obj.addProperty(
                "App::PropertyArea",
                "Area",
                "BuildingPart",
                QT_TRANSLATE_NOOP("App::Property", "The computed floor area of this floor"),
                locked=True,
            )
        if not "Description" in pl:
            obj.addProperty(
                "App::PropertyString",
                "Description",
                "Component",
                QT_TRANSLATE_NOOP("App::Property", "An optional description for this component"),
                locked=True,
            )
        if not "Tag" in pl:
            obj.addProperty(
                "App::PropertyString",
                "Tag",
                "Component",
                QT_TRANSLATE_NOOP("App::Property", "An optional tag for this component"),
                locked=True,
            )
        if not "Shape" in pl:
            obj.addProperty(
                "Part::PropertyPartShape",
                "Shape",
                "BuildingPart",
                QT_TRANSLATE_NOOP("App::Property", "The shape of this object"),
                locked=True,
            )
        if not "SavedInventor" in pl:
            obj.addProperty(
                "App::PropertyFileIncluded",
                "SavedInventor",
                "BuildingPart",
                QT_TRANSLATE_NOOP(
                    "App::Property",
                    "This property stores an OpenInventor representation for this object",
                ),
                locked=True,
            )
            obj.setEditorMode("SavedInventor", 2)
        if not "OnlySolids" in pl:
            obj.addProperty(
                "App::PropertyBool",
                "OnlySolids",
                "BuildingPart",
                QT_TRANSLATE_NOOP(
                    "App::Property",
                    "If true, only solids will be collected by this object when referenced from other files",
                ),
                locked=True,
            )
            obj.OnlySolids = True
        if not "MaterialsTable" in pl:
            obj.addProperty(
                "App::PropertyMap",
                "MaterialsTable",
                "BuildingPart",
                QT_TRANSLATE_NOOP(
                    "App::Property",
                    "A MaterialName:SolidIndexesList map that relates material names with solid indexes to be used when referencing this object from other files",
                ),
                locked=True,
            )

    def onDocumentRestored(self, obj):

        self.setProperties(obj)

        vobj = getattr(obj, "ViewObject", None)
        if vobj is None:
            return
        if hasattr(vobj, "ChildrenShapeColor"):
            # add the ChildrenShapeAppearance property:
            vobj.Proxy.setProperties(vobj)
            if vobj.getTypeIdOfProperty("ChildrenShapeColor") == "App::PropertyColor":
                # <= v0.21
                material = FreeCAD.Material()
                material.DiffuseColor = vobj.ChildrenShapeColor
            else:
                # v1.0 and v1.1dev
                material = vobj.ChildrenShapeColor
                # correct the alpha value of the diffuse color which is wrongly zero:
                material.DiffuseColor = material.DiffuseColor[:-1] + (1.0,)
            vobj.ChildrenShapeAppearance = (material,)
            vobj.setPropertyStatus("ChildrenShapeColor", "-LockDynamic")
            vobj.removeProperty("ChildrenShapeColor")

    def dumps(self):

        return None

    def loads(self, state):

        self.Type = "BuildingPart"

    def onBeforeChange(self, obj, prop):

        if prop == "Placement":
            self.oldPlacement = FreeCAD.Placement(obj.Placement)

    def onChanged(self, obj, prop):

        import math

        ArchIFC.IfcProduct.onChanged(self, obj, prop)

        # clean svg cache if needed
        if prop in ["Placement", "Group"]:
            self.svgcache = None
            self.shapecache = None

        if (prop == "Height" or prop == "HeightPropagate") and obj.Height.Value:
            self.touchChildren(obj)

        elif prop == "Placement":
            if hasattr(self, "oldPlacement") and self.oldPlacement != obj.Placement:
                deltap = obj.Placement.Base.sub(self.oldPlacement.Base)
                if deltap.Length == 0:
                    deltap = None
                deltar = obj.Placement.Rotation * self.oldPlacement.Rotation.inverted()
                if deltar.Angle < 0.0001:
                    deltar = None
                children = self.getMovableChildren(obj)
                children = utils._modifiers_filter_objects(children, False)
                for child in children:
                    if deltar:
                        child.Placement.rotate(
                            self.oldPlacement.Base,
                            deltar.Axis,
                            math.degrees(deltar.Angle),
                            comp=True,
                        )
                    if deltap:
                        child.Placement.move(deltap)

    def execute(self, obj):
        "gather all the child shapes into a compound"

        pl = obj.Placement
        shapes, materialstable = self.getShapes(obj)
        if shapes:
            import Part

            if obj.OnlySolids:
                f = []
                for s in shapes:
                    f.extend(s.Solids)
                # print("faces before compound:",len(f))
                obj.Shape = Part.makeCompound(f)
                # print("faces after compound:",len(obj.Shape.Faces))
                # print("recomputing ",obj.Label)
            else:
                obj.Shape = Part.makeCompound(shapes)
            obj.Placement = pl
        obj.Area = self.getArea(obj)
        obj.MaterialsTable = materialstable
        if obj.ViewObject:
            # update the autogroup box if needed
            obj.ViewObject.Proxy.onChanged(obj.ViewObject, "AutoGroupBox")

    def getMovableChildren(self, obj):
        "recursively get movable children"

        result = []
        for child in obj.Group:
            if child.isDerivedFrom("App::DocumentObjectGroup"):
                result.extend(self.getMovableChildren(child))
            if not hasattr(child, "MoveWithHost") or child.MoveWithHost:
                if hasattr(child, "Placement"):
                    result.append(child)
        return result

    def getArea(self, obj):
        "computes the area of this floor by adding its inner spaces"

        area = 0
        if hasattr(obj, "Group"):
            for child in obj.Group:
                if (Draft.get_type(child) in ["Space", "BuildingPart"]) and hasattr(
                    child, "IfcType"
                ):
                    area += child.Area.Value
        return area

    def getShapes(self, obj):
        "recursively get the shapes of objects inside this BuildingPart"

        shapes = []
        solidindex = 0
        materialstable = {}
        for child in Draft.get_group_contents(obj, walls=True):
            if hasattr(child, "Shape") and child.Shape:
                shapes.append(child.Shape)
                for solid in child.Shape.Solids:
                    matname = "Undefined"
                    if hasattr(child, "Material") and child.Material:
                        matname = child.Material.Name
                    if matname in materialstable:
                        materialstable[matname] = materialstable[matname] + "," + str(solidindex)
                    else:
                        materialstable[matname] = str(solidindex)
                    solidindex += 1
        return shapes, materialstable

    def getSpaces(self, obj):
        "gets the list of Spaces that have this object as their Zone property"

        g = []
        for o in obj.OutList:
            if hasattr(o, "Zone"):
                if o.Zone == obj:
                    g.append(o)
        return g

    def touchChildren(self, obj):
        "Touches all descendents where applicable"

        g = []
        if hasattr(obj, "Group"):
            g = obj.Group
        elif Draft.getType(obj) in ["Wall", "Structure"]:
            g = obj.Additions
        for child in g:
            if Draft.getType(child) in ["Wall", "Structure"]:
                if not child.Height.Value:
                    FreeCAD.Console.PrintLog("Auto-updating Height of " + child.Name + "\n")
                    self.touchChildren(child)
                    child.Proxy.execute(child)
            elif Draft.getType(child) in ["App::DocumentObjectGroup", "Group", "BuildingPart"]:
                self.touchChildren(child)

    def addObject(self, obj, child):
        "Adds an object to the group of this BuildingPart"

        if not child in obj.Group:
            g = obj.Group
            g.append(child)
            obj.Group = g

    def autogroup(self, obj, child):
        "Adds an object to the group of this BuildingPart automatically"

        if obj.ViewObject:
            if hasattr(obj.ViewObject.Proxy, "autobbox") and obj.ViewObject.Proxy.autobbox:
                if hasattr(child, "Shape") and child.Shape:
                    abb = obj.ViewObject.Proxy.autobbox
                    cbb = child.Shape.BoundBox
                    if abb.isValid():
                        if not cbb.isValid():
                            FreeCAD.ActiveDocument.recompute()
                        if not cbb.isValid():
                            cbb = FreeCAD.BoundBox()
                            for v in child.Shape.Vertexes:
                                print(v.Point)
                                cbb.add(v.Point)
                        if cbb.isValid() and abb.isInside(cbb):
                            self.addObject(obj, child)
                            return True
        return False


class ViewProviderBuildingPart:
    "A View Provider for the BuildingPart object"

    def __init__(self, vobj):

        if vobj:
            vobj.addExtension("Gui::ViewProviderGroupExtensionPython")
            vobj.Proxy = self
            self.setProperties(vobj)
            material = vobj.ShapeAppearance[0]  # App.Material() based on current preferences.
            material.DiffuseColor = ArchCommands.getDefaultColor("Helpers")
            vobj.ShapeAppearance = (material,)
            self.Object = vobj.Object

    def setProperties(self, vobj):

        pl = vobj.PropertiesList
        if not "LineWidth" in pl:
            vobj.addProperty(
                "App::PropertyFloat",
                "LineWidth",
                "BuildingPart",
                QT_TRANSLATE_NOOP("App::Property", "The line width of this object"),
                locked=True,
            )
            vobj.LineWidth = 1
        if not "OverrideUnit" in pl:
            vobj.addProperty(
                "App::PropertyString",
                "OverrideUnit",
                "BuildingPart",
                QT_TRANSLATE_NOOP("App::Property", "An optional unit to express levels"),
                locked=True,
            )
        if not "DisplayOffset" in pl:
            vobj.addProperty(
                "App::PropertyPlacement",
                "DisplayOffset",
                "BuildingPart",
                QT_TRANSLATE_NOOP("App::Property", "A transformation to apply to the level mark"),
                locked=True,
            )
            vobj.DisplayOffset = FreeCAD.Placement(
                FreeCAD.Vector(0, 0, 0), FreeCAD.Rotation(FreeCAD.Vector(1, 0, 0), 90)
            )
        if not "ShowLevel" in pl:
            vobj.addProperty(
                "App::PropertyBool",
                "ShowLevel",
                "BuildingPart",
                QT_TRANSLATE_NOOP("App::Property", "If true, show the level"),
                locked=True,
            )
            vobj.ShowLevel = True
        if not "ShowUnit" in pl:
            vobj.addProperty(
                "App::PropertyBool",
                "ShowUnit",
                "BuildingPart",
                QT_TRANSLATE_NOOP("App::Property", "If true, show the unit on the level tag"),
                locked=True,
            )
        if not "OriginOffset" in pl:
            vobj.addProperty(
                "App::PropertyBool",
                "OriginOffset",
                "BuildingPart",
                QT_TRANSLATE_NOOP(
                    "App::Property", "If true, display offset will affect the origin mark too"
                ),
                locked=True,
            )
        if not "ShowLabel" in pl:
            vobj.addProperty(
                "App::PropertyBool",
                "ShowLabel",
                "BuildingPart",
                QT_TRANSLATE_NOOP("App::Property", "If true, the object's label is displayed"),
                locked=True,
            )
            vobj.ShowLabel = True
        if not "FontName" in pl:
            vobj.addProperty(
                "App::PropertyFont",
                "FontName",
                "BuildingPart",
                QT_TRANSLATE_NOOP("App::Property", "The font to be used for texts"),
                locked=True,
            )
            vobj.FontName = params.get_param("textfont")
        if not "FontSize" in pl:
            vobj.addProperty(
                "App::PropertyLength",
                "FontSize",
                "BuildingPart",
                QT_TRANSLATE_NOOP("App::Property", "The font size of texts"),
                locked=True,
            )
            vobj.FontSize = params.get_param("textheight") * params.get_param(
                "DefaultAnnoScaleMultiplier"
            )
        if not "DiffuseColor" in pl:
            vobj.addProperty(
                "App::PropertyColorList",
                "DiffuseColor",
                "BuildingPart",
                QT_TRANSLATE_NOOP("App::Property", "The individual face colors"),
                locked=True,
            )

        # Interaction properties
        if not "SetWorkingPlane" in pl:
            vobj.addProperty(
                "App::PropertyBool",
                "SetWorkingPlane",
                "Interaction",
                QT_TRANSLATE_NOOP(
                    "App::Property",
                    "If true, when activated, the working plane will automatically adapt to this level",
                ),
                locked=True,
            )
            vobj.SetWorkingPlane = True
        if not "AutoWorkingPlane" in pl:
            vobj.addProperty(
                "App::PropertyBool",
                "AutoWorkingPlane",
                "Interaction",
                QT_TRANSLATE_NOOP(
                    "App::Property", "If set to True, the working plane will be kept on Auto mode"
                ),
                locked=True,
            )
        if not "ViewData" in pl:
            vobj.addProperty(
                "App::PropertyFloatList",
                "ViewData",
                "Interaction",
                QT_TRANSLATE_NOOP(
                    "App::Property", "Camera position data associated with this object"
                ),
                locked=True,
            )
            vobj.setEditorMode("ViewData", 2)
        if not "RestoreView" in pl:
            vobj.addProperty(
                "App::PropertyBool",
                "RestoreView",
                "Interaction",
                QT_TRANSLATE_NOOP(
                    "App::Property",
                    "If set, the view stored in this object will be restored on double-click",
                ),
                locked=True,
            )
        if not "DoubleClickActivates" in pl:
            vobj.addProperty(
                "App::PropertyBool",
                "DoubleClickActivates",
                "Interaction",
                QT_TRANSLATE_NOOP(
                    "App::Property", "If True, double-clicking this object in the tree activates it"
                ),
                locked=True,
            )
            vobj.DoubleClickActivates = True

        # inventor saving
        if not "SaveInventor" in pl:
            vobj.addProperty(
                "App::PropertyBool",
                "SaveInventor",
                "Interaction",
                QT_TRANSLATE_NOOP(
                    "App::Property",
                    "If this is enabled, the OpenInventor representation of this object will be saved in the FreeCAD file, allowing to reference it in other files in lightweight mode.",
                ),
                locked=True,
            )
        if not "SavedInventor" in pl:
            vobj.addProperty(
                "App::PropertyFileIncluded",
                "SavedInventor",
                "Interaction",
                QT_TRANSLATE_NOOP(
                    "App::Property",
                    "A slot to save the OpenInventor representation of this object, if enabled",
                ),
                locked=True,
            )
            vobj.setEditorMode("SavedInventor", 2)

        # children properties
        if not "ChildrenOverride" in pl:
            vobj.addProperty(
                "App::PropertyBool",
                "ChildrenOverride",
                "Children",
                QT_TRANSLATE_NOOP(
                    "App::Property",
                    "If true, show the objects contained in this Building Part will adopt these line, color and transparency settings",
                ),
                locked=True,
            )
        if not "ChildrenLineWidth" in pl:
            vobj.addProperty(
                "App::PropertyFloat",
                "ChildrenLineWidth",
                "Children",
                QT_TRANSLATE_NOOP("App::Property", "The line width of child objects"),
                locked=True,
            )
            vobj.ChildrenLineWidth = params.get_param_view("DefaultShapeLineWidth")
        if not "ChildrenLineColor" in pl:
            vobj.addProperty(
                "App::PropertyColor",
                "ChildrenLineColor",
                "Children",
                QT_TRANSLATE_NOOP("App::Property", "The line color of child objects"),
                locked=True,
            )
            vobj.ChildrenLineColor = params.get_param_view("DefaultShapeLineColor") | 0x000000FF
        if not "ChildrenShapeAppearance" in pl:
            vobj.addProperty(
                "App::PropertyMaterialList",
                "ChildrenShapeAppearance",
                "Children",
                QT_TRANSLATE_NOOP("App::Property", "The shape appearance of child objects"),
                locked=True,
            )
            # The default App::PropertyMaterialList does not match the preferences, we have to do:
            vobj.ChildrenShapeAppearance = (utils.get_view_material(),)
        if not "ChildrenTransparency" in pl:
            vobj.addProperty(
                "App::PropertyPercent",
                "ChildrenTransparency",
                "Children",
                QT_TRANSLATE_NOOP("App::Property", "The transparency of child objects"),
                locked=True,
            )
            vobj.ChildrenTransparency = params.get_param_view("DefaultShapeTransparency")

        # clip properties
        if not "CutView" in pl:
            vobj.addProperty(
                "App::PropertyBool",
                "CutView",
                "Clip",
                QT_TRANSLATE_NOOP("App::Property", "Cut the view above this level"),
                locked=True,
            )
        if not "CutMargin" in pl:
            vobj.addProperty(
                "App::PropertyLength",
                "CutMargin",
                "Clip",
                QT_TRANSLATE_NOOP(
                    "App::Property", "The distance between the level plane and the cut line"
                ),
                locked=True,
            )
            vobj.CutMargin = 1600
        if not "AutoCutView" in pl:
            vobj.addProperty(
                "App::PropertyBool",
                "AutoCutView",
                "Clip",
                QT_TRANSLATE_NOOP("App::Property", "Turn cutting on when activating this level"),
                locked=True,
            )

        # autogroup properties
        if not "AutogroupSize" in pl:
            vobj.addProperty(
                "App::PropertyIntegerList",
                "AutogroupSize",
                "AutoGroup",
                QT_TRANSLATE_NOOP(
                    "App::Property",
                    "The capture box for newly created objects expressed as [XMin,YMin,ZMin,XMax,YMax,ZMax]",
                ),
                locked=True,
            )
        if not "AutogroupBox" in pl:
            vobj.addProperty(
                "App::PropertyBool",
                "AutogroupBox",
                "AutoGroup",
                QT_TRANSLATE_NOOP("App::Property", "Turns auto group box on/off"),
                locked=True,
            )
        if not "AutogroupAutosize" in pl:
            vobj.addProperty(
                "App::PropertyBool",
                "AutogroupAutosize",
                "AutoGroup",
                QT_TRANSLATE_NOOP("App::Property", "Automatically set size from contents"),
                locked=True,
            )
        if not "AutogroupMargin" in pl:
            vobj.addProperty(
                "App::PropertyLength",
                "AutogroupMargin",
                "AutoGroup",
                QT_TRANSLATE_NOOP("App::Property", "A margin to use when autosize is turned on"),
                locked=True,
            )

    def getIcon(self):

        import Arch_rc

        if hasattr(self, "Object"):
            if self.Object.IfcType == "Building Storey":
                return ":/icons/Arch_Floor_Tree.svg"
            elif self.Object.IfcType == "Building":
                return ":/icons/Arch_Building_Tree.svg"
            elif self.Object.IfcType == "Annotation":
                return ":/icons/BIM_ArchView.svg"
            elif hasattr(self.Object, "IfcClass"):
                from nativeifc import ifc_viewproviders

                return ifc_viewproviders.get_icon(self)
        return ":/icons/Arch_BuildingPart_Tree.svg"

    def attach(self, vobj):

        self.Object = vobj.Object
        self.clip = None
        from pivy import coin

        self.sep = coin.SoGroup()
        self.mat = coin.SoMaterial()
        self.sep.addChild(self.mat)
        self.dst = coin.SoDrawStyle()
        self.sep.addChild(self.dst)
        self.lco = coin.SoCoordinate3()
        self.sep.addChild(self.lco)
        import PartGui  # Required for "SoBrepEdgeSet" (because a BuildingPart is not a Part::FeaturePython object).

        lin = coin.SoType.fromName("SoBrepEdgeSet").createInstance()
        if lin:
            lin.coordIndex.setValues([0, 1, -1, 2, 3, -1, 4, 5, -1])
        self.sep.addChild(lin)
        self.bbox = coin.SoSwitch()
        self.bbox.whichChild = -1
        bboxsep = coin.SoSeparator()
        self.bbox.addChild(bboxsep)
        drawstyle = coin.SoDrawStyle()
        drawstyle.style = coin.SoDrawStyle.LINES
        drawstyle.lineWidth = 3
        drawstyle.linePattern = 0x0F0F  # 0xaa
        bboxsep.addChild(drawstyle)
        self.bbco = coin.SoCoordinate3()
        bboxsep.addChild(self.bbco)
        lin = coin.SoIndexedLineSet()
        lin.coordIndex.setValues(
            [0, 1, 2, 3, 0, -1, 4, 5, 6, 7, 4, -1, 0, 4, -1, 1, 5, -1, 2, 6, -1, 3, 7, -1]
        )
        bboxsep.addChild(lin)
        self.sep.addChild(self.bbox)
        self.tra = coin.SoTransform()
        self.tra.rotation.setValue(FreeCAD.Rotation(0, 0, 90).Q)
        self.sep.addChild(self.tra)
        self.fon = coin.SoFont()
        self.sep.addChild(self.fon)
        self.txt = coin.SoAsciiText()
        self.txt.justification = coin.SoText2.LEFT
        self.txt.string.setValue("level")
        self.sep.addChild(self.txt)
        vobj.addDisplayMode(self.sep, "Default")
        self.onChanged(vobj, "ShapeAppearance")
        self.onChanged(vobj, "FontName")
        self.onChanged(vobj, "ShowLevel")
        self.onChanged(vobj, "FontSize")
        self.onChanged(vobj, "AutogroupBox")
        self.setProperties(vobj)
        return

    def getDisplayModes(self, vobj):

        return ["Default"]

    def getDefaultDisplayMode(self):

        return "Default"

    def setDisplayMode(self, mode):

        return mode

    def updateData(self, obj, prop):

        if prop in ["Placement", "LevelOffset"]:
            self.onChanged(obj.ViewObject, "OverrideUnit")
        elif prop == "Shape":
            colors = self.getColors(obj)
            if hasattr(obj.ViewObject, "DiffuseColor") and colors != obj.ViewObject.DiffuseColor:
                obj.ViewObject.DiffuseColor = colors
                self.writeInventor(obj)
        elif prop == "Group":
            self.onChanged(obj.ViewObject, "ChildrenOverride")
        elif prop == "Label":
            self.onChanged(obj.ViewObject, "ShowLabel")

    def getColors(self, obj):
        "get the colors of objects inside this BuildingPart"

        return Draft.get_diffuse_color(Draft.get_group_contents(obj, walls=True))

    def onChanged(self, vobj, prop):

        # print(vobj.Object.Label," - ",prop)

        if prop == "ShapeAppearance":
            if hasattr(vobj, "ShapeAppearance"):
                color = vobj.ShapeAppearance[0].DiffuseColor[:-1]  # remove alpha value
                self.mat.diffuseColor.setValue(*color)
        elif prop == "LineWidth":
            if hasattr(vobj, "LineWidth"):
                self.dst.lineWidth = vobj.LineWidth
        elif prop == "FontName":
            if hasattr(vobj, "FontName") and hasattr(self, "fon"):
                if vobj.FontName:
                    self.fon.name = vobj.FontName
        elif prop in ["FontSize", "DisplayOffset", "OriginOffset"]:
            if (
                hasattr(vobj, "FontSize")
                and hasattr(vobj, "DisplayOffset")
                and hasattr(vobj, "OriginOffset")
                and hasattr(self, "fon")
            ):
                fs = vobj.FontSize.Value
                if fs:
                    self.fon.size = fs
                    b = vobj.DisplayOffset.Base
                    self.tra.translation.setValue([b.x + fs / 8, b.y, b.z + fs / 8])
                    r = vobj.DisplayOffset.Rotation
                    self.tra.rotation.setValue(r.Q)
                    if vobj.OriginOffset:
                        self.lco.point.setValues(
                            [
                                [b.x - fs, b.y, b.z],
                                [b.x + fs, b.y, b.z],
                                [b.x, b.y - fs, b.z],
                                [b.x, b.y + fs, b.z],
                                [b.x, b.y, b.z - fs],
                                [b.x, b.y, b.z + fs],
                            ]
                        )
                    else:
                        self.lco.point.setValues(
                            [
                                [-fs, 0, 0],
                                [fs, 0, 0],
                                [0, -fs, 0],
                                [0, fs, 0],
                                [0, 0, -fs],
                                [0, 0, fs],
                            ]
                        )
        elif prop in ["OverrideUnit", "ShowUnit", "ShowLevel", "ShowLabel"]:
            if (
                hasattr(vobj, "OverrideUnit")
                and hasattr(vobj, "ShowUnit")
                and hasattr(vobj, "ShowLevel")
                and hasattr(vobj, "ShowLabel")
                and hasattr(self, "txt")
            ):
                offset = getattr(vobj.Object, "LevelOffset", 0)
                if hasattr(offset, "Value"):
                    offset = offset.Value
                z = vobj.Object.Placement.Base.z + offset
                q = FreeCAD.Units.Quantity(z, FreeCAD.Units.Length)
                txt = ""
                if vobj.ShowLabel:
                    txt += vobj.Object.Label
                if vobj.ShowLevel:
                    if txt:
                        txt += " "
                    if z >= 0:
                        txt += "+"
                    if vobj.OverrideUnit:
                        u = vobj.OverrideUnit
                    else:
                        u = q.getUserPreferred()[2]
                    try:
                        txt += units.display_external(float(q), None, "Length", vobj.ShowUnit, u)
                    except Exception:
                        q = q.getValueAs(q.getUserPreferred()[2])
                        d = params.get_param("Decimals", path="Units")
                        fmt = "{0:." + str(d) + "f}"
                        if not vobj.ShowUnit:
                            u = ""
                        txt += fmt.format(float(q)) + str(u)
                if not txt:
                    txt = " "  # empty texts make coin crash...
                if isinstance(txt, unicode):
                    txt = txt.encode("utf8")
                self.txt.string.setValue(txt)
        elif prop in [
            "ChildrenOverride",
            "ChildrenLineWidth",
            "ChildrenLineColor",
            "ChildrenShapeAppearance",
            "ChildrenTransparency",
        ]:
            if hasattr(vobj, "ChildrenOverride") and vobj.ChildrenOverride:
                props = [
                    "ChildrenLineWidth",
                    "ChildrenLineColor",
                    "ChildrenShapeAppearance",
                    "ChildrenTransparency",
                ]
                for child in vobj.Object.Group:
                    for prop in props:
                        if (
                            hasattr(vobj, prop)
                            and hasattr(child.ViewObject, prop[8:])
                            and not hasattr(child, "ChildrenOverride")
                        ):
                            setattr(child.ViewObject, prop[8:], getattr(vobj, prop))
        elif prop in ["CutView", "CutMargin"]:
            if (
                hasattr(vobj, "CutView")
                and FreeCADGui.ActiveDocument.ActiveView
                and hasattr(FreeCADGui.ActiveDocument.ActiveView, "getSceneGraph")
            ):
                sg = FreeCADGui.ActiveDocument.ActiveView.getSceneGraph()
                if vobj.CutView:
                    from pivy import coin

                    if self.clip:
                        sg.removeChild(self.clip)
                        self.clip = None
                    for o in Draft.get_group_contents(vobj.Object.Group, walls=True):
                        if hasattr(o.ViewObject, "Lighting"):
                            o.ViewObject.Lighting = "One side"
                    self.clip = coin.SoClipPlane()
                    self.clip.on.setValue(True)
                    norm = vobj.Object.Placement.multVec(FreeCAD.Vector(0, 0, 1))
                    mp = vobj.Object.Placement.Base
                    mp = DraftVecUtils.project(mp, norm)
                    dist = mp.Length  # - 0.1 # to not clip exactly on the section object
                    norm = norm.negative()
                    marg = 1
                    if hasattr(vobj, "CutMargin"):
                        marg = vobj.CutMargin.Value
                    if mp.getAngle(norm) > 1:
                        dist += marg
                        dist = -dist
                    else:
                        dist -= marg
                    plane = coin.SbPlane(coin.SbVec3f(norm.x, norm.y, norm.z), dist)
                    self.clip.plane.setValue(plane)
                    sg.insertChild(self.clip, 0)
                else:
                    if getattr(self, "clip", None):
                        sg.removeChild(self.clip)
                        self.clip = None
                    for o in Draft.get_group_contents(vobj.Object.Group, walls=True):
                        if hasattr(o.ViewObject, "Lighting"):
                            o.ViewObject.Lighting = "Two side"
        elif prop == "Visibility":
            # turn clipping off when turning the object off
            if hasattr(vobj, "Visibility") and not (vobj.Visibility) and hasattr(vobj, "CutView"):
                vobj.CutView = False
        elif prop == "SaveInventor":
            self.writeInventor(vobj.Object)
        elif prop in ["AutogroupBox", "AutogroupSize"]:
            if hasattr(vobj, "AutogroupBox") and hasattr(vobj, "AutogroupSize"):
                if vobj.AutogroupBox:
                    if len(vobj.AutogroupSize) >= 6:
                        self.autobbox = FreeCAD.BoundBox(*vobj.AutogroupSize[0:6])
                        self.autobbox.move(vobj.Object.Placement.Base)
                        pts = [list(self.autobbox.getPoint(i)) for i in range(8)]
                        self.bbco.point.setValues(pts)
                        self.bbox.whichChild = 0
                else:
                    self.autobbox = None
                    self.bbox.whichChild = -1
        elif prop in ["AutogroupAutosize", "AutogroupMargin"]:
            if hasattr(vobj, "AutogroupAutosize") and vobj.AutogroupAutosize:
                bbox = vobj.Object.Shape.BoundBox
                bbox.enlarge(vobj.AutogroupMargin.Value)
                vobj.AutogroupSize = [
                    int(i)
                    for i in [bbox.XMin, bbox.YMin, bbox.ZMin, bbox.XMax, bbox.YMax, bbox.ZMax]
                ]

    def onDelete(self, vobj, subelements):

        if self.clip:
            sg = FreeCADGui.ActiveDocument.ActiveView.getSceneGraph()
            sg.removeChild(self.clip)
            self.clip = None
        for o in Draft.get_group_contents(vobj.Object.Group, walls=True):
            if hasattr(o.ViewObject, "Lighting"):
                o.ViewObject.Lighting = "Two side"
        return True

    def setEdit(self, vobj, mode):
        # mode == 1 if Transform is selected in the Tree view context menu.
        # mode == 2 has been added for consistency.
        if mode == 1 or mode == 2:
            return None
        # For some reason mode is always 0 if the object is double-clicked in
        # the Tree view. Using FreeCADGui.getUserEditMode() as a workaround.
        if FreeCADGui.getUserEditMode() in ("Transform", "Cutting"):
            return None

        self.activate()
        return False  # Return `False` as we don't want to enter edit mode.

    def unsetEdit(self, vobj, mode):
        if mode == 1 or mode == 2:
            return None
        if FreeCADGui.getUserEditMode() in ("Transform", "Cutting"):
            return None

        return True

    def setupContextMenu(self, vobj, menu):
        from PySide import QtCore, QtGui
        import Draft_rc

        if FreeCADGui.activeWorkbench().name() != "BIMWorkbench":
            return
        if (not hasattr(vobj, "DoubleClickActivates")) or vobj.DoubleClickActivates:
            menuTxt = translate("Arch", "Active")
            actionActivate = QtGui.QAction(menuTxt, menu)
            actionActivate.setCheckable(True)
            if FreeCADGui.ActiveDocument.ActiveView.getActiveObject("Arch") == self.Object:
                actionActivate.setChecked(True)
            else:
                actionActivate.setChecked(False)
            actionActivate.triggered.connect(lambda _: self.activate(actionActivate))
            menu.addAction(actionActivate)

        actionSetWorkingPlane = QtGui.QAction(
            QtGui.QIcon(":/icons/Draft_SelectPlane.svg"),
            translate("Arch", "Set Working Plane"),
            menu,
        )
        QtCore.QObject.connect(
            actionSetWorkingPlane, QtCore.SIGNAL("triggered()"), self.setWorkingPlane
        )
        menu.addAction(actionSetWorkingPlane)

        actionWriteCamera = QtGui.QAction(
            QtGui.QIcon(":/icons/Draft_SelectPlane.svg"),
            translate("Arch", "Write Camera Position"),
            menu,
        )
        QtCore.QObject.connect(actionWriteCamera, QtCore.SIGNAL("triggered()"), self.writeCamera)
        menu.addAction(actionWriteCamera)

        actionCreateGroup = QtGui.QAction(translate("Arch", "New Group"), menu)
        QtCore.QObject.connect(actionCreateGroup, QtCore.SIGNAL("triggered()"), self.createGroup)
        menu.addAction(actionCreateGroup)

        actionReorder = QtGui.QAction(translate("Arch", "Reorder Children Alphabetically"), menu)
        QtCore.QObject.connect(actionReorder, QtCore.SIGNAL("triggered()"), self.reorder)
        menu.addAction(actionReorder)

        actionCloneUp = QtGui.QAction(translate("Arch", "Clone Level Up"), menu)
        QtCore.QObject.connect(actionCloneUp, QtCore.SIGNAL("triggered()"), self.cloneUp)
        menu.addAction(actionCloneUp)

    def activate(self, action=None):
        from draftutils.gui_utils import toggle_working_plane

        vobj = self.Object.ViewObject

        if (not hasattr(vobj, "DoubleClickActivates")) or vobj.DoubleClickActivates:
            if toggle_working_plane(self.Object, action, restore=True):
                print("Setting active working plane to: ", self.Object.Label)
            else:
                print("Deactivating working plane from: ", self.Object.Label)

        FreeCADGui.Selection.clearSelection()

    def setWorkingPlane(self, restore=False):
        vobj = self.Object.ViewObject

        import WorkingPlane

        wp = WorkingPlane.get_working_plane(update=False)
        autoclip = False
        if hasattr(vobj, "AutoCutView"):
            autoclip = vobj.AutoCutView
        if restore:
            if wp.label.rstrip("*") == self.Object.Label:
                prev_data = wp._previous()
                if prev_data:
                    prev_label = prev_data.get("label", "").rstrip("*")
                    prev_obj = None
                    for obj in FreeCAD.ActiveDocument.Objects:
                        if hasattr(obj, "Label") and obj.Label == prev_label:
                            prev_obj = obj
                            break

                    if prev_obj:
                        # check in which context we need to set the active object
                        context = "Arch"
                        obj_type = Draft.getType(prev_obj)
                        if obj_type == "IfcBuildingStorey":
                            context = "NativeIFC"
                        FreeCADGui.ActiveDocument.ActiveView.setActiveObject(context, prev_obj)
                        print(f"Set active object to: {prev_obj.Label} (context: {context})")

            if autoclip:
                vobj.CutView = False
        else:
            wp.align_to_selection()
            if autoclip:
                vobj.CutView = True

    def writeCamera(self):

        if hasattr(self, "Object"):
            from pivy import coin

            n = FreeCADGui.ActiveDocument.ActiveView.getCameraNode()
            FreeCAD.Console.PrintMessage(
                QT_TRANSLATE_NOOP("Draft", "Writing camera position") + "\n"
            )
            cdata = list(n.position.getValue().getValue())
            cdata.extend(list(n.orientation.getValue().getValue()))
            cdata.append(n.nearDistance.getValue())
            cdata.append(n.farDistance.getValue())
            cdata.append(n.aspectRatio.getValue())
            cdata.append(n.focalDistance.getValue())
            if isinstance(n, coin.SoOrthographicCamera):
                cdata.append(n.height.getValue())
                cdata.append(0.0)  # orthograhic camera
            elif isinstance(n, coin.SoPerspectiveCamera):
                cdata.append(n.heightAngle.getValue())
                cdata.append(1.0)  # perspective camera
            self.Object.ViewObject.ViewData = cdata

    def createGroup(self):

        if hasattr(self, "Object"):
            s = (
                'FreeCAD.ActiveDocument.getObject("%s").newObject("App::DocumentObjectGroup","Group")'
                % self.Object.Name
            )
            FreeCADGui.doCommand(s)

    def reorder(self):

        if hasattr(self, "Object"):
            if hasattr(self.Object, "Group") and self.Object.Group:
                g = self.Object.Group
                g.sort(key=lambda obj: obj.Label)
                self.Object.Group = g
                FreeCAD.ActiveDocument.recompute()

    def cloneUp(self):

        if hasattr(self, "Object"):
            if not self.Object.Height.Value:
                FreeCAD.Console.PrintError(
                    "This level has no height value. Define a height before using this function.\n"
                )
                return
            height = self.Object.Height.Value
            ng = []
            if hasattr(self.Object, "Group") and self.Object.Group:
                for o in self.Object.Group:
                    no = Draft.clone(o)
                    Draft.move(no, FreeCAD.Vector(0, 0, height))
                    ng.append(no)
            nobj = Arch.makeBuildingPart()
            Draft.formatObject(nobj, self.Object)
            nobj.Placement = self.Object.Placement
            nobj.Placement.move(FreeCAD.Vector(0, 0, height))
            nobj.IfcType = self.Object.IfcType
            nobj.Height = height
            nobj.Label = self.Object.Label
            nobj.Group = ng
            for parent in self.Object.InList:
                if (
                    hasattr(parent, "Group")
                    and hasattr(parent, "addObject")
                    and (self.Object in parent.Group)
                ):
                    parent.addObject(nobj)
            FreeCAD.ActiveDocument.recompute()
            # fix for missing IFC attributes
            for no in ng:
                if (
                    hasattr(no, "LongName")
                    and hasattr(no, "CloneOf")
                    and no.CloneOf
                    and hasattr(no.CloneOf, "LongName")
                ):
                    no.LongName = no.CloneOf.LongName
            FreeCAD.ActiveDocument.recompute()

    def dumps(self):
        return None

    def loads(self, state):
        return None

    def writeInventor(self, obj):

        def callback(match):
            return next(callback.v)

        if hasattr(obj.ViewObject, "SaveInventor") and obj.ViewObject.SaveInventor:
            if obj.Shape and obj.Shape.Faces and hasattr(obj, "SavedInventor"):
                colors = obj.ViewObject.DiffuseColor
                if len(colors) != len(obj.Shape.Faces):
                    print("Debug: Colors mismatch in", obj.Label)
                    colors = None
                iv = self.Object.Shape.writeInventor()
                import re

                if colors:
                    if len(re.findall(r"IndexedFaceSet", iv)) == len(obj.Shape.Faces):
                        # convert colors to iv representations
                        colors = [
                            "Material { diffuseColor "
                            + str(color[0])
                            + " "
                            + str(color[1])
                            + " "
                            + str(color[2])
                            + "}\n    IndexedFaceSet"
                            for color in colors
                        ]
                        # replace
                        callback.v = iter(colors)
                        iv = re.sub(r"IndexedFaceSet", callback, iv)
                    else:
                        print("Debug: IndexedFaceSet mismatch in", obj.Label)
                # save embedded file
                tf = tempfile.mkstemp(prefix=obj.Name, suffix=".iv")[1]
                f = open(tf, "w")
                f.write(iv)
                f.close()
                obj.SavedInventor = tf
                os.remove(tf)
