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

# ***************************************************************************
# *   (c) 2009, 2010                                                        *
# *   Yorik van Havre <yorik@uncreated.net>, Ken Cline <cline@frii.com>     *
# *   (c) 2020 Eliud Cabrera Castillo <e.cabrera-castillo@tum.de>           *
# *                                                                         *
# *   This file is part of the FreeCAD CAx development system.              *
# *                                                                         *
# *   This program is free software; you can redistribute it and/or modify  *
# *   it under the terms of the GNU Lesser General Public License (LGPL)    *
# *   as published by the Free Software Foundation; either version 2 of     *
# *   the License, or (at your option) any later version.                   *
# *   for detail see the LICENCE text file.                                 *
# *                                                                         *
# *   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 Library General Public License for more details.                  *
# *                                                                         *
# *   You should have received a copy of the GNU Library General Public     *
# *   License along with FreeCAD; if not, write to the Free Software        *
# *   Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  *
# *   USA                                                                   *
# *                                                                         *
# ***************************************************************************
"""Provides utility functions to do operations with groups.

The functions here are also used in the Arch Workbench as some of
the objects created with this workbench work like groups.
"""
## @package groups
# \ingroup draftutils
# \brief Provides utility functions to do operations with groups.

## \addtogroup draftutils
# @{
import FreeCAD as App
import draftutils.utils as utils

from draftutils.translate import translate
from draftutils.messages import _err


def is_group(obj):
    """Return True if the given object is considered a group.

    Parameters
    ----------
    obj : App::DocumentObject
        The object to check.

    Returns
    -------
    bool
        Returns `True` if `obj` is considered a group:

        The object is derived from `App::DocumentObjectGroup` but not
        a `'LayerContainer'`.

        Or the object is of the type `'Project'`, `'Site'`, `'Building'`,
        `'Floor'`, `'BuildingPart'` or `'Space'` from the Arch Workbench.
        Note that `'Floor'` and `'Building'` are obsolete types.

        Otherwise returns `False`.
    """
    typ = utils.get_type(obj)
    return (obj.isDerivedFrom("App::DocumentObjectGroup") and typ != "LayerContainer") or typ in (
        "Project",
        "Site",
        "Building",
        "Floor",
        "BuildingPart",
        "Space",
    )


def get_group_names(doc=None):
    """Return a list of names of existing groups in the document.

    Parameters
    ----------
    doc: App::Document, optional
        It defaults to `None`.
        A document on which to search group names.
        It if is `None` it will search the current document.

    Returns
    -------
    list of str
        A list of names of objects that are considered groups.
        See the is_group function.

        Otherwise returns an empty list.
    """
    if not doc:
        found, doc = utils.find_doc(App.activeDocument())

    if not found:
        _err(translate("draft", "No active document. Aborting."))
        return []

    glist = []

    for obj in doc.Objects:
        if is_group(obj):
            glist.append(obj.Name)

    return glist


def getGroupNames():
    """Return a list of group names. DEPRECATED."""
    utils.use_instead("get_group_names")
    return get_group_names()


def ungroup(obj):
    """Remove the object from any group to which it belongs.

    A "group" is any object returned by `get_group_names`.

    Parameters
    ----------
    obj: App::DocumentObject or str
        Any type of object.
        If it is a string, it must be the `Label` of that object.
        Since a label is not guaranteed to be unique in a document,
        it will use the first object found with this label.
    """
    if isinstance(obj, str):
        obj_str = obj

    found, obj = utils.find_object(obj, doc=App.activeDocument())
    if not found:
        _err(translate("draft", "Wrong input: object {} not in document.").format(obj_str))
        return None

    doc = obj.Document

    for name in get_group_names():
        group = doc.getObject(name)
        if obj in group.Group:
            # The list of objects cannot be modified directly,
            # so a new list is created, this new list is modified,
            # and then it is assigned over the older list.
            objects = group.Group
            objects.remove(obj)
            group.Group = objects


def get_windows(obj):
    """Return the windows and rebars inside a host.

    Parameters
    ----------
    obj: App::DocumentObject
        A scripted object of type `'Wall'` or `'Structure'`
        (Arch Workbench).
        This will be searched for objects of type `'Window'` and `'Rebar'`,
        and clones of them, and the found elements will be added
        to the output list.

        The function will search recursively all elements under `obj.OutList`,
        in case the windows and rebars are nested under other walls
        and structures.

    Returns
    -------
    list
        A list of all found windows and rebars in `obj`.
        If `obj` is itself a `'Window'` or a `'Rebar'`, or a clone of them,
        it will return the same `obj` element.
    """
    out = []
    if utils.get_type(obj) in ("Wall", "Structure"):
        for o in obj.OutList:
            out.extend(get_windows(o))
        for i in obj.InList:
            if utils.get_type(i.getLinkedObject()) == "Window" or utils.is_clone(obj, "Window"):
                if hasattr(i, "Hosts"):
                    if obj in i.Hosts:
                        out.append(i)
            elif utils.get_type(i) == "Rebar" or utils.is_clone(obj, "Rebar"):
                if hasattr(i, "Host"):
                    if obj == i.Host:
                        out.append(i)
    elif utils.get_type(obj.getLinkedObject()) in ("Window", "Rebar") or utils.is_clone(
        obj, ["Window", "Rebar"]
    ):
        out.append(obj)

    return out


def get_group_contents(objectslist, walls=False, addgroups=False, spaces=False, noarchchild=False):
    """Return a list of objects from expanding the input groups.

    The function accepts any type of object, although it is most useful
    with "groups", as it is meant to unpack the objects inside these groups.

    Parameters
    ----------
    objectslist: list
        If any object in the list is considered a group, see the `is_group`
        function, its contents (`obj.Group`) are extracted and added to the
        output list.

        Single items that aren't groups are added to the output list
        as is.

    walls: bool, optional
        It defaults to `False`.
        If it is `True`, Wall and Structure objects (Arch Workbench)
        are treated as groups; they are scanned for Window, Door,
        and Rebar objects, and these are added to the output list.

    addgroups: bool, optional
        It defaults to `False`.
        If it is `True`, the group itself is kept as part of the output list.

    spaces: bool, optional
        It defaults to `False`.
        If it is `True`, Arch Spaces are added to the output list even
        when addgroups is False (their contents are always added).

    noarchchild: bool, optional
        It defaults to `False`.
        If it is `True`, the objects inside Building and BuildingParts
        (Arch Workbench) aren't added to the output list.

    Returns
    -------
    list
        The list of objects from each group present in `objectslist`,
        plus any other individual object given in `objectslist`.
    """
    newlist = []
    if not isinstance(objectslist, list):
        objectslist = [objectslist]

    for obj in objectslist:
        if obj:
            if is_group(obj):
                if addgroups or (spaces and utils.get_type(obj) == "Space"):
                    newlist.append(obj)
                if not (noarchchild and utils.get_type(obj) in ("Building", "BuildingPart")):
                    newlist.extend(
                        get_group_contents(obj.Group, walls, addgroups, spaces, noarchchild)
                    )
            else:
                # print("adding ", obj.Name)
                newlist.append(obj)
                if walls:
                    newlist.extend(get_windows(obj))

    # Clean possible duplicates
    cleanlist = []
    for obj in newlist:
        if obj not in cleanlist:
            cleanlist.append(obj)

    return cleanlist


def getGroupContents(objectslist, walls=False, addgroups=False, spaces=False, noarchchild=False):
    """Return a list of objects from groups. DEPRECATED."""
    utils.use_instead("get_group_contents")

    return get_group_contents(objectslist, walls, addgroups, spaces, noarchchild)


def get_movable_children(objectslist, recursive=True, _donelist=[]):
    """Return a list of objects with child objects that move with a host.

    Builds a list of objects with all child objects (`obj.OutList`)
    that have their `MoveWithHost` attribute set to `True`.
    This function is mostly useful for Arch Workbench objects.

    Parameters
    ----------
    objectslist: list of App::DocumentObject
        A single scripted object or list of objects.

    recursive: bool, optional
        It defaults to `True`, in which case the function
        is called recursively to also extract the children of children
        objects.
        Otherwise, only direct children of the input objects
        are added to the output list.

    _donelist: list
        List of object names. Used internally to prevent an endless loop.

    Returns
    -------
    list
        List of children objects that have their `MoveWithHost` attribute
        set to `True`.
    """
    added = []
    if not isinstance(objectslist, list):
        objectslist = [objectslist]

    for obj in objectslist:
        if obj.Name in _donelist:
            continue

        _donelist.append(obj.Name)

        # Skips some objects that should never move their children
        if utils.get_type(obj) not in (
            "App::Part",
            "PartDesign::Body",
            "Clone",
            "SectionPlane",
            "Facebinder",
            "BuildingPart",
            "App::Link",
        ):
            children = obj.OutList
            if (
                hasattr(obj, "Proxy")
                and obj.Proxy
                and hasattr(obj.Proxy, "getSiblings")
                and utils.get_type(obj) != "Window"
            ):
                # children.extend(obj.Proxy.getSiblings(obj))
                pass

            for child in children:
                if hasattr(child, "MoveWithHost") and child.MoveWithHost:
                    if hasattr(obj, "CloneOf") and obj.CloneOf:
                        if obj.CloneOf.Name != child.Name:
                            added.append(child)
                    else:
                        added.append(child)

            if recursive:
                added.extend(get_movable_children(children, recursive, _donelist))

    return added


def getMovableChildren(objectslist, recursive=True):
    """Return a list of objects with child objects. DEPRECATED."""
    utils.use_instead("get_movable_children")
    return get_movable_children(objectslist, recursive)


## @}
