# ***************************************************************************
# *   Copyright (c) 2009, 2010 Yorik van Havre <yorik@uncreated.net>        *
# *   Copyright (c) 2009, 2010 Ken Cline <cline@frii.com>                   *
# *                                                                         *
# *   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 various functions to work with faces."""
## @package faces
# \ingroup draftgeoutils
# \brief Provides various functions to work with faces.

import lazy_loader.lazy_loader as lz

import DraftVecUtils
from FreeCAD import Base
from draftgeoutils.general import precision
from draftgeoutils.geometry import are_coplanar

# Delay import of module until first use because it is heavy
Part = lz.LazyLoader("Part", globals(), "Part")

## \addtogroup draftgeoutils
# @{


def concatenate(shape):
    """Turn several faces into one."""
    boundary_edges = getBoundary(shape)
    sorted_edges = Part.sortEdges(boundary_edges)

    try:
        wires = [Part.Wire(edges) for edges in sorted_edges]
        face = Part.makeFace(wires, "Part::FaceMakerBullseye")
    except Base.FreeCADError:
        print("DraftGeomUtils: Fails to join faces into one. "
              + "The precision of the faces would be insufficient")
        return shape
    else:
        if not wires[0].isClosed():
            return wires[0]
        else:
            return face


def getBoundary(shape):
    """Return the boundary edges of a group of faces."""
    if isinstance(shape, list):
        shape = Part.makeCompound(shape)

    # Make a lookup-table where we get the number of occurrences
    # to each edge in the fused face
    table = dict()
    for f in shape.Faces:
        for e in f.Edges:
            hash_code = e.hashCode()
            if hash_code in table:
                table[hash_code] = table[hash_code] + 1
            else:
                table[hash_code] = 1

    # Filter out the edges shared by more than one sub-face
    bound = list()
    for e in shape.Edges:
        if table[e.hashCode()] == 1:
            bound.append(e)
    return bound


def is_coplanar(faces, tol=-1):
    """Return True if all faces in the given list are coplanar.

    Parameters
    ----------
    faces: list
        List of faces to check coplanarity.
    tol: float, optional
        It defaults to `-1`, the tolerance of confusion, equal to 1e-7.
        Is the maximum deviation to be considered coplanar.

    Returns
    -------
    out: bool
        True if all face are coplanar. False in other case.
    """

    first_face = faces[0]
    for face in faces:
        if not are_coplanar(first_face, face, tol):
            return False

    return True

isCoplanar = is_coplanar


def bind(w1, w2):
    """Bind 2 wires by their endpoints and returns a face."""
    if not w1 or not w2:
        print("DraftGeomUtils: unable to bind wires")
        return None

    if w1.isClosed() and w2.isClosed():
        d1 = w1.BoundBox.DiagonalLength
        d2 = w2.BoundBox.DiagonalLength
        if d1 > d2:
            # w2.reverse()
            return Part.Face([w1, w2])
        else:
            # w1.reverse()
            return Part.Face([w2, w1])
    else:
        try:
            w3 = Part.LineSegment(w1.Vertexes[0].Point,
                                  w2.Vertexes[0].Point).toShape()
            w4 = Part.LineSegment(w1.Vertexes[-1].Point,
                                  w2.Vertexes[-1].Point).toShape()
            return Part.Face(Part.Wire(w1.Edges+[w3] + w2.Edges+[w4]))
        except Part.OCCError:
            print("DraftGeomUtils: unable to bind wires")
            return None


def cleanFaces(shape):
    """Remove inner edges from coplanar faces."""
    faceset = shape.Faces

    def find(hc):
        """Find a face with the given hashcode."""
        for f in faceset:
            if f.hashCode() == hc:
                return f

    def findNeighbour(hface, hfacelist):
        """Find the first neighbour of a face, and return its index."""
        eset = []
        for e in find(hface).Edges:
            eset.append(e.hashCode())
        for i in range(len(hfacelist)):
            for ee in find(hfacelist[i]).Edges:
                if ee.hashCode() in eset:
                    return i
        return None

    # build lookup table
    lut = {}
    for face in faceset:
        for edge in face.Edges:
            if edge.hashCode() in lut:
                lut[edge.hashCode()].append(face.hashCode())
            else:
                lut[edge.hashCode()] = [face.hashCode()]

    # print("lut:",lut)
    # take edges shared by 2 faces
    sharedhedges = []
    for k, v in lut.items():
        if len(v) == 2:
            sharedhedges.append(k)

    # print(len(sharedhedges)," shared edges:",sharedhedges)
    # find those with same normals
    targethedges = []
    for hedge in sharedhedges:
        faces = lut[hedge]
        n1 = find(faces[0]).normalAt(0.5, 0.5)
        n2 = find(faces[1]).normalAt(0.5, 0.5)
        if n1 == n2:
            targethedges.append(hedge)

    # print(len(targethedges)," target edges:",targethedges)
    # get target faces
    hfaces = []
    for hedge in targethedges:
        for f in lut[hedge]:
            if f not in hfaces:
                hfaces.append(f)

    # print(len(hfaces)," target faces:",hfaces)
    # sort islands
    islands = [[hfaces.pop(0)]]
    currentisle = 0
    currentface = 0
    found = True
    while hfaces:
        if not found:
            if len(islands[currentisle]) > (currentface + 1):
                currentface += 1
                found = True
            else:
                islands.append([hfaces.pop(0)])
                currentisle += 1
                currentface = 0
                found = True
        else:
            f = findNeighbour(islands[currentisle][currentface], hfaces)
            if f is not None:
                islands[currentisle].append(hfaces.pop(f))
            else:
                found = False

    # print(len(islands)," islands:",islands)
    # make new faces from islands
    newfaces = []
    treated = []
    for isle in islands:
        treated.extend(isle)
        fset = []
        for i in isle:
            fset.append(find(i))
        bounds = getBoundary(fset)
        shp = Part.Wire(Part.__sortEdges__(bounds))
        shp = Part.Face(shp)
        if shp.normalAt(0.5, 0.5) != find(isle[0]).normalAt(0.5, 0.5):
            shp.reverse()
        newfaces.append(shp)

    # print("new faces:",newfaces)
    # add remaining faces
    for f in faceset:
        if not f.hashCode() in treated:
            newfaces.append(f)

    # print("final faces")
    # finishing
    fshape = Part.makeShell(newfaces)
    if shape.isClosed():
        fshape = Part.makeSolid(fshape)
    return fshape


def removeSplitter(shape):
    """Return a face from removing the splitter in a list of faces.

    This is an alternative, shared edge-based version of Part.removeSplitter.
    Returns a face, or `None` if the operation failed.
    """
    lookup = dict()
    for f in shape.Faces:
        for e in f.Edges:
            h = e.hashCode()
            if h in lookup:
                lookup[h].append(e)
            else:
                lookup[h] = [e]

    edges = [e[0] for e in lookup.values() if len(e) == 1]

    try:
        face = Part.Face(Part.Wire(edges))
    except Part.OCCError:
        # operation failed
        return None
    else:
        if face.isValid():
            return face

    return None

## @}
