//
//  CRSpline.js
//  CoverPage
//
//  Created by Richard Anaya on 1/9/2013.
//  Copyright (c) 2014 Evernote. All rights reserved.
//

var CRSplinePoint = function(x,y){
    this.x = x;
    this.y = y;
    this.color = {r:255,g:0,b:0};
    this.alpha = 1;
    this.dirty = false;
    this.time = 0;
    this.normalTop = {x:0,y:0};
    this.normalBottom = {x:0,y:0};
};

/////////////////////////////////////////////////////////////////

var CRSplineCurve = function(cpEnd,spline){
    this.spline = spline;
    this.points = [];
    this.controlPointEnd = cpEnd;
};

CRSplineCurve.prototype.destroy = function(){
    this.spline = null;
    this.points = null;
    this.controlPointEnd = null;
};

CRSplineCurve.prototype.update = function() {
    this.points = [];
    var curveIndex = this.spline.getCurveIndex(this);
    if(curveIndex == 0){
        return;
    }
    var p0,p1,p2,p3;
    if(curveIndex == 1){
        p0 = this.spline.curves[curveIndex-1].controlPointEnd;
    }
    else{
        p0 = this.spline.curves[curveIndex-2].controlPointEnd;
    }
    p1 = this.spline.curves[curveIndex-1].controlPointEnd;
    p2 = this.controlPointEnd;
    if(curveIndex == this.spline.curves.length-1){
        p3 = this.controlPointEnd;
    }
    else{
        p3 = this.spline.curves[curveIndex+1].controlPointEnd;
    }

    //if we define dx=x2-x1 and dy=y2-y1, then the normals are (-dy, dx) and (dy, -dx).
    var waitForFirstPoint = true;
    var previousPoint = null;
    for(var t=.1;t<=.9;t+=.2){
        var tx = 0.5 *( (2 * p1.x) + (-p0.x + p2.x) * t + (2*p0.x - 5*p1.x + 4*p2.x - p3.x) * t*t + (-p0.x + 3*p1.x- 3*p2.x + p3.x) * t*t*t );
        var ty = 0.5 *( (2 * p1.y) + (-p0.y + p2.y) * t + (2*p0.y - 5*p1.y + 4*p2.y - p3.y) * t*t + (-p0.y + 3*p1.y- 3*p2.y + p3.y) * t*t*t );

        var p = new CRSplinePoint(tx,ty)
        if(waitForFirstPoint){
            var lastCurve = this.spline.curves[curveIndex-1];
            if(lastCurve) {
                var lastPoint = lastCurve.controlPointEnd;
                var dx = lastPoint.x- p.x;
                var dy = lastPoint.y- p.y;
                var len = Math.sqrt(dy*dy+dx*dx);
                //save normalized vectors
                p.normalTop = {x:-dy/len,y:dx/len};
                p.normalBottom = {x:dy/len,y:-dx/len};
            }
        }
        else {
            var lastPoint = this.points[this.points.length-1];
            var dx = lastPoint.x- p.x;
            var dy = lastPoint.y- p.y;
            var len = Math.sqrt(dy*dy+dx*dx);
            //save normalized vectors
            p.normalTop = {x:-dy/len,y:dx/len};
            p.normalBottom = {x:dy/len,y:-dx/len};
        }
        p.time = p1.time*(1-t)
        this.points.push(p);
        previousPoint = p;
    }

    //put normals on control point
    var lastPoint = this.points[this.points.length-1];
    var dx = lastPoint.x- this.controlPointEnd.x;
    var dy = lastPoint.y- this.controlPointEnd.y;
    var len = Math.sqrt(dy*dy+dx*dx);
    //save normalized vectors
    this.controlPointEnd.normalTop = {x:-dy/len,y:dx/len};
    this.controlPointEnd.normalBottom = {x:dy/len,y:-dx/len};
};

CRSplineCurve.prototype.update2 = function() {
    var oldPoints = this.points;
    this.points = [];


    var curveIndex = this.spline.getCurveIndex(this);
    if(curveIndex == 0){
        return;
    }
    var p0,p1,p2,p3;
    if(curveIndex == 1){
        p0 = this.spline.curves[curveIndex-1].controlPointEnd;
    }
    else{
        p0 = this.spline.curves[curveIndex-2].controlPointEnd;
    }
    p1 = this.spline.curves[curveIndex-1].controlPointEnd;
    p2 = this.controlPointEnd;
    if(curveIndex == this.spline.curves.length-1){
        p3 = this.controlPointEnd;
    }
    else{
        p3 = this.spline.curves[curveIndex+1].controlPointEnd;
    }
    //if we define dx=x2-x1 and dy=y2-y1, then the normals are (-dy, dx) and (dy, -dx).
    var waitForFirstPoint = true;
    var previousPoint = null;
    var j = 0;
    for(var t=.1;t<=.9;t+=.2){
        var tx = 0.5 *( (2 * p1.x) + (-p0.x + p2.x) * t + (2*p0.x - 5*p1.x + 4*p2.x - p3.x) * t*t + (-p0.x + 3*p1.x- 3*p2.x + p3.x) * t*t*t );
        var ty = 0.5 *( (2 * p1.y) + (-p0.y + p2.y) * t + (2*p0.y - 5*p1.y + 4*p2.y - p3.y) * t*t + (-p0.y + 3*p1.y- 3*p2.y + p3.y) * t*t*t );


        var p = new CRSplinePoint(tx,ty)
        if(waitForFirstPoint){
            var lastCurve = this.spline.curves[curveIndex-1];
            if(lastCurve) {
                var lastPoint = lastCurve.controlPointEnd;
                var dx = lastPoint.x- p.x;
                var dy = lastPoint.y- p.y;
                var len = Math.sqrt(dy*dy+dx*dx);
                //save normalized vectors
                p.normalTop = {x:-dy/len,y:dx/len};
                p.normalBottom = {x:dy/len,y:-dx/len};
            }
        }
        else {
            var lastPoint = this.points[this.points.length-1];
            var dx = lastPoint.x- p.x;
            var dy = lastPoint.y- p.y;
            var len = Math.sqrt(dy*dy+dx*dx);
            //save normalized vectors
            p.normalTop = {x:-dy/len,y:dx/len};
            p.normalBottom = {x:dy/len,y:-dx/len};
        }
        p.time = p1.time+(p2.time-p1.time)*(t);
        this.points.push(p);
        previousPoint = p;
    }

    //put normals on control point
    var lastPoint = p1;//this.points[this.points.length-1];
    var dx = lastPoint.x- this.controlPointEnd.x;
    var dy = lastPoint.y- this.controlPointEnd.y;
    var len = Math.sqrt(dy*dy+dx*dx);
    //save normalized vectors
    this.controlPointEnd.normalTop = {x:-dy/len,y:dx/len};
    this.controlPointEnd.normalBottom = {x:dy/len,y:-dx/len};
};


/////////////////////////////////////////////////////////////////

var CRSpline = function(){
    this.curves = [];
    this.minimumDistance = 20;
    this.maximumCurves = 1000;
    this.animationDuration = 1;
    this.startAlpha = 1;
    this.midAlpha = .9;
    this.endAlpha = 0;
    this.startColor = { r: 51, g: 164, b: 255 };
    this.midColor = { r: 153, g: 236, b: 255 };
    this.endColor = { r: 179, g: 241, b: 255 };
    this.startSize = 20;
    this.endSize = 2;

    this.smoothTime = .35;

    /*static const CGFloat kTailMaxWidth = 11.;
     static const CGFloat kTailMinWidthFraction = 0.1;
     static const CGFloat kTailMaxWidthHalf = kTailMaxWidth / 2.;
     static const CGFloat kTailMaxGlowWidth = 1.;
     static const CGFloat kTailMaxGlowFade = 2.;
     static const CGFloat kTailTotalMaxWidth = kTailMaxWidthHalf + kTailMaxGlowWidth + kTailMaxGlowFade;
     static const CGFloat kTailSmoothingTime = 0.35;
     static const GLsizei kTailMaskSize = kTailMaxWidth * 1.5;
     static GLfloat kTailMidColor[] = {1., 1., 1., 1.};
     static GLfloat kTailOutColor[] = {1., 1., 1., 0.};
     static const CGFloat kHeadLifeTime = 1.;
     static const CGFloat kHeadFadeLifeTime = 0.3;*/
};

CRSpline.prototype.foreachPoint = function(f){
    var curves = this.curves;
    var curvesLength = curves.length;
    var lastPoint = null;
    var ct = 0;
    for(var i = 0;i< curvesLength;i++){
        var cur = curves[i];
        var controlPointEnd = cur.controlPointEnd;
        if(i == 0){
            if(controlPointEnd.alpha!=0){
                f(controlPointEnd,lastPoint,cur);
                ct++;
                lastPoint = controlPointEnd
            }
        }
        else {
            var pointLength = cur.points.length;
            for(var j = 0 ; j < pointLength; j++){
                var point = cur.points[j];
                if(point.alpha!=0){
                    f(point,lastPoint,cur);
                    ct++;
                    lastPoint = point;
                }
            }
            if(controlPointEnd.alpha!=0){
                f(controlPointEnd,lastPoint,cur);
                ct++;
                lastPoint = controlPointEnd;
            }
        }
    }
};

CRSpline.prototype.foreachPointReverse = function(f){
    var curves = this.curves;
    var curvesLength = curves.length;
    var lastPoint = null;
    var ct = 0;
    var points = [];
    for(var i = 0;i< curvesLength;i++){
        var cur = curves[i];
        var controlPointEnd = cur.controlPointEnd;
        if(i == 0){
            if(controlPointEnd.alpha!=0){
                points.push({p:controlPointEnd,lp:lastPoint,cur:cur});
                ct++;
                lastPoint = controlPointEnd
            }
        }
        else {
            var pointLength = cur.points.length;
            for(var j = 0 ; j < pointLength; j++){
                var point = cur.points[j];
                if(point.alpha!=0){
                    points.push({p:point,lp:lastPoint,cur:cur});
                    ct++;
                    lastPoint = point;
                }
            }
            if(controlPointEnd.alpha!=0){
                points.push({p:controlPointEnd,lp:lastPoint,cur:cur});
                ct++;
                lastPoint = controlPointEnd;
            }
        }
    }
    for(var i=points.length-1;i>=0;i--){
        f(points[i].p,points[i].lp,points[i].cur);
    }
};

CRSpline.prototype.getCurveIndex = function(c){
    return this.curves.indexOf(c);
};

CRSpline.prototype.addControlPoint = function(p){
    this.lastAddedPosition = p;
    if(this.curves.length  == 0){
        var c = new CRSplineCurve(new CRSplinePoint(p.x, p.y),this)
        c.animationDuration = this.animationDuration;
        this.curves.push(c)
        c.update()
    }
    else {
        var cp = new CRSplinePoint(p.x, p.y);
        var lastCurve = this.curves[this.curves.length-1];
        var lastPoint = lastCurve.controlPointEnd;
        var distFromLastPoint = Math.sqrt(Math.pow(lastPoint.x- cp.x,2)+Math.pow(lastPoint.y- cp.y,2))
        if(distFromLastPoint > this.minimumDistance){
            var c = new CRSplineCurve(cp,this)
            c.animationDuration = this.animationDuration;
            this.curves.push(c);
            c.update();
        }

        /*Current mouse position is added to control points (-appendControlPoint:) at position n
         With each added control point some quick optimizations are performed:
         if point n is too close to point n-1, point n is removed

         if last three points are too close to each other, the point n-1 is removed

         control points are smoothed by relaxing sharp turns called “tensions” as described below

         GSx for all new and moved control points are being regenerated (-generateSplineTailSections) using the Catmul-Rom spline*/
    }
    if(this.curves.length > this.maximumCurves){
        this.removeCurveAtIndex(0)
    }
    while(this.curves.length > 0 && this.curves[0].controlPointEnd.alpha == 0){
        this.removeCurveAtIndex(0)
    }
};

CRSpline.prototype.removeCurveAtIndex = function(n){
    this.curves[n].destroy();
    this.curves.splice(n,1);
};


// t: current time, b: begining value, c: change In value, d: duration

CRSpline.prototype.easeLinear =  function (t, b, c, d) {
    if(t>d){t = d;}
    if(t<0){t = 0;}
    return b+c*(t/d);
};

CRSpline.prototype.easeOutCubic = function (t, b, c, d) {
    if(t>d){t = d;}
    if(t<0){t = 0;}
    return c*((t=t/d-1)*t*t + 1) + b;
};


CRSpline.prototype.vectorCrossProduct = function(vector1,vector2) {
    return vector1.x * vector2.y - vector1.y * vector2.x;
};


CRSpline.prototype.hypot = function(x,y) {
    return Math.sqrt(Math.pow(x,2)+Math.pow(y,2));
};

CRSpline.prototype.vectorNormalize = function(p) {
    var l = this.hypot(p.x,p.y);
    return {x:p.x/l,y:p.y/l}
};

CRSpline.prototype.vectorNorm = function(startPoint, endPoint) {
    var dx = endPoint.x - startPoint.x;
    var dy = endPoint.y - startPoint.y;
    return this.hypot(dx, dy);
};


CRSpline.prototype.vectorInterpolate = function(startPoint,endPoint,fract) {
    return {x:startPoint.x + (endPoint.x - startPoint.x) * fract, y: startPoint.y + (endPoint.y - startPoint.y) * fract};
};

CRSpline.prototype.clear = function(){
    while(this.curves.length){
        this.removeCurveAtIndex(0);
    }
};

CRSpline.prototype.rollToCenter = function(pt0,pt1,pt2) {

     var p0p1 = {x:pt1.x - pt0.x, y:pt1.y - pt0.y};
     var p0p2 = {x:pt2.x - pt0.x, y:pt2.y - pt0.y};
     var p0p2Normal = this.vectorNormalize({x:-p0p2.y,y: p0p2.x});;
     if (this.vectorCrossProduct(p0p1, p0p2) > 0) {
        p0p2Normal = {x:-p0p2Normal.x, y:-p0p2Normal.y};
     }
     var totalNorm = this.vectorNorm(pt0, pt1) + this.vectorNorm(pt1, pt2);
     var p0p2Norm = this.vectorNorm(pt0, pt2);
     var targetDistance = (totalNorm > p0p2Norm) ? Math.sqrt(totalNorm * totalNorm - p0p2Norm * p0p2Norm) / 2 : 0;
     var p0p2Center = this.vectorInterpolate(pt0, pt2, 0.5);
     return {x: p0p2Center.x + p0p2Normal.x * targetDistance, y: p0p2Center.y + p0p2Normal.y * targetDistance};
};

CRSpline.prototype.clamp = function(value,min,max){
    return Math.min(max, Math.max(min, value));
};

CRSpline.prototype.update = function(deltaTime){
    var _this = this;


    var roundness = .5;
    var strength = .5;
    if(this.curves.length >= 5){
        var lastCurveUpdated = this.curves.length-1;
        for(var i= this.curves.length-1; i >= this.curves.length-3; i--){
            this.curves[i]
            var pt0 = {x:this.curves[i].controlPointEnd.x,y:this.curves[i].controlPointEnd.y,time:this.curves[i].controlPointEnd.time};
            var pt1 = {x:this.curves[i-1].controlPointEnd.x,y:this.curves[i-1].controlPointEnd.y};
            var pt2 = {x:this.curves[i-2].controlPointEnd.x,y:this.curves[i-2].controlPointEnd.y};
            if(pt0.time > this.smoothTime){
                lastCurveUpdated = i;
                break;
            }
            var fraction = this.clamp((this.smoothTime - pt0.time) /this.smoothTime, 0, 1);
            var rolledP1 = this.rollToCenter(pt0, pt1, pt2);
            var p0p2Center = this.vectorInterpolate(pt0, pt2, 0.5);
            var targetPoint = this.vectorInterpolate(p0p2Center, rolledP1, roundness);
            var newPoint = this.vectorInterpolate(pt1, targetPoint, strength * fraction);

            if(newPoint.x == 0){
            }
            if(_.isNaN(newPoint.x)||_.isNaN(newPoint.y)){

            }
            else {
                this.curves[i-1].controlPointEnd.x = newPoint.x;
                this.curves[i-1].controlPointEnd.y = newPoint.y;
            }
        }

        for(var i= 0; i<this.curves.length; i++){
            this.curves[i].update2();
        }
    }


    /* Curve is being smoothed as described below
     Algorithm iterates thought points being not older than a predefined threshold.
     For each 3 consecutive points (A, B , C) a smoothing strength is being computed for change in position of point B. The smoothing strength is proportional to the point life time. The newer the point, the stronger it is being smoothed.
     Point D is computed such that |AB| + |BC| = |AD| + |DC|  and  |AD| = |DC|
     Point E is computed to be exactly between A and C. Vector AE is equal to vector EC.
     Point F is computed to be laying on section DE taking into the account “roundness” factor. The more round, the closer to D, the less round the flatter.
     Point B is being moved towards F depending on the “smoothing strength” computed above


    GSx for all moved control points are being regenerated (-generateSplineTailSections) using the Catmul-Rom spline


    Points beyond the curve time horizon are being removed. All points, that is control points and generated points are treated equally here.*/


    //Drawing updates curve animations first

    //Curve alpha visibility, if any
    //Curve time horizon - the tail length
    this.foreachPoint(function(p,lp,cur){
        /*p.alpha = _this.startAlpha;
        p.size = _this.startSize;
        p.color = _this.startColor;*/

        var dur = _this.animationDuration;
        if(p.time< dur){
            var prel = p.time/dur;
            var mid = .2;
            p.size = _this.easeOutCubic(p.time,_this.startSize,_this.endSize-_this.startSize,dur);
            //p.alpha = _this.easeOutCubic(p.time,_this.startAlpha,_this.endAlpha-_this.startAlpha,_this.animationDuration);
            /*p.color.r = Math.floor(_this.easeOutCubic(p.time,_this.startColor.r,_this.endColor.r-_this.startColor.r,_this.animationDuration));
            p.color.g = Math.floor(_this.easeOutCubic(p.time,_this.startColor.g,_this.endColor.g-_this.startColor.g,_this.animationDuration));
            p.color.b = Math.floor(_this.easeOutCubic(p.time,_this.startColor.b,_this.endColor.b-_this.startColor.b,_this.animationDuration));*/
            /*p.alpha = _this.startAlpha;
            p.color.r = _this.startColor.r;
            p.color.g = _this.startColor.g;
            p.color.b = _this.startColor.b;*/
            if(prel<mid){
                p.alpha = _this.easeOutCubic(p.time,_this.startAlpha,_this.midAlpha-_this.startAlpha,mid);
                p.color.r = _this.easeOutCubic(p.time,_this.startColor.r,_this.midColor.r-_this.startColor.r,mid);
                p.color.g = _this.easeOutCubic(p.time,_this.startColor.g,_this.midColor.g-_this.startColor.g,mid);
                p.color.b = _this.easeOutCubic(p.time,_this.startColor.b,_this.midColor.b-_this.startColor.b,mid);
            }
            else {
                p.alpha = _this.easeOutCubic(prel-mid,_this.midAlpha,_this.endAlpha-_this.midAlpha,1-mid);
                p.color.r = _this.easeOutCubic(prel-mid,_this.midColor.r,_this.endColor.r-_this.midColor.r,1-mid);
                p.color.g = _this.easeOutCubic(prel-mid,_this.midColor.g,_this.endColor.g-_this.midColor.g,1-mid);
                p.color.b = _this.easeOutCubic(prel-mid,_this.midColor.b,_this.endColor.b-_this.midColor.b,1-mid);
            }
            /*
            if(prel>mid){
                p.color.r = Math.floor(_this.easeOutCubic(p.time,_this.startColor.r,_this.midColor.r-_this.startColor.r,_this.animationDuration));
                p.color.g = Math.floor(_this.easeOutCubic(p.time,_this.startColor.g,_this.midColor.g-_this.startColor.g,_this.animationDuration));
                p.color.b = Math.floor(_this.easeOutCubic(p.time,_this.startColor.b,_this.midColor.b-_this.startColor.b,_this.animationDuration));
            }
            else {
                p.color.r = Math.floor(_this.easeOutCubic(p.time,_this.midColor.r,_this.endColor.r-_this.midColor.r,_this.animationDuration));
                p.color.g = Math.floor(_this.easeOutCubic(p.time,_this.midColor.g,_this.endColor.g-_this.midColor.g,_this.animationDuration));
                p.color.b = Math.floor(_this.easeOutCubic(p.time,_this.midColor.b,_this.endColor.b-_this.midColor.b,_this.animationDuration));
            } */
        }
        else {
            p.alpha = 0;
            p.size = 0;
            p.color = {r:255,g:255,b:255};
        }
        p.time += deltaTime;
    });


  /*  (NSUInteger)relaxTensionsInRange:(NSRange)range roundness:(CGFloat)roundness stregth:(CGFloat)strength
    {
        NSTimeInterval currentTime = [NSDate timeIntervalSinceReferenceDate];
        NSUInteger lastPointIndex = NSMaxRange(range) - 1;
        NSPoint pt0 = [[mControlPoints objectAtIndex:(lastPointIndex - 1)] coord];
        NSPoint pt1 = [[mControlPoints objectAtIndex:lastPointIndex] coord];
        NSPoint pt2 = NSZeroPoint;
        for (NSUInteger pointIndex = lastPointIndex - 1; pointIndex > range.location; pointIndex--) {
            NSTimeInterval pointTime = currentTime - [[mControlPoints objectAtIndex:pointIndex] timeInterval];
            if (pointTime > kTailSmoothingTime) {
                return pointIndex;
            }
            CGFloat fraction = ENCPClamp((kTailSmoothingTime - pointTime) / kTailSmoothingTime, 0., 1.);
            pt2 = pt1;
            pt1 = pt0;
            pt0 = [[mControlPoints objectAtIndex:(pointIndex - 1)] coord];
            NSPoint rolledP1 = ENCPRollPointToCenter(pt0, pt1, pt2);
            NSPoint p0p2Center = ENCPVectorInterpolate(pt0, pt2, 0.5);
            NSPoint targetPoint = ENCPVectorInterpolate(p0p2Center, rolledP1, roundness);
            NSPoint newPoint = ENCPVectorInterpolate(pt1, targetPoint, strength * fraction);

            [[mControlPoints objectAtIndex:pointIndex] setCoord:newPoint];
        }
        return range.location;
    }

  */
};