/**
 * Author:      David Thompson
 * Date:        28 June, 2019
 * Purpose:     Implement the necessary methods in order to draw the
 *              transition curve.
 */

#include <memory>
#include <cmath>
#include <iostream>

#include <QPainter>

#include "GedDesTransCurve.h"

namespace DESpot {


// CONSTANTS

// Alpha value for centripetal Catmull-Rom
const double C_ALPHA = 0.50;

// Number of sample points for handcoded curves
// 32 was selected to balance appearance, drawing performance, and
// generated EPS size. May need to reduce this number because of artifacts
// kept on screen after dragging states around
const int NUM_BETWEEN = 32;

// Apparently C++ doesn't have pi in any of its libraries
const double PI = 3.141592653589793238;

const qreal ARROW_SIZE = 12.5;

// Higher values are sharper. Must be > 0
const qreal ARROW_SHARPNESS = 2;

// DECLARATIONS (for private functions)

static QPointF uniform_catmull_rom(QPointF ctl1,
                                   QPointF start,
                                   QPointF end,
                                   QPointF ctl2,
                                   qreal t);

static QPointF centripetal_catmull_rom(QPointF ctl1,
                                       QPointF start,
                                       QPointF end,
                                       QPointF ctl2,
                                       qreal t);


// IMPLEMENTATIONS


QPainterPath GedDesTransCurve::getCurve() const {
    QPainterPath curve;

    if (points.size() > 2) {
		// Switch on curve implementation
		switch (type) {
		case CurveType::LINE:
			draw_lines(curve);
			break;
		case CurveType::CATMULL_ROM:
			draw_centripetalCatmullRom(curve);
			break;
		case CurveType::BEZIER:
			draw_quadraticAndCubic(curve);
		default:
			draw_lines(curve);
		}
    } else if (points.size() == 2) {
        curve.moveTo(points[0]);
        curve.lineTo(points[1]);
    } else {
        curve.moveTo(QPointF(0, 0));
        curve.lineTo(QPointF(0, 0));
    }

    return curve;
}


QPainterPath GedDesTransCurve::getArrow() const {

    // Get an instance of the curve that will be done
    QPainterPath curve(getCurve()), arrowPath;
    QPolygonF arrow;

    int sz = curve.elementCount();

    if (sz < 2)
        return arrowPath;

    // Get the direction vector and its normal vector for the arrow by using
    // the one from the normal
    // TODO: won't look good with beziers I believe
    // (not an issue if any interpolating splines/straight lines are being
    // used)
    QLineF direction = QLineF(curve.elementAt(1),
                              curve.elementAt(0)).unitVector();
    QLineF normal = direction.normalVector();

    QPointF directionp = QPointF(direction.dx(), direction.dy());
    QPointF normalp = QPointF(normal.dx(), normal.dy());

    // Construct the arrow polygon using the vectors
    arrow
        << curve.elementAt(0) - ARROW_SIZE
            * (directionp - (1 / ARROW_SHARPNESS) * normalp)
        << curve.elementAt(0)
        << curve.elementAt(0) - ARROW_SIZE
            * (directionp + (1 / ARROW_SHARPNESS) * normalp);
    
    // Put into a path so it can be painter easily
    arrowPath.addPolygon(arrow);

    return arrowPath;

}


void GedDesTransCurve::draw_lines(QPainterPath & curve) const {
    curve.moveTo(points[0]);
    for (int i = 1; i < points.size(); i++) {
        curve.lineTo(points[i]);
    }
}


void GedDesTransCurve::draw_quadraticAndLine(QPainterPath & curve) const {
    curve.moveTo(points[0]);
    int i;

    // Draw quadratic beziers until there is not enough points for one
    for (i = 0; points.size() - i > 2; i += 2) {
        curve.quadTo(points[i + 1],
                        points[i + 2]);
        curve.moveTo(points[i + 2]);
    }

    // Connect remaining points with lines
    for (; i < points.size() - 1; i++) {
        curve.moveTo(points[i]);
        curve.lineTo(points[i+1]);
    }
}


void GedDesTransCurve::draw_quadraticAndCubic(QPainterPath & curve) const {
    
    int sz = points.size();

    for (int i = 0; i + 2 < sz && sz - i != 4; i += 2) {
        curve.moveTo(points[i]);
        curve.quadTo(points[i + 1],
                        points[i + 2]);
    }

    // If there is an even number of points, will need to finish with cubic
    if (sz % 2 == 0 && sz >= 4) {
        curve.moveTo(points[sz - 4]);
        curve.cubicTo(points[sz - 3], points[sz - 2], points[sz - 1]);
    }
}


void GedDesTransCurve::draw_uniformCatmullRom(QPainterPath & curve) const {

    int sz = points.size();

    curve.moveTo(points[0]);
    for (int t = 1; t < NUM_BETWEEN - 1; t++) {
        curve.lineTo(uniform_catmull_rom(points[0],
                                         points[0],
                                         points[1],
                                         points[2],
                                         t / (NUM_BETWEEN * 1.0)));
    }

    for (int i = 1; i < sz - 2; i++) {
        curve.lineTo(points[i]);
        for (int t = 1; t < NUM_BETWEEN - 1; t++) {
            curve.lineTo(uniform_catmull_rom(points[i - 1],
                                             points[i],
                                             points[i + 1],
                                             points[i + 2],
                                             t / (NUM_BETWEEN * 1.0)));
        }
    }

    curve.lineTo(points[sz - 2]);
    for (int t = 1; t < NUM_BETWEEN - 1; t++) {
        curve.lineTo(uniform_catmull_rom(points[sz - 3],
                                         points[sz - 2],
                                         points[sz - 1],
                                         points[sz - 1],
                                         t / (NUM_BETWEEN * 1.0)));
    }

    curve.lineTo(points[sz - 1]);

}


void GedDesTransCurve::draw_centripetalCatmullRom(QPainterPath & curve) const {

    int sz = points.size();

    // get the direction between first two points as a unit vector, and use
    // the unit vector to generate the -1th point
    QLineF shift_begin = QLineF(points[0], points[1]).unitVector();
    QPointF shift_begin_point = QPointF(shift_begin.dx(), shift_begin.dy());

    // Same except for the end
    QLineF shift_end = QLineF(points[sz - 2], points[sz - 1]).unitVector();
    QPointF shift_end_point = QPointF(shift_end.dx(), shift_end.dy());

    // Draw the first segment, using the generate additional point
    curve.moveTo(points[0]);
    for (int t = 1; t < NUM_BETWEEN - 1; t++) {
        curve.lineTo(centripetal_catmull_rom(points[0] - shift_begin_point,
                                             points[0],
                                             points[1],
                                             points[2],
                                             t / (NUM_BETWEEN * 1.0)));
    }

    // Draw the middle segments
    for (int i = 1; i < sz - 2; i++) {
        curve.lineTo(points[i]);
        for (int t = 1; t < NUM_BETWEEN - 1; t++) {
            curve.lineTo(centripetal_catmull_rom(points[i - 1],
                                                 points[i],
                                                 points[i + 1],
                                                 points[i + 2],
                                                 t / (NUM_BETWEEN * 1.0)));
        }
    }

    // Draw the final segment, using the generated additional point
    curve.lineTo(points[sz - 2]);
    for (int t = 1; t < NUM_BETWEEN - 1; t++) {
        curve.lineTo(centripetal_catmull_rom(points[sz - 3],
                                             points[sz - 2],
                                             points[sz - 1],
                                             points[sz - 1] - shift_end_point,
                                             t / (NUM_BETWEEN * 1.0)));
    }

    curve.lineTo(points[sz - 1]);

}


// UTILITY FUNCTIONS


void GedDesTransCurve::arcParams(QPointF start, QPointF mid, QPointF end,
    QRectF & size, double & startAngle, double & sweepAngle) const {
    
    QPointF arc_center = centerOfArc(start, mid, end);
    QPointF dist_vector = arc_center - start;
    double rad = sqrt(dist_vector.rx() * dist_vector.rx()
        + dist_vector.ry() * dist_vector.ry());
    size = QRectF(arc_center - QPointF(rad, rad),
        arc_center + QPointF(rad, rad));
    startAngle = QLineF(arc_center, start).angle();
    sweepAngle = QLineF(arc_center, end).angle();// - startAngle;
}


QPointF GedDesTransCurve::centerOfArc(QPointF start, QPointF mid, QPointF end) const {
    QLineF line1(start, mid);
    QLineF line2(mid, end);
    QPointF midpoint1 = centerOfLine(line1);
    QPointF midpoint2 = centerOfLine(line2);
    QLineF normal1 = line1.normalVector();
    QLineF normal2 = line2.normalVector();
    normal1.translate(midpoint1);
    normal2.translate(midpoint2);
    QPointF * centerPtr = new QPointF;
    normal1.intersect(normal2, centerPtr);
    QPointF center = *centerPtr;
    delete centerPtr;
    return center;
}


QPointF GedDesTransCurve::centerOfLine(QLineF line) const {
    return QPointF((line.x2() - line.x1())/2 + line.x1(),
                   (line.y2() - line.y1())/2 + line.y1());
}


/**
 * Catmull-Rom spline where alpha = 0
 * Defined on t = [0, 1]
 * Necessary calculations adopted from:
 * https://en.wikipedia.org/wiki/Centripetal_Catmull%E2%80%93Rom_spline
 */
static QPointF uniform_catmull_rom(QPointF ctl1,
                                   QPointF start,
                                   QPointF end,
                                   QPointF ctl2,
                                   qreal t) {
    
    t++;

    QPointF A1 = (1 - t) * ctl1 + t * start;
    QPointF A2 = (2 - t) * start + (t - 1) * end;
    QPointF A3 = (3 - t) * end + (t - 2) * ctl2;

    QPointF B1 = ((2 - t) / 2) * A1 + (t / 2) * A2;
    QPointF B2 = ((3 - t) / 2) * A2 + ((t - 1) / 2) * A3;

    return (2 - t) * B1 + (t - 1) * B2;

}


/**
 * Catmull-Rom Spline where alpha = 0.5
 */
static QPointF centripetal_catmull_rom(QPointF ctl1,
                                       QPointF start,
                                       QPointF end,
                                       QPointF ctl2,
                                       qreal t) {

    qreal t0 = 0;
    qreal t1 = pow(sqrt(pow(start.x() - ctl1.x(), 2)
        + pow(start.y() - ctl1.y(), 2)), C_ALPHA) + t0;
    qreal t2 = pow(sqrt(pow(end.x() - start.x(), 2)
        + pow(end.y() - start.y(), 2)), C_ALPHA) + t1;
    qreal t3 = pow(sqrt(pow(ctl2.x() - end.x(), 2)
        + pow(ctl2.y() - end.y(), 2)), C_ALPHA) + t2;

    t = t1 + t * (t2 - t1);

    QPointF A1 = ((t1 - t) / (t1 - t0)) * ctl1 + ((t - t0) / (t1 - t0)) * start;
    QPointF A2 = ((t2 - t) / (t2 - t1)) * start + ((t - t1) / (t2 - t1)) * end;
    QPointF A3 = ((t3 - t)/ (t3 - t2)) * end + ((t - t2) / (t3 - t2)) * ctl2;

    QPointF B1 = ((t2 - t) / (t2 - t0)) * A1 + ((t - t0) / (t2 - t0)) * A2;
    QPointF B2 = ((t3 - t) / (t3 - t1)) * A2 + ((t - t1) / (t3 - t1)) * A3;

    return ((t2 - t) / (t2 - t1)) * B1 + ((t - t1) / (t2 - t1)) * B2;
}


}
