/*
 * Copyright (c) 2010 Ilja Booij (ibooij@gmail.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 "RideFile.h"
#include <QtGlobal>
#include <QDebug>
#include <QFile>
#include <QDateTime>

struct TacxCafFileReader : public RideFileReader {
    virtual RideFile *openRideFile(QFile &file, QStringList &errors, QList<RideFile*>* = 0) const;
    virtual bool hasWrite() const { return false; }
};

static int tacxCafFileReaderRegistered =
    RideFileFactory::instance().registerReader(
        "caf", "Tacx Fortius", new TacxCafFileReader());

/** a simple struct which describes the header of a block */
typedef struct header {
    /** fingerprint of the block, see the list of block fingerprints */
    qint16 fingerprint;
    /** nr of records in the block */
    qint32 recordCount;
    /** size of each record */
    qint32 recordSize;
} header_t;

/**
  Read header block, containing general information about file, among which the
  fingerprint of the file. Return the version of the file, 100 or 110.
  */
static qint16 readHeaderBlock(const QByteArray& block, QStringList& errors);

/**
  Read all data blocks and return a RideFile.
  */
static RideFile* readBlocks(const QByteArray& blocks, qint16 version, QStringList& errors);

/**
  read the header of one block
  */
static header_t readBlockHeader(const QByteArray& blockHeader);

/**
  Read general information about the ride
  */
static bool readRideInformationBlock(RideFile* rideFile, const QByteArray& block);

/**
  Read the actual data points
  */
static bool readRideData(RideFile *rideFile, const QByteArray& block, const int nrOfRecords, const qint16 version);

static qint8 readByteFromByteArray(const QByteArray& block);
static qint16 readShortFromByteArray(const QByteArray& block);
static quint16 readUnsignedShortFromByteArray(const QByteArray& block);
static qint32 readIntFromByteArray(const QByteArray& block);
static float readFloatFromByteArray(const QByteArray& block);

static const int TACX_CAF_FILE_FINGERPRINT = 3000;
static const int TACX_HEADER_BLOCK_SIZE = 8;
static const int TACX_BLOCK_INFO_HEADER_SIZE = 12;
static const int TACX_RIDE_DATA_BLOCK_SIZE = 10;

// Block fingerprints
static const int TACX_RIDE_INFORMATION_BLOCK = 3010;
static const int TACX_RIDE_DATA_BLOCK = 3020;

static const QString TACX_FORTIUS_DEVICE_TYPE = "Tacx Fortius";

RideFile *TacxCafFileReader::openRideFile(QFile &file, QStringList &errors, QList<RideFile*>*) const {
    if (!file.open(QFile::ReadOnly)) {
       errors << ("Could not open ride file: \""
                   + file.fileName() + "\"");
        return NULL;
    }

    QByteArray bytes = file.readAll();
    file.close();

    qint16 version = readHeaderBlock(bytes.left(TACX_HEADER_BLOCK_SIZE), errors);
    if (version == 0)
        return NULL;

    return readBlocks(bytes.mid(TACX_HEADER_BLOCK_SIZE), version, errors);
}

qint16 readHeaderBlock(const QByteArray& block, QStringList& errors) {

    if (block.size() != 8) {
        errors << "Block size is not 8, is this a valid Tacx run file?";
        return 0;
    }

    qint16 fingerprint = readShortFromByteArray(block);

    if (fingerprint != TACX_CAF_FILE_FINGERPRINT) {
        errors << ("This is not a Tacx run file");
        return 0;
    }
    qint16 version = readShortFromByteArray(block.mid(2));
    return version;
}


RideFile* readBlocks(const QByteArray& blocks, const qint16 version, QStringList& /*errors*/) {
    RideFile* rideFile = new RideFile();

    rideFile->setDeviceType(TACX_FORTIUS_DEVICE_TYPE);
    rideFile->setFileFormat("Tacx Fortius (caf)");


    QByteArray remainingBytes = blocks;
    while(!remainingBytes.isEmpty()) {
        const QByteArray blockHeaderBytes = remainingBytes.left(12);
        header_t blockHeader = readBlockHeader(blockHeaderBytes);

        switch(blockHeader.fingerprint) {
        case TACX_RIDE_INFORMATION_BLOCK:
            readRideInformationBlock(rideFile, remainingBytes.mid(12));
            break;
        case TACX_RIDE_DATA_BLOCK:
            readRideData(rideFile, remainingBytes.mid(12), blockHeader.recordCount, version);
            break;
        }

        int skipBytes = 12 + blockHeader.recordCount * blockHeader.recordSize;

        remainingBytes = remainingBytes.mid(skipBytes);
    }

    return rideFile;
}

header_t readBlockHeader(const QByteArray& blockHeader) {
    Q_ASSERT(blockHeader.size() >= 12);
    header_t header;
    header.fingerprint = readShortFromByteArray(blockHeader);
    header.recordCount = readIntFromByteArray(blockHeader.mid(4, 4));
    header.recordSize = readIntFromByteArray(blockHeader.right(4));

    return header;
}

bool readRideInformationBlock(RideFile* rideFile, const QByteArray& block) {
    // year = block[0-2]
    qint16 year = readShortFromByteArray(block);
    qint16 month = readShortFromByteArray(block.mid(2));
    qint16 day = readShortFromByteArray(block.mid(6));
    qint16 hour = readShortFromByteArray(block.mid(8));
    qint16 minute = readShortFromByteArray(block.mid(10));
    qint16 second = readShortFromByteArray(block.mid(12));

    rideFile->setStartTime(QDateTime(QDate(year, month, day), QTime(hour, minute, second)));
    rideFile->setRecIntSecs(readFloatFromByteArray(block.mid(16)));

    return true;
}

struct RideFilePoint readSinglePoint(const QByteArray& record, const double& timeInSeconds, const double& recordingIntervalInSeconds,
                                     const float& startDistance = 0.0f, const float& lastDistance = 0.0f) {
    float distance = readFloatFromByteArray(record);
    float relativeDistance = distance - startDistance;

    quint8 heartRate = readByteFromByteArray(record.mid(4));
    quint8 cadence = readByteFromByteArray(record.mid(5));
    quint16 powerX10 = readUnsignedShortFromByteArray(record.mid(6));
    double power = powerX10 / 10.0;

    double speed = (relativeDistance - lastDistance) / recordingIntervalInSeconds;

    struct RideFilePoint point(timeInSeconds, cadence, heartRate, relativeDistance / 1000.0, speed * 3.6, 0.0, power, 
                               0.0, 0.0, 0.0, 0.0, 0.0, RideFile::NoTemp,
                               0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
                               0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0);

    return point;
}

bool readRideData(RideFile *rideFile, const QByteArray& block, const int nrOfRecords, const qint16 version) {
    const int dataRecordSize = (version == 100) ? TACX_RIDE_DATA_BLOCK_SIZE : TACX_RIDE_DATA_BLOCK_SIZE + 8;

    double seconds = rideFile->recIntSecs();

    struct RideFilePoint firstDataPoint = readSinglePoint(block, seconds, rideFile->recIntSecs());
    float startDistance = firstDataPoint.km * 1000.0;
    float lastDistance = 0.0f;
    for(int i = 0; i < nrOfRecords; i++, seconds += rideFile->recIntSecs()) {
        const QByteArray& record = block.mid(i * dataRecordSize);
        struct RideFilePoint nextDataPoint = readSinglePoint(record, seconds, rideFile->recIntSecs(), startDistance, lastDistance);
        lastDistance = nextDataPoint.km * 1000.0;

        rideFile->appendPoint(nextDataPoint.secs, nextDataPoint.cad,
                              nextDataPoint.hr, nextDataPoint.km,
                              nextDataPoint.kph, nextDataPoint.nm,
                              nextDataPoint.watts, nextDataPoint.alt,
                              nextDataPoint.lon, nextDataPoint.lat,
                              nextDataPoint.headwind, 0.0, RideFile::NoTemp, 0.0, 
                              0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
                              0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0, nextDataPoint.interval);
    }
    return true;
}

qint8 readByteFromByteArray(const QByteArray& block) {
    Q_ASSERT(block.size() >= 1);
    qint8 value = 0;
    memcpy(&value, block, 1);
    return value;
}

quint16 readUnsignedShortFromByteArray(const QByteArray& block) {
    Q_ASSERT(block.size() >= 2);
    quint16 value = 0;
    memcpy(&value, block, 2);
    return value;
}

qint16 readShortFromByteArray(const QByteArray& block) {
    Q_ASSERT(block.size() >= 2);
    qint16 value = 0;
    memcpy(&value, block, 2);
    return value;
}

qint32 readIntFromByteArray(const QByteArray& block) {
    Q_ASSERT(block.size() >= 4);
    qint32 value = 0;
    memcpy(&value, block, 4);
    return value;
}

float readFloatFromByteArray(const QByteArray& block) {
    Q_ASSERT(block.size() >= 4);
    float value = 0;
    memcpy(&value, block, 4);
    return value;
}
