// SPDX-License-Identifier: GPL-2.0-or-later
/*
 * Multiindex container for selection
 *
 * Authors:
 *   Adrian Boguszewski
 *   Marc Jeanmougin
 *
 * Copyright (C) 2016 Adrian Boguszewski
 *
 * Released under GNU GPL v2+, read the file 'COPYING' for more information.
 */

#ifndef INKSCAPE_PROTOTYPE_OBJECTSET_H
#define INKSCAPE_PROTOTYPE_OBJECTSET_H

#include <string>
#include <unordered_map>

#include <boost/multi_index_container.hpp>
#include <boost/multi_index/identity.hpp>
#include <boost/multi_index/sequenced_index.hpp>
#include <boost/multi_index/hashed_index.hpp>
#include <boost/multi_index/random_access_index.hpp>
#include <boost/range/adaptor/filtered.hpp>
#include <boost/range/adaptor/transformed.hpp>
#include <boost/range/sub_range.hpp>
#include <boost/range/any_range.hpp>
#include <boost/type_traits.hpp>
#include <boost/utility/enable_if.hpp>

#include <sigc++/connection.h>

#include <inkgc/gc-soft-ptr.h>

#include "sp-object.h"
#include "sp-item.h"
#include "sp-item-group.h"
#include "desktop.h"
#include "document.h"

enum BoolOpErrors {
    DONE,
    DONE_NO_PATH,
    DONE_NO_ACTION,
    ERR_TOO_LESS_PATHS_1,
    ERR_TOO_LESS_PATHS_2,
    ERR_NO_PATHS,
    ERR_Z_ORDER
};

// boolean operation
enum bool_op
{
  bool_op_union,		// A OR B
  bool_op_inters,		// A AND B
  bool_op_diff,			// A \ B
  bool_op_symdiff,  // A XOR B
  bool_op_cut,      // coupure (pleines)
  bool_op_slice     // coupure (contour)
};
typedef enum bool_op BooleanOp;

/**
 * SiblingState enums are used to associate the current state
 * while grabbing objects. 
 * Specifically used by ObjectSet.applyAffine() to manage transforms
 * while dragging objects
 */
enum class SiblingState {
    SIBLING_NONE,		// no relation to item
    SIBLING_CLONE_ORIGINAL,	// moving both a clone and its original or any ancestor
    SIBLING_OFFSET_SOURCE,	// moving both an offset and its source
    SIBLING_TEXT_PATH,		// moving both a text-on-path and its path
    SIBLING_TEXT_FLOW_FRAME,	// moving both a flowtext and its frame
    SIBLING_TEXT_SHAPE_INSIDE,	// moving object containing sub object
};

class SPBox3D;
class Persp3D;

namespace Inkscape {

namespace XML {
class Node;
}

struct hashed{};
struct random_access{};

struct is_item {
    bool operator()(SPObject* obj) {
        return is<SPItem>(obj);
    }
};

struct is_group {
    bool operator()(SPObject* obj) {
        return is<SPGroup>(obj);
    }
};

struct object_to_item {
    typedef SPItem* result_type;
    SPItem* operator()(SPObject* obj) const {
        return cast<SPItem>(obj);
    }
};

struct object_to_node {
    typedef XML::Node* result_type;
    XML::Node* operator()(SPObject* obj) const {
        return obj->getRepr();
    }
};

struct object_to_group {
    typedef SPGroup* result_type;
    SPGroup* operator()(SPObject* obj) const {
        return cast<SPGroup>(obj);
    }
};

typedef boost::multi_index_container<
        SPObject*,
        boost::multi_index::indexed_by<
                boost::multi_index::sequenced<>,
                boost::multi_index::random_access<
                        boost::multi_index::tag<random_access>>,
                boost::multi_index::hashed_unique<
                        boost::multi_index::tag<hashed>,
                        boost::multi_index::identity<SPObject*>>
        >> MultiIndexContainer;

typedef boost::any_range<
        SPObject*,
        boost::random_access_traversal_tag,
        SPObject* const&,
        std::ptrdiff_t> SPObjectRange;

class ObjectSet {
public:
    enum CompareSize {HORIZONTAL, VERTICAL, AREA};
    typedef decltype(MultiIndexContainer().get<random_access>() | boost::adaptors::filtered(is_item()) | boost::adaptors::transformed(object_to_item())) SPItemRange;
    typedef decltype(MultiIndexContainer().get<random_access>() | boost::adaptors::filtered(is_group()) | boost::adaptors::transformed(object_to_group())) SPGroupRange;
    typedef decltype(MultiIndexContainer().get<random_access>() | boost::adaptors::filtered(is_item()) | boost::adaptors::transformed(object_to_node())) XMLNodeRange;

    ObjectSet(SPDesktop* desktop): _desktop(desktop) {
        if (desktop)
            _document = desktop->getDocument(); 
    };
    ObjectSet(SPDocument* doc): _desktop(nullptr), _document(doc) {};
    ObjectSet(): _desktop(nullptr), _document(nullptr) {}; // Used in spray-tool.h.
    virtual ~ObjectSet();
    
    void setDocument(SPDocument* doc){
        _document = doc;
    }
    

    /**
     * Add an SPObject to the set of selected objects.
     *
     * @param obj the SPObject to add
     * @param nosignal true if no signals should be sent
     */
    bool add(SPObject* object, bool nosignal = false);

    /**
     * Add an XML node's SPObject to the set of selected objects.
     *
     * @param the xml node of the item to add
     */
    void add(XML::Node *repr);

    /**  Add items from an STL iterator range to the selection.
     *  \param from the begin iterator
     *  \param to the end iterator
     */
    template <typename InputIterator>
    void add(InputIterator from, InputIterator to) {
        for(auto it = from; it != to; ++it) {
            _add(*it);
        }
        _emitChanged();
    }

    /**
     * Removes an item from the set of selected objects.
     *
     * It is ok to call this method for an unselected item.
     *
     * @param item the item to unselect
     *
     * @return is success
     */
    bool remove(SPObject* object);

    /**
     * Returns true if the given object is selected.
     */
    bool includes(SPObject *object, bool anyAncestor = false);
    bool includes(Inkscape::XML::Node *node, bool anyAncestor = false);

    /**
     * Returns ancestor if the given object has ancestor selected.
     */
    SPObject * includesAncestor(SPObject *object);

    /**
     * Set the selection to a single specific object.
     *
     * @param obj the object to select
     */
    void set(SPObject *object, bool persist_selection_context = false);
    void set(XML::Node *repr);
    /**
     * Unselects all selected objects.
     */
    void clear();

    /**
     * Returns size of the selection.
     */
    int size();

    /**
     * Returns true if no items are selected.
     */
    bool isEmpty();

    /**
     * Removes an item if selected, adds otherwise.
     *
     * @param item the item to unselect
     */
    void toggle(SPObject *obj);

    /**
     * Returns a single selected object.
     *
     * @return NULL unless exactly one object is selected
     */
    SPObject *single();

    /**
     * Returns a single selected item.
     *
     * @return NULL unless exactly one object is selected
     */
    SPItem *singleItem();

    /**
     * Returns the first selected item, returns nullptr if no items selected.
     */
    SPItem *firstItem() const;

    /**
     * Returns the last selected item, returns nullptr if no items selected.
     */
    SPItem *lastItem() const;

    /**
     * Returns the smallest item from this selection.
     */
    SPItem *smallestItem(CompareSize compare);

    /**
     * Returns the largest item from this selection.
     */
    SPItem *largestItem(CompareSize compare);

    /** Returns the list of selected objects. */
    SPObjectRange objects();

    /** Returns a range of selected SPItems. */
    SPItemRange items() {
        return SPItemRange(_container.get<random_access>()
           | boost::adaptors::filtered(is_item())
           | boost::adaptors::transformed(object_to_item()));
    };

    std::vector<SPItem*> items_vector() {
        auto i = items();
        return {i.begin(), i.end()};
    }

    /** Returns a range of selected groups. */
    SPGroupRange groups() {
        return SPGroupRange (_container.get<random_access>()
            | boost::adaptors::filtered(is_group())
            | boost::adaptors::transformed(object_to_group()));
    }

    /** Returns a range of the xml nodes of all selected objects. */
    XMLNodeRange xmlNodes() {
        return XMLNodeRange(_container.get<random_access>()
                            | boost::adaptors::filtered(is_item())
                            | boost::adaptors::transformed(object_to_node()));
    }

    /**
     * Returns a single selected object's xml node.
     *
     * @return NULL unless exactly one object is selected
     */
    XML::Node *singleRepr();

    /**
     * The top-most item, or NULL if the selection is empty.
     */
    XML::Node *topRepr() const;

    /**
     * Selects exactly the specified objects.
     *
     * @param objs the objects to select
     */
    template <class T>
    typename boost::enable_if<boost::is_base_of<SPObject, T>, void>::type
    setList(const std::vector<T*> &objs) {
        _clear();
        addList(objs);
    }
    
    /**
     * Attempt to select all the items between two child items. Must have the same parent.
     *
     * @param obj_a - The first item, doesn't have to appear first in the list.
     * @param obj_b - The last item, doesn't have to appear last in the list (optional)
     *                If selection already contains one item, will select from-to that.
     *                If not set, will use the lastItem selected in the list.
     *
     * @returns the number of items added.
     */
    int setBetween(SPObject *obj_a, SPObject *obj_b = nullptr);

    /**
     * Selects the objects with the same IDs as those in `list`.
     *
     * @todo How about adding `setIdList(std::vector<Glib::ustring> const &list)`
     * 
     * @param list the repr list to add
     */
    void setReprList(std::vector<XML::Node*> const &list);

    /**
     * Assign IDs to selected objects that don't have an ID attribute
     * Checks if the object's id attribute is NULL. If it is, assign it a unique ID
     */
    void enforceIds();

    /**
     * Adds the specified objects to selection, without deselecting first.
     *
     * @param objs the objects to select
     */
    template <class T>
    typename boost::enable_if<boost::is_base_of<SPObject, T>, void>::type
    addList(const std::vector<T*> &objs) {
        for (auto obj: objs) {
            if (!includes(obj)) {
                add(obj, true);
            }
        }
        _emitChanged();
    }

    /** Returns the bounding rectangle of the selection. */
    Geom::OptRect bounds(SPItem::BBoxType type) const;
    Geom::OptRect visualBounds() const;
    Geom::OptRect geometricBounds() const;
    Geom::OptRect strokedBounds() const;

    /**
     * Returns either the visual or geometric bounding rectangle of the selection, based on the
     * preferences specified for the selector tool
     */
    Geom::OptRect preferredBounds() const;

    /* Returns the bounding rectangle of the selectionin document coordinates.*/
    Geom::OptRect documentBounds(SPItem::BBoxType type) const;

    /**
     * Returns either the visual or geometric bounding rectangle of selection in document
     * coordinates based on preferences specified for the selector tool
     */
    Geom::OptRect documentPreferredBounds() const;

    /**
     * Returns the rotation/skew center of the selection.
     */
    std::optional<Geom::Point> center() const;

    /** Returns a list of all perspectives which have a 3D box in the current selection.
       (these may also be nested in groups) */
    std::list<Persp3D *> const perspList();

    /**
     * Returns a list of all 3D boxes in the current selection which are associated to @c
     * persp. If @c pers is @c NULL, return all selected boxes.
     */
    std::list<SPBox3D *> const box3DList(Persp3D *persp = nullptr);

    /**
     * Returns the desktop the selection is bound to
     *
     * @return the desktop the selection is bound to, or NULL if in console mode
     */
    SPDesktop *desktop() { return _desktop; }

    /**
     * Returns the document the selection is bound to
     *
     * @return the document the selection is bound to, or NULL if in console mode
     */
    SPDocument *document() { return _document; }

    //item groups operations
    //in selection-chemistry.cpp
    void deleteItems(bool skip_undo = false);
    void duplicate(bool suppressDone = false, bool duplicateLayer = false);
    void clone(bool skip_undo = false);

    /**
     * @brief Unlink all directly selected clones.
     * @param skip_undo If this is set to true the call to DocumentUndo::done is omitted.
     * @return True if anything was unlinked, otherwise false.
     */
    bool unlink(const bool skip_undo = false, const bool silent = false);
    /**
     * @brief Recursively unlink any clones present in the current selection,
     * including clones which are used to clip other objects, groups of clones etc.
     * @return true if anything was unlinked, otherwise false.
     */
    bool unlinkRecursive(const bool skip_undo = false, const bool force = false, const bool silent = false);
    void removeLPESRecursive(bool keep_paths);
    void relink();
    void cloneOriginal();
    void cloneOriginalPathLPE(bool allow_transforms = false, bool sync = false, bool skip_undo = false);
    Inkscape::XML::Node* group(bool is_anchor = false);
    void popFromGroup();
    void ungroup(bool skip_undo = false);
    void ungroup_all(bool skip_undo = false);
    
    //z-order management
    //in selection-chemistry.cpp
    void stackUp(bool skip_undo = false);
    void raise(bool skip_undo = false);
    void raiseToTop(bool skip_undo = false);
    void stackDown(bool skip_undo = false);
    void lower(bool skip_undo = false);
    void lowerToBottom(bool skip_undo = false);
    void toNextLayer(bool skip_undo = false);
    void toPrevLayer(bool skip_undo = false);
    void toLayer(SPObject *layer);
    void toLayer(SPObject *layer, Inkscape::XML::Node *after);

    //clipboard management
    //in selection-chemistry.cpp
    void copy();
    void cut();
    void pasteStyle();
    void pasteSize(bool apply_x, bool apply_y);
    void pasteSizeSeparately(bool apply_x, bool apply_y);
    void pastePathEffect();
    
    //path operations
    //in path-chemistry.cpp
    void combine(bool skip_undo = false, bool silent = false);
    void breakApart(bool skip_undo = false, bool overlapping = true, bool silent = false);
    void toCurves(bool skip_undo = false, bool clonesjustunlink = false);
    void toLPEItems();
    void pathReverse();

    // path operations
    // in path/path-object-set.cpp
    bool strokesToPaths(bool legacy = false, bool skip_undo = false);
    bool simplifyPaths(bool skip_undo = false);

    // Boolean operations
    // in splivarot.cpp
    bool pathUnion(const bool skip_undo = false, bool silent = false);
    bool pathIntersect(const bool skip_undo = false, bool silent = false);
    bool pathDiff(const bool skip_undo = false, bool silent = false);
    bool pathSymDiff(const bool skip_undo = false, bool silent = false);
    bool pathCut(const bool skip_undo = false, bool silent = false);
    bool pathSlice(const bool skip_undo = false, bool silent = false);

    //Other path operations
    //in selection-chemistry.cpp
    void toMarker(bool apply = true);
    void toGuides();
    void toSymbol();
    void unSymbol();
    void tile(bool apply = true); //"Object to Pattern"
    void untile();
    void createBitmapCopy();
    void setMask(bool apply_clip_path, bool apply_to_layer, bool remove_original);
    void editMask(bool clip);
    void unsetMask(const bool apply_clip_path, const bool delete_helper_group, bool remove_original);
    void setClipGroup();
    
    // moves
    // in selection-chemistry.cpp
    void removeLPE();
    void removeFilter();
    void reapplyAffine();
    void clearLastAffine();
    void applyAffine(Geom::Affine const &affine, bool set_i2d=true,bool compensate=true, bool adjust_transf_center=true);
    void removeTransform();
    void setScaleAbsolute(double, double, double, double);
    void setScaleRelative(const Geom::Point&, const Geom::Scale&);
    void rotateRelative(const Geom::Point&, double);
    void skewRelative(const Geom::Point&, double, double);
    void moveRelative(const Geom::Point &move, bool compensate = true);
    void moveRelative(double dx, double dy);
    void rotate(double);
    void rotateScreen(double);
    void scaleGrow(double);
    void scaleScreen(double);
    void scale(double);
    void move(double dx, double dy);
    void moveScreen(double dx, double dy);
    
    // various
    bool fitCanvas(bool with_margins, bool skip_undo = false);
    void swapFillStroke();
    void fillBetweenMany();

    SiblingState getSiblingState(SPItem *item);
    void insertSiblingState(SPObject *object, SiblingState state);
    void clearSiblingStates();

protected:
    virtual void _connectSignals(SPObject* object) {};
    virtual void _releaseSignals(SPObject* object) {};
    virtual void _emitChanged(bool persist_selection_context = false);
    void _add(SPObject* object);
    void _clear();
    void _remove(SPObject* object);
    bool _anyAncestorIsInSet(SPObject *object);
    void _removeDescendantsFromSet(SPObject *object);
    void _removeAncestorsFromSet(SPObject *object);
    SPItem *_sizeistItem(bool sml, CompareSize compare);
    SPObject *_getMutualAncestor(SPObject *object);
    virtual void _add3DBoxesRecursively(SPObject *obj);
    virtual void _remove3DBoxesRecursively(SPObject *obj);

    MultiIndexContainer _container;
    GC::soft_ptr<SPDesktop> _desktop;
    GC::soft_ptr<SPDocument> _document;
    std::list<SPBox3D *> _3dboxes;
    std::unordered_map<SPObject*, sigc::connection> _releaseConnections;

private:
    BoolOpErrors pathBoolOp(bool_op bop, const bool skip_undo, const bool checked = false,
                            const Glib::ustring icon_name = nullptr, const Glib::ustring description = "",
                            bool silent = false);
    void _disconnect(SPObject* object);
    std::map<SPObject *, SiblingState> _sibling_state;

    Geom::Affine _last_affine;
};

typedef ObjectSet::SPItemRange SPItemRange;
typedef ObjectSet::SPGroupRange SPGroupRange;
typedef ObjectSet::XMLNodeRange XMLNodeRange;

} // namespace Inkscape

#endif //INKSCAPE_PROTOTYPE_OBJECTSET_H

/*
  Local Variables:
  mode:c++
  c-file-style:"stroustrup"
  c-file-offsets:((innamespace . 0)(inline-open . 0)(case-label . +))
  indent-tabs-mode:nil
  fill-column:99
  End:
*/
// vim: filetype=cpp:expandtab:shiftwidth=4:tabstop=8:softtabstop=4:fileencoding=utf-8:textwidth=99 :
