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

__title__ = "Interpolate"
__author__ = "Christophe Grellier (Chris_G)"
__license__ = "LGPL 2.1"
__doc__ = "Interpolate a set of points."

import os
import FreeCAD
import FreeCADGui
import Part
from freecad.Curves import _utils
from freecad.Curves import ICONPATH

TOOL_ICON = os.path.join( ICONPATH, 'interpolate.svg')
debug = _utils.debug
#debug = _utils.doNothing


# ********************************************************
# **** Part.BSplineCurve.interpolate() documentation *****
# ********************************************************

#Replaces this B-Spline curve by interpolating a set of points.
#The function accepts keywords as arguments.

#interpolate(Points = list_of_points) 

#Optional arguments :

#PeriodicFlag = bool (False) : Sets the curve closed or opened.
#Tolerance = float (1e-6) : interpolating tolerance

#Parameters : knot sequence of the interpolated points.
#If not supplied, the function defaults to chord-length parameterization.
#If PeriodicFlag == True, one extra parameter must be appended.

#EndPoint Tangent constraints :

#InitialTangent = vector, FinalTangent = vector
#specify tangent vectors for starting and ending points 
#of the BSpline. Either none, or both must be specified.

#Full Tangent constraints :

#Tangents = list_of_vectors, TangentFlags = list_of_bools
#Both lists must have the same length as Points list.
#Tangents specifies the tangent vector of each point in Points list.
#TangentFlags (bool) activates or deactivates the corresponding tangent.
#These arguments will be ignored if EndPoint Tangents (above) are also defined.

#Note : Continuity of the spline defaults to C2. However, if periodic, or tangents 
#are supplied, the continuity will drop to C1.

class Interpolate:
    def __init__(self, obj , source):
        ''' Add the properties '''
        debug("\nInterpolate class Init\n")
        obj.addProperty("App::PropertyLink",           "Source",         "General",    "Source object that provides points to interpolate")
        obj.addProperty("App::PropertyLinkSubList",    "PointList",      "General",    "Point list to interpolate")
        obj.addProperty("App::PropertyBool",           "Periodic",       "General",    "Set the curve closed").Periodic = False
        obj.addProperty("App::PropertyFloat",          "Tolerance",      "General",    "Interpolation tolerance").Tolerance = 1e-7
        obj.addProperty("App::PropertyBool",           "CustomTangents", "General",    "User specified tangents").CustomTangents = False
        obj.addProperty("App::PropertyBool",           "DetectAligned",  "General",    "interpolate 3 aligned points with a line").DetectAligned = False
        obj.addProperty("App::PropertyBool",           "Polygonal",      "General",    "interpolate with a degree 1 polygonal curve").Polygonal = False
        obj.addProperty("App::PropertyBool",           "WireOutput",     "Parameters", "outputs a wire or a single edge").WireOutput = False
        obj.addProperty("App::PropertyFloatList",      "Parameters",     "Parameters", "Parameters of interpolated points")
        obj.addProperty("App::PropertyEnumeration",    "Parametrization","Parameters", "Parametrization type").Parametrization=["ChordLength","Centripetal","Uniform","Custom"]
        obj.addProperty("App::PropertyVectorList",     "Tangents",       "General",    "Tangents at interpolated points")
        obj.addProperty("App::PropertyBoolList",       "TangentFlags",   "General",    "Activation flag of tangents")
        obj.Proxy = self
        if isinstance(source, (list, tuple)):
            obj.PointList = source
            obj.setEditorMode("Source", 2)
        else:
            obj.Source = source
            obj.setEditorMode("PointList", 2)
        self.obj = obj
        obj.Parametrization = "ChordLength"
        obj.setEditorMode("CustomTangents", 2)
        obj.setEditorMode("DetectAligned", 2)

    def setTolerance(self, obj):
        try:
            l = obj.PointObject.Shape.BoundBox.DiagonalLength
            obj.ApproxTolerance = l / 10000.0
        except:
            obj.ApproxTolerance = 0.001

    def getPoints( self, obj):
        if obj.Source:
            return [v.Point for v in obj.Source.Shape.Vertexes]
        elif obj.PointList:
            vl = _utils.getShape(obj, "PointList", "Vertex")
            return [v.Point for v in vl]
        else:
            return []

    def detect_aligned_pts(self, fp, pts):
        tol = .99
        tans = fp.Tangents
        flags = [False]*len(pts) #list(fp.TangentFlags)
        for i in range(len(pts)-2):
            v1 = pts[i+1]-pts[i]
            v2 = pts[i+2]-pts[i+1]
            l1 = v1.Length
            l2 = v2.Length
            v1.normalize()
            v2.normalize()
            if v1.dot(v2) > tol:
                debug("aligned points detected : %d - %d - %d"%(i,i+1,i+2))
                tans[i] = v1.multiply(l1/3.0)
                tans[i+2] = v2.multiply(l2/3.0)
                tans[i+1] = (v1+v2).multiply(min(l1,l2)/6.0)
                flags[i] = True
                flags[i+1] = True
                flags[i+2] = True
        fp.Tangents = tans
        fp.TangentFlags = flags
        

    def execute(self, obj):
        debug("* Interpolate : execute *")
        pts = self.getPoints(obj)
        self.setParameters(obj)
        if obj.Polygonal:
            if obj.Periodic:
                pts.append(pts[0])
            poly = Part.makePolygon(pts)
            if obj.WireOutput:
                obj.Shape = poly
                return
            else:
                bs = poly.approximate(1e-8,obj.Tolerance,999,1)
        else:
            bs = Part.BSplineCurve()
            bs.interpolate(Points=pts, PeriodicFlag=obj.Periodic, Tolerance=obj.Tolerance, Parameters=obj.Parameters)
            if not (len(obj.Tangents) == len(pts) and len(obj.TangentFlags) == len(pts)): # or obj.DetectAligned:
                if obj.Periodic:
                    obj.Tangents = [bs.tangent(p)[0] for p in obj.Parameters[0:-1]]
                else:
                    obj.Tangents = [bs.tangent(p)[0] for p in obj.Parameters]
                obj.TangentFlags = [True]*len(pts)
            if obj.CustomTangents: # or obj.DetectAligned:
                #if obj.DetectAligned:
                    #self.detect_aligned_pts(obj, pts)
                bs.interpolate(Points=pts, PeriodicFlag=obj.Periodic, Tolerance=obj.Tolerance, Parameters=obj.Parameters, Tangents=obj.Tangents, TangentFlags=obj.TangentFlags) #, Scale=False)
        obj.Shape = bs.toShape()

    def setParameters(self, obj):
        # Computes a knot Sequence for a set of points
        # fac (0-1) : parameterization factor
        # fac=0 -> Uniform / fac=0.5 -> Centripetal / fac=1.0 -> Chord-Length
        pts = self.getPoints(obj)
        val = 1.0 # Chord-length
        if obj.Parametrization == "Custom":
            return
        
        elif obj.Parametrization == "Centripetal":
            val = 0.5
        elif obj.Parametrization == "Uniform":
            val = 0.0
        if obj.Periodic: # we need to add the first point as the end point
            pts.append(pts[0])
        params = [0]
        for i in range(1,len(pts)):
            p = pts[i].sub(pts[i-1])
            pl = pow(p.Length, val)
            params.append(params[-1] + pl)
        m = float(max(params))
        obj.Parameters = [p/m for p in params]

    def touch_parametrization(self, fp):
        p = fp.Parametrization
        fp.Parametrization = p
            
    def onChanged(self, fp, prop):
        if not self.getPoints(fp):
            return

        if prop in ("Parametrization", "Source", "PointList"):
            #debug("Approximate : Parametrization changed\n")
            if fp.Parametrization == "Custom":
                fp.setEditorMode("Parameters", 0)
            else:
                fp.setEditorMode("Parameters", 2)
                self.setParameters(fp)

        if prop == "Polygonal":
            group = ["CustomTangents","DetectAligned","Parameters","Parametrization","Tangents","TangentFlags"]
            if fp.Polygonal:
                _utils.setEditorMode(fp, group, 2)
                fp.setEditorMode("WireOutput", 0)
            else:
                _utils.setEditorMode(fp, group, 0)
                fp.setEditorMode("WireOutput", 2)
        if prop in ["Periodic","PointList"]:
            self.touch_parametrization(fp)
        #if prop == "Parameters":
            #self.execute(fp)

    def onDocumentRestored(self, fp):
        fp.setEditorMode("CustomTangents", 2)
        self.touch_parametrization(fp)

    def __getstate__(self):
        out = {"name": self.obj.Name}
        return out

    def __setstate__(self,state):
        self.obj = FreeCAD.ActiveDocument.getObject(state["name"])
        return None

class ViewProviderInterpolate:
    def __init__(self,vobj):
        vobj.Proxy = self
       
    def getIcon(self):
        return (TOOL_ICON)

    def attach(self, vobj):
        self.Object = vobj.Object
  
    def setEdit(self,vobj,mode):
        return False
    
    def unsetEdit(self,vobj,mode):
        return

    def __getstate__(self):
        return {"name": self.Object.Name}

    def __setstate__(self,state):
        self.Object = FreeCAD.ActiveDocument.getObject(state["name"])
        return None

    #def claimChildren(self):
        #return [self.Object.PointObject]
        
    #def onDelete(self, feature, subelements):
        #try:
            #self.Object.PointObject.ViewObject.Visibility=True
        #except Exception as err:
            #FreeCAD.Console.PrintError("Error in onDelete: {0} \n".format(err))
        #return True

class interpolate:
    def parseSel(self, selectionObject):
        verts = list()
        for obj in selectionObject:
            if obj.HasSubObjects:
                FreeCAD.Console.PrintMessage("object has subobjects %s\n"%str(obj.SubElementNames))
                for n in obj.SubElementNames:
                    if 'Vertex' in n:
                        verts.append((obj.Object,[n]))
            else:
                #FreeCAD.Console.PrintMessage("object has no subobjects\n")
                verts = obj.Object
        if verts:
            return verts
        else:
            FreeCAD.Console.PrintMessage("\nPlease select an object that has at least 2 vertexes")
            return None

    def Activated(self):
        try:
            s = FreeCADGui.activeWorkbench().Selection
        except AttributeError:
            s = FreeCADGui.Selection.getSelectionEx()
        source = self.parseSel(s)
        if not source:
            return False
        obj = FreeCAD.ActiveDocument.addObject("Part::FeaturePython","Interpolation_Curve") #add object to document
        Interpolate(obj,source)
        ViewProviderInterpolate(obj.ViewObject)
        FreeCAD.ActiveDocument.recompute()
            
    def GetResources(self):
        return {'Pixmap' : TOOL_ICON, 'MenuText': 'Interpolate', 'ToolTip': 'Interpolate points with a BSpline curve'}

FreeCADGui.addCommand('Interpolate', interpolate())