/*
 * Copyright (c) 2011 Damien Grauser (Damien.Grauser@pev-geneve.ch)
 *
 * 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 "MacroDevice.h"
#include "DownloadRideDialog.h"
#include "PowerTapUtil.h"
#include "Device.h"
#include <cmath>
#include <errno.h>

#define MACRO_DEBUG false

#define UNKNOWN 0x00

// Commands send from PC to computer
#define ACKNOWLEDGE 0x0A // Acknowledge received data
#define NUMBER_OF_TRAINING_REQUESTS 0x06 // Request the number of trainings
#define TRAINING_DETAIL_REQUEST 0x09 // Request details of a training
#define ERASE_ALL_RECORDS 0x0B // Erase all records on computer

// Commands send from computer to PC
#define NUMBER_OF_TRAININGS 0x86 // Packet with the trainings count
#define TRAINING_DETAIL 0x89 // Packet with details about a specific training
#define ERASE_DONE 0x8B //  Packet which acknowledges that all data is erased in computer

#define LAST_PAGE 0xFA0A // LastPage number

static bool macroRegistered =
    Devices::addType("O-Synce Macro PC-Link", DevicesPtr(new MacroDevices()) );

QString
MacroDevices::downloadInstructions() const
{
    return (tr("Make sure the Macro unit is turned\n"
            "on and that its display says, \"PC Link\""));
}

DevicePtr
MacroDevices::newDevice( CommPortPtr dev )
{
    return DevicePtr( new MacroDevice( dev ));
}

static QString
cEscape(char *buf, int len)
{
    char *result = new char[4 * len + 1];
    char *tmp = result;
    for (int i = 0; i < len; ++i) {
        if (buf[i] == '"')
            tmp += sprintf(tmp, "\\\"");
        else if (isprint(buf[i]))
            *(tmp++) = buf[i];
        else
            tmp += sprintf(tmp, "\\x%02x", 0xff & (unsigned) buf[i]);
    }
    return result;
}

static int
bcd2Int(char c)
{
    return (0xff & c) - (((0xff & c)/16)*6);
}

static int
hex2Int(char c)
{
    return (0xff & c);
}

static int
hexHex2Int(char c, char c2)
{
    return (0xff & c) + 256*(0xff & c2);
}

bool
MacroDevice::download( const QDir &tmpdir,
                         QList<DeviceDownloadFile> &files,
                         QString &err)
{
    if (MACRO_DEBUG) printf("download O-Synce Macro");

    if (!dev->open(err)) {
        err = tr("ERROR: open failed: ") + err;
        return false;
    }

    emit updateStatus(tr("Request number of training..."));
    if (MACRO_DEBUG) printf("Request number of training\n");

    MacroPacket cmd(NUMBER_OF_TRAINING_REQUESTS);
    cmd.addToPayload(UNKNOWN);

    if (!cmd.write(dev, err)) return false;

    if(m_Cancelled)
    {
        err = tr("download cancelled");
        return false;
    }

    MacroPacket response = MacroPacket();
    response.read(dev, 2, err);

    if (response.payload.size() == 0)
    {
        err = tr("no data");
        return false;
    }

    char count = response.payload.at(0);

    if (count == 0)
    {
        err = tr("no data");
        return false;
    }

    response.read(dev, 7*count, err);

    if (!response.verifyCheckSum(dev, err))
    {
        err = tr("data error");
        return false;
    }

    if(m_Cancelled)
    {
        err = tr("download cancelled");
        return false;
    }

    // create temporary file
    QString tmpl = tmpdir.absoluteFilePath(".macrodl.XXXXXX");
    QTemporaryFile tmp(tmpl);
    tmp.setAutoRemove(false);

    if (!tmp.open()) {
        err = tr("Failed to create temporary file ")
            + tmpl + ": " + tmp.error();
        return false;
    }

    if (MACRO_DEBUG) printf("Acknowledge");
    cmd= MacroPacket(ACKNOWLEDGE);
    cmd.addToPayload(response.command);
    if (!cmd.write(dev, err)) return false;

    // timestamp from the first training
    struct tm start;
    start.tm_sec = bcd2Int(response.payload.at(2));
    start.tm_min = bcd2Int(response.payload.at(3));
    start.tm_hour = bcd2Int(response.payload.at(4));
    start.tm_mday = bcd2Int(response.payload.at(5));
    start.tm_mon = hex2Int(response.payload.at(6)) -1;
    start.tm_year = bcd2Int(response.payload.at(7)) -100;
    start.tm_isdst = -1;

    DeviceDownloadFile file;
    file.extension = "osyn";
    file.name = tmp.fileName();
    file.startTime.setTime_t( mktime( &start ));
    files.append(file);

    QTextStream os(&tmp);
    os << hex;

    for (int i = 0; i < count; i++)
    {
        if (MACRO_DEBUG) printf("Request training %d\n",i);
        emit updateStatus( QString(tr("Request datas of training %1 / %2..."))
            .arg(i+1).arg((int)count) );

        if(m_Cancelled)
        {
            err = tr("download cancelled");
            return false;
        }

        cmd = MacroPacket(TRAINING_DETAIL_REQUEST);
        cmd.addToPayload(i);
        if (!cmd.write(dev, err)) return false;

        if(m_Cancelled)
        {
            err = tr("download cancelled");
            return false;
        }
        bool lastpage = false;
        while (!lastpage)
        {
            MacroPacket response2 = MacroPacket();
            response2.read(dev, 259, err);

            if (!response2.verifyCheckSum(dev, err))
            {
                err = tr("data error");
                return false;
            }

            if (hexHex2Int(response2.payload.at(0), response2.payload.at(1)) == LAST_PAGE)
                lastpage = true;

            //int training_flag = hex2Int(response2.payload.at(43));
            tmp.write(response2.dataArray());
            emit updateProgress( QString(tr("training %1/%2... (%3 Bytes)"))
                .arg(i+1)
                .arg((int)count)
                .arg(tmp.size()) );
            if(m_Cancelled)
            {
                err = tr("download cancelled");
                return false;
            }

            if (MACRO_DEBUG) printf("Acknowledge\n");

            cmd= MacroPacket(ACKNOWLEDGE);
            cmd.addToPayload(response2.command);
            if (!cmd.write(dev, err)) return false;
        }

     }


    tmp.close();

    dev->close();

    // QTemporaryFile initially has permissions set to 0600.
    // Make it readable by everyone.
    tmp.setPermissions(tmp.permissions()
                       | QFile::ReadOwner | QFile::ReadUser
                       | QFile::ReadGroup | QFile::ReadOther);


    return true;
}

bool
MacroDevice::cleanup( QString &err ){
    if (MACRO_DEBUG) printf("Erase all records on computer\n");

    if (!dev->open(err)) {
        err = tr("ERROR: open failed: ") + err;
    }

    MacroPacket cmd(ERASE_ALL_RECORDS);
    cmd.addToPayload(UNKNOWN);

    if (!cmd.write(dev, err)) if (MACRO_DEBUG) printf("Error during erase all records on computer\n");

    MacroPacket response = MacroPacket();
    response.read(dev, 3, err);

    if (!response.verifyCheckSum(dev, err) || (response.payload.at(0) != (char)ERASE_DONE))
    {
        if (MACRO_DEBUG) printf("Error during erase all records on computer\n");
    }

    dev->close();

    return true;
}

// --------------------------------------
// MacroPacket
// --------------------------------------

MacroPacket::MacroPacket() : command(0)
{
    checksum = 0;
    payload.clear();
}

MacroPacket::MacroPacket(char command) : command(command), checksum(command)
{
    payload.clear();
}

void
MacroPacket::addToPayload(char byte)
{
    checksum += byte;
    payload.append(byte);
}

void
MacroPacket::addToPayload(char *bytes, int len)
{
    for (int i = 0; i < len; ++i) {
        char byte = bytes[i];
        checksum += byte;
        payload.append(byte);
    }
}

bool
MacroPacket::verifyCheckSum(CommPortPtr dev, QString &err)
{
    char _checksum;
    if (MACRO_DEBUG) printf("reading checksum from device\n");
    int n = dev->read(&_checksum, 1, err);
    if (n <= 0) {
        err = (n < 0) ? (tr("read checksum error: ") + err) : tr("read timeout");
        return false;
    }
    if (MACRO_DEBUG) printf("CheckSum1 %d CheckSum2 %d", (0xff & (unsigned) checksum) , (0xff & (unsigned) _checksum));

    return checksum == _checksum;
}

QByteArray
MacroPacket::dataArray()
{
    QByteArray result;
    result.append(command);
    result.append(payload);
    result.append(checksum);

    return result;
}

char*
MacroPacket::data()
{
    QByteArray result;
    result.append(command);
    result.append(payload);
    result.append(checksum);

    return result.data();
}

bool
MacroPacket::write(CommPortPtr dev, QString &err)
{
    const char *msg = cEscape(data(), payload.count()+2).toLatin1().constData();

    if (MACRO_DEBUG) printf("writing '%s' to device\n", msg);

    int n = dev->write(data(), payload.count()+2, err);
    if (n != payload.count()+2) {
        if (n < 0) {
            if (MACRO_DEBUG) printf("failed to write %s to device: %s\n", msg, err.toLatin1().constData());
            err = QString(tr("failed to write to device: %1")).arg(err);
        }
        else {
            if (MACRO_DEBUG) printf("timeout writing %s to device\n", msg);
            err = QString(tr("timeout writing to device"));
        }
        return false;
    }

    if (MACRO_DEBUG) printf("writing to device ok\n");
    return true;
}

bool
MacroPacket::read(CommPortPtr dev, int len, QString &err)
{
    if (command == 0) {
        if (MACRO_DEBUG) printf("reading command from device\n");
        int n = dev->read(&command, 1, err);
        if (n <= 0) {
            err = (n < 0) ? (tr("read command error: ") + err) : tr("read timeout");
            return false;
        }
        checksum += command;
        len--;
        if (MACRO_DEBUG) printf("command %s\n" ,cEscape(&command,n).toLatin1().constData());
    }

    if (MACRO_DEBUG) printf("reading %d from device\n", len);
    char buf[len];
    int n = dev->read(&buf, len, err);

    if (n <= 0) {
        err = (n < 0) ? (tr("read error: ") + err) : tr("read timeout");
        return false;
    } else if (n < len) {
        err += QString(tr(", read only %1 bytes instead of: %2"))
            .arg(n).arg(len);
        return false;
    }

    if (MACRO_DEBUG) printf("payload %s\n" ,cEscape(buf,n).toLatin1().constData());
    addToPayload(buf,n);

    return true;
}
