// Copyright (C) 2015 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only

#include "qgeopositioninfosource_winrt_p.h"

#include <QtCore/qcoreapplication.h>
#include <QtCore/qdatetime.h>
#include <QtCore/private/qfunctions_winrt_p.h>
#include <QtCore/qloggingcategory.h>
#include <QtCore/qmutex.h>
#include <QtCore/qtimezone.h>

#include <functional>
#include <windows.system.h>
#include <windows.devices.geolocation.h>
#include <windows.foundation.h>
#include <windows.foundation.collections.h>

using namespace Microsoft::WRL;
using namespace Microsoft::WRL::Wrappers;
using namespace ABI::Windows::Devices::Geolocation;
using namespace ABI::Windows::Foundation;
using namespace ABI::Windows::Foundation::Collections;

typedef ITypedEventHandler<Geolocator *, PositionChangedEventArgs *> GeoLocatorPositionHandler;
typedef ITypedEventHandler<Geolocator *, StatusChangedEventArgs *> GeoLocatorStatusHandler;
typedef IAsyncOperationCompletedHandler<Geoposition*> PositionHandler;
typedef IAsyncOperationCompletedHandler<GeolocationAccessStatus> AccessHandler;

Q_DECLARE_LOGGING_CATEGORY(lcPositioningWinRT)

QT_BEGIN_NAMESPACE

static inline HRESULT await(const ComPtr<IAsyncOperation<GeolocationAccessStatus>> &asyncOp,
                            GeolocationAccessStatus *result)
{
    ComPtr<IAsyncInfo> asyncInfo;
    HRESULT hr = asyncOp.As(&asyncInfo);
    if (FAILED(hr))
        return hr;

    AsyncStatus status;
    while (SUCCEEDED(hr = asyncInfo->get_Status(&status)) && status == AsyncStatus::Started)
        QThread::yieldCurrentThread();

    if (FAILED(hr) || status != AsyncStatus::Completed) {
        HRESULT ec;
        hr = asyncInfo->get_ErrorCode(&ec);
        if (FAILED(hr))
            return hr;
        hr = asyncInfo->Close();
        if (FAILED(hr))
            return hr;
        return ec;
    }

    if (FAILED(hr))
        return hr;

    return asyncOp->GetResults(result);
}

enum class InitializationState {
    Uninitialized,
    Initializing,
    Initialized
};

class QGeoPositionInfoSourceWinRTPrivate {
public:
    ComPtr<IGeolocator> locator;
    mutable ComPtr<IGeolocatorStatics> statics;
    QTimer periodicTimer;
    QTimer singleUpdateTimer;
    QGeoPositionInfo lastPosition;
    QGeoPositionInfoSource::Error positionError = QGeoPositionInfoSource::NoError;
    EventRegistrationToken statusToken;
    EventRegistrationToken positionToken;
    QMutex mutex;
    bool updatesOngoing = false;
    int minimumUpdateInterval = -1;
    int updateInterval = -1;
    InitializationState initState = InitializationState::Uninitialized;

    PositionStatus positionStatus = PositionStatus_NotInitialized;
};

QGeoPositionInfoSourceWinRT::QGeoPositionInfoSourceWinRT(QObject *parent)
    : QGeoPositionInfoSource(parent)
    , d_ptr(new QGeoPositionInfoSourceWinRTPrivate)
{
    qRegisterMetaType<QGeoPositionInfoSource::Error>();
    qCDebug(lcPositioningWinRT) << __FUNCTION__;
    CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);
    Q_D(QGeoPositionInfoSourceWinRT);
    d->positionError = QGeoPositionInfoSource::NoError;
    d->updatesOngoing = false;
    d->positionToken.value = 0;
    d->statusToken.value = 0;
}

QGeoPositionInfoSourceWinRT::~QGeoPositionInfoSourceWinRT()
{
    qCDebug(lcPositioningWinRT) << __FUNCTION__;
    CoUninitialize();
}

int QGeoPositionInfoSourceWinRT::init()
{
    Q_D(QGeoPositionInfoSourceWinRT);
    Q_ASSERT(d->initState != InitializationState::Initializing);
    if (d->initState == InitializationState::Initialized)
        return 0;

    qCDebug(lcPositioningWinRT) << __FUNCTION__;
    d->initState = InitializationState::Initializing;
    if (!requestAccess()) {
        d->initState = InitializationState::Uninitialized;
        setError(QGeoPositionInfoSource::AccessError);
        qWarning ("Location access failed.");
        return -1;
    }
    HRESULT hr = [this, d]() {
        HRESULT hr = RoActivateInstance(HString::MakeReference(RuntimeClass_Windows_Devices_Geolocation_Geolocator).Get(),
                                        &d->locator);
        RETURN_HR_IF_FAILED("Could not initialize native location services.");

        if (d->minimumUpdateInterval == -1) {
            UINT32 interval;
            hr = d->locator->get_ReportInterval(&interval);
            RETURN_HR_IF_FAILED("Could not retrieve report interval.");
            d->minimumUpdateInterval = static_cast<int>(interval);
        }
        if (d->updateInterval == -1)
            d->updateInterval = d->minimumUpdateInterval;
        setUpdateInterval(d->updateInterval);

        return hr;
    }();
    if (FAILED(hr)) {
        d->initState = InitializationState::Uninitialized;
        setError(QGeoPositionInfoSource::UnknownSourceError);
        return -1;
    }

    d->periodicTimer.setSingleShot(true);
    connect(&d->periodicTimer, &QTimer::timeout, this, &QGeoPositionInfoSourceWinRT::virtualPositionUpdate);

    d->singleUpdateTimer.setSingleShot(true);
    connect(&d->singleUpdateTimer, &QTimer::timeout, this, &QGeoPositionInfoSourceWinRT::singleUpdateTimeOut);

    QGeoPositionInfoSource::PositioningMethods preferredMethods = preferredPositioningMethods();
    if (preferredMethods == QGeoPositionInfoSource::NoPositioningMethods)
        preferredMethods = QGeoPositionInfoSource::AllPositioningMethods;
    setPreferredPositioningMethods(preferredMethods);

    connect(this, &QGeoPositionInfoSourceWinRT::nativePositionUpdate, this, &QGeoPositionInfoSourceWinRT::updateSynchronized);
    d->initState = InitializationState::Initialized;
    return 0;
}

QGeoPositionInfo QGeoPositionInfoSourceWinRT::lastKnownPosition(bool fromSatellitePositioningMethodsOnly) const
{
    qCDebug(lcPositioningWinRT) << __FUNCTION__;
    Q_D(const QGeoPositionInfoSourceWinRT);
    Q_UNUSED(fromSatellitePositioningMethodsOnly);
    return d->lastPosition;
}

QGeoPositionInfoSource::PositioningMethods QGeoPositionInfoSourceWinRT::supportedPositioningMethods() const
{
    return requestAccess() ? QGeoPositionInfoSource::AllPositioningMethods
                           : QGeoPositionInfoSource::NoPositioningMethods;
}

void QGeoPositionInfoSourceWinRT::setPreferredPositioningMethods(QGeoPositionInfoSource::PositioningMethods methods)
{
    qCDebug(lcPositioningWinRT) << __FUNCTION__ << methods;
    Q_D(QGeoPositionInfoSourceWinRT);

    PositioningMethods previousPreferredPositioningMethods = preferredPositioningMethods();
    QGeoPositionInfoSource::setPreferredPositioningMethods(methods);
    if (previousPreferredPositioningMethods == preferredPositioningMethods()
            || d->initState == InitializationState::Uninitialized) {
        return;
    }

    const bool needsRestart = d->positionToken.value != 0 || d->statusToken.value != 0;

    if (needsRestart)
        stopHandler();

    PositionAccuracy acc = methods & PositioningMethod::SatellitePositioningMethods ?
                PositionAccuracy::PositionAccuracy_High :
                PositionAccuracy::PositionAccuracy_Default;
    HRESULT hr = [d, acc]() {
        return d->locator->put_DesiredAccuracy(acc);
    }();
    RETURN_VOID_IF_FAILED("Could not set positioning accuracy.");

    if (needsRestart)
        startHandler();
}

void QGeoPositionInfoSourceWinRT::setUpdateInterval(int msec)
{
    qCDebug(lcPositioningWinRT) << __FUNCTION__ << msec;
    Q_D(QGeoPositionInfoSourceWinRT);
    if (d->initState == InitializationState::Uninitialized) {
        d->updateInterval = msec;
        return;
    }

    // minimumUpdateInterval is initialized to the lowest possible update interval in init().
    // Passing 0 will cause an error on Windows 10.
    // See https://docs.microsoft.com/en-us/uwp/api/windows.devices.geolocation.geolocator.reportinterval
    if (msec < minimumUpdateInterval())
        msec = minimumUpdateInterval();

    const bool needsRestart = d->positionToken.value != 0 || d->statusToken.value != 0;

    if (needsRestart)
        stopHandler();

    HRESULT hr = d->locator->put_ReportInterval(static_cast<UINT32>(msec));
    if (FAILED(hr)) {
        setError(QGeoPositionInfoSource::UnknownSourceError);
        qErrnoWarning(hr, "Failed to set update interval");
        return;
    }

    d->updateInterval = msec;
    d->periodicTimer.setInterval(d->updateInterval);

    QGeoPositionInfoSource::setUpdateInterval(d->updateInterval);

    if (needsRestart)
        startHandler();
}

int QGeoPositionInfoSourceWinRT::minimumUpdateInterval() const
{
    Q_D(const QGeoPositionInfoSourceWinRT);
    return d->minimumUpdateInterval == -1 ? 1000 : d->minimumUpdateInterval;
}

void QGeoPositionInfoSourceWinRT::startUpdates()
{
    qCDebug(lcPositioningWinRT) << __FUNCTION__;
    Q_D(QGeoPositionInfoSourceWinRT);

    setError(QGeoPositionInfoSource::NoError);
    if (init() < 0)
        return;

    if (d->updatesOngoing)
        return;

    if (!startHandler())
        return;
    d->updatesOngoing = true;
    d->periodicTimer.start();
}

void QGeoPositionInfoSourceWinRT::stopUpdates()
{
    qCDebug(lcPositioningWinRT) << __FUNCTION__;
    Q_D(QGeoPositionInfoSourceWinRT);

    if (init() < 0)
        return;

    stopHandler();
    d->updatesOngoing = false;
    d->periodicTimer.stop();
}

bool QGeoPositionInfoSourceWinRT::startHandler()
{
    qCDebug(lcPositioningWinRT) << __FUNCTION__;
    Q_D(QGeoPositionInfoSourceWinRT);

    // Check if already attached
    if (d->positionToken.value != 0)
        return true;

    if (preferredPositioningMethods() == QGeoPositionInfoSource::NoPositioningMethods) {
        setError(QGeoPositionInfoSource::UnknownSourceError);
        return false;
    }

    if (!requestAccess()) {
        setError(QGeoPositionInfoSource::AccessError);
        return false;
    }

    HRESULT hr = [this, d]() {
        HRESULT hr;

        // We need to call this at least once on Windows 10 Mobile.
        // Unfortunately this operation does not have a completion handler
        // registered. That could have helped in the single update case
        ComPtr<IAsyncOperation<Geoposition*>> op;
        hr = d->locator->GetGeopositionAsync(&op);
        RETURN_HR_IF_FAILED("Could not start position operation");

        hr = d->locator->add_PositionChanged(Callback<GeoLocatorPositionHandler>(this,
                                                                                 &QGeoPositionInfoSourceWinRT::onPositionChanged).Get(),
                                             &d->positionToken);
        RETURN_HR_IF_FAILED("Could not add position handler");

        hr = d->locator->add_StatusChanged(Callback<GeoLocatorStatusHandler>(this,
                                                                                 &QGeoPositionInfoSourceWinRT::onStatusChanged).Get(),
                                             &d->statusToken);
        RETURN_HR_IF_FAILED("Could not add status handler");
        return hr;
    }();
    if (FAILED(hr)) {
        setError(QGeoPositionInfoSource::UnknownSourceError);
        return false;
    }

    return true;
}

void QGeoPositionInfoSourceWinRT::stopHandler()
{
    qCDebug(lcPositioningWinRT) << __FUNCTION__;
    Q_D(QGeoPositionInfoSourceWinRT);

    if (!d->positionToken.value)
        return;
    d->locator->remove_PositionChanged(d->positionToken);
    d->locator->remove_StatusChanged(d->statusToken);
    d->positionToken.value = 0;
    d->statusToken.value = 0;
}

void QGeoPositionInfoSourceWinRT::requestUpdate(int timeout)
{
    qCDebug(lcPositioningWinRT) << __FUNCTION__ << timeout;
    Q_D(QGeoPositionInfoSourceWinRT);

    if (init() < 0)
        return;

    setError(QGeoPositionInfoSource::NoError);
    if (timeout != 0 && timeout < minimumUpdateInterval()) {
        d->positionError = QGeoPositionInfoSource::UpdateTimeoutError;
        emit QGeoPositionInfoSource::errorOccurred(d->positionError);
        return;
    }

    if (timeout == 0)
        timeout = 2*60*1000; // Maximum time for cold start (see Android)

    if (startHandler())
        d->singleUpdateTimer.start(timeout);
}

void QGeoPositionInfoSourceWinRT::virtualPositionUpdate()
{
    qCDebug(lcPositioningWinRT) << __FUNCTION__;
    Q_D(QGeoPositionInfoSourceWinRT);
    QMutexLocker locker(&d->mutex);

    // The operating system did not provide information in time
    // Hence we send a virtual position update to keep same behavior
    // between backends.
    // This only applies to the periodic timer, not for single requests
    // We can only do this if we received a valid position before
    if (d->lastPosition.isValid()) {
        QGeoPositionInfo sent = d->lastPosition;
        sent.setTimestamp(sent.timestamp().addMSecs(updateInterval()));
        d->lastPosition = sent;
        emit positionUpdated(sent);
    }
    d->periodicTimer.start();
}

void QGeoPositionInfoSourceWinRT::singleUpdateTimeOut()
{
    Q_D(QGeoPositionInfoSourceWinRT);
    QMutexLocker locker(&d->mutex);

    if (d->singleUpdateTimer.isActive()) {
        d->positionError = QGeoPositionInfoSource::UpdateTimeoutError;
        emit QGeoPositionInfoSource::errorOccurred(d->positionError);
        if (!d->updatesOngoing)
            stopHandler();
    }
}

void QGeoPositionInfoSourceWinRT::updateSynchronized(QGeoPositionInfo currentInfo)
{
    qCDebug(lcPositioningWinRT) << __FUNCTION__ << currentInfo;
    Q_D(QGeoPositionInfoSourceWinRT);
    QMutexLocker locker(&d->mutex);

    d->periodicTimer.stop();
    d->lastPosition = currentInfo;

    if (d->updatesOngoing)
        d->periodicTimer.start();

    if (d->singleUpdateTimer.isActive()) {
        d->singleUpdateTimer.stop();
        if (!d->updatesOngoing)
        stopHandler();
    }

    emit positionUpdated(currentInfo);
}

QGeoPositionInfoSource::Error QGeoPositionInfoSourceWinRT::error() const
{
    Q_D(const QGeoPositionInfoSourceWinRT);
    qCDebug(lcPositioningWinRT) << __FUNCTION__ << d->positionError;

    // If the last encountered error was "Access denied", it is possible that the location service
    // has been enabled by now so that we are clear again.
    if ((d->positionError == QGeoPositionInfoSource::AccessError
         || d->positionError == QGeoPositionInfoSource::UnknownSourceError) && requestAccess())
        return QGeoPositionInfoSource::NoError;

    return d->positionError;
}

void QGeoPositionInfoSourceWinRT::setError(QGeoPositionInfoSource::Error positionError)
{
    Q_D(QGeoPositionInfoSourceWinRT);

    if (positionError == d->positionError)
        return;

    qCDebug(lcPositioningWinRT) << __FUNCTION__ << positionError;
    d->positionError = positionError;
    if (positionError != QGeoPositionInfoSource::NoError)
        emit QGeoPositionInfoSource::errorOccurred(positionError);
}

void QGeoPositionInfoSourceWinRT::reactOnError(QGeoPositionInfoSource::Error positionError)
{
    setError(positionError);
    stopUpdates();
}

HRESULT QGeoPositionInfoSourceWinRT::onPositionChanged(IGeolocator *locator, IPositionChangedEventArgs *args)
{
    qCDebug(lcPositioningWinRT) << __FUNCTION__;
    Q_UNUSED(locator);

    HRESULT hr;
    ComPtr<IGeoposition> position;
    hr = args->get_Position(&position);
    RETURN_HR_IF_FAILED("Could not access position object.");

    QGeoPositionInfo currentInfo;

    ComPtr<IGeocoordinate> coord;
    hr = position->get_Coordinate(&coord);
    if (FAILED(hr))
        qErrnoWarning(hr, "Could not access coordinate");

    ComPtr<IGeocoordinateWithPoint> pointCoordinate;
    hr = coord.As(&pointCoordinate);
    if (FAILED(hr))
        qErrnoWarning(hr, "Could not cast coordinate.");

    ComPtr<IGeopoint> point;
    hr = pointCoordinate->get_Point(&point);
    if (FAILED(hr))
        qErrnoWarning(hr, "Could not obtain coordinate's point.");

    BasicGeoposition pos;
    hr = point->get_Position(&pos);
    if (FAILED(hr))
        qErrnoWarning(hr, "Could not obtain point's position.");

    DOUBLE lat = pos.Latitude;
    DOUBLE lon = pos.Longitude;
    DOUBLE alt = pos.Altitude;

    bool altitudeAvailable = false;
    ComPtr<IGeoshape> shape;
    hr = point.As(&shape);
    if (SUCCEEDED(hr) && shape) {
        AltitudeReferenceSystem altitudeSystem;
        hr = shape->get_AltitudeReferenceSystem(&altitudeSystem);
        if (SUCCEEDED(hr) && altitudeSystem == AltitudeReferenceSystem_Geoid)
            altitudeAvailable = true;
    }
    if (altitudeAvailable)
        currentInfo.setCoordinate(QGeoCoordinate(lat, lon, alt));
    else
        currentInfo.setCoordinate(QGeoCoordinate(lat, lon));

    DOUBLE accuracy;
    hr = coord->get_Accuracy(&accuracy);
    if (SUCCEEDED(hr))
        currentInfo.setAttribute(QGeoPositionInfo::HorizontalAccuracy, accuracy);

    IReference<double> *altAccuracy;
    hr = coord->get_AltitudeAccuracy(&altAccuracy);
    if (SUCCEEDED(hr) && altAccuracy) {
        double value;
        hr = altAccuracy->get_Value(&value);
        currentInfo.setAttribute(QGeoPositionInfo::VerticalAccuracy, value);
    }

    IReference<double> *speed;
    hr = coord->get_Speed(&speed);
    if (SUCCEEDED(hr) && speed) {
        double value;
        hr = speed->get_Value(&value);
        currentInfo.setAttribute(QGeoPositionInfo::GroundSpeed, value);
    }

    IReference<double> *heading;
    hr = coord->get_Heading(&heading);
    if (SUCCEEDED(hr) && heading) {
        double value;
        hr = heading->get_Value(&value);
        double mod = 0;
        value = modf(value, &mod);
        value += static_cast<int>(mod) % 360;
        if (value >=0 && value <= 359) // get_Value might return nan/-nan
            currentInfo.setAttribute(QGeoPositionInfo::Direction, value);
    }

    DateTime dateTime;
    hr = coord->get_Timestamp(&dateTime);

    if (dateTime.UniversalTime > 0) {
        ULARGE_INTEGER uLarge;
        uLarge.QuadPart = dateTime.UniversalTime;
        FILETIME fileTime;
        fileTime.dwHighDateTime = uLarge.HighPart;
        fileTime.dwLowDateTime = uLarge.LowPart;
        SYSTEMTIME systemTime;
        if (FileTimeToSystemTime(&fileTime, &systemTime)) {
            currentInfo.setTimestamp(QDateTime(QDate(systemTime.wYear, systemTime.wMonth,
                                                     systemTime.wDay),
                                               QTime(systemTime.wHour, systemTime.wMinute,
                                                     systemTime.wSecond, systemTime.wMilliseconds),
                                               QTimeZone::UTC));
        }
    }

    emit nativePositionUpdate(currentInfo);

    return S_OK;
}

static inline bool isDisabledStatus(PositionStatus status)
{
    return status == PositionStatus_NoData || status == PositionStatus_Disabled
            || status == PositionStatus_NotAvailable;
}

HRESULT QGeoPositionInfoSourceWinRT::onStatusChanged(IGeolocator *, IStatusChangedEventArgs *args)
{
    Q_D(QGeoPositionInfoSourceWinRT);

    const PositionStatus oldStatus = d->positionStatus;
    HRESULT hr = args->get_Status(&d->positionStatus);
    RETURN_HR_IF_FAILED("Could not obtain position status");
    qCDebug(lcPositioningWinRT) << __FUNCTION__ << d->positionStatus;
    QGeoPositionInfoSource::Error error = QGeoPositionInfoSource::NoError;
    switch (d->positionStatus) {
    case PositionStatus::PositionStatus_NotAvailable:
        error = QGeoPositionInfoSource::UnknownSourceError;
        break;
    case PositionStatus::PositionStatus_Disabled:
        error = QGeoPositionInfoSource::AccessError;
        break;
    case PositionStatus::PositionStatus_NoData:
        error = QGeoPositionInfoSource::ClosedError;
        break;
    }
    if (error != QGeoPositionInfoSource::NoError) {
        QMetaObject::invokeMethod(this, "reactOnError", Qt::QueuedConnection,
                                  Q_ARG(QGeoPositionInfoSource::Error,
                                        QGeoPositionInfoSource::UnknownSourceError));
    }

    if (isDisabledStatus(oldStatus) != isDisabledStatus(d->positionStatus))
        emit supportedPositioningMethodsChanged();

    return S_OK;
}

bool QGeoPositionInfoSourceWinRT::requestAccess() const
{
    Q_D(const QGeoPositionInfoSourceWinRT);
    qCDebug(lcPositioningWinRT) << __FUNCTION__;
    GeolocationAccessStatus accessStatus;

    ComPtr<IAsyncOperation<GeolocationAccessStatus>> op;
    HRESULT hr = [&op, d]() {
        HRESULT hr;
        if (!d->statics) {
            hr = RoGetActivationFactory(HString::MakeReference(RuntimeClass_Windows_Devices_Geolocation_Geolocator).Get(),
                                        IID_PPV_ARGS(&d->statics));
            RETURN_HR_IF_FAILED("Could not access Geolocation Statics.");
        }

        hr = d->statics->RequestAccessAsync(&op);
        return hr;
    }();
    if (FAILED(hr)) {
        qCDebug(lcPositioningWinRT) << __FUNCTION__ << "Requesting access from Xaml thread failed";
        return false;
    }

    await(op, &accessStatus);
    return accessStatus == GeolocationAccessStatus_Allowed;
}

QT_END_NAMESPACE
