// Copyright (C) 2019  Joseph Artsimovich <joseph.artsimovich@gmail.com>, 4lex4 <4lex49@zoho.com>
// Use of this source code is governed by the GNU GPLv3 license that can be found in the LICENSE file.

#ifndef SCANTAILOR_APP_THUMBNAILSEQUENCE_H_
#define SCANTAILOR_APP_THUMBNAILSEQUENCE_H_

#include <QObject>
#include <memory>
#include <set>
#include <vector>

#include "BeforeOrAfter.h"
#include "FlagOps.h"
#include "NonCopyable.h"
#include "PageOrderProvider.h"
#include "PageRange.h"

class QGraphicsItem;
class QGraphicsView;
class PageId;
class ImageId;
class PageInfo;
class PageSequence;
class ThumbnailFactory;
class QSizeF;
class QRectF;
class QPoint;

class ThumbnailSequence : public QObject {
  Q_OBJECT
  DECLARE_NON_COPYABLE(ThumbnailSequence)

 public:
  enum SelectionAction { KEEP_SELECTION, RESET_SELECTION, FORCE_RESET_SELECTION };

  enum SelectionFlags {
    DEFAULT_SELECTION_FLAGS = 0,

    /** Indicates the item was selected by a user action, rather than programmatically. */
    SELECTED_BY_USER = 1 << 0,

    /**
     * Indicates that the request to make this item a selection leader was redundant,
     * as it's already a selection leader.
     */
    REDUNDANT_SELECTION = 1 << 1,

    /**
     * This flag is set when Ctrl-clicking the current selection leader while other
     * selected items exist.  In this case, the leader will become unselected, and
     * one of the other selected items will be promoted to a selection leader.
     * In these circumstances, scrolling to make the new selection leader visible
     * is undesireable.
     */
    AVOID_SCROLLING_TO = 1 << 2,

    /** Indicates the items selection was cleared. */
    SELECTION_CLEARED = 1 << 3
  };

  enum ViewMode { SINGLE_COLUMN, MULTI_COLUMN };

  explicit ThumbnailSequence(const QSizeF& maxLogicalThumbSize, ViewMode viewMode);

  ~ThumbnailSequence() override;

  void setThumbnailFactory(std::shared_ptr<ThumbnailFactory> factory);

  void attachView(QGraphicsView* view);

  /**
   * \brief Re-populate the list of thumbnails.
   *
   * \param pages Pages to put in the sequence.
   * \param selectionAction Whether to keep the selection, provided
   *        selected item(s) are still present in the new list of pages.
   * \param orderProvider The source of ordering information.  It will
   *        be preserved until the next reset() call and will be taken
   *        into account by other methods, like invalidateThumbnail()
   *        and insert().  A null order provider indicates to keep the
   *        order of ProjectPages.
   */
  void reset(const PageSequence& pages,
             SelectionAction selectionAction,
             std::shared_ptr<const PageOrderProvider> orderProvider = nullptr);

  /** Returns the current page order provider, which may be null. */
  std::shared_ptr<const PageOrderProvider> pageOrderProvider() const;

  PageSequence toPageSequence() const;

  /**
   * \brief Updates position of all the thumbnails in the view.
   *
   * \note This function doesn't update thumbnails appearance.
   */
  void updateSceneItemsPos();

  /**
   * \brief Updates appearance and possibly position of a thumbnail.
   *
   * If thumbnail's size or position have changed and this thumbnail
   * is a selection leader, newSelectionLeader() signal will be emitted
   * with REDUNDANT_SELECTION flag set.
   *
   * \note This function assumes the thumbnail specified by pageId
   *       is the only thumbnail at incorrect position.  If you do
   *       something that changes the logical position of more than
   *       one thumbnail at once, use invalidateAllThumbnails()
   *       instead of sequentially calling invalidateThumbnail().
   */
  void invalidateThumbnail(const PageId& pageId);

  /**
   * This signature differs from invalidateThumbnail(PageId) in that
   * it will cause PageInfo stored by ThumbnailSequence to be updated.
   */
  void invalidateThumbnail(const PageInfo& pageInfo);

  /**
   * \brief Updates appearance of all thumbnails and possibly their order.
   *
   * Whether or not order will be updated depends on whether an order provider
   * was specified by the most recent reset() call.
   */
  void invalidateAllThumbnails();

  /**
   * \brief Makes the item a selection leader, and unselects other items.
   *
   * \param pageId The page to select.
   * \param selectionAction Whether to keep the selection, provided
   *        selected item(s) are still present in the new list of pages.
   * \return true on success, false if the requested page wasn't found.
   *
   * On success, the newSelectionLeader() signal is emitted, possibly
   * with REDUNDANT_SELECTION flag set, in case our page was already the
   * selection leader.
   */
  bool setSelection(const PageId& pageId, SelectionAction selectionAction = FORCE_RESET_SELECTION);

  /**
   * \brief Returns the current selection leader.
   *
   * A null PageInfo is returned if no items are currently selected.
   */
  PageInfo selectionLeader() const;

  /**
   * \brief Returns the page immediately preceding the given one.
   *
   * A null PageInfo is returned if the given page wasn't found or
   * there are no pages preceding it.
   */
  PageInfo prevPage(const PageId& referencePage) const;

  /**
   * \brief Returns the page immediately following the given one.
   *
   * A null PageInfo is returned if the given page wasn't found or
   * there are no pages following it.
   */
  PageInfo nextPage(const PageId& referencePage) const;

  /**
   * \brief Returns the selected page preceding the given one.
   *
   * A null PageInfo is returned if the given page wasn't found or
   * there are no pages preceding it.
   */
  PageInfo prevSelectedPage(const PageId& referencePage) const;

  /**
   * \brief Returns the selected page following the given one.
   *
   * A null PageInfo is returned if the given page wasn't found or
   * there are no pages following it.
   */
  PageInfo nextSelectedPage(const PageId& referencePage) const;

  /**
   * \brief Returns the first page in the sequence.
   *
   * A null PageInfo is returned if the sequence is empty.
   */
  PageInfo firstPage() const;

  /**
   * \brief Returns the last page in the sequence.
   *
   * A null PageInfo is returned if the sequence is empty.
   */
  PageInfo lastPage() const;

  /**
   * \brief Inserts a page before the first page with matching ImageId.
   *
   * If no order provider was specified by the previous reset() call,
   * we won't allow inserting a page between two halves of another page,
   * to be compatible with what reset() does.  Otherwise, the new
   * page will be inserted at a correct position according to the current
   * order provider.  In this case \p beforeOrAfter doesn't really matter.
   *
   * If there are no pages with matching ImageId, the new page won't
   * be inserted, unless the request is to insert BEFORE a null ImageId(),
   * which would cause insertion at the end.
   */
  void insert(const PageInfo& newPage, BeforeOrAfter beforeOrAfter, const ImageId& image);

  void removePages(const std::set<PageId>& pages);

  /**
   * \brief The bounding rectangle in scene coordinates of the selection leader.
   *
   * Returns a null rectangle if no item is currently selected.
   */
  QRectF selectionLeaderSceneRect() const;

  std::set<PageId> selectedItems() const;

  std::vector<PageRange> selectedRanges() const;

  const QSizeF& getMaxLogicalThumbSize() const;

  void setMaxLogicalThumbSize(const QSizeF& size);

  ViewMode getViewMode() const;

  void setViewMode(ViewMode mode);

 public slots:
  void setSelectionModeEnabled(bool enabled);

 signals:

  void newSelectionLeader(const PageInfo& pageInfo, const QRectF& thumbRect, ThumbnailSequence::SelectionFlags flags);

  /**
   * Emitted when a user right-clicks on a page thumbnail.
   */
  void pageContextMenuRequested(const PageInfo& pageInfo, const QPoint& screenPos, bool selected);

  /**
   * Emitted when a user right clicks on area below the last page.
   * In the absence of any pages, all the area is considered to be
   * below the last page.
   */
  void pastLastPageContextMenuRequested(const QPoint& screenPos);

 private:
  class Item;
  class Impl;
  class GraphicsScene;
  class PlaceholderThumb;
  class LabelGroup;
  class CompositeItem;

  void emitNewSelectionLeader(const PageInfo& pageInfo, const CompositeItem* composite, SelectionFlags flags);

  std::unique_ptr<Impl> m_impl;
};


DEFINE_FLAG_OPS(ThumbnailSequence::SelectionFlags)

#endif  // ifndef SCANTAILOR_APP_THUMBNAILSEQUENCE_H_
