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

__title__ = "Curve re-parametrize"
__author__ = "Christophe Grellier (Chris_G)"
__license__ = "LGPL 2.1"
__doc__ = """reparametrize a curve to match another one"""

#import sys
#if sys.version_info.major >= 3:
    #from importlib import reload

import FreeCAD
import Part

from freecad.Curves import nurbs_tools
from freecad.Curves import _utils
from freecad.Curves.BSplineAlgorithms import BSplineAlgorithms




def get_ascending(mylist):
    newlist = []
    old1, old2 = mylist[0]
    for i in range(len(mylist)-1):
        p1, p2 = mylist[i+1]
        if (p1 > old1) and (p2 > old2):
            newlist.append([old1,old2])
        old1 = p1
        old2 = p2
    return newlist

def normalized_bspline(e1, reverse=False):
    bs = None
    if isinstance(e1, Part.Wire):
        bs = e1.approximate(1e-15,1e-7,25,999)
    elif isinstance(e1, Part.Edge):
        bs = e1.Curve.toBSpline(e1.FirstParameter, e1.LastParameter)
    elif isinstance(e1, Part.BSplineCurve):
        bs = e1.copy()
    else:
        _utils.error("{} not supported".format(type(e1)))
    if reverse:
        bs.reverse()
    knots = nurbs_tools.knotSeqNormalize(bs.getKnots())
    bs.setKnots(knots)
    return bs

def get_ortho_params(e1,e2,n):
    params = list()
    for i in range(n):
        fp, lp = e1.ParameterRange
        p = fp + (lp-fp) * float(i)/(n-1)
        v = e1.valueAt(p)
        d, pts, info = Part.Vertex(v).distToShape(e2)
        if not len(pts) == 1:
            print("found {} points for param {}".format(len(pts), p))
        elif info[0][5]:
            params.append([p,e2.Curve.parameter(pts[0][1])])
    return params

def get_chord_normal_params (e1,e2,n):
    params = list()
    fp, lp = e1.ParameterRange
    chord = Part.makeLine(e1.valueAt(fp), e1.valueAt(lp))
    pts = chord.discretize(n)
    plane = Part.Plane(pts[0],chord.tangentAt(chord.FirstParameter))
    for pt in pts:
        plane.Position = pt
        p1 = e1.Curve.intersect(plane)
        p2 = e2.Curve.intersect(plane)
        print("{} - {}".format(p1,p2))
        try:
            params.append([e1.Curve.parameter(FreeCAD.Vector(p1[0][0].X, p1[0][0].Y, p1[0][0].Z)),
                           e2.Curve.parameter(FreeCAD.Vector(p2[0][0].X, p2[0][0].Y, p2[0][0].Z))])
            print("{}".format(params[-1]))
        except:
            _utils.warn("chord_normal compute error")
    return params

def stretch_params(par, edge, start=0.3, end=0.3):
    #_utils.info(len(par))
    npar = par[:]
    nb_start = int(len(par)*start)
    nb_end = int(len(par)*end)
    if nb_start > 1:
        for i in range(nb_start):
            fac = 1.0 - float(i) / (nb_start-1)
            npar[i] += (edge.FirstParameter - par[0]) * fac
    else:
        npar[0] = edge.FirstParameter
    if nb_end > 1:
        for i in range(nb_end):
            fac = 1.0 - float(i) / (nb_end-1)
            npar[-1-i] += (edge.LastParameter - par[-1]) * fac
    else:
        npar[-1] = edge.LastParameter
    return npar

def deviation_filter(params, tolerance=1e-2):
    """Filter by parametric deviation"""
    new = []
    for i in range(len(params)):
        if abs(params[i][0]-params[i][1]) > tolerance:
            new.append(params[i])
        #else:
            #print("discarding parameter {}/{}".format(i,len(params)))
    return new

def show_lines(e1, e2, params, title=""):
    lines = list()
    for q1,q2 in params:
        lines.append(Part.makeLine(e1.valueAt(q1), e2.valueAt(q2)))
    com = Part.Compound(lines)
    Part.show(com, title)

def get_max_cp(curve, nb_interp):
    max_cp_u = curve.NbPoles
    # we want to use at least 10 and max 80 control points to be able to reparametrize the geometry properly
    mincp = 10
    maxcp = 80
    # since we interpolate the intersections, we cannot use fewer control points than curves
    # We need to add two since we want c2 continuity, which adds two equations
    min_u = max(nb_interp + 2, mincp)
    max_u = max(min_u, maxcp);
    # Clamp(val, min, max) : return std::max(min, std::min(val, max));
    max_cp_u = max(min_u, min(max_cp_u + 10, max_u))
    return max_cp_u

def reparametrize(ie1, ie2, num=20, smooth_start=0.2, smooth_end=0.2, method=3):
    """reparametrize(ie1, ie2, num=20, smooth_start=0.2, smooth_end=0.2, method=0)
    Reparametrize Edge ie2 according to Edge ie1.
    - num is the number of samples
    - smooth_start and smooth_end [0., 0.5] is how much the stretching of the end parameters
    is stretched into the middle of the curve.
    - method option :
    1 - Edge 1 projected on Edge 2
    2 - Edge 2 projected on Edge 1
    3 - Best of methods 1 and 2 by number of results
    4 - Normal plane of chord line
    """
    c1 = normalized_bspline(ie1, False)
    c2 = normalized_bspline(ie2, not _utils.same_direction(ie1, ie2, 10))
    e1 = c1.toShape()
    e2 = c2.toShape()

    if method == 1:
        params = get_ortho_params(e1, e2, num)
        sorted_params = get_ascending(params)
    elif method == 2:
        params = [[p[1],p[0]] for p in get_ortho_params(e2, e1, num)]
        sorted_params = get_ascending(params)
    elif method == 3:
        pa1 = get_ortho_params(e1, e2, num)
        so_pa1 = get_ascending(pa1)
        pa2 = [[p[1],p[0]] for p in get_ortho_params(e2, e1, num)]
        so_pa2 = get_ascending(pa2)
        if len(so_pa2) > len(so_pa1):
            params = pa2
            sorted_params = so_pa2
        else:
            params = pa1
            sorted_params = so_pa1
    else: #elif method == 4:
        params = get_chord_normal_params(e1,e2,num)
        sorted_params = get_ascending(params)
    p1 = [s[0] for s in sorted_params]
    p2 = [s[1] for s in sorted_params]
    gp = [1.0*i/(len(p1)-1) for i in range(len(p1))]

    #show_lines(e1, e2, sorted_params, title="first_pass")

    np1 = stretch_params(p1, e1, start=smooth_start, end=smooth_end)
    np2 = stretch_params(p2, e2, start=smooth_start, end=smooth_end)
    sorted_params = list(zip(np1,np2))
    sorted_params = deviation_filter(sorted_params, 1e-3)

    #show_lines(e1, e2, sorted_params, title="second_pass")

    bsa = BSplineAlgorithms()
    par_tolerance = 1e-15

    nc1 = bsa.reparametrizeBSplineContinuouslyApprox(c1, np1, gp, get_max_cp(c1, num))
    nc2 = bsa.reparametrizeBSplineContinuouslyApprox(c2, np2, gp, get_max_cp(c2, num))
    return nc1, nc2



def main():
    s = FreeCADGui.Selection.getSelectionEx()
    edges = []
    for so in s:
        for su in so.SubObjects:
            #subshapes(su)
            if isinstance(su, Part.Edge):
                edges.append(su)
        if not so.HasSubObjects:
            edges.append(so.Object.Shape.Wires[0])

    nc1, nc2 = reparametrize(edges[0], edges[1], 40)
    com2 = Part.Compound([nc1.toShape(), nc2.toShape()])
    Part.show(com2)

if __name__ == "__main__":
    main()