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

# ***************************************************************************
# *                                                                         *
# *   Copyright (c) 2011 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 Commands"
__author__ = "Yorik van Havre"
__url__ = "https://www.freecad.org"

## @package ArchCommands
#  \ingroup ARCH
#  \brief Utility functions for the Arch Workbench
#
#  This module provides general functions used by Arch tools
#  and utility commands

import FreeCAD
import ArchComponent
import Draft
import DraftVecUtils

from FreeCAD import Vector
from draftutils import params
from draftutils.groups import is_group

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

    # \endcond


# module functions ###############################################


def getStringList(objects):
    """getStringList(objects): returns a string defining a list
    of objects"""
    result = "["
    for o in objects:
        if len(result) > 1:
            result += ","
        result += "FreeCAD.ActiveDocument." + o.Name
    result += "]"
    return result


def getDefaultColor(objectType):
    """getDefaultColor(string): returns a color value for the given object
    type (Wall, Structure, Window, WindowGlass)"""
    alpha = 1.0
    if objectType == "Wall":
        c = params.get_param_arch("WallColor")
    elif objectType == "Structure":
        c = params.get_param_arch("StructureColor")
    elif objectType == "WindowGlass":
        c = params.get_param_arch("WindowGlassColor")
        alpha = 1.0 - params.get_param_arch("WindowTransparency") / 100.0
    elif objectType == "Rebar":
        c = params.get_param_arch("RebarColor")
    elif objectType == "Panel":
        c = params.get_param_arch("PanelColor")
    elif objectType == "Space":
        c = params.get_param_arch("defaultSpaceColor")
    elif objectType == "Helpers":
        c = params.get_param_arch("ColorHelpers")
    elif objectType == "Construction":
        c = params.get_param("constructioncolor")
        alpha = 0.2
    else:
        c = params.get_param_view("DefaultShapeColor")
    r, g, b, _ = Draft.get_rgba_tuple(c)
    return (r, g, b, alpha)


def _usedForAttachment(host, obj):
    if not getattr(obj, "AttachmentSupport", []):
        return False
    for sub in obj.AttachmentSupport:
        if sub[0] == host:
            return True
    return False


def addComponents(objectsList, host):
    """addComponents(objectsList,hostObject): adds the given object or the objects
    from the given list as components to the given host Object. Use this for
    example to add windows to a wall, or to add walls to a cell or floor."""
    if not isinstance(objectsList, list):
        objectsList = [objectsList]
    hostType = Draft.getType(host)
    if hostType in ["Floor", "Building", "Site", "Project", "BuildingPart"]:
        for o in objectsList:
            host.addObject(o)
    elif hostType in [
        "Wall",
        "CurtainWall",
        "Structure",
        "Precast",
        "Window",
        "Roof",
        "Stairs",
        "StructuralSystem",
        "Panel",
        "Component",
        "Pipe",
    ]:
        import DraftGeomUtils

        outList = host.OutListRecursive
        a = host.Additions
        x = getattr(host, "Axes", [])
        for o in objectsList:
            if hasattr(o, "Shape"):
                if Draft.getType(o) == "Window":
                    if hasattr(o, "Hosts"):
                        if not host in o.Hosts:
                            g = o.Hosts
                            g.append(host)
                            o.Hosts = g
                elif o in outList:
                    FreeCAD.Console.PrintWarning(
                        translate(
                            "Arch", "Cannot add {0} as it is already referenced by {1}."
                        ).format(o.Label, host.Label)
                        + "\n"
                    )
                elif Draft.getType(o) == "Axis":
                    if not o in x:
                        x.append(o)
                elif DraftGeomUtils.isValidPath(o.Shape) and (hostType in ["Structure", "Precast"]):
                    if _usedForAttachment(host, o):
                        o.AttachmentSupport = None
                        o.MapMode = "Deactivated"
                    host.Tool = o
                elif not o in a:
                    if hasattr(o, "Shape"):
                        a.append(o)
        host.Additions = a
        if hasattr(host, "Axes"):
            host.Axes = x
    elif hostType in ["SectionPlane"]:
        a = host.Objects
        for o in objectsList:
            if not o in a:
                a.append(o)
        host.Objects = a
    elif host.isDerivedFrom("App::DocumentObjectGroup"):
        for o in objectsList:
            host.addObject(o)


def removeComponents(objectsList, host=None):
    """removeComponents(objectsList,[hostObject]): removes the given component or
    the components from the given list from their parents. If a host object is
    specified, this function will try adding the components as holes to the host
    object instead."""
    if not isinstance(objectsList, list):
        objectsList = [objectsList]
    if host:
        if Draft.getType(host) in [
            "Wall",
            "CurtainWall",
            "Structure",
            "Precast",
            "Window",
            "Roof",
            "Stairs",
            "StructuralSystem",
            "Panel",
            "Component",
            "Pipe",
        ]:
            if getattr(host, "Tool", None) in objectsList:
                host.Tool = None
            if hasattr(host, "Axes"):
                a = host.Axes
                for o in objectsList[:]:
                    if o in a:
                        a.remove(o)
                        objectsList.remove(o)
                host.Axes = a
            s = host.Subtractions
            for o in objectsList:
                if Draft.getType(o) == "Window":
                    if hasattr(o, "Hosts"):
                        if not host in o.Hosts:
                            g = o.Hosts
                            g.append(host)
                            o.Hosts = g
                elif not o in s:
                    s.append(o)
                    if FreeCAD.GuiUp:
                        if Draft.getType(o) != "Roof":
                            setAsSubcomponent(o)
                    # Avoid cyclic dependency via Attachment Support:
                    if hasattr(o, "Base") and o.Base:
                        objList = [o, o.Base]
                    else:
                        objList = [o]
                    for i in objList:
                        if _usedForAttachment(host, i):
                            FreeCAD.Console.PrintMessage(
                                translate(
                                    "Arch",
                                    "{0} is mapped to {1}, removing the former's "
                                    + "Attachment Support to avoid cyclic dependency.",
                                ).format(o.Label, host.Label)
                                + "\n"
                            )
                            i.AttachmentSupport = None
                            i.MapMode = "Deactivated"
            host.Subtractions = s
        elif Draft.getType(host) in ["SectionPlane"]:
            a = host.Objects
            for o in objectsList:
                if o in a:
                    a.remove(o)
            host.Objects = a
    else:
        for o in objectsList:
            if o.InList:
                h = o.InList[0]
                tp = Draft.getType(h)
                if tp in ["Floor", "Building", "Site", "BuildingPart"]:
                    c = h.Group
                    if o in c:
                        c.remove(o)
                        h.Group = c
                        o.ViewObject.show()
                elif tp in ["Wall", "Structure", "Precast"]:
                    a = h.Additions
                    s = h.Subtractions
                    if o in a:
                        a.remove(o)
                        h.Additions = a
                        o.ViewObject.show()
                    elif o in s:
                        s.remove(o)
                        h.Subtractions = s
                        o.ViewObject.show()
                    elif o == h.Base:
                        h.Base = None
                        if FreeCAD.GuiUp:
                            o.ViewObject.show()
                elif tp in ["SectionPlane"]:
                    a = h.Objects
                    if o in a:
                        a.remove(o)
                        h.Objects = a
            if hasattr(o, "Hosts") and Draft.getType(o) == "Window":
                o.Hosts = []


def makeComponent(baseobj=None, name=None, delete=False):
    """makeComponent([baseobj],[name],[delete]): creates an undefined, non-parametric BIM
    component from the given base object"""
    if not FreeCAD.ActiveDocument:
        FreeCAD.Console.PrintError("No active document. Aborting\n")
        return
    obj = FreeCAD.ActiveDocument.addObject("Part::FeaturePython", "Component")
    obj.Label = name if name else translate("Arch", "Component")
    ArchComponent.Component(obj)
    if FreeCAD.GuiUp:
        ArchComponent.ViewProviderComponent(obj.ViewObject)
    if baseobj:
        import Part

        if hasattr(baseobj, "Shape"):
            obj.Shape = baseobj.Shape
            obj.Placement = baseobj.Placement
            if delete:
                FreeCAD.ActiveDocument.removeObject(baseobj.Name)
            else:
                obj.Base = baseobj
                if FreeCAD.GuiUp:
                    baseobj.ViewObject.hide()
        elif isinstance(baseobj, Part.Shape):
            obj.Shape = baseobj
    Draft.select(obj)
    return obj


def cloneComponent(obj):
    """cloneComponent(obj): Creates a clone of an object as an undefined component"""
    c = makeComponent()
    c.CloneOf = obj
    c.Placement = obj.Placement
    c.Label = obj.Label
    if hasattr(obj, "Material"):
        if obj.Material:
            c.Material = obj.Material
    if hasattr(obj, "IfcAttributes"):
        if obj.IfcAttributes:
            c.IfcAttributes = obj.IfcAttributes
    Draft.select(c)
    return c


def setAsSubcomponent(obj):
    """Sets the given object properly to become a subcomponent (addition, subtraction)
    of an Arch component"""
    Draft.ungroup(obj)
    if params.get_param_arch("applyConstructionStyle"):
        if FreeCAD.GuiUp:
            color = getDefaultColor("Construction")
            if hasattr(obj.ViewObject, "LineColor"):
                obj.ViewObject.LineColor = color
            if hasattr(obj.ViewObject, "PointColor"):
                obj.ViewObject.PointColor = color
            if hasattr(obj.ViewObject, "ShapeColor"):
                obj.ViewObject.ShapeColor = color
            if hasattr(obj.ViewObject, "Transparency"):
                obj.ViewObject.Transparency = int(color[3] * 100)
            obj.ViewObject.hide()


def copyProperties(obj1, obj2):
    """copyProperties(obj1,obj2): Copies properties values from obj1 to obj2,
    when that property exists in both objects"""
    for prop in obj1.PropertiesList:
        if prop in obj2.PropertiesList:
            if not prop in ["Proxy", "Shape"]:
                setattr(obj2, prop, getattr(obj1, prop))
    if obj1.ViewObject and obj2.ViewObject:
        for prop in obj1.ViewObject.PropertiesList:
            if prop in obj2.ViewObject.PropertiesList:
                if not prop in ["Proxy", "Shape"]:
                    setattr(obj2.ViewObject, prop, getattr(obj1.ViewObject, prop))


def splitMesh(obj, mark=True):
    """splitMesh(object,[mark]): splits the given mesh object into separated components.
    If mark is False, nothing else is done. If True (default), non-manifold components
    will be painted in red."""
    if not obj.isDerivedFrom("Mesh::Feature"):
        return []
    basemesh = obj.Mesh
    comps = basemesh.getSeparateComponents()
    nlist = []
    if comps:
        basename = obj.Name
        FreeCAD.ActiveDocument.removeObject(basename)
        for c in comps:
            newobj = FreeCAD.ActiveDocument.addObject("Mesh::Feature", basename)
            newobj.Mesh = c
            if mark and (not (c.isSolid()) or c.hasNonManifolds()):
                newobj.ViewObject.ShapeColor = (1.0, 0.0, 0.0, 1.0)
            nlist.append(newobj)
        return nlist
    return [obj]


def makeFace(wires, method=2, cleanup=False):
    """makeFace(wires): makes a face from a list of wires, finding which ones are holes"""
    # print("makeFace: start:", wires)
    import Part
    import DraftGeomUtils

    if not isinstance(wires, list):
        if len(wires.Vertexes) < 3:
            raise
        return Part.Face(wires)
    elif len(wires) == 1:
        # import Draft;Draft.printShape(wires[0])
        if len(wires[0].Vertexes) < 3:
            raise
        return Part.Face(wires[0])

    wires = wires[:]

    # print("makeFace: inner wires found")
    ext = None
    max_length = 0
    # cleaning up rubbish in wires
    if cleanup:
        for i in range(len(wires)):
            wires[i] = DraftGeomUtils.removeInterVertices(wires[i])
        # print("makeFace: garbage removed")
    for w in wires:
        # we assume that the exterior boundary is that one with
        # the biggest bounding box
        if w.BoundBox.DiagonalLength > max_length:
            max_length = w.BoundBox.DiagonalLength
            ext = w
    # print("makeFace: exterior wire", ext)
    wires.remove(ext)

    if method == 1:
        # method 1: reverse inner wires
        # all interior wires mark a hole and must reverse
        # their orientation, otherwise Part.Face fails
        for w in wires:
            # print("makeFace: reversing", w)
            w.reverse()
            # make sure that the exterior wires comes as first in the list
        wires.insert(0, ext)
        # print("makeFace: done sorting", wires)
        if wires:
            return Part.Face(wires)
    else:
        # method 2: use the cut method
        mf = Part.Face(ext)
        # print("makeFace: external face:", mf)
        for w in wires:
            f = Part.Face(w)
            # print("makeFace: internal face:", f)
            mf = mf.cut(f)
        # print("makeFace: final face:", mf.Faces)
        return mf.Faces[0]


def closeHole(shape):
    """closeHole(shape): closes a hole in an open shape"""
    import DraftGeomUtils
    import Part

    # creating an edges lookup table
    lut = {}
    for face in shape.Faces:
        for edge in face.Edges:
            hc = edge.hashCode()
            if hc in lut:
                lut[hc] = lut[hc] + 1
            else:
                lut[hc] = 1
    # filter out the edges shared by more than one face
    bound = []
    for e in shape.Edges:
        if lut[e.hashCode()] == 1:
            bound.append(e)
    bound = Part.__sortEdges__(bound)
    try:
        nface = Part.Face(Part.Wire(bound))
        shell = Part.makeShell(shape.Faces + [nface])
        solid = Part.Solid(shell)
    except Part.OCCError:
        raise
    else:
        return solid


def getCutVolume(cutplane, shapes, clip=False, depth=None):
    """getCutVolume(cutplane,shapes,[clip,depth]): returns a cut face and a cut volume
    from the given shapes and the given cutting plane. If clip is True, the cutvolume will
    also cut off everything outside the cutplane projection. If depth is non-zero, geometry
    further than this distance will be clipped off"""
    if not shapes:
        return None, None, None
    if not cutplane.Faces:
        return None, None, None
    import Part

    if not isinstance(shapes, list):
        shapes = [shapes]
    # building boundbox
    bb = shapes[0].optimalBoundingBox()
    for sh in shapes[1:]:
        bb.add(sh.optimalBoundingBox())
    bb.enlarge(1)
    # building cutplane space
    um = vm = wm = 0
    try:
        if hasattr(cutplane, "Shape"):
            p = cutplane.Shape.copy().Faces[0]
        else:
            p = cutplane.copy().Faces[0]
    except Part.OCCError:
        FreeCAD.Console.PrintMessage(translate("Arch", "Invalid cut plane") + "\n")
        return None, None, None
    ce = p.CenterOfMass
    ax = p.normalAt(0, 0)
    prm_range = p.ParameterRange  # (uMin, uMax, vMin, vMax)
    u = p.valueAt(prm_range[0], 0).sub(p.valueAt(prm_range[1], 0)).normalize()
    v = u.cross(ax)
    if not bb.isCutPlane(ce, ax):
        # FreeCAD.Console.PrintMessage(translate("Arch","No objects are cut by the plane)+"\n")
        return None, None, None
    else:
        corners = [
            FreeCAD.Vector(bb.XMin, bb.YMin, bb.ZMin),
            FreeCAD.Vector(bb.XMin, bb.YMax, bb.ZMin),
            FreeCAD.Vector(bb.XMax, bb.YMin, bb.ZMin),
            FreeCAD.Vector(bb.XMax, bb.YMax, bb.ZMin),
            FreeCAD.Vector(bb.XMin, bb.YMin, bb.ZMax),
            FreeCAD.Vector(bb.XMin, bb.YMax, bb.ZMax),
            FreeCAD.Vector(bb.XMax, bb.YMin, bb.ZMax),
            FreeCAD.Vector(bb.XMax, bb.YMax, bb.ZMax),
        ]
        for c in corners:
            dv = c.sub(ce)
            um1 = DraftVecUtils.project(dv, u).Length
            um = max(um, um1)
            vm1 = DraftVecUtils.project(dv, v).Length
            vm = max(vm, vm1)
            wm1 = DraftVecUtils.project(dv, ax).Length
            wm = max(wm, wm1)
        vu = DraftVecUtils.scaleTo(u, um)
        vui = vu.negative()
        vv = DraftVecUtils.scaleTo(v, vm)
        vvi = vv.negative()
        p1 = ce.add(vu.add(vvi))
        p2 = ce.add(vu.add(vv))
        p3 = ce.add(vui.add(vv))
        p4 = ce.add(vui.add(vvi))
        cutface = Part.makePolygon([p1, p2, p3, p4, p1])
        cutface = Part.Face(cutface)
        cutnormal = DraftVecUtils.scaleTo(ax, wm)
        cutvolume = cutface.extrude(cutnormal)
        cutnormal = cutnormal.negative()
        invcutvolume = cutface.extrude(cutnormal)
        if clip:
            extrudedplane = p.extrude(cutnormal)
            bordervolume = invcutvolume.cut(extrudedplane)
            cutvolume = cutvolume.fuse(bordervolume)
            cutvolume = cutvolume.removeSplitter()
            invcutvolume = extrudedplane
            cutface = p
        if depth:
            depthnormal = DraftVecUtils.scaleTo(cutnormal, depth)
            depthvolume = cutface.extrude(depthnormal)
            depthclipvolume = invcutvolume.cut(depthvolume)
            cutvolume = cutvolume.fuse(depthclipvolume)
            cutvolume = cutvolume.removeSplitter()
        return cutface, cutvolume, invcutvolume


def getShapeFromMesh(mesh, fast=True, tolerance=0.001, flat=False, cut=True):
    import Part
    import MeshPart
    import DraftGeomUtils

    if mesh.isSolid() and (mesh.countComponents() == 1) and fast:
        # use the best method
        faces = []
        for f in mesh.Facets:
            p = f.Points + [f.Points[0]]
            pts = []
            for pp in p:
                pts.append(FreeCAD.Vector(pp[0], pp[1], pp[2]))
            try:
                f = Part.Face(Part.makePolygon(pts))
            except Exception:
                print("getShapeFromMesh: error building face from polygon")
                # pass
            else:
                faces.append(f)
        shell = Part.makeShell(faces)
        try:
            solid = Part.Solid(shell)
        except Part.OCCError:
            print("getShapeFromMesh: error creating solid")
        else:
            try:
                solid = solid.removeSplitter()
            except Part.OCCError:
                print("getShapeFromMesh: error removing splitter")
                # pass
            return solid

    # if not mesh.isSolid():
    #    print "getShapeFromMesh: non-solid mesh, using slow method"
    faces = []
    segments = mesh.getPlanarSegments(tolerance)
    # print(len(segments))
    for i in segments:
        if len(i) > 0:
            wires = MeshPart.wireFromSegment(mesh, i)
            if wires:
                if flat:
                    nwires = []
                    for w in wires:
                        nwires.append(DraftGeomUtils.flattenWire(w))
                    wires = nwires
                try:
                    faces.append(makeFace(wires, method=int(cut) + 1))
                except Exception:
                    return None
    try:
        se = Part.makeShell(faces)
        se = se.removeSplitter()
        if flat:
            return se
    except Part.OCCError:
        print("getShapeFromMesh: error removing splitter")
        try:
            cp = Part.makeCompound(faces)
        except Part.OCCError:
            print("getShapeFromMesh: error creating compound")
            return None
        else:
            return cp
    else:
        try:
            solid = Part.Solid(se)
        except Part.OCCError:
            print("getShapeFromMesh: error creating solid")
            return se
        else:
            if solid.isClosed():
                return solid
            else:
                return se


def projectToVector(shape, vector):
    """projectToVector(shape,vector): projects the given shape on the given
    vector"""
    projpoints = []
    minl = 10000000000
    maxl = -10000000000
    for v in shape.Vertexes:
        p = DraftVecUtils.project(v.Point, vector)
        projpoints.append(p)
        l = p.Length
        if p.getAngle(vector) > 1:
            l = -l
        if l > maxl:
            maxl = l
        if l < minl:
            minl = l
    return DraftVecUtils.scaleTo(vector, maxl - minl)


def meshToShape(obj, mark=True, fast=True, tol=0.001, flat=False, cut=True):
    """meshToShape(object,[mark,fast,tol,flat,cut]): turns a mesh into a shape, joining coplanar facets. If
    mark is True (default), non-solid objects will be marked in red. Fast uses a faster algorithm by
    building a shell from the facets then removing splitter, tol is the tolerance used when converting
    mesh segments to wires, flat will force the wires to be perfectly planar, to be sure they can be
    turned into faces, but this might leave gaps in the final shell. If cut is true, holes in faces are
    made by subtraction (default)"""

    name = obj.Name
    if "Mesh" in obj.PropertiesList:
        mesh = obj.Mesh
        # plac = obj.Placement
        solid = getShapeFromMesh(mesh, fast, tol, flat, cut)
        if solid:
            if solid.isClosed() and solid.isValid():
                FreeCAD.ActiveDocument.removeObject(name)
            newobj = FreeCAD.ActiveDocument.addObject("Part::Feature", name)
            newobj.Shape = solid
            # newobj.Placement = plac #the placement is already computed in the mesh
            if (not solid.isClosed()) or (not solid.isValid()):
                if mark:
                    newobj.ViewObject.ShapeColor = (1.0, 0.0, 0.0, 1.0)
            return newobj
    return None


def removeCurves(shape, dae=False, tolerance=5):
    """removeCurves(shape,dae,tolerance=5): replaces curved faces in a shape
    with faceted segments. If dae is True, DAE triangulation options are used"""
    import Mesh

    if dae:
        from importers import importDAE

        t = importDAE.triangulate(shape.cleaned())
    else:
        t = shape.cleaned().tessellate(tolerance)
    m = Mesh.Mesh(t)
    return getShapeFromMesh(m)


def removeShape(objs, mark=True):
    """removeShape(objs,mark=True): takes an arch object (wall or structure) built on a cubic shape, and removes
    the inner shape, keeping its length, width and height as parameters. If mark is True, objects that cannot
    be processed by this function will become red."""
    import DraftGeomUtils

    if not isinstance(objs, list):
        objs = [objs]
    for obj in objs:
        if DraftGeomUtils.isCubic(obj.Shape):
            dims = DraftGeomUtils.getCubicDimensions(obj.Shape)
            if dims:
                name = obj.Name
                tp = Draft.getType(obj)
                print(tp)
                if tp == "Structure":
                    FreeCAD.ActiveDocument.removeObject(name)
                    import ArchStructure

                    str = ArchStructure.makeStructure(
                        length=dims[1], width=dims[2], height=dims[3], name=name
                    )
                    str.Placement = dims[0]
                elif tp == "Wall":
                    FreeCAD.ActiveDocument.removeObject(name)
                    import Arch

                    length = dims[1]
                    width = dims[2]
                    v1 = Vector(length / 2, 0, 0)
                    v2 = v1.negative()
                    v1 = dims[0].multVec(v1)
                    v2 = dims[0].multVec(v2)
                    line = Draft.makeLine(v1, v2)
                    Arch.makeWall(line, width=width, height=dims[3], name=name)
        else:
            if mark:
                obj.ViewObject.ShapeColor = (1.0, 0.0, 0.0, 1.0)


def mergeCells(objectslist):
    """mergeCells(objectslist): merges the objects in the given list
    into one. All objects must be of the same type and based on the Cell
    object (cells, floors, buildings, or sites)."""
    if not objectslist:
        return None
    if not isinstance(objectslist, list):
        return None
    if len(objectslist) < 2:
        return None
    typ = Draft.getType(objectslist[0])
    if not (typ in ["Cell", "Floor", "Building", "Site"]):
        return None
    for o in objectslist:
        if Draft.getType(o) != typ:
            return None
    base = objectslist.pop(0)
    for o in objectslist:
        l = base.Components
        for c in o.Components:
            if not c in l:
                l.append(c)
        base.Components = l
        FreeCAD.ActiveDocument.removeObject(o.Name)
    FreeCAD.ActiveDocument.recompute()
    return base


def download(url, force=False):
    """download(url,force=False): downloads a file from the given URL and saves it in the
    macro path. Returns the path to the saved file. If force is True, the file will be
    downloaded again evn if it already exists."""
    from urllib.request import urlopen
    import os

    name = url.split("/")[-1]
    macropath = FreeCAD.getUserMacroDir(True)
    filepath = os.path.join(macropath, name)
    if os.path.exists(filepath) and not (force):
        return filepath
    try:
        FreeCAD.Console.PrintMessage("Downloading " + url + " …\n")
        response = urlopen(url)
        s = response.read()
        f = open(filepath, "wb")
        f.write(s)
        f.close()
    except Exception:
        return None
    else:
        return filepath


def check(objectslist, includehidden=False):
    """check(objectslist,includehidden=False): checks if the given objects contain only solids"""
    objs = Draft.get_group_contents(objectslist)
    if not includehidden:
        objs = Draft.removeHidden(objs)
    bad = []
    for o in objs:
        if not hasattr(o, "Shape"):
            bad.append([o, "is not a Part-based object"])
        else:
            s = o.Shape
            if (not s.isClosed()) and (not (Draft.getType(o) == "Axis")):
                bad.append([o, translate("Arch", "is not closed")])
            elif not s.isValid():
                bad.append([o, translate("Arch", "is not valid")])
            elif (not s.Solids) and (not (Draft.getType(o) == "Axis")):
                bad.append([o, translate("Arch", "does not contain any solid")])
            else:
                f = 0
                for sol in s.Solids:
                    f += len(sol.Faces)
                    if not sol.isClosed():
                        bad.append([o, translate("Arch", "contains a non-closed solid")])
                if len(s.Faces) != f:
                    bad.append(
                        [o, translate("Arch", "contains faces that are not part of any solid")]
                    )
    return bad


def getHost(obj, strict=True):
    """getHost(obj,[strict]): returns the host of the current object. If strict is true (default),
    the host can only be an object of a higher level than the given one, or in other words, if a wall
    is contained in another wall which is part of a floor, the floor is returned instead of the parent wall
    """
    import Draft

    t = Draft.getType(obj)
    for par in obj.InList:
        if par.isDerivedFrom("Part::Feature") or par.isDerivedFrom("App::DocumentObjectGroup"):
            if strict:
                if Draft.getType(par) != t:
                    return par
                else:
                    return getHost(par, strict)
            else:
                return par
    return None


def pruneIncluded(objectslist, strict=False, silent=False):
    """pruneIncluded(objectslist,[strict]): removes from a list of Arch objects, those that are subcomponents of
    another shape-based object, leaving only the top-level shapes. If strict is True, the object
    is removed only if the parent is also part of the selection."""
    import Draft

    newlist = []
    for obj in objectslist:
        toplevel = True
        if obj.isDerivedFrom("Part::Feature"):
            if Draft.getType(obj) not in ["Window", "Clone", "Pipe", "Rebar", "Roof"]:
                for parent in obj.InList:
                    if not parent.isDerivedFrom("Part::Feature"):
                        pass
                    elif parent.isDerivedFrom("Part::Part2DObject"):
                        # don't consider 2D objects based on arch elements
                        pass
                    elif Draft.getType(parent) in [
                        "BezCurve",
                        "BSpline",
                        "Clone",
                        "Facebinder",
                        "Wire",
                        "Project",
                        "Roof",
                        "Site",
                        "Space",
                        "Window",
                    ]:
                        pass
                    elif parent.isDerivedFrom("PartDesign::FeatureBase"):
                        # don't consider a PartDesign_Clone that references obj
                        pass
                    elif parent.isDerivedFrom("PartDesign::Body") and obj == parent.BaseFeature:
                        # don't consider a PartDesign_Body with a PartDesign_Clone that references obj
                        pass
                    elif parent.isDerivedFrom("PartDesign::SubShapeBinder") or (
                        hasattr(parent, "TypeId") and parent.TypeId == "PartDesign::ShapeBinder"
                    ):
                        # don't consider a PartDesign_SubShapeBinder or PartDesign_ShapeBinder referencing this object from another object
                        pass
                    elif hasattr(parent, "Host") and parent.Host == obj:
                        pass
                    elif hasattr(parent, "Hosts") and obj in parent.Hosts:
                        pass
                    elif hasattr(parent, "TypeId") and parent.TypeId == "Part::Mirroring":
                        pass
                    elif hasattr(parent, "CloneOf"):
                        if parent.CloneOf:
                            if parent.CloneOf.Name != obj.Name:
                                toplevel = False
                        else:
                            toplevel = False
                    else:
                        toplevel = False

                    if toplevel == False and strict:
                        if parent not in objectslist and parent not in newlist:
                            toplevel = True
        if toplevel:
            newlist.append(obj)
        elif not silent:
            FreeCAD.Console.PrintWarning("pruning " + obj.Label + "\n")
    return newlist


def getAllChildren(objectlist):
    "getAllChildren(objectlist): returns all the children of all the object sin the list"
    obs = []
    for o in objectlist:
        if not o in obs:
            obs.append(o)
        if o.OutList:
            l = getAllChildren(o.OutList)
            for c in l:
                if not c in obs:
                    obs.append(c)
    return obs


def get_architectural_contents(
    initial_objects,
    recursive=True,
    discover_hosted_elements=True,
    include_components_from_additions=False,
    include_initial_objects_in_result=True,
):
    """
    Retrieves a flat list of unique architectural objects that are considered "contents" of or are
    related to the given initial_objects.

    This includes:
    - Children from .Group properties (hierarchical traversal if recursive=True).
    - Architecturally hosted elements (e.g., windows in walls, rebars in structures)
      if discover_hosted_elements=True.
    - Optionally, components from .Additions properties.

    The traversal uses a queue and ensures objects are processed only once.

    Parameters:
    -----------
    initial_objects : App::DocumentObject or list of App::DocumentObject
        The starting object(s) from which to discover contents.
    recursive : bool, optional
        If True (default), recursively find contents of discovered group-like
        objects (those with a .Group property and identified by draftutils.groups.is_group()).
    discover_hosted_elements : bool, optional
        If True (default), try to find elements like windows, doors, rebars
        that are architecturally hosted by other elements encountered during traversal.
        This relies on Draft.getType() and common Arch properties like .Hosts or .Host.
    include_components_from_additions : bool, optional
        If False (default), objects found in .Additions lists are not added.
        Set to True if these components should be part of the discovered contents.
    include_initial_objects_in_result : bool, optional
        If True (default), the objects in initial_objects themselves will be
        included in the output list (if not already discovered as a child of another).
        If False, only their "descendant" contents are returned.

    Returns:
    --------
    list of App::DocumentObject
        A flat list of unique architectural document objects.
    """

    final_contents_list = []
    queue = []

    if not isinstance(initial_objects, list):
        initial_objects_list = [initial_objects]
    else:
        initial_objects_list = list(initial_objects)  # Make a copy

    queue.extend(initial_objects_list)

    # Set to keep track of object names already added to the queue or fully processed
    # This prevents duplicates in the queue and reprocessing.
    processed_or_queued_names = set()
    for (
        item
    ) in initial_objects_list:  # Pre-populate for initial items if they are to be added later
        processed_or_queued_names.add(item.Name)

    idx = 0  # Use an index for iterating the queue, as it can grow
    while idx < len(queue):
        obj = queue[idx]
        idx += 1

        # Add the current object to the final list if it's not already there.
        # The decision to include initial_objects is handled by how they are first added to queue
        # and this check.
        if obj not in final_contents_list:
            is_initial = obj in initial_objects_list
            if (is_initial and include_initial_objects_in_result) or not is_initial:
                final_contents_list.append(obj)

        children_to_add_to_queue_next = []

        # 1. Hierarchical children from .Group (if recursive)
        if recursive and is_group(obj) and hasattr(obj, "Group") and obj.Group:
            for child in obj.Group:
                if child.Name not in processed_or_queued_names:
                    children_to_add_to_queue_next.append(child)
                    processed_or_queued_names.add(child.Name)  # Mark as queued

        # 2. Architecturally-hosted elements (if discover_hosted_elements)
        if discover_hosted_elements:
            host_types = ["Wall", "Structure", "CurtainWall", "Precast", "Panel", "Roof"]
            if Draft.getType(obj) in host_types:
                # Hosted elements are typically in the host's InList
                for item_in_inlist in obj.InList:
                    element_to_check = item_in_inlist
                    if hasattr(item_in_inlist, "getLinkedObject"):  # Resolve App::Link
                        linked = item_in_inlist.getLinkedObject()
                        if linked:
                            element_to_check = linked

                    if element_to_check.Name in processed_or_queued_names:
                        continue

                    is_confirmed_hosted = False
                    element_type = Draft.getType(element_to_check)

                    if element_type == "Window":  # This covers Arch Windows and Arch Doors
                        if hasattr(element_to_check, "Hosts") and obj in element_to_check.Hosts:
                            is_confirmed_hosted = True
                    elif element_type == "Rebar":
                        if hasattr(element_to_check, "Host") and obj == element_to_check.Host:
                            is_confirmed_hosted = True

                    if is_confirmed_hosted:
                        children_to_add_to_queue_next.append(element_to_check)
                        processed_or_queued_names.add(element_to_check.Name)  # Mark as queued

        # 3. Components from .Additions list (e.g., walls added to a main wall)
        if include_components_from_additions and hasattr(obj, "Additions") and obj.Additions:
            for addition_comp in obj.Additions:
                actual_addition = addition_comp
                if hasattr(addition_comp, "getLinkedObject"):  # Resolve if Addition is an App::Link
                    linked_add = addition_comp.getLinkedObject()
                    if linked_add:
                        actual_addition = linked_add

                if actual_addition.Name not in processed_or_queued_names:
                    children_to_add_to_queue_next.append(actual_addition)
                    processed_or_queued_names.add(actual_addition.Name)  # Mark as queued

        if children_to_add_to_queue_next:
            # Add newly-discovered children to the end of the queue. This function uses an index
            # (idx) to iterate through the queue, and queue.extend() adds new items to the end. This
            # results in a breadth-first-like traversal for objects discovered at the same "depth"
            # from different parent branches. Children of the current 'obj' will be processed after
            # 'obj's current siblings in the queue.
            queue.extend(children_to_add_to_queue_next)

    return final_contents_list


def survey(callback=False):
    """survey(): starts survey mode, where you can click edges and faces to get their lengths or area.
    Clicking on no object (on an empty area) resets the count."""
    if not callback:
        if hasattr(FreeCAD, "SurveyObserver"):
            for label in FreeCAD.SurveyObserver.labels:
                FreeCAD.ActiveDocument.removeObject(label)
            FreeCADGui.Selection.removeObserver(FreeCAD.SurveyObserver)
            del FreeCAD.SurveyObserver
            FreeCADGui.Control.closeDialog()
            if hasattr(FreeCAD, "SurveyDialog"):
                del FreeCAD.SurveyDialog
        else:
            FreeCAD.SurveyObserver = _SurveyObserver(callback=survey)
            FreeCADGui.Selection.addObserver(FreeCAD.SurveyObserver)
            FreeCAD.SurveyDialog = SurveyTaskPanel()
            FreeCADGui.Control.showDialog(FreeCAD.SurveyDialog)
    else:
        sel = FreeCADGui.Selection.getSelectionEx()
        if hasattr(FreeCAD, "SurveyObserver"):
            if not sel:
                if FreeCAD.SurveyObserver.labels:
                    for label in FreeCAD.SurveyObserver.labels:
                        FreeCAD.ActiveDocument.removeObject(label)
                    tl = FreeCAD.SurveyObserver.totalLength
                    ta = FreeCAD.SurveyObserver.totalArea
                    FreeCAD.SurveyObserver.labels = []
                    FreeCAD.SurveyObserver.selection = []
                    FreeCAD.SurveyObserver.totalLength = 0
                    FreeCAD.SurveyObserver.totalArea = 0
                    FreeCAD.SurveyObserver.totalVolume = 0
                    if not FreeCAD.SurveyObserver.cancellable:
                        FreeCAD.Console.PrintMessage("\n---- Reset ----\n\n")
                        FreeCAD.SurveyObserver.cancellable = True
                        if hasattr(FreeCAD, "SurveyDialog"):
                            FreeCAD.SurveyDialog.newline(tl, ta)
                    else:
                        FreeCADGui.Selection.removeObserver(FreeCAD.SurveyObserver)
                        del FreeCAD.SurveyObserver
                        FreeCADGui.Control.closeDialog()
                        if hasattr(FreeCAD, "SurveyDialog"):
                            del FreeCAD.SurveyDialog
            else:
                FreeCAD.SurveyObserver.cancellable = False
                basesel = FreeCAD.SurveyObserver.selection
                newsels = []
                for o in sel:
                    found = False
                    for eo in basesel:
                        if o.ObjectName == eo.ObjectName:
                            if o.SubElementNames == eo.SubElementNames:
                                found = True
                    if not found:
                        newsels.append(o)
                if newsels:
                    for o in newsels:
                        if hasattr(o.Object, "Shape"):
                            n = o.Object.Label
                            showUnit = params.get_param_arch("surveyUnits")
                            t = ""
                            u = FreeCAD.Units.Quantity()
                            if not o.HasSubObjects:
                                # entire object
                                anno = FreeCAD.ActiveDocument.addObject(
                                    "App::AnnotationLabel", "surveyLabel"
                                )
                                if hasattr(o.Object.Shape, "CenterOfMass"):
                                    anno.BasePosition = o.Object.Shape.CenterOfMass
                                else:
                                    anno.BasePosition = o.Object.Shape.BoundBox.Center
                                FreeCAD.SurveyObserver.labels.append(anno.Name)
                                if o.Object.Shape.Solids:
                                    u = FreeCAD.Units.Quantity(
                                        o.Object.Shape.Volume, FreeCAD.Units.Volume
                                    )
                                    t = u.getUserPreferred()[0]
                                    t = t.replace("^3", "³")
                                    anno.LabelText = "v " + t
                                    FreeCAD.Console.PrintMessage(
                                        "Object: " + n + ", Element: Whole, Volume: " + t + "\n"
                                    )
                                    FreeCAD.SurveyObserver.totalVolume += u.Value
                                elif o.Object.Shape.Faces:
                                    u = FreeCAD.Units.Quantity(
                                        o.Object.Shape.Area, FreeCAD.Units.Area
                                    )
                                    t = u.getUserPreferred()[0]
                                    t = t.replace("^2", "²")
                                    anno.LabelText = "a " + t
                                    FreeCAD.Console.PrintMessage(
                                        "Object: " + n + ", Element: Whole, Area: " + t + "\n"
                                    )
                                    FreeCAD.SurveyObserver.totalArea += u.Value
                                    if hasattr(FreeCAD, "SurveyDialog"):
                                        FreeCAD.SurveyDialog.update(2, t)
                                else:
                                    u = FreeCAD.Units.Quantity(
                                        o.Object.Shape.Length, FreeCAD.Units.Length
                                    )
                                    t = u.getUserPreferred()[0]
                                    anno.LabelText = "l " + t
                                    FreeCAD.Console.PrintMessage(
                                        "Object: " + n + ", Element: Whole, Length: " + t + "\n"
                                    )
                                    FreeCAD.SurveyObserver.totalLength += u.Value
                                    if hasattr(FreeCAD, "SurveyDialog"):
                                        FreeCAD.SurveyDialog.update(1, t)
                                if FreeCAD.GuiUp and t:
                                    if showUnit:
                                        QtGui.QApplication.clipboard().setText(t)
                                    else:
                                        QtGui.QApplication.clipboard().setText(str(u.Value))
                            else:
                                # single element(s)
                                for el in o.SubElementNames:
                                    e = getattr(o.Object.Shape, el)
                                    anno = FreeCAD.ActiveDocument.addObject(
                                        "App::AnnotationLabel", "surveyLabel"
                                    )
                                    if "Vertex" in el:
                                        anno.BasePosition = e.Point
                                    else:
                                        if hasattr(e, "CenterOfMass"):
                                            anno.BasePosition = e.CenterOfMass
                                        else:
                                            anno.BasePosition = e.BoundBox.Center
                                    FreeCAD.SurveyObserver.labels.append(anno.Name)
                                    if "Face" in el:
                                        u = FreeCAD.Units.Quantity(e.Area, FreeCAD.Units.Area)
                                        t = u.getUserPreferred()[0]
                                        t = t.replace("^2", "²")
                                        anno.LabelText = "a " + t
                                        FreeCAD.Console.PrintMessage(
                                            "Object: "
                                            + n
                                            + ", Element: "
                                            + el
                                            + ", Area: "
                                            + t
                                            + "\n"
                                        )
                                        FreeCAD.SurveyObserver.totalArea += u.Value
                                        if hasattr(FreeCAD, "SurveyDialog"):
                                            FreeCAD.SurveyDialog.update(2, t)
                                    elif "Edge" in el:
                                        u = FreeCAD.Units.Quantity(e.Length, FreeCAD.Units.Length)
                                        t = u.getUserPreferred()[0]
                                        anno.LabelText = "l " + t
                                        FreeCAD.Console.PrintMessage(
                                            "Object: "
                                            + n
                                            + ", Element: "
                                            + el
                                            + ", Length: "
                                            + t
                                            + "\n"
                                        )
                                        FreeCAD.SurveyObserver.totalLength += u.Value
                                        if hasattr(FreeCAD, "SurveyDialog"):
                                            FreeCAD.SurveyDialog.update(1, t)
                                    elif "Vertex" in el:
                                        u = FreeCAD.Units.Quantity(e.Z, FreeCAD.Units.Length)
                                        t = u.getUserPreferred()[0]
                                        anno.LabelText = "z " + t
                                        FreeCAD.Console.PrintMessage(
                                            "Object: "
                                            + n
                                            + ", Element: "
                                            + el
                                            + ", Zcoord: "
                                            + t
                                            + "\n"
                                        )
                                    if FreeCAD.GuiUp and t:
                                        if showUnit:
                                            QtGui.QApplication.clipboard().setText(t)
                                        else:
                                            QtGui.QApplication.clipboard().setText(str(u.Value))

                    FreeCAD.SurveyObserver.selection.extend(newsels)
            if hasattr(FreeCAD, "SurveyObserver"):
                if (
                    FreeCAD.SurveyObserver.totalLength
                    or FreeCAD.SurveyObserver.totalArea
                    or FreeCAD.SurveyObserver.totalVolume
                ):
                    msg = " Total:"
                    if FreeCAD.SurveyObserver.totalLength:
                        u = FreeCAD.Units.Quantity(
                            FreeCAD.SurveyObserver.totalLength, FreeCAD.Units.Length
                        )
                        t = u.getUserPreferred()[0]
                        msg += " Length: " + t
                    if FreeCAD.SurveyObserver.totalArea:
                        u = FreeCAD.Units.Quantity(
                            FreeCAD.SurveyObserver.totalArea, FreeCAD.Units.Area
                        )
                        t = u.getUserPreferred()[0]
                        t = t.replace("^2", "²")
                        msg += " Area: " + t
                    if FreeCAD.SurveyObserver.totalVolume:
                        u = FreeCAD.Units.Quantity(
                            FreeCAD.SurveyObserver.totalVolume, FreeCAD.Units.Volume
                        )
                        t = u.getUserPreferred()[0]
                        t = t.replace("^3", "³")
                        msg += " Volume: " + t
                    FreeCAD.Console.PrintMessage(msg + "\n")


class _SurveyObserver:
    "an observer for the survey() function"

    def __init__(self, callback):
        self.callback = callback
        self.selection = []
        self.labels = []
        self.totalLength = 0
        self.totalArea = 0
        self.totalVolume = 0
        self.cancellable = False
        self.doubleclear = False

    def addSelection(self, document, object, element, position):
        self.doubleclear = False
        self.callback(True)

    def clearSelection(self, document):
        if not self.doubleclear:
            self.doubleclear = True
        else:
            self.callback(True)


class SurveyTaskPanel:
    "A task panel for the survey tool"

    def __init__(self):
        self.form = QtGui.QWidget()
        self.form.setWindowIcon(QtGui.QIcon(":/icons/Arch_Survey.svg"))
        layout = QtGui.QVBoxLayout(self.form)
        llayout = QtGui.QHBoxLayout()
        self.descr = QtGui.QLineEdit()
        llayout.addWidget(self.descr)
        self.addButton = QtGui.QPushButton()
        llayout.addWidget(self.addButton)
        layout.addLayout(llayout)
        self.tree = QtGui.QTreeWidget()
        self.tree.setColumnCount(3)
        layout.addWidget(self.tree)
        blayout = QtGui.QHBoxLayout()
        self.clearButton = QtGui.QPushButton()
        blayout.addWidget(self.clearButton)
        self.copyLength = QtGui.QPushButton()
        blayout.addWidget(self.copyLength)
        self.copyArea = QtGui.QPushButton()
        blayout.addWidget(self.copyArea)
        layout.addLayout(blayout)
        self.export = QtGui.QPushButton()
        layout.addWidget(self.export)
        QtCore.QObject.connect(self.addButton, QtCore.SIGNAL("clicked()"), self.setText)
        QtCore.QObject.connect(self.clearButton, QtCore.SIGNAL("clicked()"), self.clear)
        QtCore.QObject.connect(self.copyLength, QtCore.SIGNAL("clicked()"), self.clipLength)
        QtCore.QObject.connect(self.copyArea, QtCore.SIGNAL("clicked()"), self.clipArea)
        QtCore.QObject.connect(self.export, QtCore.SIGNAL("clicked()"), self.exportCSV)
        QtCore.QObject.connect(
            self.tree, QtCore.SIGNAL("itemClicked(QTreeWidgetItem*,int)"), self.setDescr
        )
        self.retranslateUi(self)
        item = QtGui.QTreeWidgetItem(self.tree)
        self.tree.setCurrentItem(item)

    def retranslateUi(self, dlg):
        self.form.setWindowTitle(QtGui.QApplication.translate("Arch", "Survey", None))
        self.addButton.setText(QtGui.QApplication.translate("Arch", "Set Description", None))
        self.clearButton.setText(QtGui.QApplication.translate("Arch", "Clear", None))
        self.copyLength.setText(QtGui.QApplication.translate("Arch", "Copy Total Length", None))
        self.copyArea.setText(QtGui.QApplication.translate("Arch", "Copy Total Area", None))
        self.export.setText(QtGui.QApplication.translate("Arch", "Export CSV", None))
        self.tree.setHeaderLabels(
            [
                QtGui.QApplication.translate("Arch", "Description", None),
                QtGui.QApplication.translate("Arch", "Length", None),
                QtGui.QApplication.translate("Arch", "Area", None),
            ]
        )

    def isAllowedAlterSelection(self):
        return True

    def isAllowedAlterView(self):
        return True

    def getStandardButtons(self):
        return QtGui.QDialogButtonBox.Close

    def reject(self):
        if hasattr(FreeCAD, "SurveyObserver"):
            for label in FreeCAD.SurveyObserver.labels:
                FreeCAD.ActiveDocument.removeObject(label)
            FreeCADGui.Selection.removeObserver(FreeCAD.SurveyObserver)
            del FreeCAD.SurveyObserver
        return True

    def clear(self):
        FreeCADGui.Selection.clearSelection()

    def clipLength(self):
        if hasattr(FreeCAD, "SurveyObserver"):
            u = FreeCAD.Units.Quantity(FreeCAD.SurveyObserver.totalLength, FreeCAD.Units.Length)
            t = u.getUserPreferred()[0]
            if params.get_param_arch("surveyUnits"):
                QtGui.QApplication.clipboard().setText(t)
            else:
                QtGui.QApplication.clipboard().setText(str(u.Value / u.getUserPreferred()[1]))

    def clipArea(self):
        if hasattr(FreeCAD, "SurveyObserver"):
            u = FreeCAD.Units.Quantity(FreeCAD.SurveyObserver.totalArea, FreeCAD.Units.Area)
            t = u.getUserPreferred()[0]
            t = t.replace("^2", "²")
            if params.get_param_arch("surveyUnits"):
                QtGui.QApplication.clipboard().setText(t)
            else:
                QtGui.QApplication.clipboard().setText(str(u.Value / u.getUserPreferred()[1]))

    def newline(self, length=0, area=0):
        FreeCADGui.Selection.clearSelection()
        item = QtGui.QTreeWidgetItem(self.tree)
        if length or area:
            item.setText(0, QtGui.QApplication.translate("Arch", "Total", None))
            item.setToolTip(0, "total")
            f = QtGui.QFont()
            f.setBold(True)
            item.setFont(0, f)
            item.setFont(1, f)
            item.setFont(2, f)
        else:
            item.setText(0, self.descr.text())
            self.descr.setText("")
        self.tree.setCurrentItem(item)
        if length:
            u = FreeCAD.Units.Quantity(length, FreeCAD.Units.Length)
            t = u.getUserPreferred()[0]
            item.setText(1, t)
        if area:
            u = FreeCAD.Units.Quantity(area, FreeCAD.Units.Area)
            t = u.getUserPreferred()[0]
            t = t.replace("^2", "²")
            item.setText(2, t)
        if length or area:
            item = QtGui.QTreeWidgetItem(self.tree)
            self.tree.setCurrentItem(item)

    def update(self, column, txt):
        item = QtGui.QTreeWidgetItem(self.tree)
        self.tree.setCurrentItem(item)
        item.setText(column, txt)

    def setDescr(self, item, col):
        self.descr.setText(item.text(0))

    def setText(self):
        item = self.tree.currentItem()
        if item:
            item.setText(0, self.descr.text())
            self.descr.setText("")

    def exportCSV(self):
        import csv

        rows = self.tree.topLevelItemCount()
        if rows:
            filename = QtGui.QFileDialog.getSaveFileName(
                QtGui.QApplication.activeWindow(),
                translate("Arch", "Export CSV File"),
                None,
                "CSV file (*.csv)",
            )
            if filename:
                with open(filename[0].encode("utf8"), "w") as csvfile:
                    csvfile = csv.writer(csvfile, delimiter="\t")
                    suml = 0
                    for i in range(rows):
                        item = self.tree.topLevelItem(i)
                        row = []
                        row.append(item.text(0))
                        if item.text(1):
                            u = FreeCAD.Units.Quantity(item.text(1))
                            if item.toolTip(0) == "total":
                                row.append("=SUM(B" + str(suml + 1) + ":B" + str(i) + ")")
                            else:
                                row.append(u.Value / u.getUserPreferred()[1])
                            row.append(u.getUserPreferred()[2])
                        else:
                            row.extend(["", ""])
                        if item.text(2):
                            t = item.text(2).replace("²", "^2")
                            u = FreeCAD.Units.Quantity(t)
                            if item.toolTip(0) == "total":
                                row.append("=SUM(D" + str(suml + 1) + ":D" + str(i) + ")")
                            else:
                                row.append(u.Value / u.getUserPreferred()[1])
                            row.append(u.getUserPreferred()[2])
                        else:
                            row.extend(["", ""])
                        csvfile.writerow(row)
                        if item.toolTip(0) == "total":
                            suml = i + 1
                print("successfully exported ", filename[0])


def toggleIfcBrepFlag(obj):
    """toggleIfcBrepFlag(obj): toggles the IFC brep flag of the given object, forcing it
    to be exported as brep geometry or not."""
    if not hasattr(obj, "IfcData"):
        FreeCAD.Console.PrintMessage(
            translate("Arch", "Object does not have settable IFC attributes")
        )
    else:
        d = obj.IfcData
        if "FlagForceBrep" in d:
            if d["FlagForceBrep"] == "True":
                d["FlagForceBrep"] = "False"
                FreeCAD.Console.PrintMessage(
                    translate("Arch", "Disabling B-rep force flag of object")
                    + " "
                    + obj.Label
                    + "\n"
                )
            else:
                d["FlagForceBrep"] = "True"
                FreeCAD.Console.PrintMessage(
                    translate("Arch", "Enabling B-rep force flag of object")
                    + " "
                    + obj.Label
                    + "\n"
                )
        else:
            d["FlagForceBrep"] = "True"
            FreeCAD.Console.PrintMessage(
                translate("Arch", "Enabling B-rep force flag of object") + " " + obj.Label + "\n"
            )
        obj.IfcData = d


def makeCompoundFromSelected(objects=None):
    """makeCompoundFromSelected([objects]): Creates a new compound object from the given
    subobjects (faces, edges) or from the selection if objects is None"""
    import FreeCADGui
    import Part

    so = []
    if not objects:
        objects = FreeCADGui.Selection.getSelectionEx()
    if not isinstance(objects, list):
        objects = [objects]
    for o in objects:
        so.extend(o.SubObjects)
    if so:
        c = Part.makeCompound(so)
        Part.show(c)


def cleanArchSplitter(objects=None):
    """cleanArchSplitter([objects]): removes the splitters from the base shapes
    of the given Arch objects or selected Arch objects if objects is None"""
    import FreeCAD
    import FreeCADGui

    if not objects:
        objects = FreeCADGui.Selection.getSelection()
    if not isinstance(objects, list):
        objects = [objects]
    for obj in objects:
        if hasattr(obj, "Shape"):
            if hasattr(obj, "Base"):
                if obj.Base:
                    print("Attempting to clean splitters from ", obj.Label)
                    base = obj.Base.getLinkedObject()
                    if base.isDerivedFrom("Part::Feature"):
                        if not base.Shape.isNull():
                            base.Shape = base.Shape.removeSplitter()
    FreeCAD.ActiveDocument.recompute()


def rebuildArchShape(objects=None):
    """rebuildArchShape([objects]): takes the faces from the base shape of the given (or selected
    if objects is None) Arch objects, and tries to rebuild a valid solid from them."""
    import FreeCAD
    import Part

    if not objects and FreeCAD.GuiUp:
        objects = FreeCADGui.Selection.getSelection()
    if not isinstance(objects, list):
        objects = [objects]
    for obj in objects:
        success = False
        if hasattr(obj, "Shape"):
            if hasattr(obj, "Base"):
                if obj.Base:
                    try:
                        print("Attempting to rebuild ", obj.Label)
                        base = obj.Base.getLinkedObject()
                        if base.isDerivedFrom("Part::Feature"):
                            if not base.Shape.isNull():
                                faces = []
                                for f in base.Shape.Faces:
                                    f2 = Part.Face(f.Wires)
                                    # print("rebuilt face: isValid is ", f2.isValid())
                                    faces.append(f2)
                                if faces:
                                    shell = Part.Shell(faces)
                                    if shell:
                                        # print("rebuilt shell: isValid is ", shell.isValid())
                                        solid = Part.Solid(shell)
                                        if solid:
                                            if not solid.isValid():
                                                solid.sewShape()
                                                solid = Part.Solid(solid)
                                            # print("rebuilt solid: isValid is ",solid.isValid())
                                            if solid.isValid():
                                                base.Shape = solid
                                                success = True
                    except Exception:
                        pass
        if not success:
            print("Failed to rebuild a valid solid for object ", obj.Name)
    FreeCAD.ActiveDocument.recompute()


def getExtrusionData(shape, sortmethod="area"):
    """If a shape has been extruded, returns the base face, and extrusion vector.

    Determines if a shape appears to have been extruded from some base face, and
    extruded at the normal from that base face. IE: it looks like a cuboid.
    https://en.wikipedia.org/wiki/Cuboid#Rectangular_cuboid

    If this is the case, returns what appears to be the base face, and the vector
    used to make that extrusion.

    The base face is determined based on the sortmethod parameter, which can either
    be:

    "area" = Of the faces with the smallest area, the one with the lowest z coordinate.
    "z" = The face with the lowest z coordinate.
    a 3D vector = the face which center is closest to the given 3D point

    Parameters
    ----------
    shape: <Part.Shape>
        Shape to examine.
    sortmethod: {"area", "z"}
        Which sorting algorithm to use to determine the base face.

    Returns
    -------
    Extrusion data: list
        Two item list containing the base face, and the vector used to create the
        extrusion. In that order.
    Failure: None
        Returns None if the object does not appear to be an extrusion.
    """

    if shape.isNull():
        return None
    if not shape.Solids:
        return None
    if len(shape.Faces) < 3:
        return None
    # build faces list with normals
    faces = []
    import Part

    for f in shape.Faces:
        try:
            faces.append([f, f.normalAt(0, 0)])
        except Part.OCCError:
            return None
    # find opposite normals pairs
    pairs = []
    for i1, f1 in enumerate(faces):
        for i2, f2 in enumerate(faces):
            if f1[0].hashCode() != f2[0].hashCode():
                if round(f1[1].getAngle(f2[1]), 4) == 3.1416:
                    pairs.append([i1, i2])
    if not pairs:
        return None
    valids = []
    for pair in pairs:
        hc = [faces[pair[0]][0].hashCode(), faces[pair[1]][0].hashCode()]
        # check if other normals are all at 90 degrees
        ok = True
        for f in faces:
            if f[0].hashCode() not in hc:
                if round(f[1].getAngle(faces[pair[0]][1]), 4) != 1.5708:
                    ok = False
        if ok:
            # prefer the face with the lowest z
            if faces[pair[0]][0].CenterOfMass.z < faces[pair[1]][0].CenterOfMass.z:
                valids.append(
                    [
                        faces[pair[0]][0],
                        faces[pair[1]][0].CenterOfMass.sub(faces[pair[0]][0].CenterOfMass),
                    ]
                )
            else:
                valids.append(
                    [
                        faces[pair[1]][0],
                        faces[pair[0]][0].CenterOfMass.sub(faces[pair[1]][0].CenterOfMass),
                    ]
                )
    if valids:
        if sortmethod == "z":
            valids.sort(key=lambda v: v[0].CenterOfMass.z)
        elif sortmethod == "area":
            # sort by smallest area
            valids.sort(key=lambda v: v[0].Area)
        else:
            valids.sort(key=lambda v: (v[0].CenterOfMass.sub(sortmethod)).Length)
        return valids[0]
    return None


def printMessage(message):
    FreeCAD.Console.PrintMessage(message)
    if FreeCAD.GuiUp:
        QtGui.QMessageBox.information(None, "", message)


def printWarning(message):
    FreeCAD.Console.PrintMessage(message)
    if FreeCAD.GuiUp:
        QtGui.QMessageBox.warning(None, "", message)


def makeIfcSpreadsheet(archobj=None):
    ifc_container = None
    for obj in FreeCAD.ActiveDocument.Objects:
        if obj.Name == "IfcPropertiesContainer":
            ifc_container = obj
    if not ifc_container:
        ifc_container = FreeCAD.ActiveDocument.addObject(
            "App::DocumentObjectGroup", "IfcPropertiesContainer"
        )
    import Spreadsheet

    ifc_spreadsheet = FreeCAD.ActiveDocument.addObject("Spreadsheet::Sheet", "IfcProperties")
    ifc_spreadsheet.set("A1", translate("Arch", "Category"))
    ifc_spreadsheet.set("B1", translate("Arch", "Key"))
    ifc_spreadsheet.set("C1", translate("Arch", "Type"))
    ifc_spreadsheet.set("D1", translate("Arch", "Value"))
    ifc_spreadsheet.set("E1", translate("Arch", "Unit"))
    ifc_container.addObject(ifc_spreadsheet)
    if archobj:
        if hasattr(obj, "IfcProperties"):
            archobj.IfcProperties = ifc_spreadsheet
            return ifc_spreadsheet
        else:
            FreeCAD.Console.PrintWarning(
                translate(
                    "Arch",
                    "The object does not have an IfcProperties attribute. Cancel spreadsheet creation for object:",
                )
                + " "
                + archobj.Label
            )
            FreeCAD.ActiveDocument.removeObject(ifc_spreadsheet)
    else:
        return ifc_spreadsheet
