// *****************************************************************************
// * This file is part of the FreeFileSync project. It is distributed under    *
// * GNU General Public License: https://www.gnu.org/licenses/gpl-3.0          *
// * Copyright (C) Zenju (zenju AT freefilesync DOT org) - All Rights Reserved *
// *****************************************************************************

#ifndef GRID_H_834702134831734869987
#define GRID_H_834702134831734869987

#include <memory>
#include <optional>
#include <vector>
#include <zen/stl_tools.h>
#include <wx/scrolwin.h>


//a user-friendly, extensible and high-performance grid control
namespace zen
{
enum class ColumnType { none = -1 }; //user-defiend column type
enum class HoverArea  { none = -1 }; //user-defined area for mouse selections for a given row (may span multiple columns or split a single column into multiple areas)

//------------------------ events ------------------------------------------------
//example: wnd.Bind(EVENT_GRID_COL_LABEL_LEFT_CLICK, [this](GridClickEvent& event) { onGridLeftClick(event); });

struct GridClickEvent;
struct GridSelectEvent;
struct GridLabelClickEvent;
struct GridColumnResizeEvent;
struct GridContextMenuEvent;

wxDECLARE_EVENT(EVENT_GRID_MOUSE_LEFT_DOUBLE, GridClickEvent);
wxDECLARE_EVENT(EVENT_GRID_MOUSE_LEFT_DOWN,   GridClickEvent);
wxDECLARE_EVENT(EVENT_GRID_MOUSE_RIGHT_DOWN,  GridClickEvent);

wxDECLARE_EVENT(EVENT_GRID_SELECT_RANGE, GridSelectEvent);
//NOTE: neither first nor second row need to match EVENT_GRID_MOUSE_LEFT_DOWN/EVENT_GRID_MOUSE_LEFT_UP: user holding SHIFT; moving out of window...

wxDECLARE_EVENT(EVENT_GRID_COL_LABEL_MOUSE_LEFT,  GridLabelClickEvent);
wxDECLARE_EVENT(EVENT_GRID_COL_LABEL_MOUSE_RIGHT, GridLabelClickEvent);
wxDECLARE_EVENT(EVENT_GRID_COL_RESIZE, GridColumnResizeEvent);

//wxContextMenuEvent? => generated by wxWidgets when right mouse down/up is not handled; even OS-dependent in which case event is generated
//=> inappropriate! we know better when to show context!
wxDECLARE_EVENT(EVENT_GRID_CONTEXT_MENU, GridContextMenuEvent);


struct GridClickEvent : public wxEvent
{
    GridClickEvent(wxEventType et, ptrdiff_t row, HoverArea hoverArea, const wxPoint& mousePos) :
        wxEvent(0 /*winid*/, et), row_(row), hoverArea_(hoverArea), mousePos_(mousePos) {}
    GridClickEvent* Clone() const override { return new GridClickEvent(*this); }

    const ptrdiff_t row_; //-1 for invalid position, >= rowCount if out of range
    const HoverArea hoverArea_; //may be HoverArea::none
    const wxPoint mousePos_; //Grid-relative coordinates
};

struct GridSelectEvent : public wxEvent
{
    GridSelectEvent(size_t rowFirst, size_t rowLast, bool positive, const GridClickEvent* mouseClick) :
        wxEvent(0 /*winid*/, EVENT_GRID_SELECT_RANGE), rowFirst_(rowFirst), rowLast_(rowLast), positive_(positive),
        mouseClick_(mouseClick ? *mouseClick : std::optional<GridClickEvent>()) { assert(rowFirst <= rowLast); }
    GridSelectEvent* Clone() const override { return new GridSelectEvent(*this); }

    const size_t rowFirst_; //selected range: [rowFirst_, rowLast_)
    const size_t rowLast_;  //
    const bool positive_; //"false" when clearing selection!
    const std::optional<GridClickEvent> mouseClick_; //filled unless selection was performed via keyboard shortcuts
};

struct GridLabelClickEvent : public wxEvent
{
    GridLabelClickEvent(wxEventType et, ColumnType colType, const wxPoint& mousePos) : wxEvent(0 /*winid*/, et), colType_(colType), mousePos_(mousePos) {}
    GridLabelClickEvent* Clone() const override { return new GridLabelClickEvent(*this); }

    const ColumnType colType_; //may be ColumnType::none
    const wxPoint mousePos_; //Grid-relative coordinates
};

struct GridColumnResizeEvent : public wxEvent
{
    GridColumnResizeEvent(int offset, ColumnType colType) : wxEvent(0 /*winid*/, EVENT_GRID_COL_RESIZE), colType_(colType), offset_(offset) {}
    GridColumnResizeEvent* Clone() const override { return new GridColumnResizeEvent(*this); }

    const ColumnType colType_;
    const int offset_;
};

struct GridContextMenuEvent : public wxEvent
{
    GridContextMenuEvent(const wxPoint& mousePos) : wxEvent(0 /*winid*/, EVENT_GRID_CONTEXT_MENU), mousePos_(mousePos) {}
    GridContextMenuEvent* Clone() const override { return new GridContextMenuEvent(*this); }

    const wxPoint mousePos_; //Grid-relative coordinates
};
//------------------------------------------------------------------------------------------------------------

class GridData
{
public:
    virtual ~GridData() {}

    virtual size_t getRowCount() const = 0;

    //cell area:
    virtual std::wstring getValue(size_t row, ColumnType colType) const = 0;
    virtual void         renderRowBackgound(wxDC& dc, const wxRect& rect, size_t row,                     bool enabled, bool selected, HoverArea rowHover); //default implementation
    virtual void         renderCell        (wxDC& dc, const wxRect& rect, size_t row, ColumnType colType, bool enabled, bool selected, HoverArea rowHover);
    virtual int          getBestSize  (const wxReadOnlyDC& dc, size_t row, ColumnType colType); //must correspond to renderCell()!
    virtual HoverArea    getMouseHover(const wxReadOnlyDC& dc, size_t row, ColumnType colType, int cellRelativePosX, int cellWidth) { return HoverArea::none; }
    virtual std::wstring getToolTip                           (size_t row, ColumnType colType, HoverArea rowHover) { return std::wstring(); }

    //label area:
    virtual std::wstring getColumnLabel(ColumnType colType) const = 0;
    virtual void renderColumnLabel(wxDC& dc, const wxRect& rect, ColumnType colType, bool enabled, bool highlighted); //default implementation
    virtual std::wstring getToolTip(ColumnType colType) const { return std::wstring(); }

    //optional helper routines:
    static int getColumnGapLeft(); //for left-aligned text
    static wxColor getColorSelectionGradientFrom();
    static wxColor getColorSelectionGradientTo();

    static void drawCellText(wxDC& dc, const wxRect& rect, const std::wstring_view text,
                             int alignment = wxALIGN_LEFT | wxALIGN_CENTER_VERTICAL, const wxSize* textExtentHint = nullptr);
    static wxRect drawCellBorder(wxDC& dc, const wxRect& rect); //returns inner rectangle

    static wxRect drawColumnLabelBackground(wxDC& dc, const wxRect& rect, bool highlighted); //returns inner rectangle
    static void   drawColumnLabelText      (wxDC& dc, const wxRect& rect, const std::wstring& text, bool enabled);
};


enum class GridEventPolicy
{
    allow,
    deny
};


class Grid : public wxScrolledWindow
{
public:
    Grid(wxWindow* parent,
         wxWindowID id        = wxID_ANY,
         const wxPoint& pos   = wxDefaultPosition,
         const wxSize& size   = wxDefaultSize,
         long style           = wxTAB_TRAVERSAL | wxNO_BORDER,
         const wxString& name = wxASCII_STR(wxPanelNameStr));

    size_t getRowCount() const;

    void setRowHeight(int height);
    int getRowHeight() const;

    struct ColAttributes
    {
        ColumnType type = ColumnType::none;
        //first, client width is partitioned according to all available stretch factors, then "offset_" is added
        //universal model: a non-stretched column has stretch factor 0 with the "offset" becoming identical to final width!
        int offset  = 0;
        int stretch = 0; //>= 0
        bool visible = false;
    };

    void setColumnConfig(const std::vector<ColAttributes>& attr); //set column count + widths
    std::vector<ColAttributes> getColumnConfig() const;

    void setDataProvider(const std::shared_ptr<GridData>& dataView) { dataView_ = dataView; }
    /**/  GridData* getDataProvider()       { return dataView_.get(); }
    const GridData* getDataProvider() const { return dataView_.get(); }
    //-----------------------------------------------------------------------------

    void setColumnLabelHeight(int height);
    int getColumnLabelHeight() const;
    void showRowLabel(bool visible);

    enum ScrollBarStatus
    {
        SB_SHOW_AUTOMATIC,
        SB_SHOW_ALWAYS,
        SB_SHOW_NEVER,
    };
    //alternative until wxScrollHelper::ShowScrollbars() becomes available in wxWidgets 2.9
    void showScrollBars(ScrollBarStatus horizontal, ScrollBarStatus vertical);

    std::vector<size_t> getSelectedRows() const { return selection_.get(); }

    void selectRow(size_t row, GridEventPolicy rangeEventPolicy);
    void selectAllRows (GridEventPolicy rangeEventPolicy); //turn off range selection event when calling this function in an event handler to avoid recursion!
    void clearSelection(GridEventPolicy rangeEventPolicy); //
    void selectRange(size_t rowFirst, size_t rowLast, bool positive, GridEventPolicy rangeEventPolicy); //select [rowFirst, rowLast)

    void scrollDelta(int deltaX, int deltaY); //in scroll units

    wxWindow& getCornerWin  ();
    wxWindow& getRowLabelWin();
    wxWindow& getColLabelWin();
    wxWindow& getMainWin    ();
    const wxWindow& getMainWin() const;

    struct ColumnPosInfo
    {
        ColumnType colType   = ColumnType::none; //ColumnType::none => no column at posX!
        int cellRelativePosX = 0;
        int colWidth         = 0;
    };
    ColumnPosInfo getColumnAtWinPos(int posX) const;
    ptrdiff_t getRowAtWinPos(int posY) const; //return -1 for invalid position, >= rowCount if out of range

    std::pair<ptrdiff_t, ptrdiff_t> getVisibleRows(const wxRect& clientRect) const; //returns range [begin, end)

    void refreshCell(size_t row, ColumnType colType);

    void enableColumnMove  (bool value) { allowColumnMove_   = value; }
    void enableColumnResize(bool value) { allowColumnResize_ = value; }

    void setGridCursor(size_t row, GridEventPolicy rangeEventPolicy); //set + show + select cursor
    size_t getGridCursor() const; //returns row

    void scrollTo(size_t row);

    void makeRowVisible(size_t row);

    void Refresh(bool eraseBackground = true, const wxRect* rect = nullptr) override;
    bool Enable(bool enable = true) override;

    //############################################################################################################

private:
    void onKeyDown(wxKeyEvent& event);

    void updateWindowSizes(bool updateScrollbar = true);

    void selectWithCursor(ptrdiff_t row); //emits GridSelectEvent

    wxSize GetSizeAvailableForScrollTarget(const wxSize& size) override; //required since wxWidgets 2.9 if SetTargetWindow() is used


    int getBestColumnSize(size_t col) const; //return -1 on error

    void autoSizeColumns(GridEventPolicy columnResizeEventPolicy);

    friend class GridData;
    class SubWindow;
    class CornerWin;
    class RowLabelWin;
    class ColLabelWin;
    class MainWin;

    class Selection
    {
    public:
        void resize(size_t rowCount) { selected_.resize(rowCount, false); }

        size_t gridSize() const { return selected_.size(); }

        std::vector<size_t> get() const
        {
            std::vector<size_t> result;
            for (size_t row = 0; row < selected_.size(); ++row)
                if (selected_[row] != 0)
                    result.push_back(row);
            return result;
        }

        bool isSelected(size_t row) const { return row < selected_.size() ? selected_[row] != 0 : false; }

        bool matchesRange(size_t rowFirst, size_t rowLast, bool positive)
        {
            if (rowFirst <= rowLast && rowLast <= selected_.size())
            {
                const auto rangeEnd = selected_.begin() + rowLast;
                return std::find(selected_.begin() + rowFirst, rangeEnd, static_cast<unsigned char>(!positive)) == rangeEnd;
            }
            else
            {
                assert(false);
                return false;
            }
        }

        void clear() { selectRange(0, selected_.size(), false); }

        void selectRange(size_t rowFirst, size_t rowLast, bool positive = true) //select [rowFirst, rowLast), trims if required!
        {
            assert(rowFirst <= rowLast && rowLast <= selected_.size());
            if (rowFirst < rowLast)
                std::fill(selected_.begin() + std::min(rowFirst, selected_.size()),
                          selected_.begin() + std::min(rowLast,  selected_.size()), positive);
        }

    private:
        std::vector<unsigned char> selected_; //effectively a vector<bool> of size "number of rows"
    };

    struct VisibleColumn
    {
        ColumnType type = ColumnType::none;
        int offset  = 0;
        int stretch = 0; //>= 0
    };

    struct ColumnWidth
    {
        ColumnType type = ColumnType::none;
        int width = 0;
    };
    std::vector<ColumnWidth> getColWidths()                 const; //
    std::vector<ColumnWidth> getColWidths(int mainWinWidth) const; //evaluate stretched columns
    int                      getColWidthsSum(int mainWinWidth) const;
    std::vector<int> getColStretchedWidths(int clientWidth) const; //final width = (normalized) (stretchedWidth + offset)

    std::optional<int> getColWidth(size_t col) const
    {
        const auto& widths = getColWidths();
        if (col < widths.size())
            return widths[col].width;
        return {};
    }

    void setColumnWidth(int width, size_t col, GridEventPolicy columnResizeEventPolicy, bool notifyAsync = false);

    wxRect getColumnLabelArea(ColumnType colType) const; //returns empty rect if column not found

    //select inclusive range [rowFrom, rowTo]
    void selectRange2(size_t rowFirst, size_t rowLast, bool positive, const GridClickEvent* mouseClick, GridEventPolicy rangeEventPolicy);

    bool isSelected(size_t row) const { return selection_.isSelected(row); }

    struct ColAction
    {
        bool wantResize = false; //"!wantResize" means "move" or "single click"
        size_t col = 0;
    };
    void moveColumn(size_t colFrom, size_t colTo);

    ColumnType colToType(size_t col) const; //returns ColumnType::none on error

    /*  Grid window layout:
        _______________________________
        | CornerWin   | ColLabelWin   |
        |_____________|_______________|
        | RowLabelWin | MainWin       |
        |             |               |
        |_____________|_______________|  */
    CornerWin*   cornerWin_;
    RowLabelWin* rowLabelWin_;
    ColLabelWin* colLabelWin_;
    MainWin*     mainWin_;

    ScrollBarStatus showScrollbarH_ = SB_SHOW_AUTOMATIC;
    ScrollBarStatus showScrollbarV_ = SB_SHOW_AUTOMATIC;

    bool drawRowLabel_ = true;

    std::shared_ptr<GridData> dataView_;
    Selection selection_;
    bool allowColumnMove_   = true;
    bool allowColumnResize_ = true;

    std::vector<VisibleColumn> visibleCols_; //individual widths, type and total column count
    std::vector<ColAttributes> oldColAttributes_; //visible + nonvisible columns; use for conversion in setColumnConfig()/getColumnConfig() *only*!

    size_t rowCountOld_ = 0; //at the time of last Grid::Refresh()

    int scrollBarHeightH_ = 0; //optional: may not be known (yet)
    int scrollBarWidthV_  = 0; //
};

//------------------------------------------------------------------------------------------------------------

template <class ColAttrReal>
std::vector<ColAttrReal> makeConsistent(const std::vector<ColAttrReal>& attribs, const std::vector<ColAttrReal>& defaults)
{
    std::vector<ColAttrReal> output = attribs;
    append(output, defaults); //make sure each type is existing!
    removeDuplicatesStable(output, [](const ColAttrReal& lhs, const ColAttrReal& rhs) { return lhs.type < rhs.type; });
    return output;
}


template <class ColAttrReal>
std::vector<Grid::ColAttributes> convertColAttributes(const std::vector<ColAttrReal>& attribs, const std::vector<ColAttrReal>& defaults)
{
    std::vector<Grid::ColAttributes> output;
    for (const ColAttrReal& ca : makeConsistent(attribs, defaults))
        output.push_back({static_cast<ColumnType>(ca.type), ca.offset, ca.stretch, ca.visible});
    return output;
}


template <class ColAttrReal>
std::vector<ColAttrReal> convertColAttributes(const std::vector<Grid::ColAttributes>& attribs)
{
    using ColTypeReal = decltype(ColAttrReal().type);

    std::vector<ColAttrReal> output;
    for (const Grid::ColAttributes& ca : attribs)
        output.push_back({static_cast<ColTypeReal>(ca.type), ca.offset, ca.stretch, ca.visible});
    return output;
}
}

#endif //GRID_H_834702134831734869987
