/**********************************************************************************************
    Copyright (C) 2014 Oliver Eichler <oliver.eichler@gmx.de>
    Copyright (C) 2017 Norbert Truchsess <norbert.truchsess@t-online.de>

    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 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.

**********************************************************************************************/

#ifndef IGISPROJECT_H
#define IGISPROJECT_H

#include <QDebug>
#include <QMessageBox>
#include <QPointer>

#include "gis/IGisItem.h"
#include "gis/IWksItem.h"
#include "gis/search/CProjectFilterItem.h"
#include "gis/search/CSearch.h"
#include "helpers/CSelectCopyAction.h"

class CGisListWks;
class CGisDraw;
class CGisItemWpt;
class QDataStream;
class CDetailsPrj;
class IDevice;
class CThread;

class IGisProject : public IWksItem {
  Q_DECLARE_TR_FUNCTIONS(IGisProject)
 public:
  /// flags used to serialize trivial flags in qms file
  enum flags_e { eFlagNoCorrelation = 0x1, eFlagAutoSave = 0x2, eFlagInvalidDataOk = 0x4, eFlagAutoSyncToDev = 0x8 };

  enum sorting_roadbook_e {
    eSortRoadbookNone,
    eSortRoadbookTrackWithDouble,
    eSortRoadbookTrackWithoutDouble,
    eSortRoadbookTrackWithDetails
  };

  enum sorting_folder_e { eSortFolderTime, eSortFolderName, eSortFolderSymbol, eSortFolderRating };

  struct person_t {
    QString name;
    QString id;
    QString domain;
    IGisItem::link_t link;
  };

  struct copyright_t {
    QString author;
    QString year;
    QString license;
  };

  struct metadata_t {
    metadata_t() : time(QDateTime::currentDateTimeUtc()) {}
    QString name;
    QString desc;
    person_t author;
    copyright_t copyright;
    QList<IGisItem::link_t> links;
    QDateTime time;
    QString keywords;
    QRectF bounds;
    // -- all gpx tags - stop
    QMap<QString, QVariant> extensions;
  };

  static const QString filedialogAllSupported;
  static const QString filedialogFilterGPX;
  static const QString filedialogFilterTCX;
  static const QString filedialogFilterSML;
  static const QString filedialogFilterLOG;
  static const QString filedialogFilterQLB;
  static const QString filedialogFilterQMS;
  static const QString filedialogFilterSLF;
  static const QString filedialogFilterFIT;
  static const QString filedialogSaveFilters;
  static const QString filedialogLoadFilters;

  IGisProject(type_e type, const QString& filename, CGisListWks* parent);
  IGisProject(type_e type, const QString& filename, IDevice* parent);

  static IGisProject* create(const QString filename, CGisListWks* parent);

  /**
   * @brief Delayed delete to abort any threaded operations.
   *
   * This is the preferred way to destroy a IGisProject instance.
   */
  void destroyLater();

  /**
     @brief Ask to save the project before it is closed.

     If the project is closed, the user is asked if the project should be saved and saved on user request.

     @return True if the operation is aborted. False on "save" and "no".
   */
  bool askBeforClose();

  IGisProject& operator=(const IGisProject& p) {
    key = p.key;
    metadata = p.metadata;
    return *this;
  }

  /**
     @brief Summon the project details dialog.
   */
  void edit();

  /**
     @brief Return true if saving should be skipped.
   */
  virtual bool skipSave() const { return false; }

  virtual const QString getFileDialogFilter() const { return QString(); }

  virtual const QString getFileExtension() const { return QString(); }

  /**
     @brief Save the project using it's native format.
   */
  virtual bool save();

  /**
     @brief Save the project selecting one of the available formats.
   */
  bool saveAs(QString fn = QString(), QString filter = QString());

  /**
     @brief Save as strict GPX V 1.1 without any extensions and HTML
     @return True on success
   */
  bool saveAsStrictGpx11();

  virtual void setFilename(const QString& fn) { filename = fn; }

  virtual QString getFilename() const { return filename; }

  /**
     @brief Get the project type enumeration.

     @Note: usually dynamic_cast should be used to get a pointer of correct type.
            However if the project is serialized, a type id is needed.

     @return One of type_e
   */
  type_e getType() const { return static_cast<type_e>(type()); }

  /**
     @brief Get unique project key.
     @return A MD5 hash string
   */
  const QString& getKey() const {
    genKey();
    return key;
  }

  /**
     @brief Get the unique key of the device the project is attached to
     @return If the project is not attached to a device the string is empty
   */
  QString getDeviceKey() const;

  /**
     @brief Get the project's name
     @return The name from metadata.name
   */
  const QString& getName() const override;
  /**
     @brief Get the project's name extended with the parent's name.
     @return The name from metadata.nam appended with either the device name or the database parent folder's name.
   */
  QString getNameEx() const;

  const QDateTime& getTime() const { return metadata.time; }
  const QString& getKeywords() const { return metadata.keywords; }
  const QString& getDescription() const { return metadata.desc; }
  const QList<IGisItem::link_t>& getLinks() const { return metadata.links; }

  const metadata_t& getMetadata() const { return metadata; }

  /**
     @brief Get the sorting mode
     @return One of sorting_e
   */
  sorting_roadbook_e getSortingRoadbook() const { return sortingRoadbook; }

  sorting_folder_e getSortingFolder() const { return sortingFolder; }

  void setName(const QString& str);
  void setKeywords(const QString& str);
  void setDescription(const QString& str);
  void setLinks(const QList<IGisItem::link_t>& links);
  /**
     @brief Set change mark
   */
  void setChanged();

  /**
     @brief Set the sorting mode for the roadbook in the details dialog

     This will mark the project as changed.

     @param s the mode
   */
  void setSortingRoadbook(sorting_roadbook_e s);

  /**
     @brief Set the sorting mode for workspace folder

     This will mark the project as changed.

     @param s the mode
   */
  void setSortingFolder(sorting_folder_e s);

  /**
     @brief Get a short metadata summary
     @return Informational string.
   */
  QString getInfo(quint32) const override;

  /**
     @brief Get a temporary pointer to the item with matching key
     @param key
     @return If no item is found 0 is returned.
   */
  IGisItem* getItemByKey(const IGisItem::key_t& key);

  void getItemsByKeys(const QList<IGisItem::key_t>& keys, QList<IGisItem*>& items);
  /**
     @brief Get a list of items that are close to a given pixel coordinate of the screen

     @note: The returned pointers are just for temporary use. Best you use them to get the item's key.

     @param pos       the coordinate on the screen in pixel
     @param items     a list the item's pointer is stored to.
   */
  void getItemsByPos(const QPointF& pos, QList<IGisItem*>& items);

  void getItemsByArea(const QRectF& area, IGisItem::selflags_t flags, QList<IGisItem*>& items);

  void getNogoAreas(QList<IGisItem*>& nogos) const;

  int getItemCountByType(IGisItem::type_e type) const { return cntItemsByType[type]; }

  qreal getTotalDistance() const { return totalDistance; }
  qreal getTotalAscent() const { return totalAscent; }
  qreal getTotalDescent() const { return totalDescent; }
  bool isTrkElevationInvalid() const { return trkElevationInvalid; }
  qreal getTotalElapsedSeconds() const { return totalElapsedSeconds; }
  qreal getTotalElapsedSecondsMoving() const { return totalElapsedSecondsMoving; }

  bool doCorrelation() const { return !noCorrelation; }

  void switchOnCorrelation();

  void setAutoSave(bool on) override;

  void setInvalidDataOk(bool ok) {
    invalidDataOk = ok;
    setChanged();
  }

  bool getInvalidDataOk() const { return invalidDataOk; }

  /**
     @brief Receive the current mouse position

     Iterate over all items and pass the position

     @param pos   the mouse position on the screen in pixel
   */
  virtual void mouseMove(const QPointF& pos);

  /**
     @brief Delete items with matching key
     @param key
   */
  bool delItemByKey(const IGisItem::key_t& key, QMessageBox::StandardButtons& last);

  /**
     @brief Call IGisItem::edit() method for items with given key

     @param key   a MD5 hash key
   */
  void editItemByKey(const IGisItem::key_t& key);

  /**
     @brief Add a copy if the given item to the project

     Before the item is inserted the method will use it's key to find a duplicate item.
     If there is an item with the same item key a copy option dialog is shown. Depending
     the result the action is performed or aborted. The result will be copied into
     lastResult to repeat the same decision on subsequent items.

     @param item          pointer to item
     @param off           the offset into the tree widget, -1 for none
     @param lastResult    a reference to hold the last result of the copy option dialog
   */
  void insertCopyOfItem(IGisItem* item, int off, CSelectCopyAction::result_e& lastResult);

  /**
     @brief Check if the project was initialized correctly.

     For example a if a GPX file does not load correctly the project is invalid.

     @return True if project is valid
   */
  bool isValid() const { return valid; }

  /**
     @brief Test if this project is handled by a device
     @return The device type (IDevice::type_e). IDevice::eTypeNone if the project is not stored on a device.
   */
  const qint32 isOnDevice() const override;

  void drawItem(QPainter& p, const QPolygonF& viewport, QList<QRectF>& blockedAreas, CGisDraw* gis);
  void drawLabel(QPainter& p, const QPolygonF& viewport, QList<QRectF>& blockedAreas, const QFontMetricsF& fm,
                 CGisDraw* gis);
  void drawItem(QPainter& p, const QRectF& viewport, CGisDraw* gis);

  /**
     @brief Serialize object out of a QDataStream

     See CGisSerialization.cpp for implementation

     @param stream the binary data stream
     @return The stream object.
   */
  virtual QDataStream& operator<<(QDataStream& stream);

  /**
     @brief Serialize object into a QDataStream

     See CGisSerialization.cpp for implementation

     @param stream the binary data stream
     @return The stream object.
   */
  virtual QDataStream& operator>>(QDataStream& stream) const;

  /**
     @brief writeMetadata
     @param doc
     @return
   */
  QDomNode writeMetadata(QDomDocument& doc, bool strictGpx11);

  /**
     @brief Mount volume the project's file is stored at

     This is only valid for projects located on GPS devices.
     For all other projects the method does nothing.
   */
  void mount();
  /**
     @brief Umount volume the project's file is stored at

     This is only valid for projects located on GPS devices.
     For all other projects the method does nothing.
   */
  void umount();

  /**
     @brief Removed the projects file from disk.

     This is only valid for projects located on GPS devices.
     For all other projects the method does nothing.
   */
  bool remove();

  /**
     @brief Block update of items.

      Use this to speed up actions with many items, e.g. copy actions.
      If the blocking is stopped (yes == false) updateItems() is called.

     @param yes set true to block updating items
   */
  void blockUpdateItems(bool yes);

  void setProjectFilter(const CSearch& search);
  void setWorkspaceFilter(const CSearch& search);
  void applyFilters();

  void confirmPendingAutoSave() { autoSavePending = false; }
  void confirmPendingAutoSyncToDev() { autoSyncToDevPending = false; }

  bool findPolylineCloseBy(const QPointF& pt1, const QPointF& pt2, qint32& threshold, QPolygonF& polyline);

  void gainUserFocus(bool yes) override;

  bool hasUserFocus() const override { return keyUserFocus == key; }

  static const QString& getUserFocus() { return keyUserFocus; }

  void filterProject(bool filter);
  CProjectFilterItem* getProjectFilterItem() { return projectFilter; }

  /**
     @brief  Return state of current update block
     @return True if updates are blocked.
   */
  bool isNoUpdate() const { return noUpdate; }

 protected:
  /**
   * @brief Do not use delete directly. Use destroyLater() instead.
   */
  virtual ~IGisProject();
  using IWksItem::updateDecoration;
  void genKey() const;
  virtual void setupName(const QString& defaultName);
  /**
   * @brief Check whether it is possible to load items into the project.
   *
   * Called by IGisProject::operator<<() after all item data has been read from the stream,
   * but before the background load thread is started. If this returns false the thread is
   * not started and the project remains invalid, so the caller can safely discard it.
   *
   * The default implementation always returns true. Derived classes that depend on external
   * resources (e.g. a database connection) should override this to verify that those
   * resources are available before committing to a load.
   *
   * @return true if loading should proceed, false to skip the load thread entirely.
   */
  virtual bool isPossibleToLoad() { return true; }
  void markAsSaved();
  void readMetadata(const QDomNode& xml, metadata_t& metadata);
  void updateItems();
  void updateItemCounters();
  void updateDecoration();
  // void updateDecoration(bool saved);
  void sortItems();
  void sortItems(QList<IGisItem*>& items) const;

  /**
     @brief Converts a string with HTML tags to a string without HTML depending on the device

     Some devices e.g. Garmin can not handle HTML.

     @param str   a string
     @return A string with HTML removed depending on the device
   */
  QString html2Dev(const QString& str);

  // Those are the URIs of the GPX extensions we support
  static const QString gpxx_ns;
  static const QString gpxtpx_ns;
  static const QString wptx1_ns;
  static const QString rmc_ns;
  static const QString ql_ns;
  static const QString gs_ns;
  static const QString tp1_ns;
  // Those are standard GPX/XML namespaces
  static const QString gpx_ns;
  static const QString xsi_ns;
  static const QString gpxdata_ns;

  struct namespace_decl_t {
    QString prefix;  ///< without "xmlns:", e.g. "gpxx"
    QString uri;
  };

  /**
     @brief The extension namespaces (prefix + URI) writeMetadata() declares on a non-strict GPX file.
   */
  static const QList<namespace_decl_t>& extensionNamespaces();

  static QString keyUserFocus;

  QPointer<CDetailsPrj> dlgDetails;

  // type_e type;
  mutable QString key;
  QString filename;
  bool valid = false;
  bool noCorrelation = false;
  bool changedRoadbookMode = false;

  bool autoSavePending = false;       ///< flag to show if auto save event has been sent. will be reset by save()
  bool invalidDataOk = false;         ///< if set invalid data in GIS items will not raise any dialog
  bool autoSyncToDevPending = false;  ///< flag to show that a sync to device is already pending

  metadata_t metadata;
  /// passthrough xmlns:* declarations (prefix -> URI) found on the loaded GPX root that are not one of the namespaces above
  QMap<QString, QString> extraNamespaces;
  QString nameSuffix;

  sorting_roadbook_e sortingRoadbook = eSortRoadbookNone;
  sorting_folder_e sortingFolder = eSortFolderTime;

  qint32 cntItemsByType[IGisItem::eTypeMax];

  qint32 cntTrkPts = 0;
  qint32 cntWpts = 0;

  qreal totalDistance = 0;
  qreal totalAscent = 0;
  qreal totalDescent = 0;
  bool trkElevationInvalid = false;
  quint32 totalElapsedSeconds = 0;
  quint32 totalElapsedSecondsMoving = 0;

  QString hashTrkWpt[2];

  CSearch projectSearch = CSearch("");
  CSearch workspaceSearch = CSearch("");

  QPointer<CProjectFilterItem> projectFilter;
  QPointer<CThread> threadLoadPoject;

 private:
  bool noUpdate = false;
};
Q_DECLARE_METATYPE(IGisProject*)

class CProjectMountLock {
 public:
  CProjectMountLock(IGisProject& project) : project(project) { project.mount(); }

  ~CProjectMountLock() { project.umount(); }

 private:
  IGisProject& project;
};

#endif  // IGISPROJECT_H
