/***************************************************************************
    qgsarcgisrestquery.h
    --------------------
    begin                : December 2020
    copyright            : (C) 2020 by Nyall Dawson
    email                : nyall dot dawson 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 of the License, or     *
 *   (at your option) any later version.                                   *
 *                                                                         *
 ***************************************************************************/
#ifndef QGSARCGISRESTQUERY_H
#define QGSARCGISRESTQUERY_H


#include "qgis_core.h"
#include "qgscoordinatereferencesystem.h"
#include "qgshttpheaders.h"
#include "qgsrectangle.h"

#include <QPointer>
#include <QString>
#include <QVariantMap>

#define SIP_NO_FILE

class QgsFeedback;
class QNetworkReply;
class QgsCoordinateReferenceSystem;

/**
 * \ingroup core
 * \brief Utility functions for querying ArcGIS REST services.
 *
 * \since QGIS 3.18
 */
class CORE_EXPORT QgsArcGisRestQueryUtils
{
  public:
    /**
     * Service types
     */
    enum class ServiceTypeFilter
    {
      Vector = 1 << 0,
      Raster = 1 << 1,
      Scene = 1 << 2,
    };
    Q_DECLARE_FLAGS( ServiceTypeFilters, ServiceTypeFilter );

    /**
     * Attempts to resolve the service type from a \a url.
     *
     * This may not be successful, e.g. when the service is sitting behind an internal proxy.
     */
    static Qgis::ArcGisRestServiceType sniffServiceTypeFromUrl( const QUrl &url );

    /**
     * Attempts to resolve the service type from a \a json definition.
     */
    static Qgis::ArcGisRestServiceType sniffServiceTypeFromJson( const QVariantMap &json );

    /**
     * Retrieves JSON service info for the specified base URL.
     */
    static QVariantMap getServiceInfo(
      const QString &baseurl,
      const QString &authcfg,
      QString &errorTitle,
      QString &errorText,
      const QgsHttpHeaders &requestHeaders = QgsHttpHeaders(),
      const QString &urlPrefix = QString(),
      bool forceRefresh = false
    );

    /**
     * Retrieves JSON layer info for the specified layer URL.
     */
    static QVariantMap getLayerInfo(
      const QString &layerurl, const QString &authcfg, QString &errorTitle, QString &errorText, const QgsHttpHeaders &requestHeaders = QgsHttpHeaders(), const QString &urlPrefix = QString()
    );

    /**
     * Retrieves all object IDs for the specified layer URL.
     */
    static QVariantMap getObjectIds(
      const QString &layerurl,
      const QString &authcfg,
      QString &errorTitle,
      QString &errorText,
      const QgsHttpHeaders &requestHeaders = QgsHttpHeaders(),
      const QString &urlPrefix = QString(),
      const QgsRectangle &bbox = QgsRectangle(),
      const QString &whereClause = QString()
    );


    /**
     * Retrieves the extent for the features matching a \a whereClause.
     *
     * \since QGIS 3.28
     */
    static QgsRectangle getExtent(
      const QString &layerurl, const QString &whereClause, const QString &authcfg, const QgsHttpHeaders &requestHeaders = QgsHttpHeaders(), const QString &urlPrefix = QString()
    );

    /**
     * Retrieves all matching objects from the specified layer URL.
     *
     * \param layerurl
     * \param authcfg
     * \param objectIds
     * \param crs override CRS for retrieved features. If empty, features will be retrieved in their original CRS (i.e. the original representation from the
     * service). If set, then the service will be asked to transform the features from their original representation to the matching CRS. Since the exact transformation
     * which will be used by the service is unknown, is it highly recommended to leave this argument empty and handle any transformations on the client (QGIS) side instead.
     * The string must be value of the form "auth:code". Only the code portion will be used, and passed to the service as a integer only. It is assumed that the backend
     * has the same CRS definition for the code as the proj database, and if this is not the case then an unexpected transformation will occur.
     * \param fetchGeometry
     * \param fetchAttributes
     * \param fetchM
     * \param fetchZ
     * \param errorTitle
     * \param errorText
     * \param requestHeaders
     * \param feedback
     * \param urlPrefix
     */
    static QVariantMap getObjects(
      const QString &layerurl,
      const QString &authcfg,
      const QList<quint32> &objectIds,
      const QString &crs,
      bool fetchGeometry,
      const QStringList &fetchAttributes,
      bool fetchM,
      bool fetchZ,
      QString &errorTitle,
      QString &errorText,
      const QgsHttpHeaders &requestHeaders = QgsHttpHeaders(),
      QgsFeedback *feedback = nullptr,
      const QString &urlPrefix = QString()
    );

    /**
     * Gets a list of object IDs which fall within the specified extent.
     */
    static QList<quint32> getObjectIdsByExtent(
      const QString &layerurl,
      const QgsRectangle &filterRect,
      QString &errorTitle,
      QString &errorText,
      const QString &authcfg,
      const QgsHttpHeaders &requestHeaders = QgsHttpHeaders(),
      QgsFeedback *feedback = nullptr,
      const QString &whereClause = QString(),
      const QString &urlPrefix = QString()
    );

    /**
     * Performs a blocking request to a URL and returns the retrieved data.
     */
    static QByteArray queryService(
      const QUrl &url,
      const QString &authcfg,
      QString &errorTitle,
      QString &errorText,
      const QgsHttpHeaders &requestHeaders = QgsHttpHeaders(),
      QgsFeedback *feedback = nullptr,
      QString *contentType = nullptr,
      const QString &urlPrefix = QString(),
      bool forceRefresh = false
    );
    /**
     * Performs a blocking request to a URL and returns the retrieved JSON content.
     */
    static QVariantMap queryServiceJSON(
      const QUrl &url,
      const QString &authcfg,
      QString &errorTitle,
      QString &errorText,
      const QgsHttpHeaders &requestHeaders = QgsHttpHeaders(),
      QgsFeedback *feedback = nullptr,
      const QString &urlPrefix = QString(),
      bool forceRefresh = false
    );

    /**
     * Calls the specified \a visitor function on all folder items found within the given service data.
     */
    static void visitFolderItems( const std::function<void( const QString &folderName, const QString &url )> &visitor, const QVariantMap &serviceData, const QString &baseUrl );

    /**
     * Calls the specified \a visitor function on all service items found within the given service data.
     */
    static void visitServiceItems(
      const std::function<void( const QString &serviceName, const QString &url, Qgis::ArcGisRestServiceType serviceType )> &visitor, const QVariantMap &serviceData, const QString &baseUrl
    );

    /**
     * Encapsulates details relating to a layer item.
     *
     * \since QGIS 4.2
     */
    struct LayerItemDetails
    {
        //! Parent layer ID
        QString parentLayerId;
        //! Service type
        Qgis::ArcGisRestServiceType serviceType = Qgis::ArcGisRestServiceType::Unknown;
        //! Geometry type
        Qgis::GeometryType geometryType = Qgis::GeometryType::Unknown;
        //! Layer ID
        QString layerId;
        //! Layer name
        QString name;
        //! Description
        QString description;
        //! URL
        QString url;
        //! TRUE if layer item represents a parent layer
        bool isParentLayer = false;
        //! Coordinate reference system
        QgsCoordinateReferenceSystem crs;
        //! Map server image format
        QString format;
        //! TRUE if layer is a map server with the query capability
        bool isMapServerWithQueryCapability = false;
        //! TRUE if layer is the special map server "all layers" layer
        bool isMapServerSpecialAllLayersOption = false;
    };

    /**
     * Calls the specified \a visitor function on all layer items found within the given service data.
     */
    static void addLayerItems(
      const std::function<void( const LayerItemDetails &details )> &visitor, const QVariantMap &serviceData, const QString &parentUrl, const QString &parentSupportedFormats, Qgis::ArcGisRestServiceType serviceType
    );

    /**
     * Parses and processes a \a url.
     */
    static QUrl parseUrl( const QUrl &url, bool *isTestEndpoint = nullptr );

  private:
    static void adjustBaseUrl( QString &baseUrl, const QString &name );

    friend class TestQgsArcGisRestUtils;
};

///@cond PRIVATE
class CORE_EXPORT QgsArcGisAsyncQuery : public QObject
{
    Q_OBJECT
  public:
    QgsArcGisAsyncQuery( QObject *parent = nullptr );
    ~QgsArcGisAsyncQuery() override;

    void start( const QUrl &url, const QString &authCfg, QByteArray *result, bool allowCache = false, const QgsHttpHeaders &headers = QgsHttpHeaders(), const QString &urlPrefix = QString() );
  signals:
    void finished();
    void failed( QString errorTitle, QString errorName );
  private slots:
    void handleReply();

  private:
    QPointer<QNetworkReply> mReply;
    QByteArray *mResult = nullptr;
};

class CORE_EXPORT QgsArcGisAsyncParallelQuery : public QObject
{
    Q_OBJECT
  public:
    QgsArcGisAsyncParallelQuery( const QString &authcfg, const QgsHttpHeaders &requestHeaders, QObject *parent = nullptr );
    void start( const QVector<QUrl> &urls, QVector<QByteArray> *results, bool allowCache = false );

  signals:
    void finished( QStringList errors );
  private slots:
    void handleReply();

  private:
    QVector<QByteArray> *mResults = nullptr;
    int mPendingRequests = 0;
    QStringList mErrors;
    QString mAuthCfg;
    QgsHttpHeaders mRequestHeaders;
};

Q_DECLARE_OPERATORS_FOR_FLAGS( QgsArcGisRestQueryUtils::ServiceTypeFilters )

///@endcond

#endif // QGSARCGISRESTQUERY_H
