/***************************************************************************
 *   Copyright (C) 2010 by Volker Lanz <vl@fidra.de>                       *
 *                                                                         *
 *   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 "core/track.h"
#include "core/trackpoint.h"
#include "core/lap.h"

#include "veneer/settings.h"

#include "util/gpsutil.h"

#include <QSqlQuery>
#include <QVariant>
#include <QSqlError>
#include <QDebug>

Track::Track() :
	DbItem("track", QStringList() << "id" << "lap"),
	m_Lap(NULL),
	m_TrackPointsLoaded(false)
{
}

Track::Track(Lap* lap, const QSqlQuery& query) :
	DbItem(query.value(0).toULongLong(), "track", QStringList() << "id" << "lap"),
	m_Lap(lap),
	m_TrackPointsLoaded(false)
{
}

Track::~Track()
{
	clear();
}

void Track::clear()
{
	qDeleteAll(m_TrackPoints);
	m_TrackPoints.clear();
}

bool Track::serialize()
{
	Q_ASSERT(m_Lap);

	if (m_Lap == NULL)
	{
		qWarning() << "attempt to serialize a track without a lap";
		return false;
	}

	if (isRemoved())
		return remove();

	if (!DbItem::serialize(QVariantList() << id() << m_Lap->id()))
		return false;

	for  (TrackPoints::iterator i = m_TrackPoints.begin(); i != m_TrackPoints.end(); /* NOP */)
	{
		if ((*i)->track() != this)
			(*i)->setTrack(this);

		if (!(*i)->serialize())
			return false;

		if ((*i)->isRemoved())
		{
			delete *i;
			i = m_TrackPoints.erase(i);
		}
		else
			i++;
	}

	return true;
}

bool Track::list(Lap* lap, Tracks& result)
{
	QSqlQuery query;

	if (!query.prepare("SELECT " + Track().columnNames().join(",") + " FROM " + Track().tableName() + " WHERE lap=?"))
	{
		qWarning() << "prepare failed : " << query.lastError().text();
		return false;
	}

	query.addBindValue(lap->id());

	if (!query.exec())
	{
		qWarning() << "exec failed : " << query.lastError().text();
		return false;
	}

	while (query.next())
		result.append(new Track(lap, query));

	return true;
}

const TrackPoints& Track::trackPoints() const
{
	if (!m_TrackPointsLoaded)
	{
		TrackPoint::list(const_cast<Track*>(this), m_TrackPoints);
		m_TrackPointsLoaded = true;
	}

	return m_TrackPoints;
}

void Track::addTrackPoint(TrackPoint* tp)
{
	if (!m_TrackPointsLoaded)
	{
		TrackPoint::list(this, m_TrackPoints);
		m_TrackPointsLoaded = true;
	}

	m_TrackPoints.append(tp);
	tp->setTrack(this);
}

quint32 Track::calcTotalAscent() const
{
	if (m_TrackPoints.size() == 0)
		return 0;

	quint32 result = 0;

	TrackPoints::const_iterator cur = m_TrackPoints.constBegin();

	// find the first valid position
	while(!(*cur)->position().isValid())
		if (++cur == m_TrackPoints.constEnd())
			return 0;

	TrackPoints::const_iterator prev = cur;
	cur++;

	while (cur != m_TrackPoints.constEnd())
	{
		if ((*cur)->position().isValid() && (*prev)->position().isValid())
		{
			qint32 dist = (*prev)->position().distanceTo((*cur)->position());

			if (dist > Veneer::Settings::self()->heightCalcThreshold() && (*cur)->altitude() > (*prev)->altitude())
			{
				result += (*cur)->altitude() - (*prev)->altitude();
				prev = cur;
			}
		}

		cur++;
	}

	return result;
}

quint32 Track::calcTotalDescent() const
{
	if (m_TrackPoints.size() == 0)
		return 0;

	quint32 result = 0;

	TrackPoints::const_iterator cur = m_TrackPoints.constBegin();

	// find the first valid position
	while(!(*cur)->position().isValid())
		if (++cur == m_TrackPoints.constEnd())
			return 0;

	TrackPoints::const_iterator prev = cur;
	cur++;

	while (cur != m_TrackPoints.constEnd())
	{
		if ((*cur)->position().isValid() && (*prev)->position().isValid())
		{
			qint32 dist = (*prev)->position().distanceTo((*cur)->position());

			if (dist > Veneer::Settings::self()->heightCalcThreshold() && (*prev)->altitude() > (*cur)->altitude())
			{
				result += (*prev)->altitude() - (*cur)->altitude();
				prev = cur;
			}
		}

		cur++;
	}

	return result;
}

quint32 Track::calcMaxSpeed() const
{
	if (m_TrackPoints.size() == 0)
		return 0;

	// Put all speeds in a list, sort the list in descending order and throw away
	// the top n elements, assuming they are artifacts.
	// Make sure not to take into account trackpoints without GPS information.
	QList<quint32> result;

	const TrackPoint* prev = NULL;
	foreach(const TrackPoint* tp, m_TrackPoints)
	{
		if (tp->position().isValid() && prev && prev->position().isValid())
		{
			const qint32 secsTo = prev->timeStamp().secsTo(tp->timeStamp());
			const qint32 distance = tp->position().distanceTo(prev->position());;
			const quint32 speed = secsTo > 0 ? distance / secsTo * 3.6 : 0;
			result.append(speed);
		}

		prev = tp;
	}

	qSort(result.begin(), result.end(), qGreater<quint32>());

	const qint32 cutoff = 0.012 * result.size();

	const quint32 rval = cutoff < result.size() ? result[cutoff] : 0;

	// 	qDebug() << "total size: " << m_TrackPoints.size() << ", cutoff point: " << cutoff << ", return val: " << rval << ", values cut: " << result.mid(0, cutoff);

	return rval;
}

quint32 Track::calcAverageCadence() const
{
	if (m_TrackPoints.size() == 0)
		return 0;

	quint64 sumCadence = 0;
	quint64 count = 0;

	foreach(const TrackPoint* tp, m_TrackPoints)
		if (tp->cadence() > 0)
		{
			sumCadence += tp->cadence();
			count++;
		}

	return count == 0 ? 0 : sumCadence / count;
}

bool Track::calcHeartRates(quint32& avg, quint32& max) const
{
	quint64 sumHeartRate = 0;
	quint64 count = 0;

	if (m_TrackPoints.size() == 0)
		return false;

	max = 0;

	foreach(const TrackPoint* tp, m_TrackPoints)
	{
		if (tp->heartRate() > 0)
		{
			sumHeartRate += tp->heartRate();
			count++;
		}

		if (tp->heartRate() > max)
			max = tp->heartRate();
	}

	avg = (count == 0) ? 0 : sumHeartRate / count;;

	return true;
}

quint32 Track::calcDistance() const
{
	quint32 trackDistance = 0;
	const TrackPoint* prev = NULL;

	foreach(const TrackPoint* tp, m_TrackPoints)
	{
		if (tp->position().isValid() && prev && prev->position().isValid())
			trackDistance += tp->position().distanceTo(prev->position());

		prev = tp;
	}

	return trackDistance;
}

quint32 Track::calcDuration() const
{
	if (m_TrackPoints.size() == 0)
		return 0;

	QDateTime start = m_TrackPoints.first()->timeStamp();
	QDateTime end = m_TrackPoints.last()->timeStamp();

	return start.secsTo(end) * 1000;
}

QDateTime Track::calcStartTime() const
{
	if (m_TrackPoints.size() == 0)
		return QDateTime();

	return m_TrackPoints.first()->timeStamp();
}

void Track::calcTrackPointSpeed()
{
	const TrackPoint* prev = NULL;

	foreach(TrackPoint* tp, m_TrackPoints)
	{
		if (tp->position().isValid() && prev && prev->position().isValid())
		{
			const qint32 secsTo = prev->timeStamp().secsTo(tp->timeStamp());
			const qint32 distance = tp->position().distanceTo(prev->position());;
			const quint32 speed = secsTo > 0 ? distance / secsTo * 3.6 : 0;
			tp->setSpeed(speed);
		}

		prev = tp;
	}
}

bool Track::calcBoundaries(double& minLat, double& minLon, double& maxLat, double& maxLon) const
{
	minLat = Position::Invalid;
	minLon = Position::Invalid;
	maxLat = Position::Invalid;
	maxLon = Position::Invalid;

	foreach(const TrackPoint* tp, m_TrackPoints)
	{
		if (!tp->position().isValid())
			continue;

		if (tp->position().latitude() < minLat || minLat == Position::Invalid)
			minLat = tp->position().latitude();

		if (tp->position().latitude() > maxLat || maxLat == Position::Invalid)
			maxLat = tp->position().latitude();

		if (tp->position().longitude() < minLon || minLon == Position::Invalid)
			minLon = tp->position().longitude();

		if (tp->position().longitude() > maxLon || maxLon == Position::Invalid)
			maxLon = tp->position().longitude();
	}

	return minLat != Position::Invalid && minLon != Position::Invalid && maxLat != Position::Invalid && maxLon != Position::Invalid;
}
