/***************************************************************************
                              qgsabstractgeopdfexporter.h
                             --------------------------
    begin                : August 2019
    copyright            : (C) 2019 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 QGSABSTRACTGEOPDFEXPORTER_H
#define QGSABSTRACTGEOPDFEXPORTER_H

#include "qgis_core.h"
#include "qgsabstractmetadatabase.h"
#include "qgscoordinatereferencesystem.h"
#include "qgsfeature.h"
#include "qgslayertree.h"
#include "qgspolygon.h"

#include <QDateTime>
#include <QList>
#include <QMutex>
#include <QPainter>
#include <QString>
#include <QTemporaryDir>

#define SIP_NO_FILE

using namespace Qt::StringLiterals;


class QgsGeospatialPdfRenderedFeatureHandler;


///@cond PRIVATE
struct TreeNode
{
    QString id;
    bool initiallyVisible = false;
    QString name;
    QString mutuallyExclusiveGroupId;
    QString mapLayerId;
    std::vector< std::unique_ptr< TreeNode > > children;
    TreeNode *parent = nullptr;
    bool isRootNode = false;

    void addChild( std::unique_ptr< TreeNode > child )
    {
      child->parent = this;
      children.emplace_back( std::move( child ) );
    }

    QDomElement toElement( QDomDocument &doc ) const
    {
      QDomElement layerElement = doc.createElement( u"Layer"_s );
      layerElement.setAttribute( u"id"_s, id );
      layerElement.setAttribute( u"name"_s, name );
      layerElement.setAttribute( u"initiallyVisible"_s, initiallyVisible ? u"true"_s : u"false"_s );
      if ( !mutuallyExclusiveGroupId.isEmpty() )
        layerElement.setAttribute( u"mutuallyExclusiveGroupId"_s, mutuallyExclusiveGroupId );

      for ( const auto &child : children )
      {
        layerElement.appendChild( child->toElement( doc ) );
      }

      return layerElement;
    }

    void toChildrenElements( QDomDocument &doc, QDomElement &layerTreeElem ) const
    {
      for ( const auto &child : children )
      {
        layerTreeElem.appendChild( child->toElement( doc ) );
      }
    }

    QDomElement createIfLayerOnElement( QDomDocument &doc, QDomElement &contentElement ) const
    {
      QDomElement element = doc.createElement( u"IfLayerOn"_s );
      element.setAttribute( u"layerId"_s, id );
      contentElement.appendChild( element );
      return element;
    }

    QDomElement createNestedIfLayerOnElements( QDomDocument &doc, QDomElement &contentElement ) const
    {
      TreeNode *currentParent = parent;
      QDomElement finalElement = doc.createElement( u"IfLayerOn"_s );
      finalElement.setAttribute( u"layerId"_s, id );

      QDomElement currentElement = finalElement;
      while ( currentParent && !currentParent->isRootNode )
      {
        QDomElement ifGroupOn = doc.createElement( u"IfLayerOn"_s );
        ifGroupOn.setAttribute( u"layerId"_s, currentParent->id );
        ifGroupOn.appendChild( currentElement );
        currentElement = ifGroupOn;
        currentParent = currentParent->parent;
      }
      contentElement.appendChild( currentElement );
      return finalElement;
    }
};
///@endcond


/**
 * \class QgsAbstractGeospatialPdfExporter
 * \ingroup core
 *
 * \brief Abstract base class for Geospatial PDF exporters.
 *
 * The base class handles generic Geospatial PDF export setup, cleanup and processing steps.
 *
 * This class is a low level implementation detail only. Generally, you should use the high level interface exposed by
 * classes like QgsLayoutExporter instead.
 *
 * \warning QgsAbstractGeospatialPdfExporter is designed to be a short lived object. It should be created for a
 * single export operation only, and then immediately destroyed. Failure to correctly
 * destroy the object after exporting a PDF will leave the application in an inconsistent, unstable state.
 *
 * \note Not available in Python bindings
 *
 * \since QGIS 3.10
 */
class CORE_EXPORT QgsAbstractGeospatialPdfExporter
{
  public:
    /**
     * Returns TRUE if the current QGIS build is capable of Geospatial PDF support.
     *
     * If FALSE is returned, a user-friendly explanation why can be retrieved via
     * geospatialPDFAvailabilityExplanation().
     */
    static bool geospatialPDFCreationAvailable();

    /**
     * Returns a user-friendly, translated string explaining why Geospatial PDF export
     * support is not available on the current QGIS build (or an empty string if
     * Geospatial PDF support IS available).
     * \see geospatialPDFCreationAvailable()
     */
    static QString geospatialPDFAvailabilityExplanation();

    QgsAbstractGeospatialPdfExporter() = default;

    virtual ~QgsAbstractGeospatialPdfExporter() = default;

    /**
     * Contains information about a feature rendered inside the PDF.
     */
    struct RenderedFeature
    {
        RenderedFeature() = default;

        /**
       * Constructor for RenderedFeature.
       */
        RenderedFeature( const QgsFeature &feature, const QgsGeometry &renderedBounds )
          : feature( feature )
          , renderedBounds( renderedBounds )
        {}

        /**
       * Rendered feature.
       */
        QgsFeature feature;

        /**
       * Bounds, in PDF units, of rendered feature. (Multi)LineString or Polygon types only.
       */
        QgsGeometry renderedBounds;
    };

    /**
     * \brief Contains details of a particular input component to be used during PDF composition.
     * \ingroup core
     * \since QGIS 3.10
     */
    struct CORE_EXPORT ComponentLayerDetail
    {
        //! User-friendly name for the generated PDF layer
        QString name;

        //! Associated map layer ID, or an empty string if this component layer is not associated with a map layer
        QString mapLayerId;

        //! Optional group name, for arranging layers in top-level groups
        QString group;

        //! File path to the (already created) PDF to use as the source for this component layer
        QString sourcePdfPath;

        //! Component composition mode
        QPainter::CompositionMode compositionMode = QPainter::CompositionMode_SourceOver;

        //! Component opacity
        double opacity = 1.0;
    };

    /**
     * \brief Contains details of a control point used during georeferencing Geospatial PDF outputs.
     * \ingroup core
     * \since QGIS 3.10
     */
    struct ControlPoint
    {
        /**
       * Constructor for ControlPoint, at the specified \a pagePoint (in millimeters)
       * and \a geoPoint (in CRS units).
       */
        ControlPoint( const QgsPointXY &pagePoint, const QgsPointXY &geoPoint )
          : pagePoint( pagePoint )
          , geoPoint( geoPoint )
        {}

        //! Coordinate on the page of the control point, in millimeters
        QgsPointXY pagePoint;

        //! Georeferenced coordinate of the control point, in CRS units
        QgsPointXY geoPoint;
    };

    struct GeoReferencedSection
    {
        /**
       * Bounds of the georeferenced section on the page, in millimeters.
       *
       * \note if pageBoundsPolygon is specified then this setting is ignored.
       */
        QgsRectangle pageBoundsMm;

        /**
       * Bounds of the georeferenced section on the page, in millimeters, as a free-form polygon.
       *
       * If specified, this will be used instead of pageBoundsMm.
       */
        QgsPolygon pageBoundsPolygon;

        //! Coordinate reference system for georeferenced section
        QgsCoordinateReferenceSystem crs;

        //! List of control points corresponding to this georeferenced section
        QList< QgsAbstractGeospatialPdfExporter::ControlPoint > controlPoints;
    };

    /**
     * Called multiple times during the rendering operation, whenever a \a feature associated with the specified
     * \a layerId is rendered.
     *
     * The optional \a group argument can be used to differentiate features from the same layer exported
     * multiple times as part of different layer groups.
     */
    void pushRenderedFeature( const QString &layerId, const QgsAbstractGeospatialPdfExporter::RenderedFeature &feature, const QString &group = QString() );

    struct ExportDetails
    {
        //! Page size, in millimeters
        QSizeF pageSizeMm;

        //! Output DPI
        double dpi = 300;

        //! List of georeferenced sections
        QList< QgsAbstractGeospatialPdfExporter::GeoReferencedSection > georeferencedSections;

        //! Metadata author tag
        QString author;

        //! Metadata producer tag
        QString producer;

        //! Metadata creator tag
        QString creator;

        //! Metadata creation datetime
        QDateTime creationDateTime;

        //! Metadata subject tag
        QString subject;

        //! Metadata title tag
        QString title;

        //! Metadata keyword map
        QgsAbstractMetadataBase::KeywordMap keywords;

        /**
       * TRUE if ISO32000 extension format georeferencing should be used.
       *
       * This is a recommended setting which results in Geospatial PDF files
       * that use PDF 2.0 Geospatial Features (ISO 32000-2:2017).
       * Compatible with the built-in Acrobat geospatial tools
       */
        bool useIso32000ExtensionFormatGeoreferencing = true;

        /**
       * TRUE if OGC "best practice" format georeferencing should be used.
       *
       * \warning This results in Geospatial PDF files compatible with a unnamed suite of tools starting with Terra and ending with Go, but
       * can break compatibility with the built-in Acrobat geospatial tools (yes, Geospatial PDF
       * format is a mess!).
       *
       * \deprecated QGIS 3.42. This parameter has no longer any effect. Only ISO 32000 georeferencing is handled.
      */
        bool useOgcBestPracticeFormatGeoreferencing = false;

        /**
       * TRUE if feature vector information (such as attributes) should be exported.
       */
        bool includeFeatures = true;

        /**
       * Optional map of map layer ID to custom logical layer tree group in created PDF file.
       *
       * E.g. if the map contains "layer1": "Environment", "layer2": "Environment", "layer3": "Transport"
       * then the created PDF file will have entries in its layer tree for "Environment" and "Transport",
       * and toggling "Environment" will toggle BOTH layer1 and layer2.
       *
       * Layers which are not included in this group will always have their own individual layer tree entry
       * created for them automatically.
       *
       * If ExportDetails::useLayerTreeConfig is TRUE then this option has no effect.
       *
       * \see layerTreeGroupOrder
       */
        QMap< QString, QString > customLayerTreeGroups;

        /**
       * Optional map of map layer ID to custom layer tree name to show in the created PDF file.
       *
       * If ExportDetails::useLayerTreeConfig is TRUE then this option has no effect.
       *
       * \since QGIS 3.14
       */
        QMap< QString, QString > layerIdToPdfLayerTreeNameMap;

        /**
       * Optional map of map layer ID to initial visibility state. If a layer ID is not present in this,
       * it will default to being initially visible when opening the PDF.
       *
       * If ExportDetails::useLayerTreeConfig is TRUE then this option has no effect.
       *
       * \since QGIS 3.14
       */
        QMap< QString, bool > initialLayerVisibility;

        /**
       * Optional list of layer IDs, in the order desired to appear in the generated Geospatial PDF file.
       *
       * Layers appearing earlier in the list will show earlier in the Geospatial PDF layer tree list.
       *
       * If ExportDetails::useLayerTreeConfig is TRUE then this option has no effect.
       *
       * \see layerTreeGroupOrder
       *
       * \since QGIS 3.14
       */
        QStringList layerOrder;

        /**
       * Specifies the ordering of layer tree groups in the generated Geospatial PDF file.
       *
       * Groups appearing earlier in the list will show earlier in the Geospatial PDF layer tree list.
       *
       * If ExportDetails::useLayerTreeConfig is TRUE then this option has no effect.
       *
       * \see layerOrder
       * \see customLayerTreeGroups
       *
       * \since QGIS 3.38
       */
        QStringList layerTreeGroupOrder;

        /**
       * Contains a list of group names which should be considered as mutually exclusive.
       *
       * If ExportDetails::useLayerTreeConfig is TRUE then this option has no effect.
       *
       * \since QGIS 3.40
       */
        QSet< QString > mutuallyExclusiveGroups;

        /**
       * If set to TRUE, the layer tree from the QGIS project should be used when creating a Geospatial PDF.
       * In that case, layer/group names, order, and visibility from the QGIS project will be reflected in the output PDF.
       *
       * When active, other settings like layerOrder, customLayerTreeGroups,
       * layerTreeGroupOrder, initialLayerVisibility, mutuallyExclusiveGroups, and
       * layerIdToPdfLayerTreeNameMap have no effect.
       *
       * \see layerOrder
       * \see customLayerTreeGroups
       * \see layerTreeGroupOrder
       * \see initialLayerVisibility
       * \see layerIdToPdfLayerTreeNameMap
       * \see mutuallyExclusiveGroups
       *
       * \since QGIS 4.2
       */
        bool useLayerTreeConfig = false;
    };

    /**
     * To be called after the rendering operation is complete.
     *
     * Will export the list of PDF layer \a components to a new PDF file at \a destinationFile. The \a components
     * argument gives a list of additional layers to include in the generated PDF file. These must have already
     * been created, e.g. as a result of rendering layers to separate PDF source files.
     *
     * Any features previously collected by calls to pushRenderedFeature() will be included automatically in the Geospatial PDF
     * export.
     *
     * Returns TRUE if the operation was successful, or FALSE if an error occurred. If an error occurred, it
     * can be retrieved by calling errorMessage().
     */
    bool finalize( const QList< QgsAbstractGeospatialPdfExporter::ComponentLayerDetail > &components, const QString &destinationFile, const ExportDetails &details );

    /**
     * Returns the last error message encountered during the export.
     */
    QString errorMessage() const { return mErrorMessage; }

    /**
     * Returns a file path to use for temporary files required for Geospatial PDF creation.
     */
    QString generateTemporaryFilepath( const QString &filename ) const;

    /**
     * Returns TRUE if the specified composition \a mode is supported for layers
     * during Geospatial PDF exports.
     *
     * \since QGIS 3.14
     */
    static bool compositionModeSupported( QPainter::CompositionMode mode );

  protected:
    /**
     * Contains information relating to a single PDF layer in the Geospatial PDF export.
     */
    struct VectorComponentDetail
    {
        //! User-friendly name for the generated PDF layer
        QString name;

        //! Associated map layer ID
        QString mapLayerId;

        //! Optional layer group name
        QString group;

        //! Field name for display
        QString displayAttribute;

        //! File path to the (already created) vector dataset to use as the source for this component layer
        QString sourceVectorPath;

        //! Layer name in vector dataset to use as the source
        QString sourceVectorLayer;
    };

    /**
     * Hardcode DPI of 72 to get correct page sizes in outputs.
     * Refs discussion in https://github.com/OSGeo/gdal/pull/2961
     *
     * \note Not available in Python bindings
     * \since QGIS 4.2
     */
    static constexpr double DPI_72 = 72 SIP_SKIP;

  private:
    QMutex mMutex;
    QMap< QString, QMap< QString, QgsFeatureList > > mCollatedFeatures;

    /**
     * Returns the PDF output component details for the layer with given \a layerId.
     */
    virtual VectorComponentDetail componentDetailForLayerId( const QString &layerId ) = 0;

    /**
     * Returns the QGIS layer tree so that a composition XML can be created based on its configuration.
     */
    virtual QgsLayerTree *layerTree() const = 0;

    QList< VectorComponentDetail > mVectorComponents;

    QString mErrorMessage;
    QTemporaryDir mTemporaryDir;


    bool saveTemporaryLayers();

    QString createCompositionXml( const QList< QgsAbstractGeospatialPdfExporter::ComponentLayerDetail > &components, const ExportDetails &details ) const;
    void createMetadataXmlSection( QDomElement &compositionElem, QDomDocument &doc, const ExportDetails &details ) const;
    void createPageDimensionXmlSection( QDomElement &pageElem, QDomDocument &doc, const double pageWidthPdfUnits, const double pageHeightPdfUnits ) const;
    void createGeoreferencingXmlSection( QDomElement &pageElem, QDomDocument &doc, const ExportDetails &details, const double pageWidthPdfUnits, const double pageHeightPdfUnits ) const;
    void createContentXmlSection(
      QDomElement &contentElem,
      QDomDocument &doc,
      const QMap< QString, TreeNode * > &groupNameToTreeNode,
      const QMap< QString, TreeNode * > &layerIdToTreeNode,
      const QList<ComponentLayerDetail> &components,
      const ExportDetails &details
    ) const;

    void createLayerTreeAndContentXmlSections( QDomElement &compositionElem, QDomElement &pageElem, QDomDocument &doc, const QList<ComponentLayerDetail> &components, const ExportDetails &details ) const;
    void createLayerTreeAndContentXmlSectionsFromLayerTree(
      const QgsLayerTree *layerTree, QDomElement &compositionElem, QDomElement &pageElem, QDomDocument &doc, const QList<ComponentLayerDetail> &components, const ExportDetails &details
    ) const;

    /**
     * Creates a TreeNode structure from a given layer tree group recursively.
     */
    std::unique_ptr< TreeNode > createPdfTreeNodes( QMap< QString, TreeNode * > &groupNameToTreeNode, QMap< QString, TreeNode * > &layerIdToTreeNode, const QgsLayerTreeGroup *layerTreeGroup ) const;

    /**
     * Returns the GDAL string representation of the specified QPainter composition \a mode.
     */
    static QString compositionModeToString( QPainter::CompositionMode mode );

    friend class TestQgsLayoutGeospatialPdfExport;
    friend class TestQgsGeospatialPdfExport;
};

#endif //QGSABSTRACTGEOPDFEXPORTER_H
