/*
 * Copyright (c) 2011 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 "BingMap.h"

#include "RideItem.h"
#include "RideFile.h"
#include "IntervalItem.h"
#include "Context.h"
#include "Athlete.h"
#include "Zones.h"
#include "Settings.h"
#include "Colors.h"
#include "Units.h"
#include "TimeUtils.h"
#include "HelpWhatsThis.h"

#include <QDebug>

BingMap::BingMap(Context *context) : GcChartWindow(context), context(context), range(-1), current(NULL)
{
    setControls(NULL);

    setContentsMargins(0,0,0,0);
    layout = new QVBoxLayout();
    layout->setSpacing(0);
    layout->setContentsMargins(2,0,2,2);
    setChartLayout(layout);

    view = new QWebView();
    view->setContentsMargins(0,0,0,0);
    view->page()->view()->setContentsMargins(0,0,0,0);
    view->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding);
    view->setAcceptDrops(false);
    layout->addWidget(view);

    HelpWhatsThis *help = new HelpWhatsThis(view);
    view->setWhatsThis(help->getWhatsThisText(HelpWhatsThis::ChartRides_Map));

    webBridge = new BWebBridge(context, this);

    connect(this, SIGNAL(rideItemChanged(RideItem*)), this, SLOT(rideSelected()));
    connect(view->page()->mainFrame(), SIGNAL(javaScriptWindowObjectCleared()), this, SLOT(updateFrame()));
    connect(context, SIGNAL(intervalsChanged()), webBridge, SLOT(intervalsChanged()));
    connect(context, SIGNAL(intervalSelected()), webBridge, SLOT(intervalsChanged()));
    connect(context, SIGNAL(intervalZoom(IntervalItem*)), this, SLOT(zoomInterval(IntervalItem*)));
    connect(context, SIGNAL(configChanged(qint32)), this, SLOT(configChanged(qint32)));

    first = true;

    // get the colors setup for first run
    configChanged(CONFIG_APPEARANCE);
}

void
BingMap::configChanged(qint32)
{
    setProperty("color", GColor(CPLOTBACKGROUND));
    rideSelected();
}

void
BingMap::rideSelected()
{
    // skip display if data drawn or invalid
    if (myRideItem == NULL || !amVisible()) return;
    RideItem * ride = myRideItem;
    if (ride == current || !ride || !ride->ride()) return;
    else current = ride;

    // Route metadata ...
    setSubTitle(ride->ride()->getTag("Route", tr("Route")));

    range=-1;
    rideCP=300;

    if (context->athlete->zones()) {

        // get the right range and CP
        range = context->athlete->zones()->whichRange(ride->dateTime.date());
        if (range >= 0) rideCP = context->athlete->zones()->getCP(range);
    }

    loadRide();
}

void BingMap::loadRide()
{
    createHtml();
    view->page()->mainFrame()->setHtml(currentPage);
}

void BingMap::updateFrame()
{
    // deleting the web bridge seems to be the only way to
    // reset state between it and the webpage.
    delete webBridge;
    webBridge = new BWebBridge(context, this);
    connect(context, SIGNAL(intervalsChanged()), webBridge, SLOT(intervalsChanged()));
    connect(context, SIGNAL(intervalSelected()), webBridge, SLOT(intervalsChanged()));

    view->page()->mainFrame()->addToJavaScriptWindowObject("webBridge", webBridge);
}

void BingMap::createHtml()
{
    RideItem * ride = myRideItem;
    currentPage = "";
    double minLat, minLon, maxLat, maxLon;
    minLat = minLon = 1000;
    maxLat = maxLon = -1000; // larger than 360

    // get bounding co-ordinates for ride
    foreach(RideFilePoint *rfp, myRideItem->ride()->dataPoints()) {
        if (rfp->lat || rfp->lon) {
            minLat = std::min(minLat,rfp->lat);
            maxLat = std::max(maxLat,rfp->lat);
            minLon = std::min(minLon,rfp->lon);
            maxLon = std::max(maxLon,rfp->lon);
        }
    }

    QColor bgColor = GColor(CPLOTBACKGROUND);
    QColor fgColor = GCColor::invertColor(bgColor);

    // No GPS data, so sorry no map
    if(!ride || !ride->ride() || ride->ride()->areDataPresent()->lat == false || ride->ride()->areDataPresent()->lon == false) {
        currentPage = QString("<STYLE>BODY { background-color: %1; color: %2 }</STYLE><center>%3</center>").arg(bgColor.name()).arg(fgColor.name()).arg(tr("No GPS Data Present"));
        setIsBlank(true);
        return;
    } else {
        setIsBlank(false);
    }

    // load the Google Map v3 API
    currentPage = QString("<!DOCTYPE html> \n"
    "<html>\n"
    "<head>\n"
    "<title>Golden Cheetah Map</title>\n"

    // Load BING Rest API Services
    "<meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\"/>\n"
    "<script type=\"text/javascript\" src=\"http://ecn.dev.virtualearth.net/mapcontrol/mapcontrol.ashx?v=7.0\"></script>\n");

    // colors
    currentPage += QString("<STYLE>BODY { background-color: %1; color: %2 }</STYLE>")
                                          .arg(bgColor.name()).arg(fgColor.name());
    // local functions
    currentPage += QString("<script type=\"text/javascript\">\n"

    "var map;\n"  // the map object
    "var intervalList;\n"  // array of intervals
    "var markerList;\n"  // array of markers
    "var polyList;\n"  // array of polylines

    // Draw the entire route, we use a local webbridge
    // to supply the data to a) reduce bandwidth and
    // b) allow local manipulation. This makes the UI
    // considerably more 'snappy'
    "function drawRoute() {\n"

    // route will be drawn with these options
    "    var routeOptionsYellow = {\n"
    "        strokeColor: new Microsoft.Maps.Color(100, 255, 255, 0),\n"
    "        strokeThickness: 7,\n"
    "        strokeDashArray: '5 0',\n"
    "        zIndex: -2\n"
    "    };\n"

    // load the GPS co-ordinates
    "    var latlons = webBridge.getLatLons(0);\n" // interval "0" is the entire route

    // create the route path
    "    var route = new Array();\n"
    "    var j=0;\n"
    "    while (j < latlons.length) { \n"
    "        route.push(new Microsoft.Maps.Location(latlons[j],latlons[j+1]));\n"
    "        j += 2;\n"
    "    }\n"

    // create the route Polyline
    "    var routeYellow = new Microsoft.Maps.Polyline(route, routeOptionsYellow);\n"
    "    map.entities.push(routeYellow);\n"

    "}\n"

    "function drawIntervals() { \n"
    // intervals will be drawn with these options
    "    var polyOptions = {\n"
    "        strokeColor: new Microsoft.Maps.Color(100, 0, 0, 255),\n"
    "        strokeThickness: 5,\n"
    "        strokeDashArray: '5 0',\n"
    "        zIndex: -1\n"
    "    };\n"

    // remove previous intervals highlighted
    "    j= intervalList.length;\n"
    "    while (j) {\n"
    "       var highlighted = intervalList.pop();\n"
    "       highlighted.setOptions( { visible: false });\n"
    "       j--;\n"
    "    }\n"

    // how many to draw?
    "    var intervals = webBridge.intervalCount();\n"
    "    while (intervals > 0) {\n"
    "        var latlons = webBridge.getLatLons(intervals);\n"

            // create the route path
    "        var route = new Array();\n"
    "        var j=0;\n"
    "        while (j < latlons.length) { \n"
    "            route.push(new Microsoft.Maps.Location(latlons[j],latlons[j+1]));\n"
    "            j += 2;\n"
    "        }\n"

             // create the route Polyline
    "        var intervalHighlighter = new Microsoft.Maps.Polyline(route, polyOptions);\n"
    "        map.entities.push(intervalHighlighter);\n"
    "        intervalList.push(intervalHighlighter);\n"

    "        intervals--;\n"
    "    }\n"
    "}\n"



    // initialise function called when map loaded
    "function initialize() {\n"

    // define initial map boundary
    "    var initialViewBounds = Microsoft.Maps.LocationRect.fromCorners("
                                 "new Microsoft.Maps.Location(%1,%2), "
                                 "new Microsoft.Maps.Location(%3,%4));\n"

    // map options - we like AUTO change as you zoom in
    "    var options = {\n"
    "        credentials:\"AtgAt5wRsWGlmft5J_qUbo4rTJg_qZvepiwN_8FQMQwryIlMMCSpwCbvYiIZO5Zr\",\n" // API key, registered to GoldenCheetah
    "        bounds: initialViewBounds,\n"
    "        mapTypeId: Microsoft.Maps.MapTypeId.auto,\n"
    "        animate: true\n"
    "    };\n"

    // setup the map, and fit to contain the limits of the route
    "    map = new Microsoft.Maps.Map(document.getElementById(\"map_canvas\"),options);\n"

    // initialise local variables
    "    markerList = new Array();\n"
    "    intervalList = new Array();\n"
    "    polyList = new Array();\n"

    // draw the main route data, getting the geo
    // data from the webbridge - reduces data sent/received
    // to the map server and makes the UI pretty snappy
    "    drawRoute();\n"

    "    drawIntervals();\n"

    // catch signals to redraw intervals
    "    webBridge.drawIntervals.connect(drawIntervals);\n"

    // we're done now let the C++ side draw its overlays
    "    webBridge.drawOverlays();\n"

    "}\n"
    "</script>\n").arg(minLat,0,'g',GPS_COORD_TO_STRING).arg(minLon,0,'g',GPS_COORD_TO_STRING).arg(maxLat,0,'g',GPS_COORD_TO_STRING).arg(maxLon,0,'g',GPS_COORD_TO_STRING);

    // the main page is rather trivial
    currentPage += QString("</head>\n"
    "<body onload=\"initialize()\">\n"
    "<div id=\"map_canvas\"></div>\n"
    "</body>\n"
    "</html>\n");
}


QColor BingMap::GetColor(int watts)
{
    if (range < 0) return Qt::red;
    else return zoneColor(context->athlete->zones()->whichZone(range, watts), 7);
}

// create the ride line
void
BingMap::drawShadedRoute()
{
    int intervalTime = 60;  // 60 seconds
    double rtime=0; // running total for accumulated data
    int count=0;  // how many samples ?
    int rwatts=0; // running total of watts
    double prevtime=0; // time for previous point

    QString code;

    foreach(RideFilePoint *rfp, myRideItem->ride()->dataPoints()) {

        if (count == 0) {
            code = QString("{\nvar route = new Array();\n");
        } else {
            if (rfp->lat || rfp->lon)
                code += QString("route.push(new Microsoft.Maps.Location(%1,%2));\n").arg(rfp->lat,0,'g',GPS_COORD_TO_STRING).arg(rfp->lon,0,'g',GPS_COORD_TO_STRING);
        }

        // running total of time
        rtime += rfp->secs - prevtime;
        rwatts += rfp->watts;
        prevtime = rfp->secs;
        count++;

        // end of segment
        if (rtime >= intervalTime) {

            int avgWatts = rwatts / count;
            QColor color = GetColor(avgWatts);
            // thats this segment done, so finish off and
            // add tooltip junk
            count = rwatts = rtime = 0;

            // color the polyline
            code += QString("    var polyOptions = {\n"
                            "        strokeColor: new Microsoft.Maps.Color(200, %1, %2, %3),\n"
                            "        strokeThickness: 3,\n"
                            "        strokeDashArray: '5 0',\n"
                            "        zIndex: 1\n"
                            "    };\n"
                            "    var polyline = new Microsoft.Maps.Polyline(route, polyOptions);\n"
                            "    polyline.setOptions(polyOptions);\n"
                            "    map.entities.push(polyline);\n"
                            "}\n").arg(color.red())
                                  .arg(color.green())
                                  .arg(color.blue());
            view->page()->mainFrame()->evaluateJavaScript(code);
        }
    }
}

//
// Static helper - havervine formula for calculating the distance
//                 between 2 geo co-ordinates
//
static const double DEG_TO_RAD = 0.017453292519943295769236907684886;
static const double EARTH_RADIUS_IN_METERS = 6372797.560856;
static double ArcInRadians(double fromLat, double fromLon, double toLat, double toLon) {

    double latitudeArc  = (fromLat - toLat) * DEG_TO_RAD;
    double longitudeArc = (fromLon - toLon) * DEG_TO_RAD;
    double latitudeH = sin(latitudeArc * 0.5);
    latitudeH *= latitudeH;
    double lontitudeH = sin(longitudeArc * 0.5);
    lontitudeH *= lontitudeH;
    double tmp = cos(fromLat*DEG_TO_RAD) * cos(toLat*DEG_TO_RAD);
    return 2.0 * asin(sqrt(latitudeH + tmp*lontitudeH));
}

static double distanceBetween(double fromLat, double fromLon, double toLat, double toLon) {
    return EARTH_RADIUS_IN_METERS*ArcInRadians(fromLat, fromLon, toLat, toLon);
}

void
BingMap::createMarkers()
{
    QString code;

    //
    // START / FINISH MARKER
    //
    const QVector<RideFilePoint *> &points = myRideItem->ride()->dataPoints();

    bool loop = distanceBetween(points[0]->lat,
                                points[0]->lon,
                                points[points.count()-1]->lat,
                                points[points.count()-1]->lon) < 100 ? true : false;

    if (loop) {

        code = QString("{ var latlng = new Microsoft.Maps.Location(%1,%2); " 
                   "var pushpinOptions = { icon: 'qrc:images/maps/loop.png', height: 37, width: 32 };"
                   "var pushpin = new Microsoft.Maps.Pushpin(latlng, pushpinOptions);"
                   "map.entities.push(pushpin); }"
                   ).arg(points[0]->lat,0,'g',GPS_COORD_TO_STRING).arg(points[0]->lon,0,'g',GPS_COORD_TO_STRING);

        view->page()->mainFrame()->evaluateJavaScript(code);

    } else {
        // start / finish markers
        code = QString("{ var latlng = new Microsoft.Maps.Location(%1,%2); " 
                   "var pushpinOptions = { icon: 'qrc:images/maps/cycling.png', height: 37, width: 32 };"
                   "var pushpin = new Microsoft.Maps.Pushpin(latlng, pushpinOptions);"
                   "map.entities.push(pushpin); }"
                   ).arg(points[0]->lat,0,'g',GPS_COORD_TO_STRING).arg(points[0]->lon,0,'g',GPS_COORD_TO_STRING);
        view->page()->mainFrame()->evaluateJavaScript(code);

        code = QString("{ var latlng = new Microsoft.Maps.Location(%1,%2); " 
                   "var pushpinOptions = { icon: 'qrc:images/maps/finish.png', height: 37, width: 32 };"
                   "var pushpin = new Microsoft.Maps.Pushpin(latlng, pushpinOptions);"
                   "map.entities.push(pushpin); }"
                   ).arg(points[points.count()-1]->lat,0,'g',GPS_COORD_TO_STRING).arg(points[points.count()-1]->lon,0,'g',GPS_COORD_TO_STRING);
        view->page()->mainFrame()->evaluateJavaScript(code);
    }

    //
    // STOPS - BEER AND BURRITO TIME (> 5 mins in same spot)
    //
    double stoplat=0, stoplon=0;
    double laststoptime=0;
    double lastlat=0, lastlon=0;
    int stoptime=0;
    static const int BEERANDBURRITO = 300; // anything longer than 5 minutes

    foreach(RideFilePoint *rfp, myRideItem->ride()->dataPoints())
    {
        if (!rfp->lat || !rfp->lon) continue; // ignore blank values

        if (!stoplat || !stoplon) { // register first gps co-ord
            stoplat = rfp->lat;
            stoplon = rfp->lon;
            laststoptime = rfp->secs;
        }

        if (distanceBetween(rfp->lat, rfp->lon, stoplat, stoplon) < 20) {
            if (rfp->secs - laststoptime > myRideItem->ride()->recIntSecs()) stoptime += rfp->secs - laststoptime;
            else stoptime += myRideItem->ride()->recIntSecs();
        } else if (rfp->secs - laststoptime > myRideItem->ride()->recIntSecs()) {
            stoptime += rfp->secs - laststoptime;
            stoplat = rfp->lat;
            stoplon = rfp->lon;
        } else {
            stoptime = 0;
            stoplat = rfp->lat;
            stoplon = rfp->lon;
        }

        if (stoptime > BEERANDBURRITO) { // 3 minutes is more than a traffic light stop dude.

            if ((!lastlat && !lastlon) || distanceBetween(lastlat, lastlon, stoplat, stoplon)>100) {
                lastlat = stoplat;
                lastlon = stoplon;
                code = QString("{ var latlng = new Microsoft.Maps.Location(%1,%2); " 
                   "var pushpinOptions = { icon: 'qrc:images/maps/cycling_feed.png', height: 37, width: 32 };"
                   "var pushpin = new Microsoft.Maps.Pushpin(latlng, pushpinOptions);"
                   "map.entities.push(pushpin); }"
                    ).arg(rfp->lat,0,'g',GPS_COORD_TO_STRING).arg(rfp->lon,0,'g',GPS_COORD_TO_STRING);
                view->page()->mainFrame()->evaluateJavaScript(code);
                stoptime=0;
            }
            stoplat=stoplon=stoptime=0;
        }
        laststoptime = rfp->secs;
    }

    //
    // INTERVAL MARKERS
    //
    if (myRideItem->intervals().count() == 0) return;

    int interval=0;
    foreach (IntervalItem *x, myRideItem->intervals()) {

        int offset = myRideItem->ride()->intervalBeginSecs(x->start);
        code = QString("{ var latlng = new Microsoft.Maps.Location(%1,%2);\n" 
                   "var pushpinOptions = { };\n"
                   "var pushpin = new Microsoft.Maps.Pushpin(latlng, pushpinOptions);\n"
                   "map.entities.push(pushpin);\n"
                   "pushpin.cm1001_er_etr.dom.setAttribute('title', '%3');\n"
                   "pushpinClick= Microsoft.Maps.Events.addHandler(pushpin, 'click', function(event) { webBridge.toggleInterval(%4); });\n"
                   " }")
                   .arg(myRideItem->ride()->dataPoints()[offset]->lat,0,'g',GPS_COORD_TO_STRING)
                   .arg(myRideItem->ride()->dataPoints()[offset]->lon,0,'g',GPS_COORD_TO_STRING)
                   .arg(x->name)
                   .arg(interval);

        view->page()->mainFrame()->evaluateJavaScript(code);
        interval++;
    }

    return;
}

void BingMap::zoomInterval(IntervalItem *which)
{
    RideItem *ride = myRideItem;

    // null ride
    if (!ride || !ride->ride()) return;

    int start = ride->ride()->timeIndex(which->start);
    int end = ride->ride()->timeIndex(which->stop);

    // out of bounds
    if (start < 0 || start > ride->ride()->dataPoints().count()-1 ||
        end < 0 || end > ride->ride()->dataPoints().count()-1) return;

    // Get the bounding rectangle for this interval
    double minLat, minLon, maxLat, maxLon;
    minLat = minLon = 1000;
    maxLat = maxLon = -1000; // larger than 360

    // get bounding co-ordinates for ride
    for(int i=start; i<= end; i++) {
        RideFilePoint *rfp = ride->ride()->dataPoints().at(i);
        if (rfp->lat || rfp->lon) {
            minLat = std::min(minLat,rfp->lat);
            maxLat = std::max(maxLat,rfp->lat);
            minLon = std::min(minLon,rfp->lon);
            maxLon = std::max(maxLon,rfp->lon);
        }
    }

    // now zoom to interval
    QString code = QString("{\n"
                           "    var viewBounds = Microsoft.Maps.LocationRect.fromCorners("
                           "                                  new Microsoft.Maps.Location(%1,%2), "
                           "                                  new Microsoft.Maps.Location(%3,%4));\n"
                           "   map.setView( { bounds: viewBounds });\n }")
                    .arg(minLat,0,'g',GPS_COORD_TO_STRING)
                    .arg(minLon,0,'g',GPS_COORD_TO_STRING)
                    .arg(maxLat,0,'g',GPS_COORD_TO_STRING)
                    .arg(maxLon,0,'g',GPS_COORD_TO_STRING);
    view->page()->mainFrame()->evaluateJavaScript(code);
}

// quick diag, used to debug code only
void BWebBridge::call(int count) { qDebug()<<"webBridge call:"<<count; }

// how many intervals are highlighted?
int
BWebBridge::intervalCount()
{
    RideItem *rideItem = gm->property("ride").value<RideItem*>();
    if (rideItem) return rideItem->intervalsSelected().count();
}

// get a latlon array for the i'th selected interval
QVariantList
BWebBridge::getLatLons(int i)
{
    QVariantList latlons;
    RideItem *rideItem = gm->property("ride").value<RideItem*>();

    if (rideItem == NULL) return latlons;

    // valid highlighted interval ?
    if (i < 0 || i > rideItem->intervalsSelected().count()) {

        IntervalItem *current = rideItem->intervalsSelected().at(i);

        // so this one is the interval we need.. lets
        // snaffle up the points in this section
        foreach (RideFilePoint *p1, rideItem->ride()->dataPoints()) {
            if (p1->secs+rideItem->ride()->recIntSecs() > current->start
                && p1->secs< current->stop) {

                if (p1->lat || p1->lon) {
                    latlons << p1->lat;
                    latlons << p1->lon;
                }
            }
        }
        return latlons;

    } else {

        // get latlons for entire route
        foreach (RideFilePoint *p1, rideItem->ride()->dataPoints()) {
            if (p1->lat || p1->lon) {
                latlons << p1->lat;
                latlons << p1->lon;
            }
        }
    }
    return latlons;
}

// once the basic map and route have been marked, overlay markers, shaded areas etc
void
BWebBridge::drawOverlays()
{
    // overlay the markers
    gm->createMarkers();

    // overlay a shaded route
    gm->drawShadedRoute();
}

// interval marker was clicked on the map, toggle its display
void
BWebBridge::toggleInterval(int x)
{
    RideItem *rideItem = gm->property("ride").value<RideItem*>();
    if (x < 0 || rideItem->intervals().count() >= x) return;

//XXX WHEN DECIDED HOW TO SELECT/UNSELECT INTERVALS
#if 0
    IntervalItem *current = dynamic_cast<IntervalItem *>(context->athlete->allIntervalItems()->child(x));
    if (current) current->setSelected(!current->isSelected());
#endif
}
