// SPDX-License-Identifier: GPL-2.0-or-later
/*
 * Central facade for accessing and managing on-canvas controls.
 *
 * Author:
 *   Jon A. Cruz <jon@joncruz.org>
 *
 * Copyright 2012 Authors
 *
 * Released under GNU GPL v2+, read the file 'COPYING' for more information.
 */

#include "control-manager.h"

#include <algorithm>
#include <set>

#include <glib-object.h>

#include "display/sodipodi-ctrl.h" // for SP_TYPE_CTRL
#include "display/sp-ctrlline.h"
#include "display/sp-ctrlcurve.h"
#include "preferences.h"

using Inkscape::ControlFlags;

namespace {

// Note: The following operator overloads are local to this file at the moment to discourage flag manipulation elsewhere.
/*
ControlFlags operator |(ControlFlags lhs, ControlFlags rhs)
{
    return static_cast<ControlFlags>(static_cast<int>(lhs) | static_cast<int>(rhs));
}
*/

ControlFlags operator &(ControlFlags lhs, ControlFlags rhs)
{
    return static_cast<ControlFlags>(static_cast<int>(lhs) & static_cast<int>(rhs));
}

ControlFlags operator ^(ControlFlags lhs, ControlFlags rhs)
{
    return static_cast<ControlFlags>(static_cast<int>(lhs) ^ static_cast<int>(rhs));
}

ControlFlags& operator ^=(ControlFlags &lhs, ControlFlags rhs)
{
    lhs = lhs ^ rhs;
    return lhs;
}

} // namespace

// Default color for line:
#define LINE_COLOR_PRIMARY 0x0000ff7f
#define LINE_COLOR_SECONDARY 0xff00007f
#define LINE_COLOR_TERTIARY 0xffff007f

namespace Inkscape {

class ControlManagerImpl
{
public:
    ControlManagerImpl(ControlManager &manager);

    ~ControlManagerImpl() = default;

    SPCanvasItem *createControl(SPCanvasGroup *parent, ControlType type);

    void setControlSize(int size, bool force = false);

    void track(SPCanvasItem *anchor);

    sigc::connection connectCtrlSizeChanged(const sigc::slot<void> &slot);

    void updateItem(SPCanvasItem *item);

    bool setControlType(SPCanvasItem *item, ControlType type);

    bool setControlResize(SPCanvasItem *item, int ctrlResize);

    void setSelected(SPCanvasItem *item, bool selected);

private:
    static void thingFinalized(gpointer data, GObject *wasObj);

    void thingFinalized(GObject *wasObj);

    class PrefListener : public Inkscape::Preferences::Observer
    {
    public:
        PrefListener(ControlManagerImpl &manager) : Inkscape::Preferences::Observer("/options/grabsize/value"), _mgr(manager) {}
        ~PrefListener() override = default;

        void notify(Inkscape::Preferences::Entry const &val) override {
            int size = val.getIntLimited(3, 1, 7);
            _mgr.setControlSize(size);
        }

        ControlManagerImpl &_mgr;
    };


    ControlManager &_manager;
    sigc::signal<void> _sizeChangedSignal;
    PrefListener _prefHook;
    int _size;   // Size from the grabsize preference
    int _resize; // Way size should change from grabsize
    std::vector<SPCanvasItem *> _itemList;
    std::map<Inkscape::ControlType, std::vector<unsigned int> > _sizeTable;
    std::map<Inkscape::ControlType, GType> _typeTable;
    std::map<Inkscape::ControlType, SPCtrlShapeType> _ctrlToShape;
    std::set<Inkscape::ControlType> _resizeOnSelect;
};

ControlManagerImpl::ControlManagerImpl(ControlManager &manager) :
    _manager(manager),
    _sizeChangedSignal(),
    _prefHook(*this),
    _size(3),
    _resize(3),
    _itemList(),
    _sizeTable()
{
    _typeTable[CTRL_TYPE_UNKNOWN] = SP_TYPE_CTRL;
    _typeTable[CTRL_TYPE_LPE] = SP_TYPE_CTRL;
    _typeTable[CTRL_TYPE_ADJ_HANDLE] = SP_TYPE_CTRL;
    _typeTable[CTRL_TYPE_ANCHOR] = SP_TYPE_CTRL;
    _typeTable[CTRL_TYPE_INVISIPOINT] = SP_TYPE_CTRL;
    _typeTable[CTRL_TYPE_NODE_AUTO] = SP_TYPE_CTRL;
    _typeTable[CTRL_TYPE_NODE_CUSP] = SP_TYPE_CTRL;
    _typeTable[CTRL_TYPE_NODE_SMOOTH] = SP_TYPE_CTRL;
    _typeTable[CTRL_TYPE_NODE_SYMETRICAL] = SP_TYPE_CTRL;

    _typeTable[CTRL_TYPE_LINE] = SP_TYPE_CTRLLINE;

    // -------
    _ctrlToShape[CTRL_TYPE_UNKNOWN] = SP_CTRL_SHAPE_DIAMOND;
    _ctrlToShape[CTRL_TYPE_LPE] = SP_CTRL_SHAPE_DIAMOND;
    _ctrlToShape[CTRL_TYPE_NODE_CUSP] = SP_CTRL_SHAPE_DIAMOND;
    _ctrlToShape[CTRL_TYPE_NODE_SMOOTH] = SP_CTRL_SHAPE_SQUARE;
    _ctrlToShape[CTRL_TYPE_NODE_AUTO] = SP_CTRL_SHAPE_CIRCLE;
    _ctrlToShape[CTRL_TYPE_NODE_SYMETRICAL] = SP_CTRL_SHAPE_SQUARE;

    _ctrlToShape[CTRL_TYPE_ADJ_HANDLE] = SP_CTRL_SHAPE_CIRCLE;
    _ctrlToShape[CTRL_TYPE_INVISIPOINT] = SP_CTRL_SHAPE_SQUARE;

    // -------

    _resizeOnSelect.insert(CTRL_TYPE_NODE_AUTO);
    _resizeOnSelect.insert(CTRL_TYPE_NODE_CUSP);
    _resizeOnSelect.insert(CTRL_TYPE_NODE_SMOOTH);
    _resizeOnSelect.insert(CTRL_TYPE_NODE_SYMETRICAL);

    // -------

    // The size of the controls is determined by the grabsize preference; see the "Handle size" parameter in
    // the input/output group, on the "input devices" tab; this parameter ranges from 1 to 7; When selecting a control, we
    // increase the size by an additional 2 pixels, if _resizeOnSelect is true (see setSelected())
    
    Inkscape::Preferences *prefs = Inkscape::Preferences::get();
    prefs->addObserver(_prefHook);

    _size = prefs->getIntLimited("/options/grabsize/value", 3, 1, 7);

    // _sizeTable will have odd numbers, which allow for pixel perfect alignment (e.g. relative to grids
    // or guides, which are 1 px wide. It is not possible to accurately center a control to them if the
    // control has an even width).
    {
        unsigned int sizes[] = {7, 7, 7, 7, 7, 7, 7};
        _sizeTable[CTRL_TYPE_UNKNOWN] = std::vector<unsigned int>(sizes, sizes + (sizeof(sizes) / sizeof(sizes[0])));
    }
    {
        unsigned int sizes[] = {3, 5, 7, 9, 11, 13, 15};
        _sizeTable[CTRL_TYPE_ADJ_HANDLE] = std::vector<unsigned int>(sizes, sizes + (sizeof(sizes) / sizeof(sizes[0])));
    }
    {
        unsigned int sizes[] = {3, 5, 7, 9, 11, 13, 15};
        _sizeTable[CTRL_TYPE_ANCHOR] = std::vector<unsigned int>(sizes, sizes + (sizeof(sizes) / sizeof(sizes[0])));
    }
    {
        unsigned int sizes[] = {5, 7, 9, 11, 13, 15, 17};
        _sizeTable[CTRL_TYPE_POINT] = std::vector<unsigned int>(sizes, sizes + (sizeof(sizes) / sizeof(sizes[0])));
        _sizeTable[CTRL_TYPE_ROTATE] = std::vector<unsigned int>(sizes, sizes + (sizeof(sizes) / sizeof(sizes[0])));
        _sizeTable[CTRL_TYPE_SIZER] = std::vector<unsigned int>(sizes, sizes + (sizeof(sizes) / sizeof(sizes[0])));
        _sizeTable[CTRL_TYPE_SHAPER] = std::vector<unsigned int>(sizes, sizes + (sizeof(sizes) / sizeof(sizes[0])));
    }
    {
        unsigned int sizes[] = {5, 7, 9, 11, 13, 15, 17};
        _sizeTable[CTRL_TYPE_NODE_AUTO] = std::vector<unsigned int>(sizes, sizes + (sizeof(sizes) / sizeof(sizes[0])));
        _sizeTable[CTRL_TYPE_NODE_CUSP] = std::vector<unsigned int>(sizes, sizes + (sizeof(sizes) / sizeof(sizes[0])));
    }
    {
        unsigned int sizes[] = {3, 5, 7, 9, 11, 13, 15};
        _sizeTable[CTRL_TYPE_NODE_SMOOTH] = std::vector<unsigned int>(sizes, sizes + (sizeof(sizes) / sizeof(sizes[0])));
        _sizeTable[CTRL_TYPE_NODE_SYMETRICAL] = std::vector<unsigned int>(sizes, sizes + (sizeof(sizes) / sizeof(sizes[0])));
    }
    {
        unsigned int sizes[] = {1, 1, 1, 1, 1, 1, 1};
        _sizeTable[CTRL_TYPE_INVISIPOINT] = std::vector<unsigned int>(sizes, sizes + (sizeof(sizes) / sizeof(sizes[0])));
    }
    {
        unsigned int sizes[] = { 5, 7, 9, 11, 13, 15, 17 };
        _sizeTable[CTRL_TYPE_LPE] = std::vector<unsigned int>(sizes, sizes + (sizeof(sizes) / sizeof(sizes[0])));
    }
}


void ControlManagerImpl::setControlSize(int size, bool force)
{
    if ((size < 1) || (size > 7)) {
        g_warning("Illegal logical size set: %d", size);
    } else if (force || (size != _size)) {
        _size = size;

        for (auto & it : _itemList)
        {
            if (it) {
                updateItem(it);
            }
        }

        //_sizeChangedSignal.emit();
    }
}

SPCanvasItem *ControlManagerImpl::createControl(SPCanvasGroup *parent, ControlType type)
{
    SPCanvasItem *item = nullptr;
    unsigned int targetSize = _sizeTable[type][_size - 1];
    switch (type)
    {
        case CTRL_TYPE_ADJ_HANDLE:
            item = sp_canvas_item_new(parent, SP_TYPE_CTRL,
                                      "shape",SP_CTRL_SHAPE_CIRCLE,
                                      "size", targetSize,
                                      "filled", 1,
                                      "fill_color", 0xffffff7f,
                                      "stroked", 1,
                                      "stroke_color", 0x0000ff7f,
                                      NULL);
            break;
        case CTRL_TYPE_ANCHOR:
            item = sp_canvas_item_new(parent, SP_TYPE_CTRL,
                                      "size", targetSize,
                                      "filled", 1,
                                      "fill_color", 0xffffff7f,
                                      "stroked", 1,
                                      "stroke_color", 0x000000ff,
                                      NULL);
            break;
        case CTRL_TYPE_NODE_AUTO:
        case CTRL_TYPE_NODE_CUSP:
        case CTRL_TYPE_NODE_SMOOTH:
        case CTRL_TYPE_NODE_SYMETRICAL:
        {
            SPCtrlShapeType shape = _ctrlToShape[_ctrlToShape.count(type) ? type : CTRL_TYPE_UNKNOWN];
            item = sp_canvas_item_new(parent, SP_TYPE_CTRL,
                                      "shape", shape,
                                      "size", targetSize,
                                      NULL);
            break;
        }
        case CTRL_TYPE_INVISIPOINT:
            item = sp_canvas_item_new(parent, SP_TYPE_CTRL,
                                      "shape", SP_CTRL_SHAPE_SQUARE,
                                      "size", targetSize,
                                      NULL);
            break;
        case CTRL_TYPE_UNKNOWN:
        case CTRL_TYPE_LPE:
        default:
            item = sp_canvas_item_new(parent, SP_TYPE_CTRL, nullptr);
    }
    if (item) {
        item->ctrlType = type;
    }
    return item;
}

void ControlManagerImpl::track(SPCanvasItem *item)
{
    g_object_weak_ref( G_OBJECT(item), ControlManagerImpl::thingFinalized, this );

    _itemList.push_back(item);

    setControlSize(_size, true);
}

sigc::connection ControlManagerImpl::connectCtrlSizeChanged(const sigc::slot<void> &slot)
{
    return _sizeChangedSignal.connect(slot);
}

void ControlManagerImpl::updateItem(SPCanvasItem *item)
{
    if (item) {
        unsigned int target = _sizeTable[item->ctrlType][_size - 1] + item->ctrlResize;
        g_object_set(item, "size", target, NULL);

        sp_canvas_item_request_update(item);
    }
}

bool ControlManagerImpl::setControlType(SPCanvasItem *item, ControlType type)
{
    bool accepted = false;
    if (item && (item->ctrlType == type)) {
        // nothing to do
        accepted = true;
    } else if (item) {
        if (_ctrlToShape.count(type) && (_typeTable[type] == _typeTable[item->ctrlType])) { // compatible?
            unsigned int targetSize = _sizeTable[type][_size - 1] + item->ctrlResize;
            SPCtrlShapeType targetShape = _ctrlToShape[type];

            g_object_set(item, "shape", targetShape, "size", targetSize, NULL);
            item->ctrlType = type;
            accepted = true;
        }
    }

    return accepted;
}

bool ControlManagerImpl::setControlResize(SPCanvasItem *item, int ctrlResize)
{
    // _sizeTable will have odd numbers, which allow for pixel perfect alignment (e.g. relative to grids
    // or guides, which are 1 px wide. It is not possible to accurately center a control to them if the
    // control has an even width). ctrlResize should therefore be an even number, such that the sum (targetSize)
    // is also odd
    if(item) {
        item->ctrlResize = ctrlResize;
        unsigned int targetSize = _sizeTable[item->ctrlType][_size - 1] + item->ctrlResize;
        g_object_set(item, "size", targetSize, NULL);
        return true;
    }
    return false;
}

void ControlManagerImpl::setSelected(SPCanvasItem *item, bool selected)
{
    if (_manager.isSelected(item) != selected) {
        item->ctrlFlags ^= CTRL_FLAG_SELECTED; // toggle, since we know it is different

        if (selected && _resizeOnSelect.count(item->ctrlType)) {
            item->ctrlResize = 2;
        } else {
            item->ctrlResize = 0;
        }

        // TODO refresh colors
        unsigned int targetSize = _sizeTable[item->ctrlType][_size - 1] + item->ctrlResize;
        g_object_set(item, "size", targetSize, NULL);
    }
}

void ControlManagerImpl::thingFinalized(gpointer data, GObject *wasObj)
{
    if (data) {
        reinterpret_cast<ControlManagerImpl *>(data)->thingFinalized(wasObj);
    }
}

void ControlManagerImpl::thingFinalized(GObject *wasObj)
{
    SPCanvasItem *wasItem = reinterpret_cast<SPCanvasItem *>(wasObj);
    if (wasItem)
    {
        std::vector<SPCanvasItem *>::iterator it = std::find(_itemList.begin(), _itemList.end(), wasItem);
        if (it != _itemList.end())
        {
            _itemList.erase(it);
        }
    }
}


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

ControlManager::ControlManager() :
    _impl(new ControlManagerImpl(*this))
{
}

ControlManager::~ControlManager()
= default;

ControlManager &ControlManager::getManager()
{
    static ControlManager instance;

    return instance;
}


SPCanvasItem *ControlManager::createControl(SPCanvasGroup *parent, ControlType type)
{
    return _impl->createControl(parent, type);
}

SPCtrlLine *ControlManager::createControlLine(SPCanvasGroup *parent, CtrlLineType type)
{
    SPCtrlLine *line = SP_CTRLLINE(sp_canvas_item_new(parent, SP_TYPE_CTRLLINE, nullptr));
    if (line) {
        line->ctrlType = CTRL_TYPE_LINE;

        line->setRgba32((type == CTLINE_PRIMARY) ? LINE_COLOR_PRIMARY :
                        (type == CTLINE_SECONDARY) ? LINE_COLOR_SECONDARY : LINE_COLOR_TERTIARY);
        line->setCoords(0, 0, 0, 0);
    }
    return line;
}

SPCtrlLine *ControlManager::createControlLine(SPCanvasGroup *parent, Geom::Point const &p1, Geom::Point const &p2, CtrlLineType type)
{
    SPCtrlLine *line = createControlLine(parent, type);
    if (line) {
        line->setCoords(p1, p2);
    }
    return line;
}

SPCtrlCurve *ControlManager::createControlCurve(SPCanvasGroup *parent, Geom::Point const &p0, Geom::Point const &p1, Geom::Point const &p2, Geom::Point const &p3, CtrlLineType type)
{
    SPCtrlCurve *line = SP_CTRLCURVE(sp_canvas_item_new(parent, SP_TYPE_CTRLCURVE, nullptr));
    if (line) {
        line->ctrlType = CTRL_TYPE_LINE;

        line->setRgba32((type == CTLINE_PRIMARY) ? LINE_COLOR_PRIMARY :
                        (type == CTLINE_SECONDARY) ? LINE_COLOR_SECONDARY : LINE_COLOR_TERTIARY);
        line->setCoords(p0, p1, p2, p3);
    }
    return line;
}

void ControlManager::track(SPCanvasItem *item)
{
    _impl->track(item);
}

sigc::connection ControlManager::connectCtrlSizeChanged(const sigc::slot<void> &slot)
{
    return _impl->connectCtrlSizeChanged(slot);
}

void ControlManager::updateItem(SPCanvasItem *item)
{
    return _impl->updateItem(item);
}

bool ControlManager::setControlType(SPCanvasItem *item, ControlType type)
{
    return _impl->setControlType(item, type);
}

bool ControlManager::setControlResize(SPCanvasItem *item, int ctrlResize)
{
    return _impl->setControlResize(item, ctrlResize);
}

bool ControlManager::isActive(SPCanvasItem *item) const
{
    return (item->ctrlFlags & CTRL_FLAG_ACTIVE) != 0;
}

void ControlManager::setActive(SPCanvasItem *item, bool active)
{
    if (isActive(item) != active) {
        item->ctrlFlags ^= CTRL_FLAG_ACTIVE; // toggle, since we know it is different
        // TODO refresh size/colors
    }
}

bool ControlManager::isPrelight(SPCanvasItem *item) const
{
    return (item->ctrlFlags & CTRL_FLAG_PRELIGHT) != 0;
}

void ControlManager::setPrelight(SPCanvasItem *item, bool prelight)
{
    if (isPrelight(item) != prelight) {
        item->ctrlFlags ^= CTRL_FLAG_PRELIGHT; // toggle, since we know it is different
        // TODO refresh size/colors
    }
}

bool ControlManager::isSelected(SPCanvasItem *item) const
{
    return (item->ctrlFlags & CTRL_FLAG_SELECTED) != 0;
}

void ControlManager::setSelected(SPCanvasItem *item, bool selected)
{
    _impl->setSelected(item, selected);
}

} // namespace Inkscape

/*
  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 :
