# -*- coding: utf-8 -*-

__title__ = "Comb plot"
__author__ = "Christophe Grellier (Chris_G)"
__license__ = "LGPL 2.1"
__doc__ = 'Creates a parametric Comb plot on selected edges'

import os
import FreeCAD
import FreeCADGui
import Part
from freecad.Curves import _utils
from freecad.Curves import ICONPATH
from FreeCAD import Base
from pivy import coin

TOOL_ICON = os.path.join(ICONPATH, 'comb.svg')
DEBUG = False

def debug(string):
    if DEBUG:
        FreeCAD.Console.PrintMessage(string)
        FreeCAD.Console.PrintMessage("\n")


def getEdgeParamList(edge, start = None, end = None, num = 64):
    res = []
    if num <= 1:
        num = 2
    if not start:
        start = edge.FirstParameter
    if not end:
        end = edge.LastParameter
    step = (end - start) / (num-1)
    for i in range(num):
        res.append(start + i * step)
    return res


def getEdgePointList(edge, paramList):
    res = []
    for p in paramList:
        res.append(edge.valueAt(p))
    return res


def getEdgeCurvatureList(edge, paramList):
    res = []
    for p in paramList:
        res.append(edge.curvatureAt(p))
    return res


def getEdgeNormalList(edge, paramList):
    res = []
    for p in paramList:
        try:
            res.append(edge.normalAt(p))
        except:
            debug("Normal error")
            res.append(FreeCAD.Vector(1e-3,1e-3,1e-3))
    return res



def getEdgePointCurvNormList(edge, paramList):
    ''' No perf gain '''
    pts = []
    cur = []
    nor = []
    for p in paramList:
        pts.append(edge.valueAt(p))
        cur.append(edge.curvatureAt(p))
        nor.append(edge.normalAt(p))
    return [pts,cur,nor]


def getEdgeData(edge, paramList):
    pts = getEdgePointList(edge, paramList)
    cur = getEdgeCurvatureList(edge, paramList)
    nor = getEdgeNormalList(edge, paramList)
    return [pts,cur,nor]

def getCombPoints(data, scale):
    pts = []
    for i in range(len(data[0])):
        v = FreeCAD.Vector(data[2][i]).multiply(data[1][i]*scale)
        pts.append(data[0][i].add(v))
    return pts

def getSoPoints(data , scale):
    pts = []
    for i in range(len(data[0])):
        v = FreeCAD.Vector(data[2][i]).multiply(data[1][i]*scale)
        w = data[0][i].add(v.negative())
        pts.append((data[0][i].x,data[0][i].y,data[0][i].z))
        pts.append((w.x,w.y,w.z))
    return pts

def getCombCoords(fp):
    coords = []
    n = 0.5 * len(fp.CombPoints) / fp.Samples
    for r in range(int(n)):
        for i in range(fp.Samples):
            c = 2*r*fp.Samples + i*2
            coords += [c, c+1, -1]
    return coords

def getCurveCoords(fp):
    coords = []
    n = 0.5 * len(fp.CombPoints) / fp.Samples
    for r in range(int(n)):
        for i in range(fp.Samples):
            coords.append(2*r*fp.Samples + i*2 + 1)
        coords.append(-1)
    return coords

class isoEdge:
    def __init__(self, surf, ori, param):
        self.surf = surf
        self.face = surf.toShape()
        self.ori = ori
        self.param = param
        self.u0, self.u1, self.v0, self.v1 = surf.bounds()
        if self.ori == 'U':
            p0 = Base.Vector2d(param, self.v0)
            p1 = Base.Vector2d(param, self.v1)
            self.edge3d = surf.uIso(param).toShape()
        else:
            p0 = Base.Vector2d(self.u0, param)
            p1 = Base.Vector2d(self.u1, param)
            self.edge3d = surf.vIso(param).toShape()
        self.line2d = Part.Geom2d.Line2dSegment(p0,p1)
        self.Length = self.edge3d.Length
        self.FirstParameter = self.edge3d.FirstParameter
        self.LastParameter = self.edge3d.LastParameter

    def valueAt(self, p):
        return self.edge3d.valueAt(p)

    def normalAt(self, p):
        p3d = self.valueAt(p)
        p2d = self.surf.parameter(p3d)
        n = self.face.normalAt(p2d[0], p2d[1])
        #c = self.curvatureAt(p, 
        return n.negative()

    def curvatureAt(self, p, curvType = "Tangent"):
        if curvType == "Tangent":
            return self.edge3d.curvatureAt(p)
        p3d = self.valueAt(p)
        p2d = self.surf.parameter(p3d)
        curv = self.face.curvatureAt(p2d[0], p2d[1])
        if curvType == "Min":
            return curv[0]
        elif curvType == "Max":
            return curv[1]
        elif curvType == "Mean":
            return (curv[0] + curv[1])/2
        elif curvType == "Gauss":
            # TODO : get the Gaussian curvature formula
            return curv[0]

class Comb:
    def __init__(self, obj , edge):
        ''' Add the properties '''
        debug("Comb class Init")
        obj.addProperty("App::PropertyLinkSubList","Edge","Comb","Edge").Edge = edge
        #obj.addProperty("App::PropertyEnumeration","Type","Comb","Comb Type").Type=["Curvature","Unit Normal"]
        obj.addProperty("App::PropertyFloat","Scale","Comb","Scale (%). 0 for AutoScale").Scale=0.0
        #obj.addProperty("App::PropertyBool","ScaleAuto","Comb","Automatic Scale").ScaleAuto = True
        obj.addProperty("App::PropertyIntegerConstraint","Samples","Comb","Number of samples").Samples = 100
        obj.addProperty("App::PropertyInteger","Number","Surface","Number of surface samples").Number = 3
        obj.addProperty("App::PropertyEnumeration","Orientation","Surface","Surface Comb Orientation").Orientation=["U","V","UV"]
        #obj.addProperty("App::PropertyFloat","TotalLength","Comb","Total length of edges")
        obj.addProperty("App::PropertyVectorList","CombPoints","Comb","CombPoints")
        obj.addProperty("Part::PropertyPartShape","Shape","Comb", "Shape of comb plot")
        obj.Proxy = self
        #obj.Samples = (20,2,1000,10)
        obj.CombPoints = []
        self.edges = []
        self.TotalLength = 0.0
        self.factor = 1.0
        #self.selectedEdgesToProperty( obj, edge)
        #self.setEdgeList( obj)
        self.execute(obj)
        obj.Scale = self.factor
        
    def selectedEdgesToProperty(self, obj, edge):
        objs = []
        for o in edge:
            if isinstance(o,tuple) or isinstance(o,list):
                if o[0].Name != obj.Name:
                    objs.append(tuple(o))
            else:
                for el in o.SubElementNames:
                    if "Edge" in el:
                        if o.Object.Name != obj.Name:
                            objs.append((o.Object,el))
        if objs:
            obj.Edge = objs
            debug(str(edge) + "")
            debug(str(obj.Edge) + "")

    def computeTotalLength(self, obj):
        totalLength = 0.0
        for e in obj.Edge:
            o = e[0]
            debug(str(o) + " - ")
            for f in e[1]:
                if 'Edge' in f:
                    n = eval(f.lstrip('Edge'))
                    debug(str(n) + "")
                    if o.Shape.Edges:
                        g = o.Shape.Edges[n-1]
                        totalLength += g.Length
                elif 'Face' in f:
                    n = eval(f.lstrip('Face'))
                    debug(str(n) + "")
                    if o.Shape.Faces:
                        g = o.Shape.Faces[n-1]
                        try:
                            if 'U' in obj.Orientation:
                                bounds = g.Surface.bounds()
                                midParam = bounds[0] + (bounds[1] - bounds[0]) / 2
                                iso = isoEdge(g.Surface,'U',midParam)       #g.Surface.uIso(midParam).toShape()
                                totalLength += iso.Length
                            if 'V' in obj.Orientation:
                                bounds = g.Surface.bounds()
                                midParam = bounds[2] + (bounds[3] - bounds[2]) / 2
                                iso = isoEdge(g.Surface,'V',midParam)    #g.Surface.vIso(midParam).toShape()
                                totalLength += iso.Length
                        except:
                            debug("Surface Error")
 
 
        self.TotalLength = totalLength
        debug("Total Length : " + str(self.TotalLength) + "")

    def setEdgeList( self, obj):
        edgeList = []
        debug(str(obj.Edge) + "")
        for e in obj.Edge:
            o = e[0]
            debug(str(o.Name) + " - ")
            for f in e[1]:
                if 'Edge' in f:
                    n = eval(f.lstrip('Edge'))
                    debug('Edge ' + str(n) + "")
                    debug(str(o.Shape) + "")
                    if o.Shape.Edges:
                        edgeList.append(o.Shape.Edges[n-1])
                elif 'Face' in f:
                    n = eval(f.lstrip('Face'))
                    debug('Face ' + str(n) + "")
                    debug(str(o.Shape) + "")
                    if o.Shape.Faces:
                        g = o.Shape.Faces[n-1]
                        #try:
                        if 'U' in obj.Orientation:
                            #iso = self.getuIsoEdges(g,obj.Number)
                            edgeList += self.getuIsoEdges(g,obj.Number)
                        if 'V' in obj.Orientation:
                            edgeList += self.getvIsoEdges(g,obj.Number)
                        #edgeList += iso
                        #except:
                            #debug("Surface Error")
        self.edges = edgeList
 
 
    def getuIsoEdges(self, face, samples):
        res = []
        n = []
        bounds = face.Surface.bounds()
        if samples <= 1:
            midParam = bounds[0] + (bounds[1] - bounds[0]) / 2
            n = [midParam]
        elif samples == 2:
            n = [bounds[0],bounds[1]]
        else :
            brange = bounds[1] - bounds[0]
            for  i in range(samples-1):
                n.append(bounds[0] + brange*i/(samples-1))
            n.append(bounds[1])
        for t in n:
            res.append(isoEdge(face.Surface,'U',t))        #(face.Surface.uIso(t).toShape())
        debug("U Iso curves :")
        debug(str(res))
        return res


    def getvIsoEdges(self, face, samples):
        res = []
        n = []
        bounds = face.Surface.bounds()
        if samples <= 1:
            midParam = bounds[2] + (bounds[3] - bounds[2]) / 2
            n = [midParam]
        elif samples == 2:
            n = [bounds[2], bounds[3]]
        else :
            brange = bounds[3] - bounds[2]
            for  i in range(samples-1):
                n.append(bounds[2] + brange*i/(samples-1))
            n.append(bounds[3])
        for t in n:
            res.append(isoEdge(face.Surface,'V',t))   #(face.Surface.vIso(t).toShape())
        debug("V Iso curves :")
        debug(str(res))
        return res
    
    
    def getMaxCurv(self, obj):
        self.maxCurv = 0.001
        for e in self.edges:
            pl = getEdgeParamList(e, None, None, obj.Samples)
            cl = getEdgeCurvatureList(e, pl)
            m = max(cl)
            if self.maxCurv < m:
                self.maxCurv = m
        debug("max curvature : "+str(self.maxCurv)+"")
                
    def getCurvFactor(self, obj):
        self.factor = 1.0
        if hasattr(obj, "Scale"):
            if obj.Scale == 0.0:
                self.factor = 0.5 * self.TotalLength / self.maxCurv
            else:
                self.factor = obj.Scale
        #if hasattr(obj, "ScaleAuto"):
            #if obj.ScaleAuto:
                #self.factor = 0.5 * self.TotalLength / self.maxCurv
        debug("Curvature Factor : "+str(self.factor)+"")

    def buildPoints(self, obj):
        obj.CombPoints = []
        pts = []
        for e in self.edges:
            pl = getEdgeParamList(e, None, None, obj.Samples)
            data = getEdgeData(e, pl)
            pts += getSoPoints(data , self.factor)
        obj.CombPoints = pts
        debug(str(len(obj.CombPoints))+" Comb points")   #+str(obj.CombPoints)+"")

    def execute(self, obj):
        debug("***** execute *****")
        #self.selectedEdgesToProperty( obj, edge)
        self.setEdgeList( obj)
        self.computeTotalLength( obj)
        self.getMaxCurv( obj)
        self.getCurvFactor( obj)
        self.buildPoints( obj)
        debug("----- execute -----")

    def onChanged(self, fp, prop):
        #print fp
        if not fp.Edge:
            return
        if prop == "Edge":
            debug("Comb : Edge changed")
            #self.setEdgeList( fp)
            self.execute(fp)
        if prop == "Type":
            debug("Comb : Type Property changed")
        if prop == "Scale":
            if fp.Scale <= 0.0:
                self.factor = 0.5 * self.TotalLength / self.maxCurv
                fp.Scale = self.factor
            debug("Comb : Scale Property changed to "+str(fp.Scale)+"")
            self.execute(fp)
        if prop == "Samples":
            if fp.Samples < 10:
                fp.Samples = 10
            debug("Comb : Samples Property changed")
            self.execute(fp)
        #if prop == "ScaleAuto":
            #debug("Comb : ScaleAuto Property changed")
            #if fp.ScaleAuto:
                #self.execute(fp)
                #fp.Scale = self.factor * 100
            
    def __getstate__(self):
        return None

    def __setstate__(self,state):
        return None




class ViewProviderComb:
    def __init__(self, obj):
        "Set this object to the proxy object of the actual view provider"
        obj.addProperty("App::PropertyColor","CurveColor","Comb","Color of the curvature curve").CurveColor=(0.8,0.0,0.0)
        obj.addProperty("App::PropertyColor","CombColor","Comb","Color of the curvature comb").CombColor=(0.0,0.8,0.0)
        # TODO : add transparency property
        self.Object = obj.Object
        obj.Proxy = self
            
    def createNodes(self, fp):
        self.nodes = []
        for e in self.edges:
            node = SoComb(e)
            if not (node in self.nodes):
                self.nodes.append(node)
                self.wireframe.addChild(node)

    def attach(self, obj):
        #debug("Comb : ViewProviderComb.attach ")

        self.oldx = 0
        self.oldy = 0
        self.wireframe = coin.SoGroup()

        self.combColor  =  coin.SoBaseColor()
        self.curveColor =  coin.SoBaseColor()
        self.combColor.rgb  = (obj.CombColor[0],obj.CombColor[1],obj.CombColor[2])
        self.curveColor.rgb = (obj.CurveColor[0],obj.CurveColor[1],obj.CurveColor[2])
        self.points = coin.SoCoordinate3()
        self.combLines = coin.SoIndexedLineSet()
        self.curveLines = coin.SoIndexedLineSet()
        self.wireframe.addChild(self.points)
        self.wireframe.addChild(self.combColor)
        self.wireframe.addChild(self.combLines)
        self.wireframe.addChild(self.curveColor)
        self.wireframe.addChild(self.curveLines)
        
        #self.selectionNode = coin.SoType.fromName("SoFCSelection").createInstance()
        #self.selectionNode.documentName.setValue(FreeCAD.ActiveDocument.Name)
        #self.selectionNode.objectName.setValue(obj.Object.Name) # here obj is the ViewObject, we need its associated App Object
        #self.selectionNode.subElementName.setValue("Comb")
        #self.selectionNode.addChild(self.curveLines)
            

        #self.wireframe.addChild(self.selectionNode)
        obj.addDisplayMode(self.wireframe,"Wireframe")
        #self.onChanged(obj,"Color")

    def updateData(self, fp, prop):
        self.combLines.coordIndex.setValue(0)
        self.curveLines.coordIndex.setValue(0)
        self.points.point.setValues(0,0,[])
        
        p = fp.CombPoints
        self.points.point.setValues(0,len(p),p)
        
        i1 = getCombCoords(fp)
        self.combLines.coordIndex.setValues(0,len(i1),i1)
        #debug(""+str(i1)+"")
        
        i2 = getCurveCoords(fp)
        self.curveLines.coordIndex.setValues(0,len(i2),i2)
        #debug(""+str(i2)+"")

    def loc_cb(self, event_callback):
        event = event_callback.getEvent()
        pos = event.getPosition()
        x,y = pos.getValue()
        if event.wasCtrlDown():
            if y > self.oldy:
                self.Object.Samples +=5
            elif y < self.oldy:
                self.Object.Samples -=5
        if event.wasShiftDown():
            if y > self.oldy:
                self.Object.Scale *= 1.02
            elif y < self.oldy:
                self.Object.Scale /= 1.02
        self.oldx = x
        self.oldy = y

    def setEdit(self,vobj,mode=0):
        if mode == 0:
            FreeCAD.Console.PrintWarning("--- Entering Comb Edit Mode ---\n")
            FreeCAD.Console.PrintMessage("Nb of samples : CTRL  + Mouse Up / Down\n")
            FreeCAD.Console.PrintMessage("Comb scale :    SHIFT + Mouse Up / Down\n")
            self.view = FreeCADGui.ActiveDocument.ActiveView
            self.locCB = self.view.addEventCallbackPivy( coin.SoLocation2Event.getClassTypeId(), self.loc_cb)
            return True
        return False

    def unsetEdit(self,vobj,mode=0):
        FreeCAD.Console.PrintWarning("--- Exiting Comb Edit Mode ---\n")
        self.view.removeEventCallbackPivy( coin.SoLocation2Event.getClassTypeId(), self.locCB)
        return False

    def doubleClicked(self,vobj):
        if not hasattr(self,'editing'):
            self.editing = False
        if not self.editing:
            self.editing = True
            #self.setEdit(vobj)
            FreeCADGui.ActiveDocument.setEdit(self.Object,0)
        else:
            self.editing = False
            #self.unsetEdit(vobj)
            FreeCADGui.ActiveDocument.setEdit(self.Object,1)
        return True


    def getDisplayModes(self,obj):
         "Return a list of display modes."
         modes=[]
         modes.append("Wireframe")
         return modes

    def getDefaultDisplayMode(self):
         "Return the name of the default display mode. It must be defined in getDisplayModes."
         return "Wireframe"

    def setDisplayMode(self,mode):
         return mode

    def onChanged(self, vp, prop):
        "Here we can do something when a single property got changed"
        if prop == "Edge":
            debug("vp detected a Edge change")
        if prop == "CurveColor":
            self.curveColor.rgb = (vp.CurveColor[0],vp.CurveColor[1],vp.CurveColor[2])
        elif prop == "CombColor":
            self.combColor.rgb  = (vp.CombColor[0],vp.CombColor[1],vp.CombColor[2])
        return
        
    def getIcon(self):
        return TOOL_ICON

    def __getstate__(self):
        return None

    def __setstate__(self,state):
        return None

class ParametricComb:
    def parseSel(self, selectionObject):
        res = []
        for obj in selectionObject:
            if obj.HasSubObjects:
                i = 0
                for subobj in obj.SubObjects:
                    if issubclass(type(subobj),Part.Edge):
                        res.append((obj.Object,[obj.SubElementNames[i]]))
                        #res.append(obj.SubElementNames[i])
                    if issubclass(type(subobj),Part.Face):
                        res.append((obj.Object,[obj.SubElementNames[i]]))
                        #res.append(obj.SubElementNames[i])
                    i += 1
            else:
                i = 0
                for e in obj.Object.Shape.Edges:
                    n = "Edge"+str(i)
                    res.append((obj.Object,[n]))
                    #res.append(n)
                    i += 1
                for f in obj.Object.Shape.Faces:
                    n = "Face"+str(i)
                    res.append((obj.Object,[n]))
                    #res.append(n)
                    i += 1
        return res
    
    def findComb(self, sel):
        res = None
        module = None
        for obj in sel:
            debug("-- Parsing Object : "+str(obj.Object.Label)+"")
            try:
                module = obj.Object.Proxy.__module__
                if module == 'ParametricComb':
                    res = obj.Object
                    debug("Found active Comb : "+str(res.Label)+"")
            except:
                debug("No module found")

        return res
    
    def appendEdges(self, comb, edges):
        existingEdges = comb.Edge
        newEdges = existingEdges + edges
        comb.Edge = newEdges
    
    def Activated(self):
        s = FreeCADGui.Selection.getSelectionEx()
        edges = self.parseSel(s)
        #debug(str(edges) + "")
        combSelected = self.findComb(s)
        if not combSelected:
            obj=FreeCAD.ActiveDocument.addObject("App::FeaturePython","Comb") #add object to document
            Comb(obj,edges)
            ViewProviderComb(obj.ViewObject)
        else:
            self.appendEdges(combSelected, edges)
            
    def GetResources(self):
        return {'Pixmap' : TOOL_ICON, 'MenuText': __title__, 'ToolTip': __doc__}

FreeCADGui.addCommand('ParametricComb', ParametricComb())