//
// This file is part of the Marble Virtual Globe.
//
// This program is free software licensed under the GNU LGPL. You can
// find a copy of this license in LICENSE.txt in the top directory of
// the source code.
//
// Copyright 2010      Dennis Nienhüser <earthwings@gentoo.org>
//

#include "RoutingModel.h"

#include "MarbleDebug.h"
#include "MarbleDirs.h"
#include "MarbleMath.h"
#include "GeoDataCoordinates.h"
#include "GeoDataDocument.h"
#include "GeoDataGeometry.h"
#include "GeoDataFolder.h"
#include "GeoDataPlacemark.h"
#include "RouteRequest.h"
#include "PositionTracking.h"
#include "MarbleModel.h"
#include "MarbleWidget.h"
#include "MarbleGlobal.h"
#include "GeoDataExtendedData.h"

#include <QBuffer>
#include <QPointer>
#include <QRegExp>
#include <QVector>
#include <QHash>
#include <QByteArray>
#include <QMessageBox>
#include <QPixmap>
#include <QDomDocument>

namespace Marble
{

class RoutingModelPrivate
{
public:
    enum RouteDeviation
    {
        Unknown,
        OnRoute,
        OffRoute
    };

    RoutingModelPrivate( RouteRequest* request );

    Route m_route;

    RouteDeviation m_deviation;
    PositionTracking* m_positionTracking;
    RouteRequest* const m_request;
    GeoDataCoordinates m_position;
#if QT_VERSION >= 0x050000
    QHash<int, QByteArray> m_roleNames;
#endif

    void importPlacemark( RouteSegment &outline, QVector<RouteSegment> &segments, const GeoDataPlacemark *placemark );

    void updateViaPoints( const GeoDataCoordinates &position );
};

RoutingModelPrivate::RoutingModelPrivate( RouteRequest* request )
    : m_deviation( Unknown ),
      m_positionTracking( 0 ),
      m_request( request )
{
    // nothing to do
}

void RoutingModelPrivate::updateViaPoints( const GeoDataCoordinates &position )
{
    // Mark via points visited after approaching them in a range of 500m or less
    qreal const threshold = 500 / EARTH_RADIUS;
    for( int i=0; i<m_request->size(); ++i ) {
        if ( !m_request->visited( i ) ) {
            if ( distanceSphere( position, m_request->at( i ) ) < threshold ) {
                m_request->setVisited( i, true );
            }
        }
    }
}

void RoutingModelPrivate::importPlacemark( RouteSegment &outline, QVector<RouteSegment> &segments, const GeoDataPlacemark *placemark )
{
    const GeoDataGeometry* geometry = placemark->geometry();
    const GeoDataLineString* lineString = dynamic_cast<const GeoDataLineString*>( geometry );
    QStringList blacklist = QStringList() << "" << "Route" << "Tessellated";
    RouteSegment segment;
    bool isOutline = true;
    if ( !blacklist.contains( placemark->name() ) ) {
        if( lineString ) {
            Maneuver maneuver;
            maneuver.setInstructionText( placemark->name() );
            maneuver.setPosition( lineString->at( 0 ) );

            if ( placemark->extendedData().contains( "turnType" ) ) {
                QVariant turnType = placemark->extendedData().value( "turnType" ).value();
                // The enum value is converted to/from an int in the QVariant
                // because only a limited set of data types can be serialized with QVariant's
                // toString() method (which is used to serialize <ExtendedData>/<Data> values)
                maneuver.setDirection( Maneuver::Direction( turnType.toInt() ) );
            }

            if ( placemark->extendedData().contains( "roadName" ) ) {
                QVariant roadName = placemark->extendedData().value( "roadName" ).value();
                maneuver.setRoadName( roadName.toString() );
            }

            segment.setManeuver( maneuver );
            isOutline = false;
        }
    }

    if ( lineString ) {
        segment.setPath( *lineString );

        if ( isOutline ) {
            outline = segment;
        } else {
            segments.push_back( segment );
        }
    }
}

RoutingModel::RoutingModel( RouteRequest* request, MarbleModel *model, QObject *parent ) :
        QAbstractListModel( parent ), d( new RoutingModelPrivate( request ) )
{
   if( model )
    {
        d->m_positionTracking = model->positionTracking();
        QObject::connect( d->m_positionTracking, SIGNAL(gpsLocation(GeoDataCoordinates,qreal)),
                 this, SLOT(updatePosition(GeoDataCoordinates,qreal)) );
    }

   QHash<int, QByteArray> roles;
   roles.insert( Qt::DisplayRole, "display" );
   roles.insert( RoutingModel::TurnTypeIconRole, "turnTypeIcon" );
   roles.insert( RoutingModel::LongitudeRole, "longitude" );
   roles.insert( RoutingModel::LatitudeRole, "latitude" );
#if QT_VERSION < 0x050000
   setRoleNames( roles );
#else
   d->m_roleNames = roles;
#endif
}

RoutingModel::~RoutingModel()
{
    delete d;
}

int RoutingModel::rowCount ( const QModelIndex &parent ) const
{
    return parent.isValid() ? 0 : d->m_route.turnPoints().size();
}

QVariant RoutingModel::headerData ( int section, Qt::Orientation orientation, int role ) const
{
    if ( orientation == Qt::Horizontal && role == Qt::DisplayRole && section == 0 ) {
        return QString( "Instruction" );
    }

    return QAbstractListModel::headerData( section, orientation, role );
}

QVariant RoutingModel::data ( const QModelIndex & index, int role ) const
{
    if ( !index.isValid() ) {
        return QVariant();
    }

    if ( index.row() < d->m_route.turnPoints().size() && index.column() == 0 ) {
        const RouteSegment &segment = d->m_route.at( index.row() );
        switch ( role ) {
        case Qt::DisplayRole:
        case Qt::ToolTipRole:
            return segment.maneuver().instructionText();
            break;
        case Qt::DecorationRole:
            {
                bool const smallScreen = MarbleGlobal::getInstance()->profiles() & MarbleGlobal::SmallScreen;
                if ( segment.maneuver().hasWaypoint() ) {
                    int const size = smallScreen ? 64 : 32;
                    return d->m_request->pixmap( segment.maneuver().waypointIndex(), size, size/4 );
                } else {
                    QPixmap const pixmap = segment.maneuver().directionPixmap();
                    return smallScreen ? pixmap : pixmap.scaled( 32, 32 );
                }
            }
            break;
        case Qt::SizeHintRole:
            {
                bool const smallScreen = MarbleGlobal::getInstance()->profiles() & MarbleGlobal::SmallScreen;
                int const size = smallScreen ? 64 : 32;
                return QSize( size, size );
            }
            break;
        case RoutingModel::CoordinateRole:
            return QVariant::fromValue( segment.maneuver().position() );
            break;
        case RoutingModel::LongitudeRole:
            return QVariant::fromValue( segment.maneuver().position().longitude( GeoDataCoordinates::Degree ) );
            break;
        case RoutingModel::LatitudeRole:
            return QVariant::fromValue( segment.maneuver().position().latitude( GeoDataCoordinates::Degree ) );
            break;
        case RoutingModel::TurnTypeIconRole:
            return segment.maneuver().directionPixmap();
            break;
        default:
            return QVariant();
        }
    }

    return QVariant();
}

#if QT_VERSION >= 0x050000
QHash<int, QByteArray> RoutingModel::roleNames() const
{
    return d->m_roleNames;
}
#endif

bool RoutingModel::setCurrentRoute( GeoDataDocument* document )
{
    d->m_route = Route();
    QVector<RouteSegment> segments;
    RouteSegment outline;

    QVector<GeoDataFolder*> folders = document->folderList();
    foreach( const GeoDataFolder *folder, folders ) {
        foreach( const GeoDataPlacemark *placemark, folder->placemarkList() ) {
            d->importPlacemark( outline, segments, placemark );
        }
    }

    foreach( const GeoDataPlacemark *placemark, document->placemarkList() ) {
        d->importPlacemark( outline, segments, placemark );
    }

    if ( segments.isEmpty() ) {
        segments << outline;
    }

    // Map via points onto segments
    if ( d->m_request->size() > 1 && segments.size() > 1 ) {
        int index = 0;
        for ( int j=0; j<d->m_request->size(); ++j ) {
            QPair<int, qreal> minimum( -1, -1.0 );
            int viaIndex = -1;
            for ( int i=index; i<segments.size(); ++i ) {
                RouteSegment const & segment = segments[i];
                GeoDataCoordinates closest;
                qreal const distance = segment.distanceTo( d->m_request->at( j ), closest, closest );
                if ( minimum.first < 0 || distance < minimum.second ) {
                    minimum.first = i;
                    minimum.second = distance;
                    viaIndex = j;
                }
            }

            if ( minimum.first >= 0 ) {
                index = minimum.first;
                Maneuver viaPoint = segments[ minimum.first ].maneuver();
                viaPoint.setWaypoint( d->m_request->at( viaIndex ), viaIndex );
                segments[ minimum.first ].setManeuver( viaPoint );
            }
        }
    }

    if ( segments.size() > 0 ) {
        foreach( const RouteSegment &segment, segments ) {
            d->m_route.addRouteSegment( segment );
        }
    }

    d->m_deviation = RoutingModelPrivate::Unknown;

    beginResetModel();
    endResetModel();
    emit currentRouteChanged();
    return true;
}

void RoutingModel::exportGpx( QIODevice *device ) const
{
    QString content( "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\" ?>\n" );
    content += "<gpx xmlns=\"http://www.topografix.com/GPX/1/1\" creator=\"Marble\" version=\"1.1\" ";
    content += "xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" ";
    content += "xsi:schemaLocation=\"http://www.topografix.com/GPX/1/1 ";
    content += "http://www.topografix.com/GPX/1/1/gpx.xsd\">\n";
    content += "<metadata>\n  <link href=\"http://edu.kde.org/marble\">\n    ";
    content += "<text>Marble Virtual Globe</text>\n  </link>\n</metadata>\n";

    content += "  <rte>\n    <name>Route</name>\n";
    bool hasAltitude = false;
    for ( int i=0; !hasAltitude && i<d->m_route.size(); ++i ) {
        hasAltitude = d->m_route.at( i ).maneuver().position().altitude() != 0.0;
    }
    for ( int i=0; i<d->m_route.size(); ++i ) {
        const Maneuver &maneuver = d->m_route.at( i ).maneuver();
        qreal lon = maneuver.position().longitude( GeoDataCoordinates::Degree );
        qreal lat = maneuver.position().latitude( GeoDataCoordinates::Degree );
        QString const text = maneuver.instructionText();
        content += QString( "    <rtept lat=\"%1\" lon=\"%2\">\n" ).arg( lat, 0, 'f', 7 ).arg( lon, 0, 'f', 7 );
        content += QString( "        <name>%1</name>\n").arg( text );
        if ( hasAltitude ) {
            content += QString( "        <ele>%1</ele>\n" ).arg( maneuver.position().altitude(), 0, 'f', 2 );
        }
        content += QString( "    </rtept>\n" );
    }
    content += "  </rte>\n";

    content += "<trk>\n  <name>Route</name>\n    <trkseg>\n";
    GeoDataLineString points = d->m_route.path();
    hasAltitude = false;
    for ( int i=0; !hasAltitude && i<points.size(); ++i ) {
        hasAltitude = points[i].altitude() != 0.0;
    }
    for ( int i=0; i<points.size(); ++i ) {
        GeoDataCoordinates const &point = points[i];
        qreal lon = point.longitude( GeoDataCoordinates::Degree );
        qreal lat = point.latitude( GeoDataCoordinates::Degree );
        content += QString( "      <trkpt lat=\"%1\" lon=\"%2\">\n" ).arg( lat, 0, 'f', 7 ).arg( lon, 0, 'f', 7 );
        if ( hasAltitude ) {
            content += QString( "        <ele>%1</ele>\n" ).arg( point.altitude(), 0, 'f', 2 );
        }
        content += QString( "      </trkpt>\n" );
    }
    content += "    </trkseg>\n  </trk>\n";
    content += "</gpx>\n";

    device->write( content.toUtf8() );
}

void RoutingModel::clear()
{
    d->m_route = Route();
    beginResetModel();
    endResetModel();
    emit currentRouteChanged();
}

int RoutingModel::rightNeighbor( const GeoDataCoordinates &position, RouteRequest const *const route ) const
{
    Q_ASSERT( route && "Must not pass a null route ");

    // Quick result for trivial cases
    if ( route->size() < 3 ) {
        return route->size() - 1;
    }

    // Generate an ordered list of all waypoints
    GeoDataLineString points = d->m_route.path();
    QMap<int,int> mapping;

    // Force first mapping point to match the route start
    mapping[0] = 0;

    // Calculate the mapping between waypoints and via points
    // Need two for loops to avoid getting stuck in local minima
    for ( int j=1; j<route->size()-1; ++j ) {
        qreal minDistance = -1.0;
        for ( int i=mapping[j-1]; i<points.size(); ++i ) {
            qreal distance = distanceSphere( points[i], route->at(j) );
            if (minDistance < 0.0 || distance < minDistance ) {
                mapping[j] = i;
                minDistance = distance;
            }
        }
    }

    // Determine waypoint with minimum distance to the provided position
    qreal minWaypointDistance = -1.0;
    int waypoint=0;
    for ( int i=0; i<points.size(); ++i ) {
        qreal waypointDistance = distanceSphere( points[i], position );
        if ( minWaypointDistance < 0.0 || waypointDistance < minWaypointDistance ) {
            minWaypointDistance = waypointDistance;
            waypoint = i;
        }
    }

    // Force last mapping point to match the route destination
    mapping[route->size()-1] = points.size()-1;

    // Determine neighbor based on the mapping
    QMap<int, int>::const_iterator iter = mapping.constBegin();
    for ( ; iter != mapping.constEnd(); ++iter ) {
        if ( iter.value() > waypoint ) {
            int index = iter.key();
            Q_ASSERT( index >= 0 && index <= route->size() );
            return index;
        }
    }

    return route->size()-1;
}

void RoutingModel::updatePosition( GeoDataCoordinates location, qreal /*speed*/ )
{
    d->m_position = location;
    d->m_route.setPosition( location );

    d->updateViaPoints( d->m_position );
    qreal distance = EARTH_RADIUS * distanceSphere( location, d->m_route.positionOnRoute() );
    emit positionChanged();

    qreal deviation = 0.0;
    if ( d->m_positionTracking && d->m_positionTracking->accuracy().vertical > 0.0 ) {
        deviation = qMax<qreal>( d->m_positionTracking->accuracy().vertical, d->m_positionTracking->accuracy().horizontal );
    }
    qreal const threshold = deviation + 100.0;

    RoutingModelPrivate::RouteDeviation const deviated = distance < threshold ? RoutingModelPrivate::OnRoute : RoutingModelPrivate::OffRoute;
    if ( d->m_deviation != deviated ) {
        d->m_deviation = deviated;
        emit deviatedFromRoute( deviated == RoutingModelPrivate::OffRoute );
    }
}

bool RoutingModel::deviatedFromRoute() const
{
    return d->m_deviation == RoutingModelPrivate::OffRoute;
}

const Route & RoutingModel::route() const
{
    return d->m_route;
}

} // namespace Marble

#include "RoutingModel.moc"
