/* ============================================================
 *
 * This file is a part of kipi-plugins project
 * http://www.kipi-plugins.org
 *
 * Date        : 2006-09-19
 * Description : GPS data file parser.
 *               (GPX format http://www.topografix.com/gpx.asp).
 *
 * Copyright (C) 2006-2008 by Gilles Caulier <caulier dot gilles at gmail dot 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, 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.
 *
 * ============================================================ */

// C++ includes.

#include <cmath>
#include <cstdlib>

// Qt includes.

#include <qstring.h>
#include <qstringlist.h>
#include <qfile.h>
#include <qdom.h>
#include <qtextstream.h>

// KDE includes.

#include <kdebug.h>

// Local includes.

#include "gpsdataparser.h"

namespace KIPIGPSSyncPlugin
{

GPSDataParser::GPSDataParser()
{
    clear();
}

void GPSDataParser::clear()
{
    m_GPSDataMap.clear();
}

int GPSDataParser::numPoints()
{
    return m_GPSDataMap.count();
}

bool GPSDataParser::matchDate(const QDateTime& photoDateTime, int maxGapTime, int timeZone,
                              bool interpolate, int interpolationDstTime,
                              GPSDataContainer& gpsData)
{
    // GPS device are sync in time by satelite using GMT time.
    // If the camera time is different than GMT time, we need to convert it to GMT time
    // Using the time zone.
    QDateTime cameraGMTDateTime = photoDateTime.addSecs(timeZone*(-1));

    kdDebug() << "cameraGMTDateTime: " << cameraGMTDateTime << endl;

    // We trying to find the right date in the GPS points list.
    bool findItem = false;
    int nbSecItem = maxGapTime;
    int nbSecs;

    for (GPSDataMap::Iterator it = m_GPSDataMap.begin();
         it != m_GPSDataMap.end(); ++it )
    {
        // Here we check a possible accuracy in seconds between the
        // Camera GMT time and the GPS device GMT time.

        nbSecs = abs(cameraGMTDateTime.secsTo( it.key() ));

        // We tring to find the minimal accuracy.
        if( nbSecs < maxGapTime && nbSecs < nbSecItem)
        {
            gpsData   = m_GPSDataMap[it.key()];
            findItem  = true;
            nbSecItem = nbSecs;
        }
    }

    if (findItem) return true;

    // If we can't find it, we will trying to interpolate the GPS point.

    if (interpolate)
    {
        // The interpolate GPS point will be separate by at the maximum of 'interpolationDstTime'
        // seconds before and after the next and previous real GPS point found.

        QDateTime prevDateTime = findPrevDate(cameraGMTDateTime, interpolationDstTime);
        QDateTime nextDateTime = findNextDate(cameraGMTDateTime, interpolationDstTime);

        if (!nextDateTime.isNull() && !prevDateTime.isNull())
        {
            GPSDataContainer prevGPSPoint = m_GPSDataMap[prevDateTime];
            GPSDataContainer nextGPSPoint = m_GPSDataMap[nextDateTime];

            double alt1 = prevGPSPoint.altitude();
            double lon1 = prevGPSPoint.longitude();
            double lat1 = prevGPSPoint.latitude();
            uint   t1   = prevDateTime.toTime_t();
            double alt2 = nextGPSPoint.altitude();
            double lon2 = nextGPSPoint.longitude();
            double lat2 = nextGPSPoint.latitude();
            uint   t2   = nextDateTime.toTime_t();
            uint   tCor = cameraGMTDateTime.toTime_t();

            if (tCor-t1 != 0)
            {
                gpsData.setAltitude(alt1  + (alt2-alt1) * (tCor-t1)/(t2-t1));
                gpsData.setLatitude(lat1  + (lat2-lat1) * (tCor-t1)/(t2-t1));
                gpsData.setLongitude(lon1 + (lon2-lon1) * (tCor-t1)/(t2-t1));
                gpsData.setInterpolated(true);
                return true;
            }
        }
    }

    return false;
}

QDateTime GPSDataParser::findNextDate(const QDateTime& dateTime, int secs)
{
    // We will find the item in GPS data list where the time is
    // at the maximum bigger than 'secs' mn of the value to match.
    QDateTime itemFound = dateTime.addSecs(secs);
    bool found = false;

    for (GPSDataMap::Iterator it = m_GPSDataMap.begin();
        it != m_GPSDataMap.end(); ++it )
    {
        if (it.key() > dateTime)
        {
            if (it.key() < itemFound)
            {
                itemFound = it.key();
                found = true;
            }
        }
    }

    if (found)
        return itemFound;

    return QDateTime();
}

QDateTime GPSDataParser::findPrevDate(const QDateTime& dateTime, int secs)
{
    // We will find the item in GPS data list where the time is
    // at the maximum smaller than 'secs' mn of the value to match.
    QDateTime itemFound = dateTime.addSecs((-1)*secs);
    bool found = false;

    for (GPSDataMap::Iterator it = m_GPSDataMap.begin();
        it != m_GPSDataMap.end(); ++it )
    {
        if (it.key() < dateTime)
        {
            if (it.key() > itemFound)
            {
                itemFound = it.key();
                found = true;
            }
        }
    }

    if (found)
        return itemFound;

    return QDateTime();
}

bool GPSDataParser::loadGPXFile(const KURL& url)
{
    QFile gpxfile(url.path());

    if (!gpxfile.open(IO_ReadOnly))
        return false;

    QDomDocument gpxDoc("gpx");
    if (!gpxDoc.setContent(&gpxfile))
        return false;

    QDomElement gpxDocElem = gpxDoc.documentElement();
    if (gpxDocElem.tagName()!="gpx")
        return false;

    for (QDomNode nTrk = gpxDocElem.firstChild();
         !nTrk.isNull(); nTrk = nTrk.nextSibling())
    {
        QDomElement trkElem = nTrk.toElement();
        if (trkElem.isNull()) continue;
        if (trkElem.tagName() != "trk") continue;

        for (QDomNode nTrkseg = trkElem.firstChild();
            !nTrkseg.isNull(); nTrkseg = nTrkseg.nextSibling())
        {
            QDomElement trksegElem = nTrkseg.toElement();
            if (trksegElem.isNull()) continue;
            if (trksegElem.tagName() != "trkseg") continue;

            for (QDomNode nTrkpt = trksegElem.firstChild();
                !nTrkpt.isNull(); nTrkpt = nTrkpt.nextSibling())
            {
                QDomElement trkptElem = nTrkpt.toElement();
                if (trkptElem.isNull()) continue;
                if (trkptElem.tagName() != "trkpt") continue;

                QDateTime ptDateTime;
                double    ptAltitude  = 0.0;
                double    ptLatitude  = 0.0;
                double    ptLongitude = 0.0;

                // Get GPS position. If not available continue to next point.
                QString lat = trkptElem.attribute("lat");
                QString lon = trkptElem.attribute("lon");
                if (lat.isEmpty() || lon.isEmpty()) continue;

                ptLatitude  = lat.toDouble();
                ptLongitude = lon.toDouble();

                // Get metadata of track point (altitude and time stamp)
                for (QDomNode nTrkptMeta = trkptElem.firstChild();
                    !nTrkptMeta.isNull(); nTrkptMeta = nTrkptMeta.nextSibling())
                {
                    QDomElement trkptMetaElem = nTrkptMeta.toElement();
                    if (trkptMetaElem.isNull()) continue;
                    if (trkptMetaElem.tagName() == QString("time"))
                    {
                        // Get GPS point time stamp. If not available continue to next point.
                        QString time = trkptMetaElem.text();
                        if (time.isEmpty()) continue;
                        ptDateTime = QDateTime::fromString(time, Qt::ISODate);
                    }
                    if (trkptMetaElem.tagName() == QString("ele"))
                    {
                        // Get GPS point altitude. If not available continue to next point.
                        QString ele = trkptMetaElem.text();
                        if (!ele.isEmpty())
                            ptAltitude  = ele.toDouble();
                    }
                }

                if (ptDateTime.isNull())
                    continue;

                GPSDataContainer gpsData(ptAltitude, ptLatitude, ptLongitude, false);
                m_GPSDataMap.insert( ptDateTime, gpsData );
            }
        }
    }

    kdDebug( 51001 ) << "GPX File " << url.fileName()
                     << " parsed with " << numPoints()
                     << " points extracted" << endl;
    return true;
}

} // NameSpace KIPIGPSSyncPlugin
