# Make a copy of the shape of all children of a part and make a compound
# from them.

from __future__ import annotations

__Name__ = 'Deep Copy'
__Comment__ = 'Takes a part and makes a compound out of it'
__License__ = 'Apache-2.0'
__Web__ = 'https://www.freecadweb.org/wiki/Macro_DeepCopy'
__Wiki__ = 'https://www.freecadweb.org/wiki/Macro_DeepCopy'
__Icon__ = 'DeepCopy.svg'
__Help__ = 'Select a part and launch'
__Author__ = 'galou_breizh'
__Version__ = '1.1.0'
__Date__ = '2023-03-22'
__Status__ = 'Stable'
__Requires__ = 'FreeCAD >= v0.17'
__Files__ = 'DeepCopy.svg'

from typing import Iterable, Optional

from Arch import pruneIncluded

import FreeCAD as app
import FreeCADGui as gui


# Typing hints.
DO: app.DocumentObject
AppPart: DO  # TypeId == 'App::Part'.
DOList: Iterable[DO]


def deep_copy(doc: Optional[app.Document] = None):
    """Copy the shape of the selected objects.

    Parameters
    ----------
    - doc: document to copy the shape to. Defaults to the document of the
      selected objects.

    """
    sel = gui.Selection.getSelectionEx()
    if not sel:
        app.Console.PrintWarning('No objects selected')
        return
    # Check that all selected objects are in the same document.
    if doc is None:
        doc = sel[0].Object.Document
        for sel_object in sel:
            if sel_object.Object.Document is not doc:
                app.Console.PrintError('Selected objects belong to'
                                       ' different documents\n')
                return

    for sel_object in sel:
        deep_copy_part(sel_object.Object, doc)


def deep_copy_part(part: AppPart, doc: Optional[app.Document] = None):
    """Copy the shape of a "App::Part" object.

    Parameters
    ----------
    - doc: document to copy the shape to. Defaults to the document of the
      given object.

    """
    if (not hasattr(part, 'TypeId')) or part.TypeId != 'App::Part':
        # Part is not a part, return.
        try:
            app.Console.PrintWarning(f'"{part.Label}" ({part.Name})'
                                     ' is not a part, ignoring\n')
        except AttributeError:
            app.Console.PrintWarning('Object is not a part, ignoring\n')
        return

    if doc is None:
        doc = part.Document

    copied_subobjects = []
    for o in get_all_subobjects(part):
        copied_subobjects += copy_subobject(doc, o)

    compound = doc.addObject('Part::Compound', 'Copy of ' + part.Label)
    compound.Links = copied_subobjects
    compound.Placement = part.Placement
    doc.recompute()


def get_all_subobjects(obj: DO) -> DOList:
    """Recursively get all subobjects

    Subobjects of objects having a Shape attribute are not included otherwise
    each single feature of the object would be copied. The result is that
    bodies, compounds, and the result of boolean operations will be converted
    into a simple copy of their shape.

    """
    # Depth-first search algorithm.
    discovered = []
    # We do not need an extra copy for stack because OutList is already a copy.
    stack = obj.OutList
    while stack:
        v = stack.pop(0)
        if v not in discovered:
            discovered.append(v)
            if not hasattr(v, 'Shape'):
                stack += v.OutList
    return pruneIncluded(discovered)


def copy_subobject(doc: app.DocumentObject, obj: DO) -> DOList:
    """Copy the shape of an object

    Some GUI attributes are also copied

    """
    copied_object = []
    if not hasattr(obj, 'Shape') or obj.Shape.isNull():
        return copied_object
    vobj = obj.ViewObject
    try:
        copy = doc.addObject('Part::Feature', obj.Name + '_Shape')
        copy.Shape = obj.Shape
        copy.Label = 'Copy of ' + obj.Label
    except AttributeError as e:
        app.Console.PrintLog(e)
        pass
    else:
        copied_object = [copy]
    vo_copy = copy.ViewObject
    # Implementation note: cannot use __setattr__ and __getattribute__.
    try:
        vo_copy.ShapeColor = vobj.ShapeColor
    except AttributeError:
        pass
    try:
        vo_copy.LineColor = vobj.LineColor
    except AttributeError:
        pass
    try:
        vo_copy.PointColor = vobj.PointColor
    except AttributeError:
        pass
    try:
        vo_copy.DiffuseColor = vobj.DiffuseColor
    except AttributeError:
        pass
    try:
        vo_copy.Transparency = vobj.Transparency
    except AttributeError:
        pass
    return copied_object


if __name__ == '__main__':
    deep_copy()