/*
 * Copyright (c) 2010 Mark Liversedge (liversedge@gmail.com)
 * Copyright (c) 2016 Alejandro Martinez (amtriathlon@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 "DataProcessor.h"
#include "Settings.h"
#include "Units.h"
#include "Colors.h"
#include "HelpWhatsThis.h"
#include "LocationInterpolation.h"
#include <algorithm>
#include <QVector>

#ifndef MATHCONST_PI
#define MATHCONST_PI 		    3.141592653589793238462643383279502884L /* pi */
#endif

// Config widget used by the Preferences/Options config panes
class FixRunningPower;
class FixRunningPowerConfig : public DataProcessorConfig
{
    Q_DECLARE_TR_FUNCTIONS(FixRunningPowerConfig)

    friend class ::FixRunningPower;
    protected:
        QDoubleSpinBox *equipWeight;
        QDoubleSpinBox *draftM;
        QDoubleSpinBox *windSpeed;
        QDoubleSpinBox *windHeading;

    public:
        FixRunningPowerConfig(QWidget *parent) : DataProcessorConfig(parent) {

            HelpWhatsThis *help = new HelpWhatsThis(parent);
            parent->setWhatsThis(help->getWhatsThisText(HelpWhatsThis::MenuBar_Edit_EstimatePowerValues));

            QFormLayout *layout = newQFormLayout(this);
            layout->setContentsMargins(0,0,0,0);
            setContentsMargins(0,0,0,0);

            equipWeight = new QDoubleSpinBox();
            equipWeight->setMaximum(9.9);
            equipWeight->setMinimum(0);
            equipWeight->setSingleStep(0.1);
            equipWeight->setDecimals(1);
            equipWeight->setSuffix(" " + tr("kg"));

            draftM = new QDoubleSpinBox();
            draftM->setMaximum(1.0);
            draftM->setMinimum(0.1);
            draftM->setSingleStep(0.1);
            draftM->setDecimals(1);

            windSpeed = new QDoubleSpinBox();
            windSpeed->setMaximum(99.9);
            windSpeed->setMinimum(0);
            windSpeed->setSingleStep(0.1);
            windSpeed->setDecimals(1);
            windSpeed->setSuffix(" " + tr("km/h"));

            windHeading = new QDoubleSpinBox();
            windHeading->setMaximum(180.0);
            windHeading->setMinimum(-179.0);
            windHeading->setSingleStep(1);
            windHeading->setDecimals(0);
            windHeading->setSuffix(" " + tr("°"));

            layout->addRow(tr("Equipment Weight"), equipWeight);
            layout->addRow(tr("Draft Mult."), draftM);
            layout->addRow(tr("Wind Speed"), windSpeed);
            layout->addRow(tr("Wind Direction"), windHeading);
        }

        //~FixRunningPowerConfig() {} // deliberately not declared since Qt will delete
                              // the widget and its children when the config pane is deleted

        void readConfig() {
            double MEquip = appsettings->value(NULL, GC_DPRP_EQUIPWEIGHT, "0.7").toDouble();
            equipWeight->setValue(MEquip);
            double Draft = appsettings->value(NULL, GC_DPDR_DRAFTM, "1.0").toDouble();
            draftM->setValue(Draft);
            windSpeed->setValue(0.0);
            windHeading->setValue(0.0);
        }

        void saveConfig() {
            appsettings->setValue(GC_DPRP_EQUIPWEIGHT, equipWeight->value());
            appsettings->setValue(GC_DPDR_DRAFTM, draftM->value());
        }
};


// RideFile Dataprocessor -- Derive estimated running power data based on
//                           speed/elevation/weight with di Prampero coeff.
//
class FixRunningPower : public DataProcessor {
    Q_DECLARE_TR_FUNCTIONS(FixRunningPower)

    public:
        FixRunningPower() {}
        ~FixRunningPower() {}

        // the processor
        bool postProcess(RideFile *, DataProcessorConfig* config, QString op) override;

        // the config widget
        DataProcessorConfig* processorConfig(QWidget *parent, const RideFile * ride = NULL) const override {
            Q_UNUSED(ride);
            return new FixRunningPowerConfig(parent);
        }

        // Localized Name
        QString name() const override {
            return (tr("Estimate Running Power"));
        }

        QString id() const override {
            return "::FixRunningPower";
        }

        QString legacyId() const override {
            return "Estimate Running Power";
        }

        QString explain() const override {
            return tr("Derive estimated running power data based on "
                      "speed/elevation/weight etc using di Prampero "
                      "coefficcients\n\n"
                      "Equipment Weight parameter is added to athlete's"
                      " weight to compound total mass, it should "
                      "include apparel, shoes, etc\n\n"
                      "Draft Mult. parameter is the multiplier "
                      "to adjust for drafting, 1 is no drafting "
                      " and 0.7 seems legit for drafting in a group\n\n"
                      "wind speed shall be indicated in kph\n"
                      "wind direction (origin) unit is degrees "
                      "from -179 to +180 (-90=W, 0=N, 90=E, 180=S)\n"
                      "Note: when the file already contains wind data, "
                      "it will be overridden if wind speed is set\n\n"
                      "The activity has to be a Run with Speed and "
                      "Altitude.");
        }
};

static bool FixRunningPowerAdded = DataProcessorFactory::instance().registerProcessor(new FixRunningPower());

bool
FixRunningPower::postProcess(RideFile *ride, DataProcessorConfig *config=0, QString op="")
{
    Q_UNUSED(op)

    // get settings
    double MEquip; // Equipment weight kg
    double DraftM;  // Drafting Multiplier
    double windSpeed; // wind speed
    double windHeading; //wind direction
    if (config == NULL) { // being called automatically
        MEquip = appsettings->value(NULL, GC_DPRP_EQUIPWEIGHT, "0.7").toDouble();
        DraftM = appsettings->value(NULL, GC_DPDR_DRAFTM, "1.0").toDouble();
        windSpeed = 0.0;
        windHeading = 0.0;
    } else { // being called manually
        MEquip = ((FixRunningPowerConfig*)(config))->equipWeight->value();
        DraftM = ((FixRunningPowerConfig*)(config))->draftM->value();
        windSpeed = ((FixRunningPowerConfig*)(config))->windSpeed->value();                // kph
        windHeading = ((FixRunningPowerConfig*)(config))->windHeading->value() / 180 * MATHCONST_PI; // rad
    }

    // if not a run do nothing !
    if (!ride->isRun()) return false;

    // if called automatically and power already present, do nothing !
    if (!config && ride->areDataPresent()->watts) return false;

    // no dice if we don't have alt and speed
    if (!ride->areDataPresent()->alt || !ride->areDataPresent()->kph) return false;

    // Power Estimation Constants (mostly constants...)
    double H = ride->getHeight(); //Height in m
    double M = ride->getWeight(); //Weight kg
    double Mtotal = M + MEquip;
    double T = 15; // Temp degC if not in ride data
    double W = 0;  // headwind (from records or based on wind parameters entered manually)
    double bearing = 0.0; //runner direction used to compute headwind
    double Cx = 0.9; // Axial force coefficient
    double FA = 0.226 * 0.2025 * pow(H, 0.725) * pow(M, 0.425); // Frontal Area
    double Cr = 3.72; // di Prampero cost of flat running
    double eff = 0.228; // di Prampero efficiency (metabolic to external)

    // apply the change
    ride->command->startLUW("Estimate Running Power");

    if (ride->areDataPresent()->slope) {
        for (int i=0; i<ride->dataPoints().count(); i++) {
            RideFilePoint *p = ride->dataPoints()[i];

            // compute bearing in order to calculate headwind
            if (i>=1)
            {
                RideFilePoint *prevPoint = ride->dataPoints()[i-1];

                // ensure a movement occurred and valid lat/lon in order to compute cyclist direction
                if (prevPoint->lat != p->lat || prevPoint->lon != p->lon)
                {
                    geolocation prevLoc(prevPoint->lat, prevPoint->lon, prevPoint->alt);
                    geolocation loc(p->lat, p->lon, p->alt);

                    if (prevLoc.IsReasonableGeoLocation() && loc.IsReasonableGeoLocation())
                    {
                        bearing = prevLoc.BearingTo(loc);
                    }
                }
            }
            // else keep previous bearing (or 0 at the beginning)

            // wind parameters to be considered
            if (windSpeed || windHeading) // if wind parameters were entered manually then use it and override rideFile headwind
                W = cos(bearing - windHeading) * windSpeed * 0.27777777777778; // Absolute wind speed relative to cyclist orientation (m/s)
            else if (ride->areDataPresent()->headwind) //  otherwise use headwind from rideFile records (typ. weather forecast included in FIT files)
                W = (p->headwind - p->kph) * 0.27777777777778; // Compute absolute wind speed relative to cyclist orientation (m/s)
            else
                W = 0.0; //otherwise assume no wind

            double V = p->kph * 0.27777777777778; // Runner speed m/s
            // if the file has temperature, use it
            if (ride->areDataPresent()->temp) T = p->temp;
            // Air Density based on altitude and temperature
            double rho = 353 * exp(-p->alt * .0001253) / (273 + T);
            double vw=V+W; // Wind speed against runner = speed + wind speed
            double Slope = atan(p->slope * .01);
            double forw = Cr * Mtotal * eff * V;
            double aero = 0.5 * rho * Cx * FA * DraftM * (vw*vw) * V;
            double grav = 9.81 * Mtotal * sin(Slope) * V;
            double chgV = (p->kphd > 1 ? 1 : p->kphd) * Mtotal * V;

            // Power = moving forward + aerodynamic + gravity + change of speed
            double watts = forw + aero + grav + chgV;
            ride->command->setPointValue(i, RideFile::watts, watts > 0 ? (watts > 1000 ? 1000 : watts) : 0);
        }

        int smoothPoints = 3;
        // initialise rolling average
        double rtot = 0;
        for (int i=smoothPoints; i>0 && ride->dataPoints().count()-i >=0; i--) {
            rtot += ride->dataPoints()[ride->dataPoints().count()-i]->watts;
        }

        // now run backwards setting the rolling average
        for (int i=ride->dataPoints().count()-1; i>=smoothPoints; i--) {
            double here = ride->dataPoints()[i]->watts;
            ride->dataPoints()[i]->watts = rtot / smoothPoints;
            if (ride->dataPoints()[i]->watts<0) ride->dataPoints()[i]->watts = 0;
                rtot -= here;
                rtot += ride->dataPoints()[i-smoothPoints]->watts;
        }
        ride->setDataPresent(ride->watts, true);
    }

    ride->command->endLUW();

    return true;
}
