/* ============================================================
 *
 * This file is a part of digiKam project
 * https://www.digikam.org
 *
 * Date        : 2023-05-15
 * Description : geolocation engine based on Marble.
 *               (c) 2007-2022 Marble Team
 *               https://invent.kde.org/education/marble/-/raw/master/data/credits_authors.html
 *
 * SPDX-FileCopyrightText: 2023-2024 by Gilles Caulier <caulier dot gilles at gmail dot com>
 *
 * SPDX-License-Identifier: LGPL-2.1-or-later
 *
 * ============================================================ */

#include "GeoDataTrack.h"
#include "GeoDataGeometry_p.h"

// C++ includes

#include <iterator>

// Qt includes

#include <QMap>
#include <QDateTime>

// Local includes

#include "GeoDataLatLonAltBox.h"
#include "GeoDataTypes.h"
#include "GeoDataLineString.h"
#include "GeoDataExtendedData.h"
#include "digikam_debug.h"

namespace Marble
{

class Q_DECL_HIDDEN GeoDataTrackPrivate : public GeoDataGeometryPrivate
{
public:

    GeoDataTrackPrivate()
        : m_lineStringNeedsUpdate(false),
          m_interpolate(false)
    {
    }

    GeoDataGeometryPrivate* copy() const override
    {
        return new GeoDataTrackPrivate(*this);
    }

    void equalizeWhenSize()
    {
        m_when.reserve(m_coordinates.size());

        while (m_when.size() < m_coordinates.size())
        {
            //fill coordinates without time information with null QDateTime
            m_when.append(QDateTime());
        }
    }

    mutable GeoDataLineString m_lineString;
    mutable bool m_lineStringNeedsUpdate;

    bool m_interpolate;

    QVector<QDateTime> m_when;
    QVector<GeoDataCoordinates> m_coordinates;

    GeoDataExtendedData m_extendedData;
};

GeoDataTrack::GeoDataTrack()
    : GeoDataGeometry(new GeoDataTrackPrivate())
{

}

GeoDataTrack::GeoDataTrack(const GeoDataTrack& other)
    : GeoDataGeometry(other)
{

}

GeoDataTrack& GeoDataTrack::operator=(const GeoDataTrack& other)
{
    GeoDataGeometry::operator=(other);

    return *this;
}

const char* GeoDataTrack::nodeType() const
{
    return GeoDataTypes::GeoDataTrackType;
}

EnumGeometryId GeoDataTrack::geometryId() const
{
    return GeoDataTrackId;
}

GeoDataGeometry* GeoDataTrack::copy() const
{
    return new GeoDataTrack(*this);
}

bool GeoDataTrack::operator==(const GeoDataTrack& other) const
{
    Q_D(const GeoDataTrack);
    const GeoDataTrackPrivate* const otherD = other.d_func();

    return equals(other) &&
           d->m_when == otherD->m_when &&
           d->m_coordinates == otherD->m_coordinates &&
           d->m_extendedData == otherD->m_extendedData &&
           d->m_interpolate == otherD->m_interpolate;
}

bool GeoDataTrack::operator!=(const GeoDataTrack& other) const
{
    return !this->operator==(other);
}

int GeoDataTrack::size() const
{
    Q_D(const GeoDataTrack);
    return d->m_coordinates.size();
}

bool GeoDataTrack::interpolate() const
{
    Q_D(const GeoDataTrack);
    return d->m_interpolate;
}

void GeoDataTrack::setInterpolate(bool on)
{
    detach();

    Q_D(GeoDataTrack);
    d->m_interpolate = on;
}

QDateTime GeoDataTrack::firstWhen() const
{
    Q_D(const GeoDataTrack);

    if (d->m_when.isEmpty())
    {
        return QDateTime();
    }

    return d->m_when.first();
}

QDateTime GeoDataTrack::lastWhen() const
{
    Q_D(const GeoDataTrack);

    if (d->m_when.isEmpty())
    {
        return QDateTime();
    }

    return d->m_when.last();
}

QVector<GeoDataCoordinates> GeoDataTrack::coordinatesList() const
{
    Q_D(const GeoDataTrack);
    return d->m_coordinates;
}

QVector<QDateTime> GeoDataTrack::whenList() const
{
    Q_D(const GeoDataTrack);
    return d->m_when;
}

GeoDataCoordinates GeoDataTrack::coordinatesAt(const QDateTime& when) const
{
    Q_D(const GeoDataTrack);

    if (d->m_when.isEmpty())
    {
        return GeoDataCoordinates();
    }

    if (d->m_when.contains(when))
    {
        //exact match found
        const int index = d->m_when.indexOf(when);

        if (index < d->m_coordinates.size())
        {
            return d->m_coordinates.at(index);
        }
    }

    if (!interpolate())
    {
        return GeoDataCoordinates();
    }

    typedef QMap<QDateTime, GeoDataCoordinates> PointMap;
    PointMap pointMap;

    for (int i = 0; i < qMin(d->m_when.size(), d->m_coordinates.size()); ++i)
    {
        if (d->m_when.at(i).isValid())
        {
            pointMap[d->m_when.at(i)] = d->m_coordinates.at(i);
        }
    }

    QMap<QDateTime, GeoDataCoordinates>::const_iterator nextEntry = const_cast<const PointMap&>(pointMap).upperBound(when);

    // No tracked point happened before "when"
    if (nextEntry == pointMap.constBegin())
    {
        qCDebug(DIGIKAM_MARBLE_LOG) << "No tracked point before " << when;
        return GeoDataCoordinates();
    }

    if (nextEntry == pointMap.constEnd())
    {
        qCDebug(DIGIKAM_MARBLE_LOG) << "No track point after" << when;
        return GeoDataCoordinates();
    }

    QMap<QDateTime, GeoDataCoordinates>::const_iterator previousEntry = std::prev(nextEntry, 1);
    GeoDataCoordinates previousCoord = previousEntry.value();

    QDateTime previousWhen = previousEntry.key();
    QDateTime nextWhen = nextEntry.key();
    GeoDataCoordinates nextCoord = nextEntry.value();

    int interval = previousWhen.msecsTo(nextWhen);
    int position = previousWhen.msecsTo(when);
    qreal t = (qreal)position / (qreal)interval;

    return previousCoord.interpolate(nextCoord, t);
}

GeoDataCoordinates GeoDataTrack::coordinatesAt(int index) const
{
    Q_D(const GeoDataTrack);
    return d->m_coordinates.at(index);
}

void GeoDataTrack::addPoint(const QDateTime& when, const GeoDataCoordinates& coord)
{
    detach();

    Q_D(GeoDataTrack);
    d->equalizeWhenSize();
    d->m_lineStringNeedsUpdate = true;
    int i = 0;

    while (i < d->m_when.size())
    {
        if (d->m_when.at(i) > when)
        {
            break;
        }

        ++i;
    }

    d->m_when.insert(i, when);
    d->m_coordinates.insert(i, coord);
}

void GeoDataTrack::appendCoordinates(const GeoDataCoordinates& coord)
{
    detach();

    Q_D(GeoDataTrack);
    d->equalizeWhenSize();
    d->m_lineStringNeedsUpdate = true;
    d->m_coordinates.append(coord);
}

void GeoDataTrack::appendAltitude(qreal altitude)
{
    detach();

    Q_D(GeoDataTrack);
    d->m_lineStringNeedsUpdate = true;
    Q_ASSERT(!d->m_coordinates.isEmpty());

    if (d->m_coordinates.isEmpty())
    {
        return;
    }

    GeoDataCoordinates coordinates = d->m_coordinates.takeLast();
    coordinates.setAltitude(altitude);
    d->m_coordinates.append(coordinates);
}

void GeoDataTrack::appendWhen(const QDateTime& when)
{
    detach();

    Q_D(GeoDataTrack);
    d->m_when.append(when);
}

void GeoDataTrack::clear()
{
    detach();

    Q_D(GeoDataTrack);
    d->m_when.clear();
    d->m_coordinates.clear();
    d->m_lineStringNeedsUpdate = true;
}

void GeoDataTrack::removeBefore(const QDateTime& when)
{
    detach();

    Q_D(GeoDataTrack);
    Q_ASSERT(d->m_coordinates.size() == d->m_when.size());

    if (d->m_when.isEmpty())
    {
        return;
    }

    d->equalizeWhenSize();

    while (!d->m_when.isEmpty() && d->m_when.first() < when)
    {
        d->m_when.takeFirst();
        d->m_coordinates.takeFirst();
    }
}

void GeoDataTrack::removeAfter(const QDateTime& when)
{
    detach();

    Q_D(GeoDataTrack);
    Q_ASSERT(d->m_coordinates.size() == d->m_when.size());

    if (d->m_when.isEmpty())
    {
        return;
    }

    d->equalizeWhenSize();

    while (!d->m_when.isEmpty() && d->m_when.last() > when)
    {
        d->m_when.takeLast();
        d->m_coordinates.takeLast();
    }
}

const GeoDataLineString* GeoDataTrack::lineString() const
{
    Q_D(const GeoDataTrack);

    if (d->m_lineStringNeedsUpdate)
    {
        d->m_lineString = GeoDataLineString();
        d->m_lineString.append(coordinatesList());
        d->m_lineStringNeedsUpdate = false;
    }

    return &d->m_lineString;
}

GeoDataExtendedData& GeoDataTrack::extendedData()
{
    detach();

    Q_D(GeoDataTrack);
    return d->m_extendedData;
}

const GeoDataExtendedData& GeoDataTrack::extendedData() const
{
    Q_D(const GeoDataTrack);
    return d->m_extendedData;
}

void GeoDataTrack::setExtendedData(const GeoDataExtendedData& extendedData)
{
    detach();

    Q_D(GeoDataTrack);
    d->m_extendedData = extendedData;
}

const GeoDataLatLonAltBox& GeoDataTrack::latLonAltBox() const
{
    return lineString()->latLonAltBox();
}

//TODO
void GeoDataTrack::pack(QDataStream& stream) const
{
    GeoDataGeometry::pack(stream);
}
//TODO
void GeoDataTrack::unpack(QDataStream& stream)
{
    GeoDataGeometry::unpack(stream);
}

} //namespace Marble
