/*
 * Copyright (c) 2009 Andy M. Froncioni (me@andyfroncioni.com)
 *
 * This program is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License as published by the Free
 * Software Foundation; either version 2 of the License, or (at your option)
 * any later version.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for
 * more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with this program; if not, write to the Free Software Foundation, Inc., 51
 * Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 */

#include "Aerolab.h"
#include "AerolabWindow.h"
#include "MainWindow.h"
#include "IntervalItem.h"
#include "RideFile.h"
#include "RideItem.h"
#include "Settings.h"
#include "Units.h"
#include "Colors.h"

#include <math.h>
#include <assert.h>
#include <qwt_series_data.h>
#include <qwt_legend.h>
#include <qwt_plot_curve.h>
#include <qwt_plot_grid.h>
#include <qwt_plot_marker.h>
#include <qwt_symbol.h>
#include <set>
#include <QDebug>

#define PI M_PI

static inline double
max(double a, double b) { if (a > b) return a; else return b; }
static inline double
min(double a, double b) { if (a < b) return a; else return b; }

/*----------------------------------------------------------------------
 * Interval plotting
 *--------------------------------------------------------------------*/

class IntervalAerolabData : public QwtSeriesData<QPointF>
{
    public:
        Aerolab *aerolab;
        MainWindow *mainWindow;
        IntervalAerolabData
        (
            Aerolab    *aerolab,
            MainWindow *mainWindow
        ) : aerolab( aerolab ), mainWindow( mainWindow ) { }

        double x( size_t ) const;
        double y( size_t ) const;
        size_t size() const;
        //virtual QwtData *copy() const;

        void init();

        IntervalItem *intervalNum( int ) const;

        int intervalCount() const;

        virtual QPointF sample(size_t i) const;
        virtual QRectF boundingRect() const;
};

/*
 * HELPER FUNCTIONS:
 *    intervalNum - returns a pointer to the nth selected interval
 *    intervalCount - returns the number of highlighted intervals
 */
// ------------------------------------------------------------------------------------------------------------
// note this is operating on the children of allIntervals and not the
// intervalWidget (QTreeWidget) -- this is why we do not use the
// selectedItems() member. N starts a one not zero.
// ------------------------------------------------------------------------------------------------------------
IntervalItem *IntervalAerolabData::intervalNum
(
    int number
) const
{
    int                    highlighted  = 0;
    const QTreeWidgetItem *allIntervals = mainWindow->allIntervalItems();
    for ( int ii = 0; ii < allIntervals->childCount(); ii++)

    {
        IntervalItem *current = (IntervalItem *) allIntervals->child( ii );

        if ( current == NULL)
        {
            return NULL;
        }
        if ( current->isSelected() == true )
        {
            ++highlighted;
        }
        if ( highlighted == number )
        {
            return current;
        }
    }

    return NULL;
}

// ------------------------------------------------------------------------------------------------------------
// how many intervals selected?
// ------------------------------------------------------------------------------------------------------------
int IntervalAerolabData::intervalCount() const
{
    int highlighted = 0;

    if ( mainWindow->allIntervalItems() != NULL )
    {
        const QTreeWidgetItem *allIntervals = mainWindow->allIntervalItems();
        for ( int ii = 0; ii < allIntervals->childCount(); ii++)
        {
            IntervalItem *current = (IntervalItem *) allIntervals->child( ii );
            if ( current != NULL )
            {
                if ( current->isSelected() == true )
                {
                    ++highlighted;
                }
            }
        }
    }
    return highlighted;
}
/*
 * INTERVAL HIGHLIGHTING CURVE
 * IntervalAerolabData - implements the qwtdata interface where
 *                    x,y return point co-ordinates and
 *                    size returns the number of points
 */
// The interval curve data is derived from the intervals that have
// been selected in the MainWindow leftlayout for each selected
// interval we return 4 data points; bottomleft, topleft, topright
// and bottom right.
//
// the points correspond to:
// bottom left = interval start, 0 watts
// top left = interval start, maxwatts
// top right = interval stop, maxwatts
// bottom right = interval stop, 0 watts
//
double IntervalAerolabData::x
(
    size_t number
) const
{
    // for each interval there are four points, which interval is this for?
    // interval numbers start at 1 not ZERO in the utility functions

    double result = 0;

    int interval_no = number ? 1 + number / 4 : 1;
    // get the interval
    IntervalItem *current = intervalNum( interval_no );

    if ( current != NULL )
    {
        double multiplier = aerolab->useMetricUnits ? 1 : MILES_PER_KM;
        // which point are we returning?
//qDebug() << "number = " << number << endl;
        switch ( number % 4 )
        {
            case 0 : result = aerolab->bydist ? multiplier * current->startKM : current->start/60; // bottom left
                 break;
            case 1 : result = aerolab->bydist ? multiplier * current->startKM : current->start/60; // top left
                 break;
            case 2 : result = aerolab->bydist ? multiplier * current->stopKM  : current->stop/60;  // bottom right
                 break;
            case 3 : result = aerolab->bydist ? multiplier * current->stopKM  : current->stop/60;  // top right
                 break;
        }
    }
    return result;
}
double IntervalAerolabData::y
(
    size_t number
) const
{
    // which point are we returning?
    double result = 0;
    switch ( number % 4 )
    {
        case 0 : result = -5000;  // bottom left
                 break;
        case 1 : result = 5000;  // top left - set to out of bound value
                 break;
        case 2 : result = 5000;  // top right - set to out of bound value
                 break;
        case 3 : result = -5000;  // bottom right
                 break;
    }
    return result;
}

size_t IntervalAerolabData::size() const
{
    return intervalCount() * 4;
}

QPointF IntervalAerolabData::sample(size_t i) const {
    return QPointF(x(i), y(i));
}

QRectF IntervalAerolabData::boundingRect() const
{
    return QRectF(-5000, 5000, 10000, 10000);
}

//**********************************************
//**        END IntervalAerolabData           **
//**********************************************


Aerolab::Aerolab(
    AerolabWindow *parent,
    MainWindow    *mainWindow
):
  QwtPlot(parent),
  mainWindow(mainWindow),
  parent(parent),
  rideItem(NULL),
  smooth(1), bydist(true), autoEoffset(true) {

  crr       = 0.005;
  cda       = 0.500;
  totalMass = 85.0;
  rho       = 1.236;
  eta       = 1.0;
  eoffset   = 0.0;

  useMetricUnits = mainWindow->useMetricUnits;

  insertLegend(new QwtLegend(), QwtPlot::BottomLegend);
  setCanvasBackground(Qt::white);
  canvas()->setFrameStyle(QFrame::NoFrame);

  setXTitle();
  setAxisTitle(yLeft, tr("Elevation (m)"));
  setAxisScale(yLeft, -300, 300);
  setAxisTitle(xBottom, tr("Distance (km)"));
  setAxisScale(xBottom, 0, 60);

  veCurve = new QwtPlotCurve(tr("V-Elevation"));
  altCurve = new QwtPlotCurve(tr("Elevation"));

  // get rid of nasty blank space on right of the plot
  veCurve->setYAxis( yLeft );
  altCurve->setYAxis( yLeft );

  intervalHighlighterCurve = new QwtPlotCurve();
  intervalHighlighterCurve->setBaseline(-5000);
  intervalHighlighterCurve->setYAxis( yLeft );
  intervalHighlighterCurve->setData( new IntervalAerolabData( this, mainWindow ) );
  intervalHighlighterCurve->attach( this );
  this->legend()->remove( intervalHighlighterCurve ); // don't show in legend

  grid = new QwtPlotGrid();
  grid->enableX(false);
  grid->attach(this);

  configChanged();
}


void
Aerolab::configChanged()
{
  useMetricUnits = mainWindow->useMetricUnits;

  // set colors
  setCanvasBackground(GColor(CPLOTBACKGROUND));
  QPen vePen = QPen(GColor(CAEROVE));
  vePen.setWidth(appsettings->value(this, GC_LINEWIDTH, 2.0).toDouble());
  veCurve->setPen(vePen);
  QPen altPen = QPen(GColor(CAEROEL));
  altPen.setWidth(appsettings->value(this, GC_LINEWIDTH, 2.0).toDouble());
  altCurve->setPen(altPen);
  QPen gridPen(GColor(CPLOTGRID));
  gridPen.setStyle(Qt::DotLine);
  grid->setPen(gridPen);

  QPen ihlPen = QPen( GColor( CINTERVALHIGHLIGHTER ) );
  ihlPen.setWidth(1);
  intervalHighlighterCurve->setPen( ihlPen );

  QColor ihlbrush = QColor(GColor(CINTERVALHIGHLIGHTER));
  ihlbrush.setAlpha(40);
  intervalHighlighterCurve->setBrush(ihlbrush);   // fill below the line

  this->legend()->remove( intervalHighlighterCurve ); // don't show in legend
}

void
Aerolab::setData(RideItem *_rideItem, bool new_zoom) {

  // HARD-CODED DATA: p1->kph
  double vfactor = 3.600;
  double m       =  totalMass;
  double small_number = 0.00001;

  rideItem = _rideItem;
  RideFile *ride = rideItem->ride();

  veArray.clear();
  altArray.clear();
  distanceArray.clear();
  timeArray.clear();
  useMetricUnits = true;

  if( ride ) {

    const RideFileDataPresent *dataPresent = ride->areDataPresent();
    //setTitle(ride->startTime().toString(GC_DATETIME_FORMAT));

    if( dataPresent->watts ) {

      // If watts are present, then we can fill the veArray data:
      const RideFileDataPresent *dataPresent = ride->areDataPresent();
      int npoints = ride->dataPoints().size();
      double dt = ride->recIntSecs();
      veArray.resize(dataPresent->watts ? npoints : 0);
      altArray.resize(dataPresent->alt ? npoints : 0);
      timeArray.resize(dataPresent->watts ? npoints : 0);
      distanceArray.resize(dataPresent->watts ? npoints : 0);

      // quickly erase old data
      veCurve->setVisible(false);
      altCurve->setVisible(false);

      // detach and re-attach the ve curve:
      veCurve->detach();
      if (!veArray.empty()) {
        veCurve->attach(this);
        veCurve->setVisible(dataPresent->watts);
      }

      // detach and re-attach the ve curve:
      bool have_recorded_alt_curve = false;
      altCurve->detach();
      if (!altArray.empty()) {
        have_recorded_alt_curve = true;
        altCurve->attach(this);
        altCurve->setVisible(dataPresent->alt);
      }

      // Fill the virtual elevation profile with data from the ride data:
      double t = 0.0;
      double vlast = 0.0;
      double e     = 0.0;
      arrayLength = 0;
      foreach(const RideFilePoint *p1, ride->dataPoints()) {
      if ( arrayLength == 0 )
        e = eoffset;

      timeArray[arrayLength]  = p1->secs / 60.0;
      if ( have_recorded_alt_curve )
        altArray[arrayLength] = (useMetricUnits
                   ? p1->alt
                   : p1->alt * FEET_PER_METER);

      // Unpack:
      double power = max(0, p1->watts);
      double v     = p1->kph/vfactor;
      double headwind = v;
      if( dataPresent->headwind ) {
        headwind   = p1->headwind/vfactor;
      }
      double f     = 0.0;
      double a     = 0.0;

      // Use km data insteed of formula for file with a stop (gap).
      //d += v * dt;
      //distanceArray[arrayLength] = d/1000;

      distanceArray[arrayLength] = p1->km;



      if( v > small_number ) {
        f  = power/v;
        a  = ( v*v - vlast*vlast ) / ( 2.0 * dt * v );
      } else {
        a = ( v - vlast ) / dt;
      }

      f *= eta; // adjust for drivetrain efficiency if using a crank-based meter
      double s   = slope( f, a, m, crr, cda, rho, headwind );
      double de  = s * v * dt;

      e += de;
      t += dt;
      veArray[arrayLength] = e;

      vlast = v;

      ++arrayLength;
    }

  } else {
      veCurve->setVisible(false);
      altCurve->setVisible(false);
  }
    recalc(new_zoom);
    adjustEoffset();
  } else {
    //setTitle("no data");

  }
}

void
Aerolab::setAxisTitle(int axis, QString label)
{
    // setup the default fonts
    QFont stGiles; // hoho - Chart Font St. Giles ... ok you have to be British to get this joke
    stGiles.fromString(appsettings->value(this, GC_FONT_CHARTLABELS, QFont().toString()).toString());
    stGiles.setPointSize(appsettings->value(NULL, GC_FONT_CHARTLABELS_SIZE, 8).toInt());

    QwtText title(label);
    title.setFont(stGiles);
    QwtPlot::setAxisFont(axis, stGiles);
    QwtPlot::setAxisTitle(axis, title);
}

void
Aerolab::adjustEoffset() {

    if (autoEoffset && !altArray.empty()) {
        double idx = axisScaleDiv( QwtPlot::xBottom )->lowerBound();
        parent->eoffsetSlider->setEnabled(false);

        if (bydist) {
            int v = 100*(altArray.at(rideItem->ride()->distanceIndex(idx))-veArray.at(rideItem->ride()->distanceIndex(idx)));
            parent->eoffsetSlider->setValue(intEoffset()+v);
        }
        else {
            int v = 100*(altArray.at(rideItem->ride()->timeIndex(60*idx))-veArray.at(rideItem->ride()->timeIndex(60*idx)));
            parent->eoffsetSlider->setValue(intEoffset()+v);
        }
    } else
        parent->eoffsetSlider->setEnabled(true);
}


struct DataPoint {
  double time, hr, watts, speed, cad, alt;
  DataPoint(double t, double h, double w, double s, double c, double a) :
    time(t), hr(h), watts(w), speed(s), cad(c), alt(a) {}
};

void
Aerolab::recalc( bool new_zoom ) {

  if (timeArray.empty())
    return;
  int rideTimeSecs = (int) ceil(timeArray[arrayLength - 1]);
  int totalRideDistance = (int ) ceil(distanceArray[arrayLength - 1]);

  // If the ride is really long, then avoid it like the plague.
  if (rideTimeSecs > 7*24*60*60) {
    QVector<double> data;

    if (!veArray.empty()){
      veCurve->setData(data, data);
    }
    if( !altArray.empty()) {
      altCurve->setData(data, data);
    }
    return;
  }

  QVector<double> &xaxis = (bydist?distanceArray:timeArray);
  int startingIndex = 0;
  int totalPoints   = arrayLength - startingIndex;

  // set curves
  if (!veArray.empty()) {
      veCurve->setData(xaxis.data() + startingIndex, veArray.data() + startingIndex, totalPoints);
  }

  if (!altArray.empty()){
      altCurve->setData(xaxis.data() + startingIndex, altArray.data() + startingIndex, totalPoints);
  }

  if( new_zoom )
      setAxisScale(xBottom, 0.0, (bydist?totalRideDistance:rideTimeSecs));



  setYMax(new_zoom );
  refreshIntervalMarkers();

  replot();
}



void
Aerolab::setYMax(bool new_zoom)
{
          if (veCurve->isVisible())
          {

             if ( useMetricUnits )

             {

                 setAxisTitle( yLeft, tr("Elevation (m)") );

             }

             else

             {

                 setAxisTitle( yLeft, tr("Elevation (')") );

             }

            double minY = 0.0;
            double maxY = 0.0;

            //************

  //if (veCurve->isVisible()) {
   // setAxisTitle(yLeft, tr("Elevation"));
    if ( !altArray.empty() ) {
   //   setAxisScale(yLeft,
   //          min( veCurve->minYValue(), altCurve->minYValue() ) - 10,
   //          10.0 + max( veCurve->maxYValue(), altCurve->maxYValue() ) );

        minY = min( veCurve->minYValue(), altCurve->minYValue() ) - 10;
        maxY = 10.0 + max( veCurve->maxYValue(), altCurve->maxYValue() );

    } else {
      //setAxisScale(yLeft,
      //       veCurve->minYValue() ,
      //       1.05 * veCurve->maxYValue() );

        if ( new_zoom )

              {

                minY = veCurve->minYValue();

                maxY = veCurve->maxYValue();

              }

              else

              {

                minY = parent->getCanvasTop();

                maxY = parent->getCanvasBottom();

              }

              //adjust eooffset
              // TODO



    }

    setAxisScale( yLeft, minY, maxY );
    setAxisLabelRotation(yLeft,270);
    setAxisLabelAlignment(yLeft,Qt::AlignVCenter);
  }

  enableAxis(yLeft, veCurve->isVisible());
}



void
Aerolab::setXTitle() {

  if (bydist)
    setAxisTitle(xBottom, tr("Distance ")+QString(useMetricUnits?"(km)":"(miles)"));
  else
    setAxisTitle(xBottom, tr("Time (minutes)"));
}

void
Aerolab::setAutoEoffset(int value)
{
    autoEoffset = value;
    adjustEoffset();
}

void
Aerolab::setByDistance(int value) {
  bydist = value;
  setXTitle();
  recalc(true);
}


double
Aerolab::slope(
           double f,
           double a,
           double m,
           double crr,
           double cda,
           double rho,
           double v
        )  {

  double g = 9.80665;

  // Small angle version of slope calculation:
  double s = f/(m*g) - crr - cda*rho*v*v/(2.0*m*g) - a/g;

  return s;
}

// At slider 1000, we want to get max Crr=0.1000
// At slider 1    , we want to get min Crr=0.0001
void
Aerolab::setIntCrr(
           int value
            ) {

  crr = (double) value / 1000000.0;

  recalc(false);
}

// At slider 1000, we want to get max CdA=1.000
// At slider 1    , we want to get min CdA=0.001
void
Aerolab::setIntCda(
           int value
            )  {
  cda = (double) value / 10000.0;
  recalc(false);
}

// At slider 1000, we want to get max CdA=1.000
// At slider 1    , we want to get min CdA=0.001
void
Aerolab::setIntTotalMass(
             int value
              ) {

  totalMass = (double) value / 100.0;
  recalc(false);
}


// At slider 1000, we want to get max CdA=1.000
// At slider 1    , we want to get min CdA=0.001
void
Aerolab::setIntRho(
           int value
            ) {

  rho = (double) value / 10000.0;
  recalc(false);
}


// At slider 1000, we want to get max CdA=1.000
// At slider 1    , we want to get min CdA=0.001
void
Aerolab::setIntEta(
                     int value
                     ) {

  eta = (double) value / 10000.0;
  recalc(false);
}


// At slider 1000, we want to get max CdA=1.000
// At slider 1    , we want to get min CdA=0.001
void
Aerolab::setIntEoffset(
                     int value
                     ) {

  eoffset = (double) value / 100.0;
  recalc(false);
}










void Aerolab::pointHover (QwtPlotCurve *curve, int index)
{
    if ( index >= 0 && curve != intervalHighlighterCurve )
    {
        double x_value = curve->sample( index ).x();
        double y_value = curve->sample( index ).y();
        // output the tooltip

        QString text = QString( "%1 %2 %3 %4 %5" )
              . arg( this->axisTitle( curve->xAxis() ).text() )
              . arg( x_value, 0, 'f', 3 )
              . arg( "\n" )
              . arg( this->axisTitle( curve->yAxis() ).text() )
              . arg( y_value, 0, 'f', 3 );

        // set that text up
        tooltip->setText( text );
    }
    else
    {
        // no point
        tooltip->setText( "" );
    }
}

void Aerolab::refreshIntervalMarkers()
{
    foreach( QwtPlotMarker *mrk, d_mrk )
    {
        mrk->detach();
        delete mrk;
    }
    d_mrk.clear();

    QRegExp wkoAuto("^(Peak *[0-9]*(s|min)|Entire workout|Find #[0-9]*) *\\([^)]*\\)$");
    if ( rideItem->ride() )
    {
        foreach(const RideFileInterval &interval, rideItem->ride()->intervals()) {
            // skip WKO autogenerated peak intervals
            if (wkoAuto.exactMatch(interval.name))
                continue;
            QwtPlotMarker *mrk = new QwtPlotMarker;
            d_mrk.append(mrk);
            mrk->attach(this);
            mrk->setLineStyle(QwtPlotMarker::VLine);
            mrk->setLabelAlignment(Qt::AlignRight | Qt::AlignTop);
            mrk->setLinePen(QPen(GColor(CPLOTMARKER), 0, Qt::DashDotLine));
            QwtText text(interval.name);
            text.setFont(QFont("Helvetica", 10, QFont::Bold));
            text.setColor(GColor(CPLOTMARKER));
            if (!bydist)
                mrk->setValue(interval.start / 60.0, 0.0);
            else
                mrk->setValue((useMetricUnits ? 1 : MILES_PER_KM) *
                                rideItem->ride()->timeToDistance(interval.start), 0.0);
            mrk->setLabel(text);
        }
    }
}

/*
 * Estimate CdA and Crr usign energy balance in segments defined by
 * non-zero altitude.
 * Returns an explanatory error message ff it fails to do the estimation,
 * otherwise it updates cda and crr and returns an empty error message.
 * Author: Alejandro Martinez
 * Date: 23-aug-2012
 */
QString Aerolab::estimateCdACrr(RideItem *rideItem)
{
    // HARD-CODED DATA: p1->kph
    const double vfactor = 3.600;
    const double g = 9.80665;
    RideFile *ride = rideItem->ride();
    QString errMsg;

    if(ride) {
        const RideFileDataPresent *dataPresent = ride->areDataPresent();
        if(dataPresent->alt && dataPresent->watts) {
            double dt = ride->recIntSecs();
            int npoints = ride->dataPoints().size();
            double X1[npoints], X2[npoints], Egain[npoints];
            int nSeg = -1;
            double altInit = 0, vInit = 0;
            /* For each segment, defined between points with alt != 0,
             * this loop computes X1, X2 and Egain to verify:
             * Aero-Loss + RR-Loss = Egain
             * where
             *      Aero-Loss = X1[nSgeg] * CdA
             *      RR-Loss = X2[nSgeg] * Crr
             * are the aero and rr components of the energy loss with
             *      X1[nSeg] = sum(0.5 * rho * headwind*headwind * distance)
             *      X2[nSeg] = sum(totalMass * g * distance)
             * and the energy gain sums power in the segment with
             * potential and kinetic variations:
             *      Egain = sum(eta * power * dt) +
             *              totalMass * (g * (altInit - alt) +
             *              0.5 * (vInit*vInit - v*v))
             */
            foreach(const RideFilePoint *p1, ride->dataPoints()) {
                // Unpack:
                double power = max(0, p1->watts);
                double v     = p1->kph/vfactor;
                double distance = v * dt;
                double headwind = v;
                if( dataPresent->headwind ) {
                    headwind   = p1->headwind/vfactor;
                }
                double alt = p1->alt;
                // start initial segment
                if (nSeg < 0 && alt != 0) {
                    nSeg = 0;
                    X1[nSeg] = X2[nSeg] = Egain[nSeg] = 0.0;
                    altInit = alt;
                    vInit = v;
                }
                // accumulate segment data
                if (nSeg >= 0) {
                    // X1[nSgeg] * CdA == Aero-Loss
                    X1[nSeg] += 0.5 * rho * headwind*headwind * distance;
                    // X2[nSgeg] * Crr == RR-Loss
                    X2[nSeg] += totalMass * g * distance;
                    // Energy supplied
                    Egain[nSeg] += eta * power * dt;
                }
                // close current segment and start a new one
                if (nSeg >= 0 && alt != 0) {
                    // Add change in potential and kinetic energy
                    Egain[nSeg] += totalMass * (g * (altInit - alt) + 0.5 * (vInit*vInit - v*v));
                    // Start a new segment
                    nSeg++;
                    X1[nSeg] = X2[nSeg] = Egain[nSeg] = 0.0;
                    altInit = alt;
                    vInit = v;
                }
            }
            /* At least two segmentes needed to approximate:
             *     X1 * CdA + X2 * Crr = Egain
             * which, in matrix form, is:
             *    X * [ CdA ; Crr ] = Egain
             * and pre-multiplying by X transpose (X'):
             *    X'* X [ CdA ; Crr ] = X' * Egain
             * which is a system with two equations and two unknowns
             *    A * [ CdA ; Crr ] = B
             */
            if (nSeg >= 2) {
                // compute the normal equation: A * [ CdA ; Crr ] = B
                // A = X'*X
                // B = X'*Egain
                double A11 = 0, A12 = 0, A21 = 0, A22 = 0, B1 = 0, B2 = 0;
                for (int i = 0; i < nSeg; i++) {
                    A11 += X1[i] * X1[i];
                    A12 += X1[i] * X2[i];
                    A21 += X2[i] * X1[i];
                    A22 += X2[i] * X2[i];
                    B1  += X1[i] * Egain[i];
                    B2  += X2[i] * Egain[i];
                }
                // Solve the normal equation
                // A11 * CdA + A12 * Crr = B1
                // A21 * CdA + A22 * Crr = B2
                double det = A11 * A22 - A12 * A21;
                if (fabs(det) > 0.00) {
                    // round and update if the values are in Aerolab's range
                    double cda = floor(10000 * (A22 * B1 - A12 * B2) / det + 0.5) / 10000;
                    double crr = floor(1000000 * (A11 * B2 - A21 * B1) / det + 0.5) / 1000000;
                    if (cda >= 0.001 and cda <= 1.0 and crr >= 0.0001 and crr <= 0.1) {
                        this->cda = cda;
                        this->crr = crr;
                        errMsg = ""; // No error
                    } else {
                        errMsg = tr("Estimates out-of-range");
                    }
                } else {
                    errMsg = tr("At least two segments must be independent");
                }
            } else {
                errMsg = tr("At least two segments must be defined");
            }
        } else {
            errMsg = tr("Altitude and Power data must be present");
        }
    } else {
        errMsg = tr("No ride selected");
    }
    return errMsg;
}
