/*
 * Copyright (c) 2010 Mark Liversedge (liversedge@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 "LTMWindow.h"
#include "LTMTool.h"
#include "LTMPlot.h"
#include "LTMSettings.h"
#include "LTMChartParser.h"
#include "AbstractView.h"
#include "Context.h"
#include "Context.h"
#include "Athlete.h"
#include "Banister.h"
#include "RideCache.h"
#include "RideFileCache.h"
#include "Settings.h"
#include "cmath"
#include "float.h"
#include "Units.h" // for MILES_PER_KM
#include "HelpWhatsThis.h"
#include "GcOverlayWidget.h"
#include "Seasons.h"

#include <QWebEngineSettings>

#include <QtGlobal>
#include <QtGui>
#include <QString>
#include <QDebug>
#include <QStyle>
#include <QStyleFactory>

// span slider specials
#include <qxtspanslider.h>
#include <QStyleFactory>
#include <QStyle>
#include <QScrollBar>

#include <qwt_plot_panner.h>
#include <qwt_plot_zoomer.h>
#include <qwt_plot_picker.h>
#include <qwt_plot_marker.h>

LTMWindow::LTMWindow(Context *context) :
            GcChartWindow(context), context(context), dirty(true), stackDirty(true), compareDirty(true), firstshow(true)
{
    useToToday = useCustom = false;
    plotted = DateRange(QDate(01,01,01), QDate(01,01,01));
    lastRefresh = QTime::currentTime().addSecs(-10);

    // the plot
    QVBoxLayout *mainLayout = new QVBoxLayout;

    QPalette palette;
    palette.setBrush(QPalette::Window, QBrush(GColor(CTRENDPLOTBACKGROUND)));

    // single plot
    plotWidget = new QWidget(this);
    plotWidget->setPalette(palette);
    QVBoxLayout *plotLayout = new QVBoxLayout(plotWidget);
    plotLayout->setSpacing(0);
    plotLayout->setContentsMargins(0,0,0,0);
    
    ltmPlot = new LTMPlot(this, context, 0);
    spanSlider = new QxtSpanSlider(Qt::Horizontal, this);
    spanSlider->setFocusPolicy(Qt::NoFocus);
    spanSlider->setHandleMovementMode(QxtSpanSlider::NoOverlapping);
    spanSlider->setLowerValue(0);
    spanSlider->setUpperValue(15);

    QFont smallFont;
    smallFont.setPointSize(6);

    scrollLeft = new QPushButton("<", this);
    scrollLeft->setFont(smallFont);
    scrollLeft->setAutoRepeat(true);
    scrollLeft->setFixedHeight(16);
    scrollLeft->setFixedWidth(16);
    scrollLeft->setContentsMargins(0,0,0,0);

    scrollRight = new QPushButton(">", this);
    scrollRight->setFont(smallFont);
    scrollRight->setAutoRepeat(true);
    scrollRight->setFixedHeight(16);
    scrollRight->setFixedWidth(16);
    scrollRight->setContentsMargins(0,0,0,0);

    QHBoxLayout *span = new QHBoxLayout;
    span->addWidget(scrollLeft);
    span->addWidget(spanSlider);
    span->addWidget(scrollRight);
    plotLayout->addWidget(ltmPlot);
    plotLayout->addLayout(span);

#ifdef Q_OS_MAC
    // BUG in QMacStyle and painting of spanSlider
    // so we use a plain style to avoid it, but only
    // on a MAC, since win and linux are fine
    QStyle *style = QStyleFactory::create("fusion");
    spanSlider->setStyle(style);
    scrollLeft->setStyle(style);
    scrollRight->setStyle(style);
#endif

    // the stack of plots
    plotsWidget = new QWidget(this);
    plotsWidget->setPalette(palette);
    plotsLayout = new QVBoxLayout(plotsWidget);
    plotsLayout->setSpacing(0);
    plotsLayout->setContentsMargins(0,0,0,0);

    plotArea = new QScrollArea(this);
#ifdef Q_OS_WIN
    QStyle *cde = QStyleFactory::create(OS_STYLE);
    plotArea->setStyle(cde);
#endif
    plotArea->setAutoFillBackground(false);
    plotArea->setWidgetResizable(true);
    plotArea->setWidget(plotsWidget);
    plotArea->setFrameStyle(QFrame::NoFrame);
    plotArea->setContentsMargins(0,0,0,0);
    plotArea->setPalette(palette);

    // the data table
    QFont defaultFont; // mainwindow sets up the defaults.. we need to apply
    dataSummary = new QWebEngineView(this);
    // stop stealing focus!
    dataSummary->settings()->setAttribute(QWebEngineSettings::FocusOnNavigationEnabled, false);
    //XXXdataSummary->setEnabled(false); // stop grabbing focus
    if (dpiXFactor > 1) {
    // 80 lines per page on hidpi screens (?)
        int pixelsize = pixelSizeForFont(defaultFont, QApplication::primaryScreen()->geometry().height()/80);
        dataSummary->settings()->setFontSize(QWebEngineSettings::DefaultFontSize, pixelsize);
    } else {
        dataSummary->settings()->setFontSize(QWebEngineSettings::DefaultFontSize, defaultFont.pointSize()+1);
    }
    dataSummary->settings()->setFontFamily(QWebEngineSettings::StandardFont, defaultFont.family());
    dataSummary->setContentsMargins(0,0,0,0);
    dataSummary->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
    dataSummary->setAcceptDrops(false);

    // compare plot page
    compareplotsWidget = new QWidget(this);
    compareplotsWidget->setPalette(palette);
    compareplotsLayout = new QVBoxLayout(compareplotsWidget);
    compareplotsLayout->setSpacing(0);
    compareplotsLayout->setContentsMargins(0,0,0,0);

    compareplotArea = new QScrollArea(this);
#ifdef Q_OS_WIN
    cde = QStyleFactory::create(OS_STYLE);
    compareplotArea->setStyle(cde);
#endif
    compareplotArea->setAutoFillBackground(false);
    compareplotArea->setWidgetResizable(true);
    compareplotArea->setWidget(compareplotsWidget);
    compareplotArea->setFrameStyle(QFrame::NoFrame);
    compareplotArea->setContentsMargins(0,0,0,0);
    compareplotArea->setPalette(palette);

    // the stack
    stackWidget = new QStackedWidget(this);
    stackWidget->addWidget(plotWidget);
    stackWidget->addWidget(dataSummary);
    stackWidget->addWidget(plotArea);
    stackWidget->addWidget(compareplotArea);
    stackWidget->setCurrentIndex(0);
    mainLayout->addWidget(stackWidget);
    setChartLayout(mainLayout);

    HelpWhatsThis *helpStack = new HelpWhatsThis(stackWidget);
    stackWidget->setWhatsThis(helpStack->getWhatsThisText(HelpWhatsThis::ChartTrends_MetricTrends));

    // reveal controls
    QHBoxLayout *revealLayout = new QHBoxLayout;
    revealLayout->setContentsMargins(0,0,0,0);
    revealLayout->addStretch();
    revealLayout->addWidget(new QLabel(tr("Group by"),this));

    rGroupBy = new QxtStringSpinBox(this);
    QStringList strings;
    strings << tr("Days")
            << tr("Weeks")
            << tr("Months")
            << tr("Years")
            << tr("Time Of Day")
            << tr("All");
    rGroupBy->setStrings(strings);
    rGroupBy->setValue(0);
    rGroupBy->setMinimumWidth(100);

    revealLayout->addWidget(rGroupBy);
    rData = new QCheckBox(tr("Data Table"), this);
    rStack = new QCheckBox(tr("Stacked"), this);
    QVBoxLayout *checks = new QVBoxLayout;
    checks->setSpacing(2 *dpiXFactor);
    checks->setContentsMargins(0,0,0,0);
    checks->addWidget(rData);
    checks->addWidget(rStack);
    revealLayout->addLayout(checks);
    revealLayout->addStretch();
    setRevealLayout(revealLayout);

    // add additional menu items before setting
    // controls since the menu is SET from setControls
    QAction *showsettings = new QAction(tr("Chart Settings..."));
    addAction(showsettings);
    QAction *exportData = new QAction(tr("Export Chart Data..."), this);
    addAction(exportData);

    // the controls
    QWidget *c = new QWidget;
    c->setContentsMargins(0,0,0,0);
    HelpWhatsThis *helpConfig = new HelpWhatsThis(c);
    c->setWhatsThis(helpConfig->getWhatsThisText(HelpWhatsThis::ChartTrends_MetricTrends));
    QVBoxLayout *cl = new QVBoxLayout(c);
    cl->setContentsMargins(0,0,0,0);
    cl->setSpacing(0);
    setControls(c);

    // the popup
    popup = new GcPane();
    ltmPopup = new LTMPopup(context);
    QVBoxLayout *popupLayout = new QVBoxLayout();
    popupLayout->addWidget(ltmPopup);
    popup->setLayout(popupLayout);

    ltmTool = new LTMTool(context, &settings);

    // the banister overlay
    QWidget *ban=new QWidget(this);
    addHelper(tr("Banister Model"), ban);

    QGridLayout *bang= new QGridLayout(ban);
    bang->setColumnStretch(0, 40);
    bang->setColumnStretch(1, 30);
    bang->setColumnStretch(2, 20);

    // interactive elements
    banCombo = new QComboBox(this);
    banPerf = new QComboBox(this);
    banT1 = new QDoubleSpinBox(this);
    banT2 = new QDoubleSpinBox(this);

    // labels etc
    ilabel = new QLabel(tr("Impulse Metric"), this);
    perflabel = new QLabel(tr("Perf. Metric"), this);
    plabel = new QLabel(tr("Peak"), this);
    peaklabel = new QLabel(this);
    peaklabel->setText("296w on 3rd July");
    t1label1 = new QLabel(tr("Positive decay"), this);
    t1label2 = new QLabel(tr("days"), this);
    t2label1 = new QLabel(tr("Negative decay"), this);
    t2label2 = new QLabel(tr("days"), this);
    RMSElabel = new QLabel(this);
    RMSElabel->setText(tr("RMSE 2.9 for 22 tests."));

    // add to layout
    bang->addWidget(ilabel,0,0);
    bang->addWidget(banCombo,0,1,1,2,Qt::AlignLeft);
    bang->addWidget(perflabel,1,0);
    bang->addWidget(banPerf,1,1,1,2,Qt::AlignLeft);
    bang->addWidget(plabel, 2,0);
    bang->addWidget(peaklabel,2,1,1,2,Qt::AlignLeft);
    bang->addWidget(t1label1,3,0);
    bang->addWidget(banT1,3,1);
    bang->addWidget(t1label2,3,2);
    bang->addWidget(t2label1,4,0);
    bang->addWidget(banT2,4,1);
    bang->addWidget(t2label2,4,2);
    bang->addWidget(RMSElabel,5,0,1,3);

    // initialise
    settings.ltmTool = ltmTool;
    settings.groupBy = LTM_DAY;
    settings.legend = ltmTool->showLegend->isChecked();
    settings.events = ltmTool->showEvents->isChecked();
    settings.shadeZones = ltmTool->shadeZones->isChecked();
    settings.showData = ltmTool->showData->isChecked();
    settings.stack = ltmTool->showStack->isChecked();
    settings.stackWidth = ltmTool->stackSlider->value();
    rData->setChecked(ltmTool->showData->isChecked());
    rStack->setChecked(ltmTool->showStack->isChecked());
    cl->addWidget(ltmTool);

    connect(this, SIGNAL(dateRangeChanged(DateRange)), this, SLOT(dateRangeChanged(DateRange)));
    connect(this, SIGNAL(styleChanged(int)), this, SLOT(styleChanged(int)));
    connect(ltmTool, SIGNAL(filterChanged()), this, SLOT(filterChanged()));
    connect(this, SIGNAL(perspectiveFilterChanged(QString)), this, SLOT(filterChanged()));
    connect(this, SIGNAL(perspectiveChanged(Perspective*)), this, SLOT(filterChanged()));
    connect(context, SIGNAL(homeFilterChanged()), this, SLOT(filterChanged()));
    connect(ltmTool->groupBy, SIGNAL(currentIndexChanged(int)), this, SLOT(groupBySelected(int)));
    connect(rGroupBy, SIGNAL(valueChanged(int)), this, SLOT(rGroupBySelected(int)));
    connect(ltmTool->applyButton, SIGNAL(clicked(bool)), this, SLOT(applyClicked(void)));
    connect(ltmTool->shadeZones, SIGNAL(stateChanged(int)), this, SLOT(shadeZonesClicked(int)));
    connect(ltmTool->showData, SIGNAL(stateChanged(int)), this, SLOT(showDataClicked(int)));
    connect(ltmTool->showBanister, SIGNAL(stateChanged(int)), this, SLOT(refresh()));
    connect(rData, SIGNAL(stateChanged(int)), this, SLOT(showDataClicked(int)));
    connect(ltmTool->showStack, SIGNAL(stateChanged(int)), this, SLOT(showStackClicked(int)));
    connect(rStack, SIGNAL(stateChanged(int)), this, SLOT(showStackClicked(int)));
    connect(ltmTool->stackSlider, SIGNAL(valueChanged(int)), this, SLOT(zoomSliderChanged()));
    connect(ltmTool->showLegend, SIGNAL(stateChanged(int)), this, SLOT(showLegendClicked(int)));
    connect(ltmTool->showEvents, SIGNAL(stateChanged(int)), this, SLOT(showEventsClicked(int)));
    connect(ltmTool, SIGNAL(useCustomRange(DateRange)), this, SLOT(useCustomRange(DateRange)));
    connect(ltmTool, SIGNAL(useThruToday()), this, SLOT(useThruToday()));
    connect(ltmTool, SIGNAL(useStandardRange()), this, SLOT(useStandardRange()));
    connect(ltmTool, SIGNAL(curvesChanged()), this, SLOT(refresh()));
    connect(context, SIGNAL(filterChanged()), this, SLOT(filterChanged()));
    connect(context, SIGNAL(refreshUpdate(QDate)), this, SLOT(refreshUpdate(QDate)));
    connect(context, SIGNAL(refreshEnd()), this, SLOT(refresh()));
    connect(context, SIGNAL(estimatesRefreshed()), this, SLOT(refresh()));

    // comparing things
    connect(context, SIGNAL(compareDateRangesStateChanged(bool)), this, SLOT(compareChanged()));
    connect(context, SIGNAL(compareDateRangesChanged()), this, SLOT(compareChanged()));

    connect(context, SIGNAL(rideAdded(RideItem*)), this, SLOT(refresh(void)));
    connect(context, SIGNAL(rideDeleted(RideItem*)), this, SLOT(refresh(void)));
    connect(context, SIGNAL(rideSaved(RideItem*)), this, SLOT(refresh(void)));
    connect(context, SIGNAL(configChanged(qint32)), this, SLOT(configChanged(qint32)));
    connect(context, SIGNAL(presetSelected(int)), this, SLOT(presetSelected(int)));
    connect(context->athlete->seasons, SIGNAL(seasonsChanged()), this, SLOT(refreshPlot()));

    // custom menu item
    connect(exportData, SIGNAL(triggered()), this, SLOT(exportData()));
    connect(showsettings, SIGNAL(triggered()), this, SIGNAL(showControls()));

    // normal view
    connect(spanSlider, SIGNAL(lowerPositionChanged(int)), this, SLOT(spanSliderChanged()));
    connect(spanSlider, SIGNAL(upperPositionChanged(int)), this, SLOT(spanSliderChanged()));
    connect(scrollLeft, SIGNAL(clicked()), this, SLOT(moveLeft()));
    connect(scrollRight, SIGNAL(clicked()), this, SLOT(moveRight()));

    // refresh banister data when combo changes
    connect(banCombo, SIGNAL(currentIndexChanged(int)), this, SLOT(refreshBanister()));
    connect(banPerf, SIGNAL(currentIndexChanged(int)), this, SLOT(refreshBanister()));
    connect(banT1, SIGNAL(valueChanged(double)), this, SLOT(tuneBanister()));
    connect(banT2, SIGNAL(valueChanged(double)), this, SLOT(tuneBanister()));
    configChanged(CONFIG_APPEARANCE);
}

LTMWindow::~LTMWindow()
{
    delete popup;
    if (dataSummary) delete dataSummary->page();
}

void
LTMWindow::hideBasic()
{
    ltmTool->hideBasic();
}

void
LTMWindow::showBanister(bool relevant)
{
    RideMetricFactory &factory = RideMetricFactory::instance();

    if (relevant && banister()) {

        // we reset the combo so lets remember where we were at
        int remember=0;
        if (banCombo->count()) remember=banCombo->currentIndex();
        int rememberPerf=0;
        if (banPerf->count()) rememberPerf=banPerf->currentIndex();

        QStringList symbols;
        QStringList perfSymbols;

        // lets setup the widgets
        banCombo->clear();
        banPerf->clear();
        foreach(MetricDetail metricDetail, settings.metrics) {
            if (metricDetail.type == METRIC_BANISTER) {
                if (!symbols.contains(metricDetail.symbol)) {
                    const RideMetric *m = factory.rideMetric(metricDetail.symbol);
                    if (m) {
                        symbols << metricDetail.symbol;

                        // bloody TM in bikescore is TEE DEE US
                        QString name=m->name();
                        if (name.startsWith("BikeScore")) name = QString("BikeScore");

                        banCombo->addItem(name, QVariant(metricDetail.symbol));
                    }
                }
                if (!perfSymbols.contains(metricDetail.perfSymbol)) {
                    const RideMetric *m = factory.rideMetric(metricDetail.perfSymbol);
                    if (m) {
                        perfSymbols << metricDetail.perfSymbol;
                        banPerf->addItem(m->name(), QVariant(metricDetail.perfSymbol));
                    }
                }
            }
        }

        // go back to remembered value
        if (remember < banCombo->count()) banCombo->setCurrentIndex(remember);
        if (rememberPerf < banPerf->count()) banPerf->setCurrentIndex(rememberPerf);

        // now get the metric values etc
        refreshBanister();
        overlayWidget->show();

    } else {

        // ignore it
        overlayWidget->hide();
    }
}

bool
LTMWindow::event(QEvent *event)
{
    // nasty nasty nasty hack to move widgets as soon as the widget geometry
    // is set properly by the layout system, by default the width is 100 and
    // we wait for it to be set properly then put our helper widget on the RHS
    if (event->type() == QEvent::Resize && geometry().width() != 100) {

        // put somewhere nice on first show
        if (firstshow) {
            firstshow = false;
            helperWidget()->resize(400*dpiXFactor, 150*dpiYFactor);
            helperWidget()->move(mainWidget()->geometry().width()-(500*dpiXFactor), 90*dpiYFactor);
        }

        // if off the screen move on screen
        if (helperWidget()->geometry().x() > geometry().width() || helperWidget()->geometry().x() < geometry().x()) {
            helperWidget()->move(mainWidget()->geometry().width()-(500*dpiXFactor), 90*dpiYFactor);
        }
    }
    return QWidget::event(event);
}

void
LTMWindow::tuneBanister()
{

    // if we have a banister...
    if (banCombo->count() && banPerf->count() && banT1->value() >0 && banT2->value()>0) {

        // lookup and set
        Banister *banister = context->athlete->getBanisterFor(banCombo->currentData().toString(), banPerf->currentData().toString(),0,0);

        // when user adjusts the t1/t2 parameters we need to refit
        if (banT1->value() < banister->t1 || banT1->value() > banister->t1 ||
            banT2->value() < banister->t2 || banT2->value() > banister->t2) {

            // lets adjust it them
            banister->setDecay(banT1->value(), banT2->value());
        }

        // replot
        refreshPlot();
    }
}
void
LTMWindow::refreshBanister()
{
    int index = banCombo->currentIndex();
    int perfIndex = banPerf->currentIndex();

    if (index >= 0 && perfIndex >= 0) {

        // lookup and set
        Banister *banister = context->athlete->getBanisterFor(banCombo->currentData().toString(), banPerf->currentData().toString(),0,0);
        banT1->setValue(banister->t1);
        banT2->setValue(banister->t2);

        double perf=0.0;
        int CP=0;
        QDate when = banister->getPeakPerf(settings.start.date(), settings.end.date(), perf, CP);

        // set peak label
        if (CP >0 && when != QDate()) peaklabel ->setText(tr("%1 watts on %2").arg(CP).arg(when.toString("d MMM yyyy")));
        else if (perf >0.0 && when != QDate()) peaklabel ->setText(tr("%1 on %2").arg(perf, 0, 'f', 1).arg(when.toString("d MMM yyyy")));
        else peaklabel->setText("");

        // set RMSE for current view
        int count;
        double RMSE = banister->RMSE(settings.start.date(), settings.end.date(), count);
        if (count && RMSE >0) RMSElabel->setText(tr("RMSE %1 for %2 tests.").arg(RMSE, 0, 'f', 2).arg(count));
        else RMSElabel->setText("");

    } else {
        // clear
        peaklabel->setText("");
        RMSElabel->setText("");
    }
}

void
LTMWindow::configChanged(qint32)
{
    // tinted palette for headings etc
    QPalette palette;
    palette.setBrush(QPalette::Window, QBrush(GColor(CTRENDPLOTBACKGROUND)));
    palette.setColor(QPalette::WindowText, GColor(CPLOTMARKER));
    palette.setColor(QPalette::Text, GColor(CPLOTMARKER));
    palette.setColor(QPalette::Base, GCColor::alternateColor(GColor(CPLOTBACKGROUND)));
    setPalette(palette);

    // inverted palette for data etc
    QPalette whitepalette;
    whitepalette.setBrush(QPalette::Window, QBrush(GColor(CTRENDPLOTBACKGROUND)));
    whitepalette.setColor(QPalette::WindowText, GCColor::invertColor(GColor(CTRENDPLOTBACKGROUND)));
    whitepalette.setColor(QPalette::Base, GCColor::alternateColor(GColor(CPLOTBACKGROUND)));
    whitepalette.setColor(QPalette::Text, GCColor::invertColor(GColor(CTRENDPLOTBACKGROUND)));

    QFont font;
    font.setPointSize(12); // reasonably big
    ilabel->setFont(font);
    perflabel->setFont(font);
    plabel->setFont(font);
    t1label1->setFont(font);
    t1label2->setFont(font);
    plabel->setFont(font);
    peaklabel->setFont(font);
    t2label1->setFont(font);
    t2label2->setFont(font);
    RMSElabel->setFont(font);
    banT1->setFont(font);
    banT2->setFont(font);
    banCombo->setFont(font);
    banPerf->setFont(font);

    ilabel->setPalette(palette);
    perflabel->setPalette(palette);
    plabel->setPalette(palette);
    t1label1->setPalette(palette);
    t1label2->setPalette(palette);
    plabel->setPalette(palette);
    peaklabel->setPalette(whitepalette);
    t2label1->setPalette(palette);
    t2label2->setPalette(palette);
    RMSElabel->setPalette(whitepalette);

#ifndef Q_OS_MAC
    banT1->setStyleSheet(AbstractView::ourStyleSheet());
    banT2->setStyleSheet(AbstractView::ourStyleSheet());
    banCombo->setStyleSheet(AbstractView::ourStyleSheet());
    plotArea->setStyleSheet(AbstractView::ourStyleSheet());
    compareplotArea->setStyleSheet(AbstractView::ourStyleSheet());
#endif
    refresh();
}

void
LTMWindow::styleChanged(int style)
{
    if (style) {
        // hide spanslider
        spanSlider->hide();
        scrollLeft->hide();
        scrollRight->hide();
    } else {
        // show spanslider
        spanSlider->show();
        scrollLeft->show();
        scrollRight->show();
    }
}

void
LTMWindow::compareChanged()
{
    if (!amVisible()) {
        compareDirty = true;
        return;
    }

    if (isCompare()) {

        // refresh plot handles the compare case
        refreshPlot();

    } else {

        // forced refresh back to normal
        stackDirty = dirty = true;
        filterChanged(); // forces reread etc
    }
    repaint();
}

void
LTMWindow::rideSelected() { } // deprecated

void
LTMWindow::presetSelected(int index)
{
    // apply a preset if we are configured to do that...
    if (ltmTool->usePreset->isChecked()) {

        // save chart setup
        int groupBy = settings.groupBy;
        bool legend = settings.legend;
        bool events = settings.events;
        bool stack = settings.stack;
        bool shadeZones = settings.shadeZones;
        QDateTime start = settings.start;
        QDateTime end = settings.end;

        // apply preset
        settings = context->athlete->presets[index];

        // now get back the local chart setup
        settings.ltmTool = ltmTool;
        settings.bests = &bestsresults;
        settings.groupBy = groupBy;
        settings.legend = legend;
        settings.events = events;
        settings.stack = stack;
        settings.shadeZones = shadeZones;
        settings.start = start;
        settings.end = end;

        // Set the specification
        FilterSet fs;
        fs.addFilter(context->isfiltered, context->filters);
        fs.addFilter(context->ishomefiltered, context->homeFilters);
        fs.addFilter(ltmTool->isFiltered(), ltmTool->filters());
        if (myPerspective) fs.addFilter(myPerspective->isFiltered(), myPerspective->filterlist(DateRange()));
        settings.specification.setFilterSet(fs);
        settings.specification.setDateRange(DateRange(settings.start.date(), settings.end.date()));

        ltmTool->applySettings();
        refresh();

        setProperty("subtitle", settings.name);

    } else {

        setProperty("subtitle", property("title").toString());
    }
}

void
LTMWindow::refreshUpdate(QDate here)
{
    if (isVisible() && here > settings.start.date() && lastRefresh.secsTo(QTime::currentTime()) > 5) {
        lastRefresh = QTime::currentTime();
        refresh();
    }
}

void
LTMWindow::moveLeft()
{
    // move across by 5% of the span, or to zero if not much left
    int span = spanSlider->upperValue() - spanSlider->lowerValue();
    int delta = span / 20;
    if (delta > (spanSlider->lowerValue() - spanSlider->minimum()))
        delta = spanSlider->lowerValue() - spanSlider->minimum();

    spanSlider->setLowerValue(spanSlider->lowerValue()-delta);
    spanSlider->setUpperValue(spanSlider->upperValue()-delta);

    spanSliderChanged();
}

void
LTMWindow::moveRight()
{
    // move across by 5% of the span, or to zero if not much left
    int span = spanSlider->upperValue() - spanSlider->lowerValue();
    int delta = span / 20;
    if (delta > (spanSlider->maximum() - spanSlider->upperValue()))
        delta = spanSlider->maximum() - spanSlider->upperValue();

    spanSlider->setLowerValue(spanSlider->lowerValue()+delta);
    spanSlider->setUpperValue(spanSlider->upperValue()+delta);

    spanSliderChanged();
}

void
LTMWindow::spanSliderChanged()
{
    // so reset the axis range for ltmPlot
    ltmPlot->setAxisScale(QwtAxis::XBottom, spanSlider->lowerValue(), spanSlider->upperValue());
    ltmPlot->replot();
}

void
LTMWindow::refreshPlot()
{
    if (amVisible() == true) {

        if (isCompare()) {

            // COMPARE PLOTS
            showBanister(false); // never
            stackWidget->setCurrentIndex(3);
            refreshCompare();

        } else if (ltmTool->showData->isChecked()) {

            //  DATA TABLE
            showBanister(false); // never
            stackWidget->setCurrentIndex(1);
            refreshDataTable();

        } else {

            if (ltmTool->showStack->isChecked()) {

                // STACK PLOTS
                refreshStackPlots();
                stackWidget->setCurrentIndex(2);
                stackDirty = false;

            } else {

                // NORMAL PLOTS
                plotted = DateRange(settings.start.date(), settings.end.date());
                ltmPlot->setData(&settings);
                stackWidget->setCurrentIndex(0);
                dirty = false;

                spanSlider->setMinimum(ltmPlot->axisScaleDiv(QwtAxis::XBottom).lowerBound());
                spanSlider->setMaximum(ltmPlot->axisScaleDiv(QwtAxis::XBottom).upperBound());
                spanSlider->setLowerValue(spanSlider->minimum());
                spanSlider->setUpperValue(spanSlider->maximum());
            }
        }
    }
}

void
LTMWindow::refreshCompare()
{
    // not if in compare mode
    if (!isCompare()) return; 

    // setup stacks but only if needed
    //if (!stackDirty) return; // lets come back to that!

    setUpdatesEnabled(false);

    // delete old and create new...
    //    QScrollArea *plotArea;
    //    QWidget *plotsWidget;
    //    QVBoxLayout *plotsLayout;
    //    QList<LTMSettings> plotSettings;
    foreach (LTMPlot *p, compareplots) {
        compareplotsLayout->removeWidget(p);
        delete p;
    }
    compareplots.clear();
    compareplotSettings.clear();

    if (compareplotsLayout->count() == 1) {
        compareplotsLayout->takeAt(0); // remove the stretch
    }

    // now lets create them all again
    // based upon the current settings
    // we create a plot for each curve
    // but where they are stacked we put
    // them all in the SAME plot
    // so we go once through picking out
    // the stacked items and once through
    // for all the rest of the curves
    LTMSettings plotSetting = settings;
    plotSetting.metrics.clear();
    foreach(MetricDetail m, settings.metrics) {
        if (m.stack) plotSetting.metrics << m;
    }

    int position = 0;

    // create ltmPlot with this
    // Reserving space in list to prevent reallocations and reorderings that break the pointers
    // to the LTMSettings
    if (plotSetting.metrics.count()) {

        compareplotSettings.reserve(1 + settings.metrics.size());
        compareplotSettings << plotSetting;

        // create and setup the plot
        LTMPlot *stacked = new LTMPlot(this, context, position++);
        stacked->setCompareData(&compareplotSettings.last()); // setData using the compare data
        stacked->setFixedHeight(200); // maybe make this adjustable later

        // now add
        compareplotsLayout->addWidget(stacked);
        compareplots << stacked;
    } else {
        compareplotSettings.reserve(settings.metrics.size());
    }

    // OK, now one plot for each curve
    // that isn't stacked!
    foreach(MetricDetail m, settings.metrics) {

        // ignore stacks
        if (m.stack) continue;

        plotSetting = settings;
        plotSetting.metrics.clear();
        plotSetting.metrics << m;
        compareplotSettings << plotSetting;

        // create and setup the plot
        LTMPlot *plot = new LTMPlot(this, context, position++);
        plot->setCompareData(&compareplotSettings.last()); // setData using the compare data

        // now add
        compareplotsLayout->addWidget(plot);
        compareplots << plot;
    }

    // squash em up
    compareplotsLayout->addStretch();

    // set a common X-AXIS
    if (settings.groupBy != LTM_TOD) {
        int MAXX=0;
        foreach(LTMPlot *p, compareplots) {
            if (p->getMaxX() > MAXX) MAXX=p->getMaxX();
        }
        foreach(LTMPlot *p, compareplots) {
            p->setMaxX(MAXX);
        }
    }


    // resize to choice
    zoomSliderChanged();

    // we no longer dirty
    compareDirty = false;

    setUpdatesEnabled(true);
}

void 
LTMWindow::refreshStackPlots()
{
    // not if in compare mode
    if (isCompare()) return; 

    // setup stacks but only if needed
    //if (!stackDirty) return; // lets come back to that!

    setUpdatesEnabled(false);

    // delete old and create new...
    //    QScrollArea *plotArea;
    //    QWidget *plotsWidget;
    //    QVBoxLayout *plotsLayout;
    //    QList<LTMSettings> plotSettings;
    foreach (LTMPlot *p, plots) {
        plotsLayout->removeWidget(p);
        delete p;
    }
    plots.clear();
    plotSettings.clear();

    if (plotsLayout->count() == 1) {
        plotsLayout->takeAt(0); // remove the stretch
    }

    // now lets create them all again
    // based upon the current settings
    // we create a plot for each curve
    // but where they are stacked we put
    // them all in the SAME plot
    // so we go once through picking out
    // the stacked items and once through
    // for all the rest of the curves
    LTMSettings plotSetting = settings;
    plotSetting.metrics.clear();
    foreach(MetricDetail m, settings.metrics) {
        if (m.stack) plotSetting.metrics << m;
    }

    int position = 0;

    // create ltmPlot with this
    // Reserving space in list to prevent reallocations and reorderings that break the pointers
    // to the LTMSettings
    if (plotSetting.metrics.count()) {

        plotSettings.reserve(1 + settings.metrics.size());
        plotSettings << plotSetting;

        // create and setup the plot
        LTMPlot *stacked = new LTMPlot(this, context, position++);
        stacked->setData(&plotSettings.last());
        stacked->setFixedHeight(200); // maybe make this adjustable later

        // now add
        plotsLayout->addWidget(stacked);
        plots << stacked;
    } else {
        plotSettings.reserve(settings.metrics.size());
    }

    // OK, now one plot for each curve
    // that isn't stacked!
    foreach(MetricDetail m, settings.metrics) {

        // ignore stacks
        if (m.stack) continue;

        plotSetting = settings;
        plotSetting.metrics.clear();
        plotSetting.metrics << m;
        plotSettings << plotSetting;

        // create and setup the plot
        LTMPlot *plot = new LTMPlot(this, context, position++);
        plot->setData(&plotSettings.last());

        // now add
        plotsLayout->addWidget(plot);
        plots << plot;
    }

    // squash em up
    plotsLayout->addStretch();

    // resize to choice
    zoomSliderChanged();

    // we no longer dirty
    stackDirty = false;

    setUpdatesEnabled(true);
}

void
LTMWindow::zoomSliderChanged()
{
    static int add[] = { 0, 20, 50, 80, 100, 150, 200, 400 };
    int index = ltmTool->stackSlider->value();

    settings.stackWidth = ltmTool->stackSlider->value();
    setUpdatesEnabled(false);

    // do the compare and the noncompare plots
    // at the same time, as we don't need to worry
    // about optimising out as its fast anyway
    foreach(LTMPlot *plot, plots) {
        plot->setFixedHeight(150 + add[index]);
    }
    foreach(LTMPlot *plot, compareplots) {
        plot->setFixedHeight(150 + add[index]);
    }
    setUpdatesEnabled(true);
}

void
LTMWindow::useCustomRange(DateRange range)
{
    // plot using the supplied range
    useCustom = true;
    useToToday = false;
    custom = range;
    dateRangeChanged(custom);
}

void
LTMWindow::useStandardRange()
{
    useToToday = useCustom = false;
    dateRangeChanged(myDateRange);
}

void
LTMWindow::useThruToday()
{
    // plot using the supplied range
    useCustom = false;
    useToToday = true;
    custom = myDateRange;
    if (custom.to > QDate::currentDate()) custom.to = QDate::currentDate();
    dateRangeChanged(custom);
}

// total redraw, reread data etc
void
LTMWindow::refresh()
{
    setProperty("color", GColor(CTRENDPLOTBACKGROUND)); // called on config change

    // not if in compare mode
    if (isCompare()) return; 

    // refresh for changes to ridefiles / zones
    if (amVisible() == true) {

        bestsresults.clear();
        bestsresults = RideFileCache::getAllBestsFor(context, settings.metrics, settings.specification);

        refreshPlot();
        repaint(); // title changes color when filters change

        // set spanslider to limits of ltmPlot

    } else {
        stackDirty = dirty = true;
    }
}

void
LTMWindow::dateRangeChanged(DateRange range)
{
    // do we need to use custom range?
    if (useCustom || useToToday) range = custom;

    // we already plotted that date range
    if (amVisible() || dirty || range.from != plotted.from || range.to  != plotted.to) {

         settings.bests = &bestsresults;

        // we let all the state get updated, but lets not actually plot
        // whilst in compare mode -- but when compare mode ends we will
        // call filterChanged, so need to record the fact that the date
        // range changed whilst we were in compare mode
        if (!isCompare()) {

            // apply filter to new date range too -- will also refresh plot
            filterChanged();
        } else {

            // we've been told to redraw so maybe
            // compare mode was switched whilst we were
            // not visible, lets refresh
            if (compareDirty) compareChanged();
        }
    }
}

void
LTMWindow::filterChanged()
{
    // ignore in compare mode
    if (isCompare()) return;

    if (amVisible() == false) return;

    if (useCustom) {

        settings.start = QDateTime(custom.from, QTime(0,0));
        settings.end   = QDateTime(custom.to, QTime(24,0,0));

    } else if (useToToday) {

        settings.start = QDateTime(myDateRange.from, QTime(0,0));
        settings.end   = QDateTime(myDateRange.to, QTime(24,0,0));

        QDate today = QDate::currentDate();
        if (settings.end.date() > today) settings.end = QDateTime(today, QTime(24,0,0));

    } else {

        settings.start = QDateTime(myDateRange.from, QTime(0,0));
        settings.end   = QDateTime(myDateRange.to, QTime(24,0,0));

    }
    settings.title = myDateRange.name;

    // Set the specification
    FilterSet fs;
    fs.addFilter(context->isfiltered, context->filters);
    fs.addFilter(context->ishomefiltered, context->homeFilters);
    fs.addFilter(ltmTool->isFiltered(), ltmTool->filters());
    if (myPerspective) fs.addFilter(myPerspective->isFiltered(), myPerspective->filterlist(DateRange()));
    settings.specification.setFilterSet(fs);
    settings.specification.setDateRange(DateRange(settings.start.date(), settings.end.date()));

    // if we want weeks and start is not a monday go back to the monday
    int dow = settings.start.date().dayOfWeek();
    if (settings.groupBy == LTM_WEEK && dow >1 && settings.start != QDateTime(QDate(), QTime(0,0)))
        settings.start = settings.start.addDays(-1*(dow-1));

    // we need to get data again and apply filter
    bestsresults.clear();
    bestsresults = RideFileCache::getAllBestsFor(context, settings.metrics, settings.specification);
    settings.bests = &bestsresults;

    refreshPlot();

    repaint(); // just for the title..
}

void
LTMWindow::rGroupBySelected(int selected)
{
    if (selected >= 0) {
        settings.groupBy = ltmTool->groupBy->itemData(selected).toInt();
        ltmTool->groupBy->setCurrentIndex(selected);
    }
}

void
LTMWindow::groupBySelected(int selected)
{
    if (selected >= 0) {
        settings.groupBy = ltmTool->groupBy->itemData(selected).toInt();
        rGroupBy->setValue(selected);
        refreshPlot();
    }
}

void
LTMWindow::showDataClicked(int state)
{
    bool checked = state;

    // only change if changed, to avoid endless looping
    if (ltmTool->showData->isChecked() != checked) ltmTool->showData->setChecked(checked);
    if (rData->isChecked()!=checked) rData->setChecked(checked);

    if (settings.showData != checked) {
        settings.showData = checked;
        refreshPlot();
    }
}

void
LTMWindow::shadeZonesClicked(int state)
{
    bool checked = state;

    // only change if changed, to avoid endless looping
    if (ltmTool->shadeZones->isChecked() != checked) ltmTool->shadeZones->setChecked(checked);
    settings.shadeZones = state;
    refreshPlot();
}

void
LTMWindow::showLegendClicked(int state)
{
    settings.legend = state;
    refreshPlot();
}

void
LTMWindow::showEventsClicked(int state)
{
    settings.events = bool(state);
    refreshPlot();
}

void
LTMWindow::showStackClicked(int state)
{
    bool checked = state;

    // only change if changed, to avoid endless looping
    if (ltmTool->showStack->isChecked() != checked) ltmTool->showStack->setChecked(checked);
    if (rStack->isChecked() != checked) rStack->setChecked(checked);

    settings.stack = checked;
    refreshPlot();
}

void
LTMWindow::applyClicked()
{
    if (ltmTool->charts->selectedItems().count() == 0) return;

    int selected = ltmTool->charts->invisibleRootItem()->indexOfChild(ltmTool->charts->selectedItems().first());
    if (selected >= 0) {

        // save chart setup
        int groupBy = settings.groupBy;
        bool legend = settings.legend;
        bool events = settings.events;
        bool stack = settings.stack;
        bool shadeZones = settings.shadeZones;
        QDateTime start = settings.start;
        QDateTime end = settings.end;

        // apply preset
        settings = context->athlete->presets[selected];

        // now get back the local chart setup
        settings.ltmTool = ltmTool;
        settings.bests = &bestsresults;
        settings.groupBy = groupBy;
        settings.legend = legend;
        settings.events = events;
        settings.stack = stack;
        settings.shadeZones = shadeZones;
        settings.start = start;
        settings.end = end;

        // Set the specification
        FilterSet fs;
        fs.addFilter(context->isfiltered, context->filters);
        fs.addFilter(context->ishomefiltered, context->homeFilters);
        fs.addFilter(ltmTool->isFiltered(), ltmTool->filters());
        if (myPerspective) fs.addFilter(myPerspective->isFiltered(), myPerspective->filterlist(DateRange()));
        settings.specification.setFilterSet(fs);
        settings.specification.setDateRange(DateRange(settings.start.date(), settings.end.date()));

        ltmTool->applySettings();
        refresh();
    }
}

int
LTMWindow::groupForDate(QDate date)
{
    switch(settings.groupBy) {
    case LTM_WEEK:
        {
        // must start from 1 not zero!
        return 1 + ((date.toJulianDay() - settings.start.date().toJulianDay()) / 7);
        }
    case LTM_MONTH: return (date.year()*12) + date.month();
    case LTM_YEAR:  return date.year();
    case LTM_DAY:
    default:
        return date.toJulianDay();

    }
}
void
LTMWindow::pointClicked(QwtPlotCurve*curve, int index)
{
    // initialize date and time to senseful boundaries
    QDate start = QDate(1900,1,1);
    QDate end   = QDate(2999,12,31);
    QTime time = QTime(0, 0, 0, 0);

    // now fill the correct values for context
    if (settings.groupBy != LTM_TOD) {
      // get the date range for this point (for all date-dependent grouping)
      LTMScaleDraw *lsd = new LTMScaleDraw(settings.start,
                          groupForDate(settings.start.date()),
                          settings.groupBy);
      lsd->dateRange((int)round(curve->sample(index).x()), start, end); }
    else {
      // special treatment for LTM_TOD as time dependent grouping
      time = QTime((int)round(curve->sample(index).x()), 0, 0, 0);
    }
    // feed the popup with data
    ltmPopup->setData(settings, start, end, time);
    popup->show();
}

class GroupedData {

    public:
        QVector<double> x, y; // x and y data for each metric
        int maxdays;
};

void
LTMWindow::refreshDataTable()
{
    // get string
    QString summary = dataTable(true);

    // now set it
    dataSummary->page()->setHtml(summary);
}

// for storing curve data without using a curve
class TableCurveData {
    public:
        TableCurveData() { n=0; x.resize(0); y.resize(0); }
        QVector<double> x,y;
        int n;
};

QString
LTMWindow::dataTable(bool html)
{
    // truncate date range to the actual data when not set to any date
    if (context->athlete->rideCache->rides().count()) {

        QDateTime first = context->athlete->rideCache->rides().first()->dateTime;
        QDateTime last = context->athlete->rideCache->rides().last()->dateTime;

        // end
        if (settings.end == QDateTime() || settings.end.date() > QDate::currentDate().addYears(40))
                settings.end = last;

        // start
        if (settings.start == QDateTime() || settings.start.date() < QDate::currentDate().addYears(-40))
            settings.start = first;
    }

    // now set to new (avoids a weird crash)
    QString summary;

    QColor bgColor = GColor(CTRENDPLOTBACKGROUND);
    QColor altColor = GCColor::alternateColor(bgColor);

    // html page prettified with a title
    if (html) {

        summary = GCColor::css();
        summary += "<center>";

        // device summary for ride summary, otherwise how many activities?
        summary += "<p><h3>" + settings.title + tr(" grouped by ");

        switch (settings.groupBy) {
        case LTM_DAY :
            summary += tr("day");
            break;
        case LTM_WEEK :
            summary += tr("week");
            break;
        case LTM_MONTH :
            summary += tr("month");
            break;
        case LTM_YEAR :
            summary += tr("year");
            break;
        case LTM_TOD :
            summary += tr("time of day");
            break;
        case LTM_ALL :
            summary += tr("All");
            break;
        }
        summary += "</h3><p>";
    }

    //
    // STEP1: AGGREGATE DATA INTO GROUPBY FOR EACH METRIC
    //        This is performed by reusing the existing code in
    //        LTMPlot for creating curve data, but storing it
    //        in columns and forceing zero values
    QList<TableCurveData> columns;
    bool first=true;
    int rows = 0;
    bool firstXvalue=true;
    double lowestFirstXvalue = DBL_MAX;
    double highestFirstXvalue = 0.0;

    // create curve data for each metric detail to iterate over
    foreach(MetricDetail metricDetail, settings.metrics) {
        TableCurveData add;

        ltmPlot->settings=&settings; // for stack mode ltmPlot isn't set
        if (settings.groupBy != LTM_TOD)
            ltmPlot->createCurveData(context, &settings, metricDetail, add.x, add.y, add.n, true);
        else
            ltmPlot->createTODCurveData(context, &settings, metricDetail, add.x, add.y, add.n, true);

        // adjust to avoid empty chart when there is only 1 group
        if (settings.groupBy != LTM_TOD) add.n++;

        columns << add;

        // check if "x" value of all metrics is the same for all colums and find
        // the lowest "x" value and highest "x" value to which all columns need to be aligned
        if (add.n > 0) {
            if (firstXvalue) {
                lowestFirstXvalue = highestFirstXvalue = add.x[0];
                firstXvalue = false;
            } else {
                if (add.x[0] < lowestFirstXvalue) {
                    lowestFirstXvalue = add.x[0];
                }
                if (add.x[0] > highestFirstXvalue) {
                    highestFirstXvalue = add.x[0];
                }
            }
        }

        // truncate to shortest set of rows available as 
        // we dont pad with zeroes in the data table
        if (first) rows=add.n;
        else if (add.n < rows) rows=add.n;
        first=false;
    }

    // align the starting X values of all columns using the
    // lowest xValue and highest xValue as borders
    // for columns which have data at all - and if there is something to adjust
    if (!firstXvalue && lowestFirstXvalue != highestFirstXvalue) {
        for (int i = 0; i< columns.count(); i++) {
            if (columns[i].n > 0) {
                // Prepend on vector is prohibitively expensive since requires
                // full vector copy for each prepend. Much faster to convert
                // to Qlist, do our business, then convert back.
                QList<double> tx = columns[i].x.toList();
                QList<double> ty = columns[i].y.toList();

                double xValue = columns[i].x[0];
                while (xValue > lowestFirstXvalue) {
                    xValue--;
                    tx.prepend(xValue);
                    ty.prepend(0.0);
                }

                columns[i].x = tx.toVector();
                columns[i].y = ty.toVector();
                columns[i].n += tx.size();
            }
        }
        // adjust number of visible rows in table
        rows += qRound(highestFirstXvalue-lowestFirstXvalue);
    }


    //
    // STEP 2: PREPARE HTML TABLE FROM AGGREGATED DATA
    //         But note there will be no data if there are no curves of if there
    //         is no date range selected of no data anyway!
    //
    if (rows) {

        // formatting ...
        LTMScaleDraw lsd(settings.start, groupForDate(settings.start.date()), settings.groupBy);

        QString sLabel = (settings.groupBy == LTM_TOD) ? tr("Time of Day") : tr("Date");
        if (html) {
            // table and headings 50% for 1 metric, 70% for 2 metrics, 90% for 3 metrics or more
            QString tableStart = "<table border=0 cellspacing=3 width=\"%1%%\"><tr><td align=\"center\" valigne=\"top\"><b>%2</b></td>";
            tableStart = tableStart.arg(settings.metrics.count() >= 3 ? 90 : (30 + (settings.metrics.count() * 20))).arg(sLabel);

            summary += tableStart;
        } else {
            summary += sLabel;
        }

        QList<QVector<double> > hdatas;
        QList<QString> fontcolors;

        // highlight
        for (int a=0; a < settings.metrics.count(); a++) {
            MetricDetail metricDetail = settings.metrics[a];

            int brightness = metricDetail.penColor.red() *0.299 + metricDetail.penColor.green()*0.587 + metricDetail.penColor.blue()*0.114;
            fontcolors.append( brightness > 128 ? "#000" : "#fff" );

            // highlight lowest / top N values
            if (metricDetail.lowestN > 0 || metricDetail.topN > 0) {
                QMap<double, int> sortedList;

                // copy the yvalues, retaining the offset
                for(int i=0; i<columns[a].y.count(); i++) {
                    // pmc metrics we highlight TROUGHS
                    if (metricDetail.type == METRIC_STRESS || metricDetail.type == METRIC_PM) {
                        if (i && i < (columns[a].y.count()-1) // not at start/end
                            && ((columns[a].y[i-1] > columns[a].y[i] && columns[a].y[i+1] > columns[a].y[i]) || // is a trough
                                (columns[a].y[i-1] < columns[a].y[i] && columns[a].y[i+1] < columns[a].y[i])))  // is a peak
                            sortedList.insert(columns[a].y[i], i);
                    } else
                        sortedList.insert(columns[a].y[i], i);
                }

                // copy the top N values
                QVector<double> hdata;
                hdata.resize(metricDetail.topN + metricDetail.lowestN);


                // QMap orders the list so start at the top and work
                // backwards for topN
                int counter = 0;
                QMapIterator<double, int> i(sortedList);
                if (metricDetail.topN) {
                    i.toBack();
                    while (i.hasPrevious() && counter < metricDetail.topN) {
                        i.previous();
                        hdata[counter] = i.value();
                        counter++;
                    }
                }

                if (metricDetail.lowestN) {
                    i.toFront();
                    counter = 0; // and forwards for bottomN
                    while (i.hasNext() && counter < metricDetail.lowestN) {
                        i.next();
                        hdata[metricDetail.topN + counter] = i.value();
                        counter++;
                    }
                }
                hdatas.append(hdata);
            } else {
                // add an empty vector to maintain alignment with fontcolors
                QVector<double> hdata;
                hdatas.append(hdata);
            }
        }

        // metric name
        for (int i=0; i < settings.metrics.count(); i++) {

            QString metricSummary;

            if (html) metricSummary = "<td align=\"center\" style=\"font-weight:bold;background-color:%2;color:%3\" valign=\"top\">%1</td>";
            else metricSummary = ", %1";

            QString name = settings.metrics[i].uname;

            if (name == "Coggan Acute Training Load" || name == tr("Coggan Acute Training Load")) name = "ATL";
            if (name == "Coggan Chronic Training Load" || name == tr("Coggan Chronic Training Load")) name = "CTL";
            if (name == "Coggan Training Stress Balance" || name == tr("Coggan Training Stress Balance")) name = "TSB";

            metricSummary = metricSummary.arg(name);

            if (html) {
                QString bcolor = settings.metrics[i].penColor.lighter(80).name();
                metricSummary = metricSummary.arg(bcolor);
                metricSummary = metricSummary.arg(fontcolors.at(i));
            }

            summary += metricSummary;
        }

        if (html) {

            // html table and units on next line
            summary += "</tr><tr><td></td>";

            // units
            for (int i=0; i < settings.metrics.count(); i++) {
                QString metricSummary = "<td align=\"center\" style=\"font-weight:bold;background-color:%2;color:%3\" valign=\"top\">"
                        "%1</td>";
                QString units = settings.metrics[i].uunits;
                QString bcolor = settings.metrics[i].penColor.lighter(80).name();


                if (units == "seconds" || units == tr("seconds")) units = tr("hours");
                if (units == settings.metrics[i].uname) units = "";
                metricSummary = metricSummary.arg(units != "" ? QString("(%1)").arg(units) : "");
                metricSummary = metricSummary.arg(bcolor);
                metricSummary = metricSummary.arg(fontcolors.at(i));

                summary += metricSummary;
            }
            summary += "</tr>";

        } else {

            // end of heading for CSV
            summary += "\n";
        }

        for(int row=0; row<rows; row++) {

            QString rowSummary;

            // in day mode we don't list all the zeroes .. its too many!
            bool nonzero = false;
            if (settings.groupBy == LTM_DAY) {

                // nonzeros?
                for(int j=0; j<columns.count(); j++) 
                    if (int(columns[j].y[row])) nonzero = true;

                // skip all zeroes if day mode
                if (nonzero == false) continue;
            }

            // alternating colors on html output
            if (html) {
                if (row%2) rowSummary += "<tr bgcolor='" + altColor.name() + "'>";
                else rowSummary += "<tr>";
            }

            // First column, date / month year etc
            QString sDate = lsd.label(columns[0].x[row]+0.5).text().replace("\n", " ");
            if (html) rowSummary += QString("<td align=\"center\" valign=\"top\">%1</td>").arg(sDate);
            else rowSummary += (settings.groupBy == LTM_ALL || settings.groupBy == LTM_TOD) ? sDate : lsd.toDate(columns[0].x[row]+0.5).toString(Qt::ISODate);

            // Remaining columns - each metric value
            for(int j=0; j<columns.count(); j++) {

                QString metricSummary;

                if (html) metricSummary += "<td align=\"center\" style=\"%2\" valign=\"top\">%1</td>";
                else metricSummary += ", %1";

                // now format the actual value....
                QString valueString;
                double value = columns[j].y[row];

                // Format minutes in sexagesimal format
                if (LTMPlot::isMinutes(settings.metrics[j].uunits)) {
                    valueString = time_to_string(value * 60, true);
                } else {
                    int precision = 1;
                    const RideMetric *m = settings.metrics[j].metric;
                    if (m != NULL) {

                        // we have a metric so lets be precise ...
                        precision = m->precision();

                        // handle precision of 1 for seconds converted to hours
                        if (settings.metrics[j].uunits == "seconds" || settings.metrics[j].uunits == tr("seconds")) precision = 1;
                    }
                    valueString.setNum(value, 'f', precision);
                }

                metricSummary = metricSummary.arg(valueString);

                //
                if (hdatas.at(j).contains(row)) {
                    QString c = QString("background-color:%1;color:%2").arg(settings.metrics[j].penColor.name()).arg(fontcolors.at(j));
                    metricSummary = metricSummary.arg(c);
                } else
                    metricSummary = metricSummary.arg("");

                rowSummary += metricSummary;
            }

            // ok, this row is done
            if (html) rowSummary += "</tr>";
            else rowSummary += "\n"; // csv newline

            summary += rowSummary;
        }

        // close table on html page
        if (html) summary += "</table>";
    }

    // all done !
    if (html) summary += "</center>";

    return summary;
}

void
LTMWindow::exportConfig()
{
    // collect the config to export
    QList<LTMSettings> mine;
    mine << settings;
    mine[0].title = mine[0].name = title();

    // get a filename
    QString filename = title()+".xml";
    filename = QFileDialog::getSaveFileName(this, tr("Export Chart Config"),  filename, title()+".xml (*.xml)");

    // export it!
    if (!filename.isEmpty()) {
        LTMChartParser::serialize(filename, mine);
    }
}


void
LTMWindow::exportData()
{
    QString filename = title()+".csv";
    filename = QFileDialog::getSaveFileName(this, tr("Save Chart Data as CSV"),  QString(), title()+".csv (*.csv)");

    if (!filename.isEmpty()) {

        // can we open the file ?
        QFile f(filename);
        if (!f.open(QIODevice::WriteOnly | QIODevice::Text)) return; // couldn't open file

        // generate file content
        QString content = dataTable(false); // want csv

        // open stream and write header
        QTextStream stream(&f);
#if QT_VERSION < 0x060000
        stream.setCodec("UTF-8"); // Names and Units can be translated
#endif
        stream << content;

        // and we're done
        f.close();
    }
}
