/****************************************************************************
**
** Copyright (C) 2016 Jolla Ltd.
** Contact: Aaron McCarthy <aaron.mccarthy@jollamobile.com>
** Copyright (C) 2016 The Qt Company Ltd.
** Contact: https://www.qt.io/licensing/
**
** This file is part of the QtPositioning module of the Qt Toolkit.
**
** $QT_BEGIN_LICENSE:LGPL$
** Commercial License Usage
** Licensees holding valid commercial Qt licenses may use this file in
** accordance with the commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and The Qt Company. For licensing terms
** and conditions see https://www.qt.io/terms-conditions. For further
** information use the contact form at https://www.qt.io/contact-us.
**
** GNU Lesser General Public License Usage
** Alternatively, this file may be used under the terms of the GNU Lesser
** General Public License version 3 as published by the Free Software
** Foundation and appearing in the file LICENSE.LGPL3 included in the
** packaging of this file. Please review the following information to
** ensure the GNU Lesser General Public License version 3 requirements
** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
**
** GNU General Public License Usage
** Alternatively, this file may be used under the terms of the GNU
** General Public License version 2.0 or (at your option) the GNU General
** Public license version 3 or any later version approved by the KDE Free
** Qt Foundation. The licenses are as published by the Free Software
** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
** included in the packaging of this file. Please review the following
** information to ensure the GNU General Public License requirements will
** be met: https://www.gnu.org/licenses/gpl-2.0.html and
** https://www.gnu.org/licenses/gpl-3.0.html.
**
** $QT_END_LICENSE$
**
****************************************************************************/

#include "qgeopositioninfosource_geocluemaster.h"

#include <geoclue_interface.h>
#include <position_interface.h>
#include <velocity_interface.h>

#include <QtCore/QDateTime>
#include <QtCore/QFile>
#include <QtCore/QSaveFile>
#include <QtCore/QStandardPaths>
#include <QtCore/QVariantMap>
#include <QtCore/QtNumeric>
#include <QtCore/QLoggingCategory>
#include <QtDBus/QDBusMetaType>

#ifndef QT_NO_DATASTREAM
#include <QtCore/QDataStream>
#endif

Q_DECLARE_LOGGING_CATEGORY(lcPositioningGeoclue)

#define MINIMUM_UPDATE_INTERVAL 1000
#define UPDATE_TIMEOUT_COLD_START 120000

QT_BEGIN_NAMESPACE

namespace
{

double knotsToMetersPerSecond(double knots)
{
    return knots * 1852.0 / 3600.0;
}

}

QGeoPositionInfoSourceGeoclueMaster::QGeoPositionInfoSourceGeoclueMaster(QObject *parent)
:   QGeoPositionInfoSource(parent), m_master(new QGeoclueMaster(this)), m_provider(0), m_pos(0),
    m_vel(0), m_requestTimer(this), m_lastVelocityIsFresh(false), m_regularUpdateTimedOut(false),
    m_lastVelocity(qQNaN()), m_lastDirection(qQNaN()), m_lastClimb(qQNaN()), m_lastPositionFromSatellite(false),
    m_running(false), m_error(NoError)
{
    qDBusRegisterMetaType<Accuracy>();

#ifndef QT_NO_DATASTREAM
    // Load the last known location
    QFile file(QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) +
               QStringLiteral("/qtposition-geoclue"));
    if (file.open(QIODevice::ReadOnly)) {
        QDataStream out(&file);
        out >> m_lastPosition;
    }
#endif

    connect(m_master, SIGNAL(positionProviderChanged(QString,QString,QString,QString)),
            this, SLOT(positionProviderChanged(QString,QString,QString,QString)));

    m_requestTimer.setSingleShot(true);
    connect(&m_requestTimer, SIGNAL(timeout()), this, SLOT(requestUpdateTimeout()));

    setPreferredPositioningMethods(AllPositioningMethods);
}

QGeoPositionInfoSourceGeoclueMaster::~QGeoPositionInfoSourceGeoclueMaster()
{
#if !defined(QT_NO_DATASTREAM) && QT_CONFIG(temporaryfile)
    if (m_lastPosition.isValid()) {
        QSaveFile file(QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) +
                       QStringLiteral("/qtposition-geoclue"));
        if (file.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
            QDataStream out(&file);
            // Only save position and timestamp.
            out << QGeoPositionInfo(m_lastPosition.coordinate(), m_lastPosition.timestamp());
            file.commit();
        }
    }
#endif

    cleanupPositionSource();
}

void QGeoPositionInfoSourceGeoclueMaster::positionUpdateFailed()
{
    qCDebug(lcPositioningGeoclue) << "position update failed.";

    m_lastVelocityIsFresh = false;
    if (m_running && !m_regularUpdateTimedOut) {
        m_regularUpdateTimedOut = true;
        emit updateTimeout();
    }
}

void QGeoPositionInfoSourceGeoclueMaster::updatePosition(PositionFields fields, int timestamp,
                                                         double latitude, double longitude,
                                                         double altitude, Accuracy accuracy)
{
    if (m_requestTimer.isActive())
        m_requestTimer.stop();

    QGeoCoordinate coordinate(latitude, longitude);
    if (fields & Altitude)
        coordinate.setAltitude(altitude);

    m_lastPosition = QGeoPositionInfo(coordinate, QDateTime::fromSecsSinceEpoch(timestamp));

    m_lastPositionFromSatellite = accuracy.level() == Accuracy::Detailed;

    if (!qIsNaN(accuracy.horizontal()))
        m_lastPosition.setAttribute(QGeoPositionInfo::HorizontalAccuracy, accuracy.horizontal());
    if (!qIsNaN(accuracy.vertical()))
        m_lastPosition.setAttribute(QGeoPositionInfo::VerticalAccuracy, accuracy.vertical());

    if (m_lastVelocityIsFresh) {
        if (!qIsNaN(m_lastVelocity))
            m_lastPosition.setAttribute(QGeoPositionInfo::GroundSpeed, m_lastVelocity);
        if (!qIsNaN(m_lastDirection))
            m_lastPosition.setAttribute(QGeoPositionInfo::Direction, m_lastDirection);
        if (!qIsNaN(m_lastClimb))
            m_lastPosition.setAttribute(QGeoPositionInfo::VerticalSpeed, m_lastClimb);
        m_lastVelocityIsFresh = false;
    }

    m_regularUpdateTimedOut = false;

    emit positionUpdated(m_lastPosition);

    qCDebug(lcPositioningGeoclue) << m_lastPosition;

    // Only stop positioning if regular updates not active.
    if (!m_running) {
        cleanupPositionSource();
        m_master->releaseMasterClient();
    }
}

void QGeoPositionInfoSourceGeoclueMaster::velocityUpdateFailed()
{
    qCDebug(lcPositioningGeoclue) << "velocity update failed.";

    // Set the velocitydata non-fresh.
    m_lastVelocityIsFresh = false;
}

void QGeoPositionInfoSourceGeoclueMaster::updateVelocity(VelocityFields fields, int timestamp,
                                                         double speed, double direction,
                                                         double climb)
{
    Q_UNUSED(timestamp);

    // Store the velocity and mark it as fresh. Simple but hopefully adequate.
    m_lastVelocity = (fields & Speed) ? knotsToMetersPerSecond(speed) : qQNaN();
    m_lastDirection = (fields & Direction) ? direction : qQNaN();
    m_lastClimb = (fields & Climb) ? climb : qQNaN();
    m_lastVelocityIsFresh = true;

    qCDebug(lcPositioningGeoclue) << m_lastVelocity << m_lastDirection << m_lastClimb;
}

void QGeoPositionInfoSourceGeoclueMaster::cleanupPositionSource()
{
    qCDebug(lcPositioningGeoclue) << "cleaning up position source";

    if (m_provider)
        m_provider->RemoveReference();
    delete m_provider;
    m_provider = 0;
    delete m_pos;
    m_pos = 0;
    delete m_vel;
    m_vel = 0;
}

void QGeoPositionInfoSourceGeoclueMaster::setOptions()
{
    if (!m_provider)
        return;

    QVariantMap options;
    options.insert(QStringLiteral("UpdateInterval"), updateInterval());

    m_provider->SetOptions(options);
}

void QGeoPositionInfoSourceGeoclueMaster::setUpdateInterval(int msec)
{
    QGeoPositionInfoSource::setUpdateInterval(qMax(minimumUpdateInterval(), msec));
    setOptions();
}

void QGeoPositionInfoSourceGeoclueMaster::setPreferredPositioningMethods(PositioningMethods methods)
{
    PositioningMethods previousPreferredPositioningMethods = preferredPositioningMethods();
    QGeoPositionInfoSource::setPreferredPositioningMethods(methods);
    if (previousPreferredPositioningMethods == preferredPositioningMethods())
        return;

    qCDebug(lcPositioningGeoclue) << "requested to set methods to" << methods
                                  << ", and set them to:" << preferredPositioningMethods();

    m_lastVelocityIsFresh = false;
    m_regularUpdateTimedOut = false;

    // Don't start Geoclue provider until necessary. Don't currently have a master client, no need
    // no recreate one.
    if (!m_master->hasMasterClient())
        return;

    // Free potential previous sources, because new requirements can't be set for the client
    // (creating a position object after changing requirements seems to fail).
    cleanupPositionSource();
    m_master->releaseMasterClient();

    // Restart Geoclue provider with new requirements.
    configurePositionSource();
    setOptions();
}

QGeoPositionInfo QGeoPositionInfoSourceGeoclueMaster::lastKnownPosition(bool fromSatellitePositioningMethodsOnly) const
{
    if (fromSatellitePositioningMethodsOnly && !m_lastPositionFromSatellite)
        return QGeoPositionInfo();

    return m_lastPosition;
}

QGeoPositionInfoSourceGeoclueMaster::PositioningMethods QGeoPositionInfoSourceGeoclueMaster::supportedPositioningMethods() const
{
    return AllPositioningMethods;
}

void QGeoPositionInfoSourceGeoclueMaster::startUpdates()
{
    if (m_running) {
        qCDebug(lcPositioningGeoclue) << "already running.";
        return;
    }

    m_running = true;

    qCDebug(lcPositioningGeoclue) << "starting updates";

    // Start Geoclue provider.
    if (!m_master->hasMasterClient()) {
        configurePositionSource();
        setOptions();
    }

    // Emit last known position on start.
    if (m_lastPosition.isValid()) {
        QMetaObject::invokeMethod(this, "positionUpdated", Qt::QueuedConnection,
                                  Q_ARG(QGeoPositionInfo, m_lastPosition));
    }
}

int QGeoPositionInfoSourceGeoclueMaster::minimumUpdateInterval() const
{
    return MINIMUM_UPDATE_INTERVAL;
}

void QGeoPositionInfoSourceGeoclueMaster::stopUpdates()
{
    if (!m_running) {
        qCDebug(lcPositioningGeoclue) << "already stopped.";
        return;
    }

    qCDebug(lcPositioningGeoclue) << "stopping updates";

    if (m_pos) {
        disconnect(m_pos, SIGNAL(PositionChanged(qint32,qint32,double,double,double,Accuracy)),
                   this, SLOT(positionChanged(qint32,qint32,double,double,double,Accuracy)));
    }

    if (m_vel) {
        disconnect(m_vel, SIGNAL(VelocityChanged(qint32,qint32,double,double,double)),
                   this, SLOT(velocityChanged(qint32,qint32,double,double,double)));
    }

    m_running = false;

    // Only stop positioning if single update not requested.
    if (!m_requestTimer.isActive()) {
        cleanupPositionSource();
        m_master->releaseMasterClient();
    }
}

void QGeoPositionInfoSourceGeoclueMaster::requestUpdate(int timeout)
{
    if (timeout < minimumUpdateInterval() && timeout != 0) {
        emit updateTimeout();
        return;
    }
    if (m_requestTimer.isActive()) {
        qCDebug(lcPositioningGeoclue) << "request timer was active, ignoring startUpdates.";
        return;
    }

    if (!m_master->hasMasterClient()) {
        configurePositionSource();
        setOptions();
    }

    // Create better logic for timeout value (specs leave it impl dependant).
    // Especially if there are active updates ongoing, there is no point of waiting
    // for whole cold start time.
    m_requestTimer.start(timeout ? timeout : UPDATE_TIMEOUT_COLD_START);

    if (m_pos) {
        QDBusPendingReply<qint32, qint32, double, double, double, Accuracy> reply = m_pos->GetPosition();
        QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(reply, this);
        connect(watcher, SIGNAL(finished(QDBusPendingCallWatcher*)),
                this, SLOT(getPositionFinished(QDBusPendingCallWatcher*)));
    }
}

void QGeoPositionInfoSourceGeoclueMaster::positionProviderChanged(const QString &name,
                                                                  const QString &description,
                                                                  const QString &service,
                                                                  const QString &path)
{
    Q_UNUSED(name);
    Q_UNUSED(description);

    cleanupPositionSource();

    if (service.isEmpty() || path.isEmpty()) {
        if (!m_regularUpdateTimedOut) {
            m_regularUpdateTimedOut = true;
            emit updateTimeout();
        }
        return;
    }

    qCDebug(lcPositioningGeoclue) << "position provider changed to" << name;

    m_provider = new OrgFreedesktopGeoclueInterface(service, path, QDBusConnection::sessionBus());
    m_provider->AddReference();

    m_pos = new OrgFreedesktopGeocluePositionInterface(service, path, QDBusConnection::sessionBus());

    if (m_running) {
        connect(m_pos, SIGNAL(PositionChanged(qint32,qint32,double,double,double,Accuracy)),
                this, SLOT(positionChanged(qint32,qint32,double,double,double,Accuracy)));
    }

    // Get the current position immediately.
    QDBusPendingReply<qint32, qint32, double, double, double, Accuracy> reply = m_pos->GetPosition();
    QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(reply, this);
    connect(watcher, SIGNAL(finished(QDBusPendingCallWatcher*)),
            this, SLOT(getPositionFinished(QDBusPendingCallWatcher*)));

    setOptions();

    m_vel = new OrgFreedesktopGeoclueVelocityInterface(service, path, QDBusConnection::sessionBus());
    if (m_vel->isValid() && m_running) {
        connect(m_vel, SIGNAL(VelocityChanged(qint32,qint32,double,double,double)),
                this, SLOT(velocityChanged(qint32,qint32,double,double,double)));
    }
}

void QGeoPositionInfoSourceGeoclueMaster::requestUpdateTimeout()
{
    qCDebug(lcPositioningGeoclue) << "request update timeout occurred.";

    // If we end up here, there has not been valid position update.
    emit updateTimeout();

    // Only stop positioning if regular updates not active.
    if (!m_running) {
        cleanupPositionSource();
        m_master->releaseMasterClient();
    }
}

void QGeoPositionInfoSourceGeoclueMaster::getPositionFinished(QDBusPendingCallWatcher *watcher)
{
    QDBusPendingReply<qint32, qint32, double, double, double, Accuracy> reply = *watcher;
    watcher->deleteLater();

    if (reply.isError())
        return;

    PositionFields fields = static_cast<PositionFields>(reply.argumentAt<0>());

    qCDebug(lcPositioningGeoclue) << "got position update with fields" << int(fields);

    if (fields & Latitude && fields & Longitude) {
        qint32 timestamp = reply.argumentAt<1>();
        double latitude = reply.argumentAt<2>();
        double longitude = reply.argumentAt<3>();
        double altitude = reply.argumentAt<4>();
        Accuracy accuracy = reply.argumentAt<5>();
        updatePosition(fields, timestamp, latitude, longitude, altitude, accuracy);
    }
}

void QGeoPositionInfoSourceGeoclueMaster::positionChanged(qint32 fields, qint32 timestamp, double latitude, double longitude, double altitude, const Accuracy &accuracy)
{
    PositionFields pFields = static_cast<PositionFields>(fields);

    qCDebug(lcPositioningGeoclue) << "position changed with fields" << fields;

    if (pFields & Latitude && pFields & Longitude)
        updatePosition(pFields, timestamp, latitude, longitude, altitude, accuracy);
    else
        positionUpdateFailed();
}

void QGeoPositionInfoSourceGeoclueMaster::velocityChanged(qint32 fields, qint32 timestamp, double speed, double direction, double climb)
{
    VelocityFields vFields = static_cast<VelocityFields>(fields);

    if (vFields == NoVelocityFields)
        velocityUpdateFailed();
    else
        updateVelocity(vFields, timestamp, speed, direction, climb);
}

void QGeoPositionInfoSourceGeoclueMaster::configurePositionSource()
{
    qCDebug(lcPositioningGeoclue);

    bool created = false;

    switch (preferredPositioningMethods()) {
    case SatellitePositioningMethods:
        created = m_master->createMasterClient(Accuracy::Detailed, QGeoclueMaster::ResourceGps);
        break;
    case NonSatellitePositioningMethods:
        created = m_master->createMasterClient(Accuracy::None, QGeoclueMaster::ResourceCell | QGeoclueMaster::ResourceNetwork);
        break;
    case AllPositioningMethods:
        created = m_master->createMasterClient(Accuracy::None, QGeoclueMaster::ResourceAll);
        break;
    default:
        qWarning("QGeoPositionInfoSourceGeoclueMaster unknown preferred method.");
        m_error = UnknownSourceError;
        emit QGeoPositionInfoSource::error(m_error);
        return;
    }

    if (!created) {
        m_error = UnknownSourceError;
        emit QGeoPositionInfoSource::error(m_error);
    }
}

QGeoPositionInfoSource::Error QGeoPositionInfoSourceGeoclueMaster::error() const
{
    return m_error;
}

QT_END_NAMESPACE
