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

__title__ = "Nurbs tools"
__author__ = "Christophe Grellier (Chris_G)"
__license__ = "LGPL 2.1"
__doc__ = "Collection of tools for Nurbs."

import FreeCAD
import Part

#import _utils
#debug = _utils.debug
#debug = _utils.doNothing

message = FreeCAD.Console.PrintMessage

def get_bspline_data(curve): # returns a dictionary of the BSpline data
    """returns a dictionary of the BSpline data
    """
    dic = dict()
    dic["Type"] = curve.__repr__()
    dic["Continuity"] = curve.Continuity
    dic["Degree"] = curve.Degree
    dic["KnotSequence"] = curve.KnotSequence
    dic["Poles"] = curve.getPoles()
    dic["Weights"] = curve.getWeights()
    dic["isClosed"] = curve.isClosed()
    dic["isPeriodic"] = curve.isPeriodic()
    dic["isRational"] = curve.isRational()
    return dic
    

def is_same(c1, c2, tol=1e-7, full=False): # Check if BSpline curves c1 and c2 are equal
    """Check if BSpline curves c1 and c2 are equal
    return a bool
    """
    valid = True
    if full:
        message("\nCurves comparison\n")
    dat1 = get_bspline_data(c1)
    dat2 = get_bspline_data(c2)
    for key in ['Type', 'Continuity', 'Degree', 'isClosed', 'isPeriodic', 'isRational']:
        if not dat1[key] == dat2[key]:
            if full:
                message("{} mismatch : {} != {}\n".format(key, str(dat1[key]), str(dat2[key])))
                valid = False
            else:
                return False
    for key in ["KnotSequence", "Poles", "Weights"]:
        if not len(dat1[key]) == len(dat2[key]):
            if full:
                message("{} list length mismatch : {} != {}\n".format(key, str(len(dat1[key])), str(len(dat2[key]))))
                valid = False
            else:
                return False
    i = 0
    for k1, k2 in zip(dat1["KnotSequence"], dat2["KnotSequence"]):
        if abs(k1-k2) > tol:
            if full:
                message("Knot #{} mismatch : {} != {}\n".format(i, k1, k2))
                valid = False
            else:
                return False
        i += 1
    i = 0
    for p1, p2 in zip(dat1["Poles"], dat2["Poles"]):
        if p1.distanceToPoint(p2) > tol:
            if full:
                message("Pole #{} mismatch : {} != {}\n".format(i, p1, p2))
                valid = False
            else:
                return False
        i += 1
    i = 0
    for w1, w2 in zip(dat1["Weights"], dat2["Weights"]):
        if abs(w1-w2) > tol:
            if full:
                message("Weight #{} mismatch : {} != {}\n".format(i, w1, w2))
                valid = False
            else:
                return False
        i += 1
    if full:
        if valid:
            message("Curves are matching.")
        else:
            return False
    return True
    
def remove_duplicates(curves, tol=1e-7): # remove duplicate curves from a list
    "remove duplicate curves from a list"
    ret = []
    dups = 0
    for i, c1 in enumerate(curves):
        found = False
        for j, c2 in enumerate(ret):
            #print("checking curves #{} and #{}".format(i,j+i+1))
            if is_same(c1, c2, tol):
                found = True
                dups += 1
                break
        if not found:
            ret.append(c1)
    message("Removed {} duplicate curves\n".format(dups))
    return ret

def is_subsegment(edge_1, edge_2, num=20, tol=1e-7): # check if edge_1 is a trim of edge_2.
    """check if edge_1 is a trim of edge_2.
    Usage :
    is_subsegment(edge_1, edge_2, num=20, tol=1e-7) ---> bool
    'num' points are sampled on edge_1
    return False if a point is farther than tol.
    """
    try:
        e1 = edge_1.toShape()
        e2 = edge_2.toShape()
    except AttributeError:
        e1 = edge_1
        e2 = edge_2
    dist = 0
    for p in e1.discretize(num):
        d, pts, info = Part.Vertex(p).distToShape(e2)
        if d > tol:
            return False
    return True

def remove_subsegments(edges, num=20, tol=1e-7): # remove subsegment edges from a list
    "remove subsegment edges from a list"
    ret = []
    dups = 0
    for i, e1 in enumerate(edges):
        found = False
        for j, e2 in enumerate(edges):
            if not i == j:
                if is_subsegment(e1, e2, num, tol):
                    if is_subsegment(e2, e1, num, tol): # e1 == e2
                        for k, e3 in enumerate(ret):
                            if is_subsegment(e1, e3, num, tol):
                                found = True
                                dups += 1
                                break
                    else:
                        found = True
                        dups += 1
                        break
        if not found:
            ret.append(e1)
    message("Removed {} subsegment edges\n".format(dups))
    return ret

def error(s):
    FreeCAD.Console.PrintError(s)

class BsplineBasis(object):
    """Computes basis functions of a bspline curve, and its derivatives"""
    def __init__(self):
        self.knots = [0.0, 0.0, 1.0, 1.0]
        self.degree = 1

    def find_span(self,u):
        """ Determine the knot span index.
        - input: parameter u (float)
        - output: the knot span index (int)
        Nurbs Book Algo A2.1 p.68
        """
        n = len(self.knots)-self.degree-1
        if u == self.knots[n+1]:
            return n-1
        low = self.degree
        high = n+1
        mid = int((low+high)/2)
        while (u < self.knots[mid] or u >= self.knots[mid+1]):
            if (u < self.knots[mid]):
                high = mid
            else:
                low = mid
            mid = int((low+high)/2)
        return mid

    def basis_funs(self, i, u):
        """ Compute the nonvanishing basis functions.
        - input: start index i (int), parameter u (float)
        - output: basis functions values N (list of floats)
        Nurbs Book Algo A2.2 p.70
        """
        N = [0. for x in range(self.degree+1)]
        N[0] = 1.0
        left = [0.0]
        right = [0.0]
        for j in range(1,self.degree+1):
            left.append(u-self.knots[i+1-j])
            right.append(self.knots[i+j]-u)
            saved = 0.0
            for r in range(j):
                temp = N[r] / (right[r+1] + left[j-r])
                N[r] = saved + right[r+1] * temp
                saved = left[j-r]*temp
            N[j] = saved
        return N

    def ders_basis_funs(self, i, u, n):
        """ Compute nonzero basis functions and their derivatives.
        First section is A2.2 modified to store functions and knot differences.
        - input: start index i (int), parameter u (float), number of derivatives n (int)
        - output: basis functions and derivatives ders (array2d of floats)
        Nurbs Book Algo A2.3 p.72
        """
        ders = [[0.0 for x in range(self.degree+1)] for y in range(n+1)]
        ndu = [[1.0 for x in range(self.degree+1)] for y in range(self.degree+1)] 
        ndu[0][0] = 1.0
        left = [0.0]
        right = [0.0]
        for j in range(1,self.degree+1):
            left.append(u-self.knots[i+1-j])
            right.append(self.knots[i+j]-u)
            saved = 0.0
            for r in range(j):
                ndu[j][r] = right[r+1] + left[j-r]
                temp = ndu[r][j-1] / ndu[j][r]
                ndu[r][j] = saved + right[r+1] * temp
                saved = left[j-r]*temp
            ndu[j][j] = saved

        for j in range(0,self.degree+1):
            ders[0][j] = ndu[j][self.degree]
        for r in range(0,self.degree+1):
            s1 = 0
            s2 = 1
            a = [[0.0 for x in range(self.degree+1)] for y in range(2)]
            a[0][0] = 1.0
            for k in range(1,n+1):
                d = 0.0
                rk = r-k
                pk = self.degree-k
                if r >= k:
                    a[s2][0] = a[s1][0] / ndu[pk+1][rk]
                    d = a[s2][0] * ndu[rk][pk]
                if rk >= -1:
                    j1 = 1
                else:
                    j1 = -rk
                if (r-1) <= pk:
                    j2 = k-1
                else:
                    j2 = self.degree-r
                for j in range(j1,j2+1):
                    a[s2][j] = (a[s1][j]-a[s1][j-1]) / ndu[pk+1][rk+j]
                    d += a[s2][j] * ndu[rk+j][pk]
                if r <= pk:
                    a[s2][k] = -a[s1][k-1] / ndu[pk+1][r]
                    d += a[s2][k] * ndu[r][pk]
                ders[k][r] = d
                j = s1
                s1 = s2
                s2 = j
        r = self.degree
        for k in range(1,n+1):
            for j in range(0,self.degree+1):
                ders[k][j] *= r
            r *= (self.degree-k)
        return ders

    def evaluate(self, u, d):
        """ Compute the derivative d of the basis functions.
        - input: parameter u (float), derivative d (int)
        - output: derivative d of the basis functions (list of floats)
        """
        n = len(self.knots)-self.degree-1
        f = [0.0 for x in range(n)]
        span = self.find_span(u)
        ders = self.ders_basis_funs(span, u, d)
        for i,val in enumerate(ders[d]):
            f[span-self.degree+i] = val
        return f

# This KnotVector class is equivalent to the following knotSeq* functions
# I am not sure what is best: a class or a set of independent functions ?

class KnotVector(object):
    """Knot vector object to use in Bsplines"""
    def __init__(self, v=[0.0, 1.0]):
        self._vector = v
        self._min_max()

    def __repr__(self):
        return "KnotVector(%s)"%str(self._vector)

    @property
    def vector(self):
        return self._vector

    @vector.setter
    def vector(self, v):
        self._vector = v
        self._vector.sort()
        self._min_max()

    def _min_max(self):
        """Compute the min and max values of the knot vector"""
        self.maxi = max(self._vector)
        self.mini = min(self._vector)

    def reverse(self):
        """Reverse the knot vector"""
        newknots = [(self.maxi + self.mini - k) for k in self._vector]
        newknots.reverse()
        self._vector = newknots

    def normalize(self):
        """Normalize the knot vector"""
        self.scale()

    def scale(self, length=1.0):
        """Scales the knot vector to a given length"""
        if length <= 0.0:
            error("scale error : bad value")
        else:
            ran = self.maxi - self.mini
            newknots = [length * (k-self.mini)/ran for k in self._vector]
            self._vector = newknots
            self._min_max()

    def reversed_param(self, pa):
        """Returns the image of the parameter when the knot vector is reversed"""
        newvec = KnotVector()
        newvec.vector = [self._vector[0], pa, self._vector[-1]]
        newvec.reverse()
        return newvec.vector[1]

    def create_uniform(self, degree, nb_poles):
        """Create a uniform knotVector from given degree and Nb of poles"""
        if degree >= nb_poles:
            error("create_uniform : degree >= nb_poles")
        else:
            nb_int_knots = nb_poles - degree - 1
            start = [0.0 for k in range(degree+1)]
            mid = [float(k) for k in range(1,nb_int_knots+1)]
            end = [float(nb_int_knots+1) for k in range(degree+1)]
            self._vector = start + mid + end
            self._min_max()

    def get_mults(self):
        """Get the list of multiplicities of the knot vector"""
        no_duplicates = list(set(self._vector))
        return [self._vector.count(k) for k in no_duplicates]

    def get_knots(self):
        """Get the list of unique knots, without duplicates"""
        return list(set(self._vector))

# ---------------------------------------------------

def knotSeqReverse(knots):
    """Reverse a knot vector
    revKnots = knotSeqReverse(knots)"""
    ma = max(knots)
    mi = min(knots)
    newknots = [ma+mi-k for k in knots]
    newknots.reverse()
    return newknots

def knotSeqNormalize(knots):
    """Normalize a knot vector
    normKnots = knotSeqNormalize(knots)"""
    ma = max(knots)
    mi = min(knots)
    ran = ma-mi
    newknots = [(k-mi)/ran for k in knots]
    return newknots

def knotSeqScale(knots, length = 1.0, start = 0.0):
    """Scales a knot vector to a given length
    newknots = knotSeqScale(knots, length = 1.0)"""
    if length <= 0.0:
        error("knotSeqScale : length <= 0.0")
    else:
        ma = max(knots)
        mi = min(knots)
        ran = ma-mi
        newknots = [start+(length*(k-mi)/ran) for k in knots]
        return newknots

def paramReverse(pa,fp,lp):
    """Returns the image of parameter param when knot sequence [fp,lp] is reversed.
    newparam = paramReverse(param,fp,lp)"""
    seq = [fp,pa,lp]
    return knotSeqReverse(seq)[1]

def createKnots(degree, nbPoles):
    """Create a uniform knotVector from given degree and Nb of poles
    knotVector = createKnots(degree, nbPoles)"""
    if degree >= nbPoles:
        error("createKnots : degree >= nbPoles")
    else:
        nbIntKnots = nbPoles - degree - 1
        start = [0.0 for k in range(degree+1)]
        mid = [float(k) for k in range(1,nbIntKnots+1)]
        end = [float(nbIntKnots+1) for k in range(degree+1)]
        return start+mid+end

def createKnotsMults(degree, nbPoles):
    """Create a uniform knotVector and a multiplicities list from given degree and Nb of poles
    knots, mults = createKnotsMults(degree, nbPoles)"""
    if degree >= nbPoles:
        error("createKnotsMults : degree >= nbPoles")
    else:
        nbIntKnots = nbPoles - degree - 1
        knots = [0.0] + [float(k) for k in range(1,nbIntKnots+1)] + [float(nbIntKnots+1)]
        mults = [degree+1] + [1 for k in range(nbIntKnots)] + [degree+1]
        return knots, mults

# ---------------------------------------------------

def nearest_parameter(bs,pt):
    try:
        par = bs.parameter(pt)
    except Part.OCCError:
        # failed. We try with distToShape
        error("parameter error at %f"%par)
        v = Part.Vertex(pt)
        e = bs.toShape()
        d,p,i = v.distToShape(e)
        pt1 = p[0][1]
        par = bs.parameter(pt1)
    return par

def bspline_copy(bs, reverse = False, scale = 1.0):
    """Copy a BSplineCurve, with knotvector optionally reversed and scaled
    newbspline = bspline_copy(bspline, reverse = False, scale = 1.0)
    Part.BSplineCurve.buildFromPolesMultsKnots( poles, mults , knots, periodic, degree, weights, CheckRational )"""
    mults = bs.getMultiplicities()
    weights = bs.getWeights()
    poles = bs.getPoles()
    knots = bs.getKnots()
    perio = bs.isPeriodic()
    ratio = bs.isRational()
    if scale:
        knots = knotSeqScale(knots, scale)
    if reverse:
        mults.reverse()
        weights.reverse()
        poles.reverse()
        knots = knotSeqReverse(knots)
    bspline = Part.BSplineCurve()
    bspline.buildFromPolesMultsKnots(poles, mults , knots, perio, bs.Degree, weights, ratio)
    return bspline

def curvematch(c1, c2, par1, level=0, scale=1.0):
    '''Modifies the start of curve C2 so that it joins curve C1 at parameter par1
    - level (integer) is the level of continuity at join point (C0, G1, G2, G3, etc)
    - scale (float) is a scaling factor of the modified poles of curve C2
    newC2 = curvematch(C1, C2, par1, level=0, scale=1.0)'''
    c1 = c1.toNurbs()
    c2 = c2.toNurbs()
    len1 = c1.length()
    #len2 = c2.length()
    len2 = c2.EndPoint.distanceToPoint(c2.StartPoint)
    # scale the knot vector of C2
    seq2 = knotSeqScale(c2.KnotSequence, 1.0 * abs(scale) * len2)
    # get a scaled / reversed copy of C1
    if scale < 0:
        bs1 = bspline_copy(c1, True, len1) # reversed
    else:
        bs1 = bspline_copy(c1, False, len1) # not reversed
    if par1 <= c1.FirstParameter:
        par = c1.FirstParameter
    elif par1 >= c1.LastParameter:
        par = c1.LastParameter
    else:
        par = par1
    pt1 = c1.value(par) # point on input curve C1
    npar = nearest_parameter(bs1,pt1)

    p1 = bs1.getPoles()
    basis1 = BsplineBasis()
    basis1.knots = bs1.KnotSequence
    basis1.degree = bs1.Degree
    
    p2 = c2.getPoles()
    basis2 = BsplineBasis()
    basis2.knots = seq2
    basis2.degree = c2.Degree
    
    # Compute the (level+1) first poles of C2
    l = 0
    while l <= level:
        #FreeCAD.Console.PrintMessage("\nDerivative %d\n"%l)
        ev1 = basis1.evaluate(npar,d=l)
        ev2 = basis2.evaluate(c2.FirstParameter,d=l)
        #FreeCAD.Console.PrintMessage("Basis %d - %r\n"%(l,ev1))
        #FreeCAD.Console.PrintMessage("Basis %d - %r\n"%(l,ev2))
        poles1 = FreeCAD.Vector()
        for i in range(len(ev1)):
            poles1 += 1.0*ev1[i]*p1[i]
        val = ev2[l]
        if val == 0:
            error("Zero !")
            break
        else:
            poles2 = FreeCAD.Vector()
            for i in range(l):
                poles2 += 1.0*ev2[i]*p2[i]
            np = (poles1-poles2)
            np.multiply(1.0/val)
            #FreeCAD.Console.PrintMessage("Moving P%d from (%0.2f,%0.2f,%0.2f) to (%0.2f,%0.2f,%0.2f)\n"%(l,p2[l].x,p2[l].y,p2[l].z,np.x,np.y,np.z))
            p2[l] = np
        l += 1
    nc = c2.copy()
    for i in range(len(p2)):
        nc.setPole(i+1,p2[i])
    return nc

class blendCurve(object):
    def __init__(self, e1 = None, e2 = None):
        self.param1 = 0.0
        self.param2 = 0.0
        self.cont1 = 1
        self.cont2 = 1
        self.scale1 = 1.0
        self.scale2 = 1.0
        self.Curve = None
        self.autoScale = True
        self.maxDegree = 25 # int(Part.BSplineCurve().MaxDegree)
        self.setEdges(e1,e2)

    def setEdges(self, e1, e2):
        if e1 and e2:
            self.edge1 = e1.Curve.toBSpline(e1.FirstParameter, e1.LastParameter)
            self.edge2 = e2.Curve.toBSpline(e2.FirstParameter, e2.LastParameter)
            if self.param1 < e1.FirstParameter:
                self.param1 = e1.FirstParameter
            elif self.param1 > e1.LastParameter:
                self.param1 = e1.LastParameter
            if self.param2 < e2.FirstParameter:
                self.param2 = e2.FirstParameter
            elif self.param2 > e2.LastParameter:
                self.param2 = e2.LastParameter
        else:
            error("blendCurve initialisation error")
            self.edge1 = None
            self.edge2 = None
            self.param1 = 0.0
            self.param2 = 0.0
    
    def getChord(self):
        v1 = self.edge1.value(self.param1)
        v2 = self.edge2.value(self.param2)
        try:
            return Part.LineSegment(v1,v2)
        except Part.OCCError: # v1 == v2
            return False
    
    #def getChordLength(self):
        #ls = self.getChord()
        #if not ls:
            #self.chordLength = 0
        #else:
            #self.chordLength = ls.length()
        #if self.chordLength < 1e-6:
            #error("error : chordLength < 1e-6")
            #self.chordLength = 1.0

    # doesn't work
    #def autoscale(self, w1, w2, w3, maxscale, numsteps):
        #minscale = .01
        #best = 1e50
        #res = None
        #old_scale1 = self.scale1
        #old_scale2 = self.scale2
        #sign1 = self.scale1 / abs(self.scale1)
        #sign2 = self.scale2 / abs(self.scale2)
        #r = [minscale + float(i)*(maxscale-minscale)/(numsteps-1) for i in range(numsteps)]
        #for s1 in r:
            #for s2 in r:
                #self.scale1 = sign1 * s1
                #self.scale2 = sign2 * s2
                #self.compute()
                #a,b,c = eval_smoothness(self.shape(), samples=10)
                #score = a*w1 + b*w2 + c*w3
                #if score < best:
                    #best = score
                    #res = (self.scale1, self.scale2)
        #self.scale1 = old_scale1
        #self.scale2 = old_scale2
        #return res

    def compute(self):
        nbPoles = self.cont1 + self.cont2 + 2
        e = self.getChord()
        if not e:
            self.Curve = None
            return
        try:
            poles = e.discretize(nbPoles)
        except Part.OCCError:
            self.Curve = None
            return
        degree = nbPoles - 1
        if degree > self.maxDegree:
            degree = self.maxDegree
        knots, mults = createKnotsMults(degree, nbPoles)
        weights = [1.0 for k in range(nbPoles)]
        be = Part.BSplineCurve()
        be.buildFromPolesMultsKnots(poles, mults , knots, False, degree, weights, False)
        nc = curvematch(self.edge1, be, self.param1, self.cont1, self.scale1)
        rev = bspline_copy(nc, True, False)
        self.Curve = curvematch(self.edge2, rev, self.param2, self.cont2, self.scale2)

    def getPoles(self):
        if self.Curve:
            return self.Curve.getPoles()
    
    def getCurves(self):
        result = []
        e1 = self.edge1.copy()
        e2 = self.edge2.copy()
        if self.scale1 > 0:
            if self.param1 > e1.FirstParameter:
                e1.segment(e1.FirstParameter, self.param1)
                result.append(e1)
        elif self.scale1 < 0:
            if self.param1 < e1.LastParameter:
                e1.segment(self.param1, e1.LastParameter)
                result.append(e1)
        if self.Curve:
            result.append(self.Curve)
        if self.scale2 > 0:
            if self.param2 > e2.FirstParameter:
                e2.segment(e2.FirstParameter, self.param2)
                result.append(e2)
        elif self.scale2 < 0:
            if self.param2 < e2.LastParameter:
                e2.segment(self.param2, e2.LastParameter)
                result.append(e2)
        return result
    
    def getEdges(self):
        return [c.toShape() for c in self.getCurves()]

    def getWire(self):
        return Part.Wire(Part.__sortEdges__(self.getEdges()))
    
    def getJoinedCurve(self):
        c = self.getCurves()
        c0 = c[0]
        for cu in c[1:]:
            c0.join(cu)
        return c0

    def shape(self):
        if self.Curve:
            return self.Curve.toShape()
        else:
            return None

    def curve(self):
        return self.Curve
            

# ---------------------------------------------------

def move_param(c,p1,p2):
    c1 = c.copy()
    c2 = c.copy()
    c1.segment(c.FirstParameter,float(p2))
    c2.segment(float(p2),c.LastParameter)
    #print("\nSegment 1 -> %r"%c1.getKnots())
    #print("Segment 2 -> %r"%c2.getKnots())
    knots1 = knotSeqScale(c1.getKnots(), p1-c.FirstParameter)
    knots2 = knotSeqScale(c2.getKnots(), c.LastParameter-p1)
    c1.setKnots(knots1)
    c2.setKnots(knots2)
    #print("New 1 -> %r"%c1.getKnots())
    #print("New 2 -> %r"%c2.getKnots())
    return c1,c2

def move_params(c,p1,p2):
    curves = list()
    p1.insert(0,c.FirstParameter)
    p1.append(c.LastParameter)
    p2.insert(0,c.FirstParameter)
    p2.append(c.LastParameter)
    for i in range(len(p1)-1):
        c1 = c.copy()
        c1.segment(p2[i],p2[i+1])
        knots1 = knotSeqScale(c1.getKnots(), p1[i+1]-p1[i], p1[i])
        print("%s -> %s"%(c1.getKnots(),knots1))
        c1.setKnots(knots1)
        curves.append(c1)
    return curves

def join_curve(c1,c2):
    c = Part.BSplineCurve()
    # poles (sequence of Base.Vector), [mults , knots, periodic, degree, weights (sequence of float), CheckRational]
    new_poles = c1.getPoles()
    new_poles.extend(c2.getPoles()[1:])
    new_weights = c1.getWeights()
    new_weights.extend(c2.getWeights()[1:])
    new_mults = c1.getMultiplicities()[:-1]
    new_mults.append(c1.Degree)
    new_mults.extend(c2.getMultiplicities()[1:])
    knots1 = c1.getKnots()
    knots2 = [knots1[-1] + k for k in c2.getKnots()]
    new_knots = knots1
    new_knots.extend(knots2[1:])
    print("poles   -> %r"%new_poles)
    print("weights -> %r"%new_weights)
    print("mults   -> %r"%new_mults)
    print("knots   -> %r"%new_knots)
    c.buildFromPolesMultsKnots(new_poles, new_mults, new_knots, False, c1.Degree, new_weights, True)
    return c

def join_curves(curves):
    c0 = curves[0]
    for c in curves[1:]:
        c0 = join_curve(c0,c)
    return c0

def reparametrize(c, p1, p2):
    '''Reparametrize a BSplineCurve so that parameter p1 is moved to p2'''
    if not isinstance(p1,(list, tuple)):
        c1,c2 = move_param(c, p1, p2)
        c = join_curve(c1,c2)
        return c
    else:
        curves = move_params(c, p1, p2)
        c = join_curves(curves)
        return c

def param_samples(edge, samples=10):
    fp = edge.FirstParameter
    lp = edge.LastParameter
    ra = lp-fp
    return [fp+float(i)*ra/(samples-1) for i in range(samples)]

# doesn't work
#def eval_smoothness(edge, samples=10):
    #params = param_samples(edge, samples)
    ## compute length score
    #chord = edge.valueAt(edge.LastParameter) - edge.valueAt(edge.FirstParameter)
    #if chord.Length > 1e-7: 
        #length_score = (edge.Length / chord.Length) - 1.0
    #else:
        #length_score = None
    ## compute tangent and curvature scores
    #tans = list()
    #curv = list()
    #for p in params:
        #tans.append(edge.tangentAt(p))
        #curv.append(edge.curvatureAt(p))
    #poly = Part.makePolygon(tans)
    #tangent_score = poly.Length
    #m = max(curv)
    #if m > 1e-7:
        #curvature_score = (m-min(curv))/m
    #else:
        #curvature_score = 0.0
    #return length_score,tangent_score,curvature_score
    
class EdgeInterpolator(object):
    """interpolate data along a path shape
    ei = EdgeInterpolator(edge or wire)"""
    def __init__(self, shape):
        self.data = list()
        self.pts = list()
        self.parameters = list()
        self.curve = Part.BSplineCurve()
        if isinstance(shape, Part.Edge):
            self.path = shape
        elif isinstance(shape, Part.Wire):
            path = shape.approximate(1e-8,1e-3,99,5) # &tol2d,&tol3d,&maxseg,&maxdeg
            self.path = path.toShape()
        else:
            FreeCAD.Console.PrintError("EdgeInterpolator input must be edge or wire")
            raise ValueError

    def add_data(self, p, dat):
        """add a datum on path, at given parameter
        ei.add_data(parameter, datum)"""
        if len(self.data) == 0:
            self.data.append((p,dat))
        else:
            if type(dat) == type(self.data[0][1]):
                self.data.append((p,dat))
            else:
                FreeCAD.Console.PrintError("Bad type of data")

    def add_mult_data(self, dat):
        """add multiple data values"""
        if isinstance(dat,(list,tuple)):
            for d in dat:
                self.add_data(d[0],d[1])
        else:
            FreeCAD.Console.PrintError("Argument must be list or tuple")

    def get_point(self, val):
        v = FreeCAD.Vector(0,0,0)
        if isinstance(val,(list, tuple)):
            if len(val) >= 1:
                v.x = val[0]
            if len(val) >= 2:
                v.y = val[1]
            if len(val) >= 3:
                v.z = val[2]
        elif isinstance(val,FreeCAD.Base.Vector2d):
            v.x = val.x
            v.y = val.y
        elif isinstance(val,FreeCAD.Vector):
            v = val
        else:
            FreeCAD.Console.PrintError("Failed to convert %s data to FreeCAD.Vector"%type(val))
        return v

    def vec_to_dat(self, v):
        val = self.data[0][1]
        if isinstance(val,(list, tuple)):
            if len(val) == 1:
                return [v.x]
            elif len(val) == 2:
                return [v.x, v.y]
            if len(val) == 3:
                return [v.x, v.y, v.z]
        elif isinstance(val,FreeCAD.Base.Vector2d):
            return FreeCAD.Base.Vector2d(v.x, v.y)
        elif isinstance(val,FreeCAD.Vector):
            return v
        else:
            FreeCAD.Console.PrintError("Failed to convert FreeCAD.Vector to %s"%type(val))

    def build_params_and_points(self):
        self.sort()
        for t in self.data:
            self.parameters.append(t[0])
            self.pts.append(self.get_point(t[1]))
           
    def sort(self):
        from operator import itemgetter
        self.data = sorted(self.data,key=itemgetter(0))

    def interpolate(self):
        if len(self.data) > 1:
            self.build_params_and_points()
            self.curve.interpolate(Points=self.pts, Parameters=self.parameters)

    def valueAt(self, p):
        if len(self.data) > 1:
            vec = self.curve.value(p)
            return self.vec_to_dat(vec)
        else:
            return self.data[0][1]


        


def test(parm):
    bb = BsplineBasis()
    bb.knots = [0.,0.,0.,0.,1.,2.,3.,3.,3.,3.]
    bb.degree = 3
    #parm = 2.5

    span = bb.find_span(parm)
    print("Span index : %d"%span)
    f0 = bb.evaluate(parm,d=0)
    f1 = bb.evaluate(parm,d=1)
    f2 = bb.evaluate(parm,d=2)
    print("Basis functions    : %r"%f0)
    print("First derivatives  : %r"%f1)
    print("Second derivatives : %r"%f2)
    
    # compare to splipy results
    try:
        import splipy
    except ImportError:
        print("splipy is not installed.")
        return False
    
    basis1 = splipy.BSplineBasis(order=bb.degree+1, knots=bb.knots)
    
    print("splipy results :")
    print(basis1.evaluate(parm,d=0).A1.tolist())
    print(basis1.evaluate(parm,d=1).A1.tolist())
    print(basis1.evaluate(parm,d=2).A1.tolist())