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

# ***************************************************************************
# *   Copyright (c) 2014 Yorik van Havre <yorik@uncreated.net>              *
# *                                                                         *
# *   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.                                 *
# *                                                                         *
# *   This program 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 this program; if not, write to the Free Software   *
# *   Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  *
# *   USA                                                                   *
# *                                                                         *
# ***************************************************************************

from PySide.QtCore import QT_TRANSLATE_NOOP
import FreeCAD
import Part
import Path
import Path.Op.Base as PathOp
import Path.Op.PocketBase as PathPocketBase
import PathScripts.PathUtils as PathUtils

# lazily loaded modules
from lazy_loader.lazy_loader import LazyLoader

__title__ = "CAM 3D Pocket Operation"
__author__ = "Yorik van Havre <yorik@uncreated.net>"
__url__ = "https://www.freecad.org"
__doc__ = "Class and implementation of the 3D Pocket operation."
__created__ = "2014"

if False:
    Path.Log.setLevel(Path.Log.Level.DEBUG, Path.Log.thisModule())
    Path.Log.trackModule(Path.Log.thisModule())
else:
    Path.Log.setLevel(Path.Log.Level.INFO, Path.Log.thisModule())


translate = FreeCAD.Qt.translate


class ObjectPocket(PathPocketBase.ObjectPocket):
    """Proxy object for Pocket operation."""

    def pocketOpFeatures(self, obj):
        return PathOp.FeatureNoFinalDepth

    def initPocketOp(self, obj):
        """initPocketOp(obj) ... setup receiver"""
        if not hasattr(obj, "HandleMultipleFeatures"):
            obj.addProperty(
                "App::PropertyEnumeration",
                "HandleMultipleFeatures",
                "Pocket",
                QT_TRANSLATE_NOOP(
                    "App::Property",
                    "Choose how to process multiple Base Geometry features.",
                ),
            )

        if not hasattr(obj, "AdaptivePocketStart"):
            obj.addProperty(
                "App::PropertyBool",
                "AdaptivePocketStart",
                "Pocket",
                QT_TRANSLATE_NOOP(
                    "App::Property",
                    "Use adaptive algorithm to eliminate excessive air milling above planar pocket top.",
                ),
            )
        if not hasattr(obj, "AdaptivePocketFinish"):
            obj.addProperty(
                "App::PropertyBool",
                "AdaptivePocketFinish",
                "Pocket",
                QT_TRANSLATE_NOOP(
                    "App::Property",
                    "Use adaptive algorithm to eliminate excessive air milling below planar pocket bottom.",
                ),
            )
        if not hasattr(obj, "ProcessStockArea"):
            obj.addProperty(
                "App::PropertyBool",
                "ProcessStockArea",
                "Pocket",
                QT_TRANSLATE_NOOP(
                    "App::Property",
                    "Process the model and stock in an operation with no Base Geometry selected.",
                ),
            )

        # populate the property enumerations
        for n in self.propertyEnumerations():
            setattr(obj, n[0], n[1])

    @classmethod
    def propertyEnumerations(self, dataType="data"):
        """propertyEnumerations(dataType="data")... return property enumeration lists of specified dataType.
        Args:
            dataType = 'data', 'raw', 'translated'
        Notes:
        'data' is list of internal string literals used in code
        'raw' is list of (translated_text, data_string) tuples
        'translated' is list of translated string literals
        """

        enums = {
            "HandleMultipleFeatures": [
                (translate("CAM_Pocket", "Collectively"), "Collectively"),
                (translate("CAM_Pocket", "Individually"), "Individually"),
            ],
        }

        if dataType == "raw":
            return enums

        data = list()
        idx = 0 if dataType == "translated" else 1

        Path.Log.debug(enums)

        for k, v in enumerate(enums):
            data.append((v, [tup[idx] for tup in enums[v]]))
        Path.Log.debug(data)

        return data

    def opOnDocumentRestored(self, obj):
        """opOnDocumentRestored(obj) ... adds the properties if they doesn't exist."""
        super().opOnDocumentRestored(obj)
        self.initPocketOp(obj)

    def pocketInvertExtraOffset(self):
        return False

    def opUpdateDepths(self, obj):
        """opUpdateDepths(obj) ... Implement special depths calculation."""
        # Set Final Depth to bottom of model if whole model is used
        if not obj.Base or len(obj.Base) == 0:
            if len(self.job.Model.Group) == 1:
                finDep = self.job.Model.Group[0].Shape.BoundBox.ZMin
            else:
                finDep = min([m.Shape.BoundBox.ZMin for m in self.job.Model.Group])
            obj.setExpression("OpFinalDepth", "{} mm".format(finDep))

    def areaOpShapes(self, obj):
        """areaOpShapes(obj) ... return shapes representing the solids to be removed."""
        Path.Log.track()

        subObjTups = []
        removalshapes = []

        if obj.Base:
            Path.Log.debug("base items exist.  Processing... ")
            for base in obj.Base:
                Path.Log.debug("obj.Base item: {}".format(base))

                # Check if all subs are faces
                allSubsFaceType = True
                Faces = []
                for sub in base[1]:
                    if "Face" in sub:
                        face = getattr(base[0].Shape, sub)
                        Faces.append(face)
                        subObjTups.append((sub, face))
                    else:
                        allSubsFaceType = False
                        break

                if len(Faces) == 0:
                    allSubsFaceType = False

                if allSubsFaceType is True and obj.HandleMultipleFeatures == "Collectively":
                    (fzmin, fzmax) = self.getMinMaxOfFaces(Faces)
                    if obj.FinalDepth.Value < fzmin:
                        Path.Log.warning(
                            translate(
                                "CAM",
                                "Final depth set below ZMin of face(s) selected.",
                            )
                        )

                    if obj.AdaptivePocketStart is True or obj.AdaptivePocketFinish is True:
                        pocketTup = self.calculateAdaptivePocket(obj, base, subObjTups)
                        if pocketTup is not False:
                            obj.removalshape = pocketTup[0]
                            removalshapes.append(pocketTup)  # (shape, isHole, detail)
                    else:
                        shape = Part.makeCompound(Faces)
                        env = PathUtils.getEnvelope(
                            base[0].Shape, subshape=shape, depthparams=self.depthparams
                        )
                        rawRemovalShape = env.cut(base[0].Shape)
                        faceExtrusions = [f.extrude(FreeCAD.Vector(0.0, 0.0, 1.0)) for f in Faces]
                        obj.removalshape = _identifyRemovalSolids(rawRemovalShape, faceExtrusions)
                        removalshapes.append(
                            (obj.removalshape, False, "3DPocket")
                        )  # (shape, isHole, detail)
                else:
                    for sub in base[1]:
                        if "Face" in sub:
                            shape = Part.makeCompound([getattr(base[0].Shape, sub)])
                        else:
                            edges = [getattr(base[0].Shape, sub) for sub in base[1]]
                            shape = Part.makeFace(edges, "Part::FaceMakerSimple")

                        env = PathUtils.getEnvelope(
                            base[0].Shape, subshape=shape, depthparams=self.depthparams
                        )
                        rawRemovalShape = env.cut(base[0].Shape)
                        faceExtrusions = [shape.extrude(FreeCAD.Vector(0.0, 0.0, 1.0))]
                        obj.removalshape = _identifyRemovalSolids(rawRemovalShape, faceExtrusions)
                        removalshapes.append((obj.removalshape, False, "3DPocket"))

        else:  # process the job base object as a whole
            Path.Log.debug("processing the whole job base object")
            for base in self.model:
                if obj.ProcessStockArea is True:
                    job = PathUtils.findParentJob(obj)

                    stockEnvShape = PathUtils.getEnvelope(
                        job.Stock.Shape, subshape=None, depthparams=self.depthparams
                    )

                    rawRemovalShape = stockEnvShape.cut(base.Shape)
                else:
                    env = PathUtils.getEnvelope(
                        base.Shape, subshape=None, depthparams=self.depthparams
                    )
                    rawRemovalShape = env.cut(base.Shape)

                # Identify target removal shapes after cutting envelope with base shape
                removalSolids = [
                    s
                    for s in rawRemovalShape.Solids
                    if Path.Geom.isRoughly(s.BoundBox.ZMax, rawRemovalShape.BoundBox.ZMax)
                ]

                # Fuse multiple solids
                if len(removalSolids) > 1:
                    seed = removalSolids[0]
                    for tt in removalSolids[1:]:
                        fusion = seed.fuse(tt)
                        seed = fusion
                    removalShape = seed
                else:
                    removalShape = removalSolids[0]

                obj.removalshape = removalShape
                removalshapes.append((obj.removalshape, False, "3DPocket"))

        return removalshapes

    def areaOpSetDefaultValues(self, obj, job):
        """areaOpSetDefaultValues(obj, job) ... set default values"""
        obj.StepOver = 50
        obj.ZigZagAngle = 45
        obj.HandleMultipleFeatures = "Collectively"
        obj.AdaptivePocketStart = False
        obj.AdaptivePocketFinish = False
        obj.ProcessStockArea = False

    # methods for eliminating air milling with some pockets: adaptive start and finish
    def calculateAdaptivePocket(self, obj, base, subObjTups):
        """calculateAdaptivePocket(obj, base, subObjTups)
        Orient multiple faces around common facial center of mass.
        Identify edges that are connections for adjacent faces.
        Attempt to separate unconnected edges into top and bottom loops of the pocket.
        Trim the top and bottom of the pocket if available and requested.
        return: tuple with pocket shape information"""
        low = []
        high = []
        removeList = []
        Faces = []
        allEdges = []
        makeHighFace = 0
        tryNonPlanar = False
        isHighFacePlanar = True
        isLowFacePlanar = True

        for sub, face in subObjTups:
            Faces.append(face)

        # identify max and min face heights for top loop
        (zmin, zmax) = self.getMinMaxOfFaces(Faces)

        # Order faces around common center of mass
        subObjTups = self.orderFacesAroundCenterOfMass(subObjTups)
        # find connected edges and map to edge names of base
        (connectedEdges, touching) = self.findSharedEdges(subObjTups)
        (low, high) = self.identifyUnconnectedEdges(subObjTups, touching)

        if len(high) > 0 and obj.AdaptivePocketStart is True:
            # attempt planar face with top edges of pocket
            allEdges = []
            makeHighFace = 0
            tryNonPlanar = False
            for sub, face, ei in high:
                allEdges.append(face.Edges[ei])

            (hzmin, hzmax) = self.getMinMaxOfFaces(allEdges)

            try:
                highFaceShape = Part.Face(Part.Wire(Part.__sortEdges__(allEdges)))
            except Exception as ee:
                Path.Log.warning(ee)
                Path.Log.error(
                    translate(
                        "CAM",
                        "A planar adaptive start is unavailable. The non-planar will be attempted.",
                    )
                )
                tryNonPlanar = True
            else:
                makeHighFace = 1

            if tryNonPlanar is True:
                try:
                    highFaceShape = Part.makeFilledFace(
                        Part.__sortEdges__(allEdges)
                    )  # NON-planar face method
                except Exception as eee:
                    Path.Log.warning(eee)
                    Path.Log.error(
                        translate("CAM", "The non-planar adaptive start is also unavailable.")
                        + "(1)"
                    )
                    isHighFacePlanar = False
                else:
                    makeHighFace = 2

            if makeHighFace > 0:
                FreeCAD.ActiveDocument.addObject("Part::Feature", "topEdgeFace")
                highFace = FreeCAD.ActiveDocument.ActiveObject
                highFace.Shape = highFaceShape
                removeList.append(highFace.Name)

            # verify non-planar face is within high edge loop Z-boundaries
            if makeHighFace == 2:
                mx = hzmax + obj.StepDown.Value
                mn = hzmin - obj.StepDown.Value
                if highFace.Shape.BoundBox.ZMax > mx or highFace.Shape.BoundBox.ZMin < mn:
                    Path.Log.warning(
                        "ZMaxDiff: {};  ZMinDiff: {}".format(
                            highFace.Shape.BoundBox.ZMax - mx,
                            highFace.Shape.BoundBox.ZMin - mn,
                        )
                    )
                    Path.Log.error(
                        translate("CAM", "The non-planar adaptive start is also unavailable.")
                        + "(2)"
                    )
                    isHighFacePlanar = False
                    makeHighFace = 0
        else:
            isHighFacePlanar = False

        if len(low) > 0 and obj.AdaptivePocketFinish is True:
            # attempt planar face with bottom edges of pocket
            allEdges = []
            for sub, face, ei in low:
                allEdges.append(face.Edges[ei])

            # (lzmin, lzmax) = self.getMinMaxOfFaces(allEdges)

            try:
                lowFaceShape = Part.Face(Part.Wire(Part.__sortEdges__(allEdges)))
                # lowFaceShape = Part.makeFilledFace(Part.__sortEdges__(allEdges))  # NON-planar face method
            except Exception as ee:
                Path.Log.error(ee)
                Path.Log.error("An adaptive finish is unavailable.")
                isLowFacePlanar = False
            else:
                FreeCAD.ActiveDocument.addObject("Part::Feature", "bottomEdgeFace")
                lowFace = FreeCAD.ActiveDocument.ActiveObject
                lowFace.Shape = lowFaceShape
                removeList.append(lowFace.Name)
        else:
            isLowFacePlanar = False

        # Start with a regular pocket envelope
        strDep = obj.StartDepth.Value
        finDep = obj.FinalDepth.Value
        cuts = []
        starts = []
        finals = []
        starts.append(obj.StartDepth.Value)
        finals.append(zmin)
        if obj.AdaptivePocketStart is True or len(subObjTups) == 1:
            strDep = zmax + obj.StepDown.Value
            starts.append(zmax + obj.StepDown.Value)

        finish_step = obj.FinishDepth.Value if hasattr(obj, "FinishDepth") else 0.0
        depthparams = PathUtils.depth_params(
            clearance_height=obj.ClearanceHeight.Value,
            safe_height=obj.SafeHeight.Value,
            start_depth=strDep,
            step_down=obj.StepDown.Value,
            z_finish_step=finish_step,
            final_depth=finDep,
            user_depths=None,
        )
        shape = Part.makeCompound(Faces)
        env = PathUtils.getEnvelope(base[0].Shape, subshape=shape, depthparams=depthparams)
        cuts.append(env.cut(base[0].Shape))

        # Might need to change to .cut(job.Stock.Shape) if pocket has no bottom
        # job = PathUtils.findParentJob(obj)
        # envBody = env.cut(job.Stock.Shape)

        if isHighFacePlanar is True and len(subObjTups) > 1:
            starts.append(hzmax + obj.StepDown.Value)
            # make shape to trim top of reg pocket
            strDep1 = obj.StartDepth.Value + (hzmax - hzmin)
            if makeHighFace == 1:
                # Planar face
                finDep1 = highFace.Shape.BoundBox.ZMin + obj.StepDown.Value
            else:
                # Non-Planar face
                finDep1 = hzmin + obj.StepDown.Value
            depthparams1 = PathUtils.depth_params(
                clearance_height=obj.ClearanceHeight.Value,
                safe_height=obj.SafeHeight.Value,
                start_depth=strDep1,
                step_down=obj.StepDown.Value,
                z_finish_step=finish_step,
                final_depth=finDep1,
                user_depths=None,
            )
            envTop = PathUtils.getEnvelope(
                base[0].Shape, subshape=highFace.Shape, depthparams=depthparams1
            )
            cbi = len(cuts) - 1
            cuts.append(cuts[cbi].cut(envTop))

        if isLowFacePlanar is True and len(subObjTups) > 1:
            # make shape to trim top of pocket
            if makeHighFace == 1:
                # Planar face
                strDep2 = lowFace.Shape.BoundBox.ZMax
            else:
                # Non-Planar face
                strDep2 = hzmax
            finDep2 = obj.FinalDepth.Value
            depthparams2 = PathUtils.depth_params(
                clearance_height=obj.ClearanceHeight.Value,
                safe_height=obj.SafeHeight.Value,
                start_depth=strDep2,
                step_down=obj.StepDown.Value,
                z_finish_step=finish_step,
                final_depth=finDep2,
                user_depths=None,
            )
            envBottom = PathUtils.getEnvelope(
                base[0].Shape, subshape=lowFace.Shape, depthparams=depthparams2
            )
            cbi = len(cuts) - 1
            cuts.append(cuts[cbi].cut(envBottom))

        # package pocket details into tuple
        cbi = len(cuts) - 1
        pocket = (cuts[cbi], False, "3DPocket")
        if FreeCAD.GuiUp:
            import FreeCADGui

            for rn in removeList:
                FreeCADGui.ActiveDocument.getObject(rn).Visibility = False

        for rn in removeList:
            FreeCAD.ActiveDocument.getObject(rn).purgeTouched()
            self.tempObjectNames.append(rn)
        return pocket

    def orderFacesAroundCenterOfMass(self, subObjTups):
        """orderFacesAroundCenterOfMass(subObjTups)
        Order list of faces by center of mass in angular order around
        average center of mass for all faces. Positive X-axis is zero degrees.
        return: subObjTups [ordered/sorted]"""
        import math

        newList = []
        vectList = []
        comList = []
        sortList = []
        subCnt = 0
        sumCom = FreeCAD.Vector(0.0, 0.0, 0.0)
        avgCom = FreeCAD.Vector(0.0, 0.0, 0.0)

        def getDrctn(vectItem):
            return vectItem[3]

        def getFaceIdx(sub):
            return int(sub.replace("Face", "")) - 1

        # get CenterOfMass for each face and add to sumCenterOfMass for average calculation
        for sub, face in subObjTups:
            # for (bsNm, fIdx, eIdx, vIdx) in bfevList:
            # face = FreeCAD.ActiveDocument.getObject(bsNm).Shape.Faces[fIdx]
            subCnt += 1
            com = face.CenterOfMass
            comList.append((sub, face, com))
            sumCom = sumCom.add(com)  # add sub COM to sum

        # Calculate average CenterOfMass for all faces combined
        avgCom.x = sumCom.x / subCnt
        avgCom.y = sumCom.y / subCnt
        avgCom.z = sumCom.z / subCnt

        # calculate vector (mag, direct) for each face from avgCom
        for sub, face, com in comList:
            adjCom = com.sub(avgCom)  # effectively treats avgCom as origin for each face.
            mag = math.sqrt(adjCom.x**2 + adjCom.y**2)  # adjCom.Length without Z values
            drctn = 0.0
            # Determine direction of vector
            if adjCom.x > 0.0:
                if adjCom.y > 0.0:  # Q1
                    drctn = math.degrees(math.atan(adjCom.y / adjCom.x))
                elif adjCom.y < 0.0:
                    drctn = -math.degrees(math.atan(adjCom.x / adjCom.y)) + 270.0
                elif adjCom.y == 0.0:
                    drctn = 0.0
            elif adjCom.x < 0.0:
                if adjCom.y < 0.0:
                    drctn = math.degrees(math.atan(adjCom.y / adjCom.x)) + 180.0
                elif adjCom.y > 0.0:
                    drctn = -math.degrees(math.atan(adjCom.x / adjCom.y)) + 90.0
                elif adjCom.y == 0.0:
                    drctn = 180.0
            elif adjCom.x == 0.0:
                if adjCom.y < 0.0:
                    drctn = 270.0
                elif adjCom.y > 0.0:
                    drctn = 90.0
            vectList.append((sub, face, mag, drctn))

        # Sort faces by directional component of vector
        sortList = sorted(vectList, key=getDrctn)

        # remove magnitute and direction values
        for sub, face, mag, drctn in sortList:
            newList.append((sub, face))

        # Rotate list items so highest face is first
        zmax = newList[0][1].BoundBox.ZMax
        idx = 0
        for i in range(0, len(newList)):
            (sub, face) = newList[i]
            fIdx = getFaceIdx(sub)
            # face = FreeCAD.ActiveDocument.getObject(bsNm).Shape.Faces[fIdx]
            if face.BoundBox.ZMax > zmax:
                zmax = face.BoundBox.ZMax
                idx = i
            if face.BoundBox.ZMax == zmax:
                if fIdx < getFaceIdx(newList[idx][0]):
                    idx = i
        if idx > 0:
            for z in range(0, idx):
                newList.append(newList.pop(0))

        return newList

    def findSharedEdges(self, subObjTups):
        """findSharedEdges(self, subObjTups)
        Find connected edges given a group of faces"""
        checkoutList = []
        searchedList = []
        shared = []
        touching = {}
        touchingCleaned = {}

        # Prepare dictionary for edges in shared
        for sub, face in subObjTups:
            touching[sub] = []

        # prepare list of indexes as proxies for subObjTups items
        numFaces = len(subObjTups)
        for nf in range(0, numFaces):
            checkoutList.append(nf)

        for co in range(0, len(checkoutList)):
            if len(checkoutList) < 2:
                break

            # Checkout  first sub for analysis
            checkedOut1 = checkoutList.pop()
            searchedList.append(checkedOut1)
            (sub1, face1) = subObjTups[checkedOut1]

            # Compare checked out sub to others for shared
            for co in range(0, len(checkoutList)):
                # Checkout  second sub for analysis
                (sub2, face2) = subObjTups[co]

                # analyze two subs for common faces
                for ei1 in range(0, len(face1.Edges)):
                    edg1 = face1.Edges[ei1]
                    for ei2 in range(0, len(face2.Edges)):
                        edg2 = face2.Edges[ei2]
                        if edg1.isSame(edg2) is True:
                            Path.Log.debug(
                                "{}.Edges[{}] connects at {}.Edges[{}]".format(sub1, ei1, sub2, ei2)
                            )
                            shared.append((sub1, face1, ei1))
                            touching[sub1].append(ei1)
                            touching[sub2].append(ei2)
        # Efor
        # Remove duplicates from edge lists
        for sub in touching:
            touchingCleaned[sub] = []
            for s in touching[sub]:
                if s not in touchingCleaned[sub]:
                    touchingCleaned[sub].append(s)

        return (shared, touchingCleaned)

    def identifyUnconnectedEdges(self, subObjTups, touching):
        """identifyUnconnectedEdges(subObjTups, touching)
        Categorize unconnected edges into two groups, if possible: low and high"""
        # Identify unconnected edges
        # (should be top edge loop if all faces form loop with bottom face(s) included)
        high = []
        low = []
        holding = []

        for sub, face in subObjTups:
            holding = []
            for ei in range(0, len(face.Edges)):
                if ei not in touching[sub]:
                    holding.append((sub, face, ei))
            # Assign unconnected edges based upon category: high or low
            if len(holding) == 1:
                high.append(holding.pop())
            elif len(holding) == 2:
                edg0 = holding[0][1].Edges[holding[0][2]]
                edg1 = holding[1][1].Edges[holding[1][2]]
                if self.hasCommonVertex(edg0, edg1, show=False) < 0:
                    # Edges not connected - probably top and bottom if faces in loop
                    if edg0.CenterOfMass.z > edg1.CenterOfMass.z:
                        high.append(holding[0])
                        low.append(holding[1])
                    else:
                        high.append(holding[1])
                        low.append(holding[0])
                else:
                    # Edges are connected - all top, or all bottom edges
                    com = FreeCAD.Vector(0, 0, 0)
                    com.add(edg0.CenterOfMass)
                    com.add(edg1.CenterOfMass)
                    avgCom = FreeCAD.Vector(com.x / 2.0, com.y / 2.0, com.z / 2.0)
                    if avgCom.z > face.CenterOfMass.z:
                        high.extend(holding)
                    else:
                        low.extend(holding)
            elif len(holding) > 2:
                # attempt to break edges into two groups of connected edges.
                # determine which group has higher center of mass, and assign as high, the other as low
                (lw, hgh) = self.groupConnectedEdges(holding)
                low.extend(lw)
                high.extend(hgh)
            # Eif
        # Efor
        return (low, high)

    def hasCommonVertex(self, edge1, edge2, show=False):
        """findCommonVertexIndexes(edge1, edge2, show=False)
        Compare vertexes of two edges to identify a common vertex.
        Returns the vertex index of edge1 to which edge2 is connected"""
        if show is True:
            Path.Log.info("New findCommonVertex()... ")

        oIdx = 0
        listOne = edge1.Vertexes
        listTwo = edge2.Vertexes

        # Find common vertexes
        for o in listOne:
            if show is True:
                Path.Log.info("   one ({}, {}, {})".format(o.X, o.Y, o.Z))
            for t in listTwo:
                if show is True:
                    Path.Log.error("two ({}, {}, {})".format(t.X, t.Y, t.Z))
                if o.X == t.X:
                    if o.Y == t.Y:
                        if o.Z == t.Z:
                            if show is True:
                                Path.Log.info("found")
                            return oIdx
            oIdx += 1
        return -1

    def groupConnectedEdges(self, holding):
        """groupConnectedEdges(self, holding)
        Take edges and determine which are connected.
        Group connected chains/loops into: low and high"""
        holds = []
        grps = []
        searched = []
        stop = False
        attachments = []
        loops = 1

        def updateAttachments(grps):
            atchmnts = []
            lenGrps = len(grps)
            if lenGrps > 0:
                lenG0 = len(grps[0])
                if lenG0 < 2:
                    atchmnts.append((0, 0))
                else:
                    atchmnts.append((0, 0))
                    atchmnts.append((0, lenG0 - 1))
            if lenGrps == 2:
                lenG1 = len(grps[1])
                if lenG1 < 2:
                    atchmnts.append((1, 0))
                else:
                    atchmnts.append((1, 0))
                    atchmnts.append((1, lenG1 - 1))
            return atchmnts

        def isSameVertex(o, t):
            if o.X == t.X:
                if o.Y == t.Y:
                    if o.Z == t.Z:
                        return True
            return False

        for hi in range(0, len(holding)):
            holds.append(hi)

        # Place initial edge in first group and update attachments
        h0 = holds.pop()
        grps.append([h0])
        attachments = updateAttachments(grps)

        while len(holds) > 0:
            if loops > 500:
                Path.Log.error("BREAK --- LOOPS LIMIT of 500 ---")
                break
            save = False

            h2 = holds.pop()
            (sub2, face2, ei2) = holding[h2]

            # Cycle through attachments for connection to existing
            for g, t in attachments:
                h1 = grps[g][t]
                (sub1, face1, ei1) = holding[h1]

                edg1 = face1.Edges[ei1]
                edg2 = face2.Edges[ei2]

                # CV = self.hasCommonVertex(edg1, edg2, show=False)

                # Check attachment based on attachments order
                if t == 0:
                    # is last vertex of h2 == first vertex of h1
                    e2lv = len(edg2.Vertexes) - 1
                    one = edg2.Vertexes[e2lv]
                    two = edg1.Vertexes[0]
                    if isSameVertex(one, two) is True:
                        # Connected, insert h1 in front of h2
                        grps[g].insert(0, h2)
                        stop = True
                else:
                    # is last vertex of h1 == first vertex of h2
                    e1lv = len(edg1.Vertexes) - 1
                    one = edg1.Vertexes[e1lv]
                    two = edg2.Vertexes[0]
                    if isSameVertex(one, two) is True:
                        # Connected, append h1 after h2
                        grps[g].append(h2)
                        stop = True

                if stop is True:
                    # attachment was found
                    attachments = updateAttachments(grps)
                    holds.extend(searched)
                    stop = False
                    break
                else:
                    # no attachment found
                    save = True
            # Efor
            if save is True:
                searched.append(h2)
                if len(holds) == 0:
                    if len(grps) == 1:
                        h0 = searched.pop(0)
                        grps.append([h0])
                        attachments = updateAttachments(grps)
                        holds.extend(searched)
            # Eif
            loops += 1
        # Ewhile

        low = []
        high = []
        if len(grps) == 1:
            grps.append([])
        grp0 = []
        grp1 = []
        com0 = FreeCAD.Vector(0, 0, 0)
        com1 = FreeCAD.Vector(0, 0, 0)
        if len(grps[0]) > 0:
            for g in grps[0]:
                grp0.append(holding[g])
                (sub, face, ei) = holding[g]
                com0 = com0.add(face.Edges[ei].CenterOfMass)
            com0z = com0.z / len(grps[0])
        if len(grps[1]) > 0:
            for g in grps[1]:
                grp1.append(holding[g])
                (sub, face, ei) = holding[g]
                com1 = com1.add(face.Edges[ei].CenterOfMass)
            com1z = com1.z / len(grps[1])

        if len(grps[1]) > 0:
            if com0z > com1z:
                low = grp1
                high = grp0
            else:
                low = grp0
                high = grp1
        else:
            low = grp0
            high = grp0

        return (low, high)

    def getMinMaxOfFaces(self, Faces):
        """getMinMaxOfFaces(Faces)
        return the zmin and zmax values for given set of faces or edges."""
        zmin = Faces[0].BoundBox.ZMax
        zmax = Faces[0].BoundBox.ZMin
        for f in Faces:
            if f.BoundBox.ZMin < zmin:
                zmin = f.BoundBox.ZMin
            if f.BoundBox.ZMax > zmax:
                zmax = f.BoundBox.ZMax
        return (zmin, zmax)


def _identifyRemovalSolids(sourceShape, commonShapes):
    """_identifyRemovalSolids(sourceShape, commonShapes)
    Loops through solids in sourceShape to identify commonality with solids in commonShapes.
    The sourceShape solids with commonality are returned as Part.Compound shape."""
    common = Part.makeCompound(commonShapes)
    removalSolids = [s for s in sourceShape.Solids if s.common(common).Volume > 0.0]
    return Part.makeCompound(removalSolids)


def _extrudeBaseDown(base):
    """_extrudeBaseDown(base)
    Extrudes and fuses all non-vertical faces downward to a level 1.0 mm below base ZMin."""
    allExtrusions = list()
    zMin = base.Shape.BoundBox.ZMin
    bbFace = Path.Geom.makeBoundBoxFace(base.Shape.BoundBox, offset=5.0)
    bbFace.translate(FreeCAD.Vector(0.0, 0.0, float(int(base.Shape.BoundBox.ZMin - 5.0))))
    direction = FreeCAD.Vector(0.0, 0.0, -1.0)

    # Make projections of each non-vertical face and extrude it
    for f in base.Shape.Faces:
        fbb = f.BoundBox
        if not Path.Geom.isRoughly(f.normalAt(0, 0).z, 0.0):
            pp = bbFace.makeParallelProjection(f.Wires[0], direction)
            face = Part.Face(Part.Wire(pp.Edges))
            face.translate(FreeCAD.Vector(0.0, 0.0, fbb.ZMin))
            ext = face.extrude(FreeCAD.Vector(0.0, 0.0, zMin - fbb.ZMin - 1.0))
            allExtrusions.append(ext)

    # Fuse all extrusions together
    seed = allExtrusions.pop()
    fusion = seed.fuse(allExtrusions)
    fusion.translate(FreeCAD.Vector(0.0, 0.0, zMin - fusion.BoundBox.ZMin - 1.0))

    return fusion.cut(base.Shape)


def SetupProperties():
    return PathPocketBase.SetupProperties() + ["HandleMultipleFeatures"]


def Create(name, obj=None, parentJob=None):
    """Create(name) ... Creates and returns a Pocket operation."""
    if obj is None:
        obj = FreeCAD.ActiveDocument.addObject("Path::FeaturePython", name)
    obj.Proxy = ObjectPocket(obj, name, parentJob)
    return obj
