/*
 * Copyright (c) 2012 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 "AddIntervalDialog.h"
#include "MainWindow.h"
#include "IntervalItem.h"
#include "RideFile.h"
#include <QMap>
#include <assert.h>
#include <math.h>

AddIntervalDialog::AddIntervalDialog(MainWindow *mainWindow) :
    mainWindow(mainWindow)
{
    setAttribute(Qt::WA_DeleteOnClose);
    setWindowTitle(tr("Add Intervals"));
    QVBoxLayout *mainLayout = new QVBoxLayout(this);

    intervalMethodWidget = new QWidget();
    QHBoxLayout *intervalMethodLayout = new QHBoxLayout;
    QLabel *intervalMethodLabel = new QLabel(tr("Method: "), this);
    intervalMethodLayout->addWidget(intervalMethodLabel);
    QButtonGroup *methodButtonGroup = new QButtonGroup(this);
    methodFirst = new QRadioButton(tr("First"));
    methodFirst->setChecked(true);
    methodButtonGroup->addButton(methodFirst);
    intervalMethodLayout->addWidget(methodFirst);
    methodBestPower = new QRadioButton(tr("Peak Power"));
    methodButtonGroup->addButton(methodBestPower);
    intervalMethodLayout->addWidget(methodBestPower);
    //mainLayout->addLayout(intervalMethodLayout);
    intervalMethodWidget->setLayout(intervalMethodLayout);
    mainLayout->addWidget(intervalMethodWidget);

    intervalPeakPowerWidget = new QWidget();
    intervalPeakPowerTypeLayout = new QHBoxLayout;
    QLabel *intervalPeakPowerTypeLabel = new QLabel(tr("Type: "), this);
    intervalPeakPowerTypeLayout->addWidget(intervalPeakPowerTypeLabel);
    QButtonGroup *peakPowerTypeButtonGroup = new QButtonGroup(this);
    peakPowerStandard = new QRadioButton(tr("Standard"));
    peakPowerStandard->setChecked(true);
    peakPowerTypeButtonGroup->addButton(peakPowerStandard);
    intervalPeakPowerTypeLayout->addWidget(peakPowerStandard);
    peakPowerCustom = new QRadioButton(tr("Custom"));
    peakPowerTypeButtonGroup->addButton(peakPowerCustom);
    intervalPeakPowerTypeLayout->addWidget(peakPowerCustom);
    //mainLayout->addLayout(intervalPeakPowerTypeLayout);
    intervalPeakPowerWidget->setLayout(intervalPeakPowerTypeLayout);
    intervalPeakPowerWidget->hide();
    mainLayout->addWidget(intervalPeakPowerWidget);

    intervalTypeWidget = new QWidget();
    QHBoxLayout *intervalTypeLayout = new QHBoxLayout;
    QLabel *intervalTypeLabel = new QLabel(tr("Interval length: "), this);
    intervalTypeLayout->addWidget(intervalTypeLabel);
    QButtonGroup *typeButtonGroup = new QButtonGroup(this);
    typeTime = new QRadioButton(tr("By time"));
    typeTime->setChecked(true);
    typeButtonGroup->addButton(typeTime);
    intervalTypeLayout->addWidget(typeTime);
    typeDistance = new QRadioButton(tr("By distance"));
    typeButtonGroup->addButton(typeDistance);
    intervalTypeLayout->addWidget(typeDistance);
    //mainLayout->addLayout(intervalTypeLayout);
    intervalTypeWidget->setLayout(intervalTypeLayout);
    mainLayout->addWidget(intervalTypeWidget);

    intervalTimeWidget = new QWidget();
    QHBoxLayout *intervalTimeLayout = new QHBoxLayout;
    QLabel *intervalTimeLabel = new QLabel(tr("Time: "), this);
    intervalTimeLayout->addWidget(intervalTimeLabel);
    hrsSpinBox = new QDoubleSpinBox(this);
    hrsSpinBox->setDecimals(0);
    hrsSpinBox->setMinimum(0.0);
    hrsSpinBox->setSuffix(" hrs");
    hrsSpinBox->setSingleStep(1.0);
    hrsSpinBox->setAlignment(Qt::AlignRight);
    intervalTimeLayout->addWidget(hrsSpinBox);
    minsSpinBox = new QDoubleSpinBox(this);
    minsSpinBox->setDecimals(0);
    minsSpinBox->setRange(0.0, 59.0);
    minsSpinBox->setSuffix(" mins");
    minsSpinBox->setSingleStep(1.0);
    minsSpinBox->setWrapping(true);
    minsSpinBox->setAlignment(Qt::AlignRight);
    minsSpinBox->setValue(1.0);
    intervalTimeLayout->addWidget(minsSpinBox);
    secsSpinBox = new QDoubleSpinBox(this);
    secsSpinBox->setDecimals(0);
    secsSpinBox->setRange(0.0, 59.0);
    secsSpinBox->setSuffix(" secs");
    secsSpinBox->setSingleStep(1.0);
    secsSpinBox->setWrapping(true);
    secsSpinBox->setAlignment(Qt::AlignRight);
    intervalTimeLayout->addWidget(secsSpinBox);
    //mainLayout->addLayout(intervalLengthLayout);
    intervalTimeWidget->setLayout(intervalTimeLayout);
    mainLayout->addWidget(intervalTimeWidget);

    intervalDistanceWidget = new QWidget();
    QHBoxLayout *intervalDistanceLayout = new QHBoxLayout;
    QLabel *intervalDistanceLabel = new QLabel(tr("Distance: "), this);
    intervalDistanceLayout->addWidget(intervalDistanceLabel);
    kmsSpinBox = new QDoubleSpinBox(this);
    kmsSpinBox->setDecimals(0);
    kmsSpinBox->setRange(0.0, 999);
    kmsSpinBox->setValue(5.0);
    kmsSpinBox->setSuffix(" km");
    kmsSpinBox->setSingleStep(1.0);
    kmsSpinBox->setAlignment(Qt::AlignRight);
    intervalDistanceLayout->addWidget(kmsSpinBox);
    msSpinBox = new QDoubleSpinBox(this);
    msSpinBox->setDecimals(0);
    msSpinBox->setRange(0.0, 999);
    msSpinBox->setSuffix(" m");
    msSpinBox->setSingleStep(1.0);
    msSpinBox->setAlignment(Qt::AlignRight);
    intervalDistanceLayout->addWidget(msSpinBox);
    //mainLayout->addLayout(intervalDistanceLayout);
    intervalDistanceWidget->setLayout(intervalDistanceLayout);
    intervalDistanceWidget->hide();
    mainLayout->addWidget(intervalDistanceWidget);

    intervalCountWidget = new QWidget();
    QHBoxLayout *intervalCountLayout = new QHBoxLayout;
    QLabel *intervalCountLabel = new QLabel(tr("How many to find: "), this);
    intervalCountLayout->addWidget(intervalCountLabel);
    countSpinBox = new QDoubleSpinBox(this);
    countSpinBox->setDecimals(0);
    countSpinBox->setMinimum(1.0);
    countSpinBox->setValue(5.0); // lets default to the top 5 powers
    countSpinBox->setSingleStep(1.0);
    countSpinBox->setAlignment(Qt::AlignRight);
    intervalCountLayout->addWidget(countSpinBox);
    //mainLayout->addLayout(intervalCountLayout);
    intervalCountWidget->setLayout(intervalCountLayout);
    mainLayout->addWidget(intervalCountWidget);







    QLabel *resultsLabel = new QLabel(tr("Results:"), this);
    mainLayout->addWidget(resultsLabel);

    // user can select from the results to add
    // to the ride intervals
    resultsTable = new QTableWidget;
    mainLayout->addWidget(resultsTable);
    resultsTable->setColumnCount(5);
    resultsTable->setColumnHidden(3, true); // has start time in secs
    resultsTable->setColumnHidden(4, true); // has stop time in secs
    resultsTable->horizontalHeader()->hide();
//    resultsTable->verticalHeader()->hide();
    resultsTable->setShowGrid(false);

    QHBoxLayout *buttonLayout = new QHBoxLayout;
    createButton = new QPushButton(tr("&Create Intervals"), this);
    buttonLayout->addWidget(createButton);
    doneButton = new QPushButton(tr("&Done"), this);
    buttonLayout->addWidget(doneButton);
    addButton = new QPushButton(tr("&Add to Intervals"));
    buttonLayout->addWidget(addButton);
    mainLayout->addLayout(buttonLayout);

    connect(methodFirst, SIGNAL(clicked()), this, SLOT(methodFirstClicked()));
    connect(methodBestPower, SIGNAL(clicked()), this, SLOT(methodBestPowerClicked()));
    connect(peakPowerStandard, SIGNAL(clicked()), this, SLOT(peakPowerStandardClicked()));
    connect(peakPowerCustom, SIGNAL(clicked()), this, SLOT(peakPowerCustomClicked()));
    connect(typeTime, SIGNAL(clicked()), this, SLOT(typeTimeClicked()));
    connect(typeDistance, SIGNAL(clicked()), this, SLOT(typeDistanceClicked()));




    connect(createButton, SIGNAL(clicked()), this, SLOT(createClicked()));
    connect(doneButton, SIGNAL(clicked()), this, SLOT(doneClicked()));
    connect(addButton, SIGNAL(clicked()), this, SLOT(addClicked()));
}

void
AddIntervalDialog::methodFirstClicked()
{
    intervalPeakPowerWidget->hide();
    intervalTypeWidget->show();
    if (typeDistance->isChecked())
        typeDistanceClicked();
    else
        typeTimeClicked();
    intervalCountWidget->show();
}

void
AddIntervalDialog::methodBestPowerClicked()
{
    intervalPeakPowerWidget->show();
    if (peakPowerCustom->isChecked())
        peakPowerCustomClicked();
    else
        peakPowerStandardClicked();
}

void
AddIntervalDialog::peakPowerStandardClicked()
{
    intervalTypeWidget->hide();
    intervalTimeWidget->hide();
    intervalDistanceWidget->hide();
    intervalCountWidget->hide();
}

void
AddIntervalDialog::peakPowerCustomClicked()
{
    intervalTypeWidget->show();
    if (typeDistance->isChecked())
        typeDistanceClicked();
    else
        typeTimeClicked();
    intervalCountWidget->show();
}

void
AddIntervalDialog::typeTimeClicked()
{
    intervalDistanceWidget->hide();
    intervalTimeWidget->show();
}

void
AddIntervalDialog::typeDistanceClicked()
{
    intervalDistanceWidget->show();
    intervalTimeWidget->hide();
}




// little helper function
static void
clearResultsTable(QTableWidget *resultsTable)
{
    // zap the 3 main cols and two hidden ones
    for (int i=0; i<resultsTable->rowCount(); i++) {
        for (int j=0; j<resultsTable->columnCount(); j++)
            delete resultsTable->takeItem(i,j);
    }
}

static double
intervalDuration(const RideFilePoint *start, const RideFilePoint *stop, const RideFile *ride)
{
    return stop->secs - start->secs + ride->recIntSecs();
}

static double
intervalDistance(const RideFilePoint *start, const RideFilePoint *stop, const RideFile *)
{
    return 1000*(stop->km - start->km);// + (ride->recIntSecs()*stop->kph/3600));
}

static bool
intervalsOverlap(const AddIntervalDialog::AddedInterval &a,
                 const AddIntervalDialog::AddedInterval &b)
{
    if ((a.start <= b.start) && (a.stop > b.start))
        return true;
    if ((b.start <= a.start) && (b.stop > a.start))
        return true;
    return false;
}

struct CompareBests {
    // Sort by decreasing power and increasing start time.
    bool operator()(const AddIntervalDialog::AddedInterval &a,
                    const AddIntervalDialog::AddedInterval &b) const {
        if (a.avg > b.avg)
            return true;
        if (b.avg > a.avg)
            return false;
        return a.start < b.start;
    }
};

void
AddIntervalDialog::createClicked()
{
    const RideFile *ride = mainWindow->currentRide();
    if (!ride) {
        QMessageBox::critical(this, tr("Select Ride"), tr("No ride selected!"));
        return;
    }

    int maxIntervals = (int) countSpinBox->value();

    double windowSizeSecs = (hrsSpinBox->value() * 3600.0
                             + minsSpinBox->value() * 60.0
                             + secsSpinBox->value());

    double windowSizeMeters = (kmsSpinBox->value() * 1000.0
                             + msSpinBox->value());

    if (windowSizeSecs == 0.0) {
        QMessageBox::critical(this, tr("Bad Interval Length"),
                              tr("Interval length must be greater than zero!"));
        return;
    }

    bool byTime = typeTime->isChecked();

    QList<AddedInterval> results;
    if (methodBestPower->isChecked()) {
        if (peakPowerStandard->isChecked())
            findPeakPowerStandard(ride, results);
        else
            findBests(byTime, ride, (byTime?windowSizeSecs:windowSizeMeters), maxIntervals, results, "");
    }
    else
        findFirsts(byTime, ride, (byTime?windowSizeSecs:windowSizeMeters), maxIntervals, results);

    // clear the table
    clearResultsTable(resultsTable);

    // populate the table
    resultsTable->setRowCount(results.size());
    int row = 0;
    foreach (const AddedInterval &interval, results) {

        double secs = interval.start;
        double mins = floor(secs / 60);
        secs = secs - mins * 60.0;
        double hrs = floor(mins / 60);
        mins = mins - hrs * 60.0;

        // check box
        QCheckBox *c = new QCheckBox;
        c->setCheckState(Qt::Checked);
        resultsTable->setCellWidget(row, 0, c);

        // start time
        QString start = "%1:%2:%3";
        start = start.arg(hrs, 0, 'f', 0);
        start = start.arg(mins, 2, 'f', 0, QLatin1Char('0'));
        start = start.arg(round(secs), 2, 'f', 0, QLatin1Char('0'));

        QTableWidgetItem *t = new QTableWidgetItem;
        t->setText(start);
        t->setFlags(t->flags() & (~Qt::ItemIsEditable));
        resultsTable->setItem(row, 1, t);

        QTableWidgetItem *n = new QTableWidgetItem;
        n->setText(interval.name);
        n->setFlags(n->flags() | (Qt::ItemIsEditable));
        resultsTable->setItem(row, 2, n);

        // hidden columns - start, stop
        QString strt = QString("%1").arg(interval.start); // can't use secs as it gets modified
        QTableWidgetItem *st = new QTableWidgetItem;
        st->setText(strt);
        resultsTable->setItem(row, 3, st);

        QString stp = QString("%1").arg(interval.stop); // was interval.start+x
        QTableWidgetItem *sp = new QTableWidgetItem;
        sp->setText(stp);
        resultsTable->setItem(row, 4, sp);

        row++;
    }
    resultsTable->resizeColumnToContents(0);
    resultsTable->resizeColumnToContents(1);
    resultsTable->setColumnWidth(2,200);
}

void
AddIntervalDialog::findFirsts(bool typeTime, const RideFile *ride, double windowSize,
                              int maxIntervals, QList<AddedInterval> &results)
{
    QList<AddedInterval> bests;

    double secsDelta = ride->recIntSecs();
    double totalWatts = 0.0;
    QList<const RideFilePoint*> window;

    // ride is shorter than the window size!
    if (typeTime && windowSize > ride->dataPoints().last()->secs + secsDelta) return;
    else if (windowSize > ride->dataPoints().last()->km*1000) return;

    double rest = 0;
    // We're looking for intervals with durations in [windowSizeSecs, windowSizeSecs + secsDelta).
    foreach (const RideFilePoint *point, ride->dataPoints()) {

        // Add points until interval duration is >= windowSizeSecs.
        totalWatts += point->watts;
        window.append(point);
        double duration = intervalDuration(window.first(), window.last(), ride);
        double distance = intervalDistance(window.first(), window.last(), ride);

        if ((typeTime && duration >= (windowSize - rest)) ||
            (!typeTime && distance >= (windowSize - rest))) {
            double start = window.first()->secs;
            double stop = start + duration - (typeTime?0:ride->recIntSecs()); // correction for distance
            double avg = totalWatts * secsDelta / duration;
            bests.append(AddedInterval(start, stop, avg));
            rest = (typeTime?duration-windowSize+rest:distance-windowSize+rest);
            // Discard points
            window.clear();
            totalWatts = 0;

            // For distance the last point of an interval is also the first point of the next
            if (!typeTime)  {
                totalWatts += point->watts;
                window.append(point);
            }
        }
    }

    while (!bests.empty() && (results.size() < maxIntervals)) {
        AddedInterval candidate = bests.takeFirst();

        QString name = "#%1 %2%3  (%4w)";
        name = name.arg(results.count()+1);

        if (typeTime)  {
            // best n mins
            if (windowSize < 60) {
                // whole seconds
                name = name.arg(windowSize);
                name = name.arg("sec");
            } else if (windowSize >= 60 && !(((int)windowSize)%60)) {
                // whole minutes
                name = name.arg(windowSize/60);
                name = name.arg("min");
            } else {
                double secs = windowSize;
                double mins = ((int) secs) / 60;
                secs = secs - mins * 60.0;
                double hrs = ((int) mins) / 60;
                mins = mins - hrs * 60.0;
                QString tm = "%1:%2:%3";
                tm = tm.arg(hrs, 0, 'f', 0);
                tm = tm.arg(mins, 2, 'f', 0, QLatin1Char('0'));
                tm = tm.arg(secs, 2, 'f', 0, QLatin1Char('0'));

                // mins and secs
                name = name.arg(tm);
                name = name.arg("");
            }
        } else {
            // best n mins
            if (windowSize < 1000) {
                // whole seconds
                name = name.arg(windowSize);
                name = name.arg("m");
            } else {
                double dist = windowSize;
                double kms = ((int) dist) / 1000;
                dist = dist - kms * 1000.0;
                double ms = dist;

                QString tm = "%1,%2";
                tm = tm.arg(kms);
                tm = tm.arg(ms);

                // km and m
                name = name.arg(tm);
                name = name.arg("km");
            }
        }
        name = name.arg(round(candidate.avg));
        candidate.name = name;

        results.append(candidate);
    }

}

void
AddIntervalDialog::findPeakPowerStandard(const RideFile *ride, QList<AddedInterval> &results)
{
    findBests(true, ride, 5, 1, results, "Peak 5s");
    findBests(true, ride, 10, 1, results, "Peak 10s");
    findBests(true, ride, 20, 1, results, "Peak 20s");
    findBests(true, ride, 30, 1, results, "Peak 30s");
    findBests(true, ride, 60, 1, results, "Peak 1min");
    findBests(true, ride, 120, 1, results, "Peak 2min");
    findBests(true, ride, 300, 1, results, "Peak 5min");
    findBests(true, ride, 600, 1, results, "Peak 10min");
    findBests(true, ride, 1200, 1, results, "Peak 20min");
    findBests(true, ride, 1800, 1, results, "Peak 30min");
    findBests(true, ride, 3600, 1, results, "Peak 60min");
}

void
AddIntervalDialog::findBests(bool typeTime, const RideFile *ride, double windowSize,
                              int maxIntervals, QList<AddedInterval> &results, QString prefix)
{
    QList<AddedInterval> bests;
    QList<AddedInterval> _results;

    double secsDelta = ride->recIntSecs();
    double totalWatts = 0.0;
    QList<const RideFilePoint*> window;

    // ride is shorter than the window size!
    if (typeTime && windowSize > ride->dataPoints().last()->secs + secsDelta) return;
    else if (windowSize > ride->dataPoints().last()->km*1000) return;

    // We're looking for intervals with durations in [windowSizeSecs, windowSizeSecs + secsDelta).
    foreach (const RideFilePoint *point, ride->dataPoints()) {
        // Discard points until interval duration is < windowSizeSecs + secsDelta.
        while ((typeTime && !window.empty() && intervalDuration(window.first(), point, ride) >= windowSize + secsDelta) ||
               (!typeTime && window.length()>1 && intervalDistance(window.at(1), point, ride) >= windowSize)) {
            totalWatts -= window.first()->watts;
            window.takeFirst();
        }
        // Add points until interval duration or distance is >= windowSize.
        totalWatts += point->watts;
        window.append(point);
        double duration = intervalDuration(window.first(), window.last(), ride);
        double distance = intervalDistance(window.first(), window.last(), ride);

        if ((typeTime && duration >= windowSize) ||
            (!typeTime && distance >= windowSize)) {
            double start = window.first()->secs;
            double stop = window.last()->secs; //start + duration;
            double avg = totalWatts * secsDelta / duration;
            bests.append(AddedInterval(start, stop, avg));
        }
    }

    std::sort(bests.begin(), bests.end(), CompareBests());

    while (!bests.empty() && (_results.size() < maxIntervals)) {
        AddedInterval candidate = bests.takeFirst();
        bool overlaps = false;
        foreach (const AddedInterval &existing, _results) {
            if (intervalsOverlap(candidate, existing)) {
                overlaps = true;
                break;
            }
        }
        if (!overlaps) {
            QString name = prefix;
            if (prefix == "") {
                name = "Best %2%3 #%1";
                name = name.arg(_results.count()+1);
                if (typeTime)  {
                    // best n mins
                    if (windowSize < 60) {
                        // whole seconds
                        name = name.arg(windowSize);
                        name = name.arg("sec");
                    } else if (windowSize >= 60 && !(((int)windowSize)%60)) {
                        // whole minutes
                        name = name.arg(windowSize/60);
                        name = name.arg("min");
                    } else {
                        double secs = windowSize;
                        double mins = ((int) secs) / 60;
                        secs = secs - mins * 60.0;
                        double hrs = ((int) mins) / 60;
                        mins = mins - hrs * 60.0;
                        QString tm = "%1:%2:%3";
                        tm = tm.arg(hrs, 0, 'f', 0);
                        tm = tm.arg(mins, 2, 'f', 0, QLatin1Char('0'));
                        tm = tm.arg(secs, 2, 'f', 0, QLatin1Char('0'));

                        // mins and secs
                        name = name.arg(tm);
                        name = name.arg("");
                    }
                } else {
                    // best n mins
                    if (windowSize < 1000) {
                        // whole seconds
                        name = name.arg(windowSize);
                        name = name.arg("m");
                    } else {
                        double dist = windowSize;
                        double kms = ((int) dist) / 1000;
                        dist = dist - kms * 1000.0;
                        double ms = dist;

                        QString tm = "%1,%2";
                        tm = tm.arg(kms);
                        tm = tm.arg(ms);

                        // km and m
                        name = name.arg(tm);
                        name = name.arg("km");
                    }
                }
            }
            name += " (%4w)";
            name = name.arg(round(candidate.avg));
            candidate.name = name;
            name = "";
            _results.append(candidate);
        }
    }
    results.append(_results);
}

void
AddIntervalDialog::doneClicked()
{
    clearResultsTable(resultsTable); // clear out that table!
    done(0);
}

void
AddIntervalDialog::addClicked()
{
    // run through the table row by row
    // and when the checkbox is shown
    // get name from column 2
    // get start in secs as a string from column 3
    // get stop in secs as a string from column 4
    for (int i=0; i<resultsTable->rowCount(); i++) {

        // is it checked?
        QCheckBox *c = (QCheckBox *)resultsTable->cellWidget(i,0);
        if (c->isChecked()) {
            double start = resultsTable->item(i,3)->text().toDouble();
            double stop = resultsTable->item(i,4)->text().toDouble();
            QString name = resultsTable->item(i,2)->text();
            const RideFile *ride = mainWindow->currentRide();

            QTreeWidgetItem *allIntervals = mainWindow->mutableIntervalItems();
            QTreeWidgetItem *last =
                new IntervalItem(ride, name, start, stop,
                                 ride->timeToDistance(start),
                                 ride->timeToDistance(stop),
                                 allIntervals->childCount()+1);
            last->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled | Qt::ItemIsDragEnabled);
            // add
            allIntervals->addChild(last);
        }
    }
    mainWindow->updateRideFileIntervals();
}
