/*
 * Copyright (c)  2007 Sean C. Rhea (srhea@srhea.net),
 *                     Justin F. Knotzke (jknotzke@shampoo.ca)
 * Copyright (c)  2009 Greg Lonnon (greg.lonnon@gmail.com)
 *
 * Additional contributions from:
 *     Steve Gribble  (gribble [at] cs.washington.edu)  [December 3, 2009]
 *     Daniel Stark [December 3, 2009]
 *
 * 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 "Computrainer3dpFile.h"

#include <QDateTime>
#include <QDebug>
#include <QRegExp>
#include <QString>
#include <QTextStream>
#include <QVector>

#include <algorithm>            // for std::sort
#include <assert.h>
#include <stdint.h>    // for int8_t, int16_t, etc.
#include <iostream>

#include "math.h"
#include "Units.h"


static int Computrainer3dpFileReaderRegistered =
    RideFileFactory::instance().registerReader("3dp",
                                               "Computrainer 3dp",
                                               new
                                               Computrainer3dpFileReader
                                               ());

RideFile *Computrainer3dpFileReader::openRideFile(QFile & file,
                                                  QStringList & errors,
                                                  QList<RideFile*>*)
    const
{
    // open up the .3dp file, prepare a little-endian-ordered
    // QDataStream
    if (!file.open(QFile::ReadOnly)) {
        errors << ("Could not open ride file: \"" + file.fileName() +
                   "\"");
        return NULL;
    }
    RideFile *rideFile = new RideFile();
    QDataStream is(&file);

    // Note that QT4.6 and above default to 64 bit floats.  For
    // backwards and forwards compatibility, we'll freeze the stream
    // version we use for parsing at 4.0, and then add in LittleEndian
    // number format, which is what Computrainer3dp has.
    is.setVersion(QDataStream::Qt_4_0);
    is.setByteOrder(QDataStream::LittleEndian);

    // start parsing the header

    // looks like the first part is a header... ignore it.
    is.skipRawData(4);

    // the next 4 bytes are the ASCII characters 'perf'
    char perfStr[5];
    is.readRawData(perfStr, 4);
    perfStr[4] = '\0';
    if(strcmp(perfStr,"perf"))
    {
        errors << "File is encrypted.";
        return NULL;
    }


    // not sure what the next 8 bytes are; skip them
    is.skipRawData(0x8);

    // the next 65 bytes are a null-terminated and padded
    // ASCII user name string
    char userName[65];
    is.readRawData(userName, 65);

    // next is a single byte of user age, in years.  I guess
    // Computrainer doesn't allow people to get older than 255
    // years. ;)
    uint8_t age;
    is >> age;

    // not sure what the next 6 bytes are; skip them.
    is.skipRawData(6);

    // next is a (4 byte) C-style floating point with weight in kg
    float weight;
    is >> weight;

    // next is the upper heart rate limit (4 byte int)
    uint32_t upperHR;
    is >> upperHR;

    // and then the resting heart rate (4 byte int)
    uint32_t lowerHR;
    is >> lowerHR;

    // then year, month, day, hour, minute the exercise started
    // (4, 1, 1, 1, 1 bytes)
    uint32_t year;
    is >> year;
    uint8_t month;
    is >> month;
    uint8_t day;
    is >> day;
    uint8_t hour;
    is >> hour;
    uint8_t minute;
    is >> minute;

    // the number of exercise data points in the file (4 byte int)
    uint32_t numberSamples;
    is >> numberSamples;

    // go back to the start, and skip header to go to
    // the start of the data samples.
    file.seek(0);
    is.skipRawData(0xf8);

    // we'll keep track of the altitude over time.  since computrainer
    // gives us slope, we can calculate change in altitude if we know
    // change in distance traveled, so we also need to keep track of
    // the previous sample's distance.
    float altitude = 100.0;  // arbitrary starting altitude of 100m
    float lastKM = 0;

    // computrainer 3d software lets you start your ride partway into
    // a course.  if you do this, then the first distance reported in
    // the corresponding log file will be that offset, rather than
    // zero.  so, we'll stash away the first reported distance, and
    // use that to offset distances that we report to GC so that they
    // are zero-based (i.e., so that the first data point is at
    // distance zero).
    float firstKM = 0;
    bool gotFirstKM = false;

    // computrainer doesn't have a fixed inter-sample-interval; GC
    // expects one, and estimating one by averaging causes problems
    // for some calculations that GC does.  also, computrainer samples
    // so frequently (once every 30-50ms) that the O(n^2) critical
    // power plot calculation takes waaaaay too long.  to solve both
    // problems at once, we smooth the file, emitting an averaged data
    // point every 250 milliseconds.
    //
    // for HR, cadence, watts, and speed, we'll do time averaging to
    // figure out the correct average since the last emitted point.
    // for distance and altitude, we just need to interpolate from the
    // last data point in the computrainer file itself.
    float lastAltitude = 100.0;
    uint32_t lastEmittedMS = 0;
    uint32_t lastSampleMS = 0;
    double hr_sum = 0.0, cad_sum=0.0, speed_sum=0.0, watts_sum=0.0;

#define CT_EMIT_MS 250

    // loop over each sample in the file, do the averaging, interpolation,
    // and emit smoothed points every CT_EMIT_MS milliseconds
    for (; numberSamples; numberSamples--) {

        // 1 byte heart rate, in BPM
        uint8_t hr;
        is >> hr;

        // 1 byte cadence, in RPM
        uint8_t cad;
        is >> cad;

        // 2 unsigned bytes of watts
        uint16_t watts;
        is >> watts;

        // 4 bytes of floating point speed (in mph/160 !!)
        float speed;
        is >> speed;
        speed = speed * 160 * KM_PER_MILE;  // convert to kph

        // 4 bytes of total elapsed time, in milliseconds
        uint32_t ms;
        is >> ms;

        // 2 signed bytes of 100 * [percent grade]
        // (i.e., grade == 100 * 100 * rise/run !!)
        int16_t grade;
        is >> grade;

        // not sure what the next 2 bytes are
        is.skipRawData(2);

        // 4 bytes of floating point total distance traveled, in KM
        float km;
        is >> km;
        if (!gotFirstKM) {
          firstKM = km;
          gotFirstKM = true;
        }
        // subtract off the first KM so that distances are zero-based.
        km -= firstKM;

        // calculate change in altitude over the past interval.
        // first, calculate grade measured as rise/run.
        float floatGrade;
        floatGrade = 0.01 * 0.01 * grade;  // floatgrade = rise/run
        // then, convert grade to angle (in radians).
        float angle = atan(floatGrade);
        // calculate distance traveled over past interval
        float delta_distance_meters = (1000.0) * (km - lastKM);
        // change in altitude is:
        //     sin(angle) * (distance traveled in past interval).
        altitude = altitude + delta_distance_meters*sin(angle);

        // not sure what the next 28 bytes are.
        is.skipRawData(0x1c);

        // OK -- we've pulled the next data point out of the ride
        // file.  let's figure out if it's time to emit the next
        // CT_EMIT_MS interval(s).  if so, emit it(them), and reset
        // the averaging sums.
        if (ms == 0) {
          // special case first data point
          rideFile->appendPoint((double) ms/1000, (double) cad,
                                (double) hr, km, speed, 0.0, watts,
                                altitude, 0, 0, 0.0, 0.0, RideFile::noTemp, 0.0, 0);
        }
        // while loop since an interval in the .3dp file might
        // span more than one CT_EMIT_MS interval
        while ((ms - lastEmittedMS) >= CT_EMIT_MS) {
          uint32_t sum_interval_ms;
          float interpol_km, interpol_alt, interpol_fraction;

          // figure out the averaging sum update interval (i.e., time
          // since we last added to the averaging sums).  it's either
          // the time since the last sample from the CT file, or
          // CT_EMIT_MS, depending on whether we've gone through this
          // while loop already, or this is the first loop through.
          if (lastSampleMS > lastEmittedMS)
            sum_interval_ms = (lastEmittedMS + CT_EMIT_MS) - lastSampleMS;
          else
            sum_interval_ms = CT_EMIT_MS;

          // update averaging sums with final bit of this sampling interval
          hr_sum += ((double) hr) * ((double) sum_interval_ms);
          cad_sum += ((double) cad) * ((double) sum_interval_ms);
          speed_sum += ((double) speed) * ((double) sum_interval_ms);
          watts_sum += ((double) watts) * ((double) sum_interval_ms);

          // figure out interpolation points based on time from previous
          // sample from the computrainer file
          interpol_fraction =
            ((float) ((lastEmittedMS + CT_EMIT_MS) - lastSampleMS)) /
            ((float) (ms - lastSampleMS));
          interpol_km = lastKM + (km - lastKM) * interpol_fraction;
          interpol_alt = lastAltitude +
            (altitude - lastAltitude) * interpol_fraction;

          // update last sample emit time
          lastEmittedMS = lastEmittedMS + CT_EMIT_MS;

          // emit averages and interpolated distance/altitude
          rideFile->appendPoint(
                                ((double) lastEmittedMS) / 1000,
                                ((double) cad_sum) / CT_EMIT_MS,
                                ((double) hr_sum) / CT_EMIT_MS,
                                interpol_km,
                                ((double) speed_sum) / CT_EMIT_MS,
                                0.0,
                                ((double) watts_sum) / CT_EMIT_MS,
                                interpol_alt,
                                0, // lon
                                0, // lat
                                0.0, // headwind
                                0.0, // slope
                                RideFile::noTemp, // temp
                                0.0,
                                0);

          // reset averaging sums
          hr_sum = cad_sum = speed_sum = watts_sum = 0.0;
        }

        // update averaging sums with interval to current
        // data point in .3dp file
        if (ms > lastEmittedMS) {
          uint32_t sum_interval_ms;

          if (lastSampleMS > lastEmittedMS)
            sum_interval_ms = ms - lastSampleMS;
          else
            sum_interval_ms = ms - lastEmittedMS;
          hr_sum += ((double) hr) * ((double) sum_interval_ms);
          cad_sum += ((double) cad) * ((double) sum_interval_ms);
          speed_sum += ((double) speed) * ((double) sum_interval_ms);
          watts_sum += ((double) watts) * ((double) sum_interval_ms);
        }

        // stash away distance, altitude, and time at end this
        // interval so can calculate distance traveled over next
        // interval in next loop iteration, and so we can interpolate.
        lastSampleMS = ms;
        lastKM = km;
        lastAltitude = altitude;
    }

    // convert the start time we parsed from the header into
    // what GC wants.
    QDateTime dateTime;
    QDate date;
    QTime time;
    date.setDate(year, month, day);
    time.setHMS(hour, minute, 0, 0);
    dateTime.setDate(date);
    dateTime.setTime(time);
    rideFile->setStartTime(dateTime);

    rideFile->setRecIntSecs(((double) CT_EMIT_MS) / 1000.0);

    // tell GC what kind of device a computrainer is
    rideFile->setDeviceType("Computrainer");
    rideFile->setFileFormat("Computrainer 3DP (3dp)");

    // all done!  close up.
    file.close();
    return rideFile;
}
