// SPDX-License-Identifier: GPL-2.0-or-later
/**
 * @file
 * Undo History dialog - implementation.
 */
/* Author:
 *   Gustav Broberg <broberg@kth.se>
 *   Abhishek Sharma
 *   Jon A. Cruz <jon@joncruz.org>
 *
 * Copyright (C) 2014 Authors
 * Released under GNU GPL v2+, read the file 'COPYING' for more information.
 */

#include "undo-history.h"

#include "document-undo.h"
#include "document.h"
#include "inkscape.h"
#include "ui/icon-loader.h"
#include "util/signal-blocker.h"

#include "desktop.h"


namespace Inkscape {
namespace UI {
namespace Dialog {

/* Rendering functions for custom cell renderers */
void CellRendererSPIcon::render_vfunc(const Cairo::RefPtr<Cairo::Context>& cr,
                                      Gtk::Widget& widget,
                                      const Gdk::Rectangle& background_area,
                                      const Gdk::Rectangle& cell_area,
                                      Gtk::CellRendererState flags)
{
    // if this event type doesn't have an icon...
    if ( !Inkscape::Verb::get(_property_event_type)->get_image() ) return;

    // if the icon isn't cached, render it to a pixbuf
    if ( !_icon_cache[_property_event_type] ) {

        Glib::ustring image_name = Inkscape::Verb::get(_property_event_type)->get_image();
        Gtk::Image* icon = Gtk::manage(new Gtk::Image());
        icon = sp_get_icon_image(image_name, Gtk::ICON_SIZE_MENU);

        if (icon) {

            // check icon type (inkscape, gtk, none)
            if ( GTK_IS_IMAGE(icon->gobj()) ) {
                _property_icon = sp_get_icon_pixbuf(image_name, 16);
            } else {
                delete icon;
                return;
            }

            delete icon;
            property_pixbuf() = _icon_cache[_property_event_type] = _property_icon.get_value();
        }

    } else {
        property_pixbuf() = _icon_cache[_property_event_type];
    }

    Gtk::CellRendererPixbuf::render_vfunc(cr, widget, background_area,
                                          cell_area, flags);
}


void CellRendererInt::render_vfunc(const Cairo::RefPtr<Cairo::Context>& cr,
                                   Gtk::Widget& widget,
                                   const Gdk::Rectangle& background_area,
                                   const Gdk::Rectangle& cell_area,
                                   Gtk::CellRendererState flags)
{
    if( _filter(_property_number) ) {
        std::ostringstream s;
        s << _property_number << std::flush;
        property_text() = s.str();
        Gtk::CellRendererText::render_vfunc(cr, widget, background_area,
                                            cell_area, flags);
    }
}

const CellRendererInt::Filter& CellRendererInt::no_filter = CellRendererInt::NoFilter();

UndoHistory& UndoHistory::getInstance()
{
    return *new UndoHistory();
}

UndoHistory::UndoHistory()
    : UI::Widget::Panel("/dialogs/undo-history", SP_VERB_DIALOG_UNDO_HISTORY),
      _document_replaced_connection(),
      _desktop(getDesktop()),
      _document(_desktop ? _desktop->doc() : nullptr),
      _event_log(_desktop ? _desktop->event_log : nullptr),
      _columns(_event_log ? &_event_log->getColumns() : nullptr),
      _scrolled_window(),
      _event_list_store(),
      _event_list_selection(_event_list_view.get_selection()),
      _deskTrack(),
      _desktopChangeConn(),
      _callback_connections()
{
    if ( !_document || !_event_log || !_columns ) return;

    set_size_request(-1, 95);

    _getContents()->pack_start(_scrolled_window);
    _scrolled_window.set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_AUTOMATIC);

    // connect with the EventLog
    _connectEventLog();

    _event_list_view.set_enable_search(false);
    _event_list_view.set_headers_visible(false);

    CellRendererSPIcon* icon_renderer = Gtk::manage(new CellRendererSPIcon());
    icon_renderer->property_xpad() = 2;
    icon_renderer->property_width() = 24;
    int cols_count = _event_list_view.append_column("Icon", *icon_renderer);

    Gtk::TreeView::Column* icon_column = _event_list_view.get_column(cols_count-1);
    icon_column->add_attribute(icon_renderer->property_event_type(), _columns->type);

    CellRendererInt* children_renderer = Gtk::manage(new CellRendererInt(greater_than_1));
    children_renderer->property_weight() = 600; // =Pango::WEIGHT_SEMIBOLD (not defined in old versions of pangomm)
    children_renderer->property_xalign() = 1.0;
    children_renderer->property_xpad() = 2;
    children_renderer->property_width() = 24;

    cols_count = _event_list_view.append_column("Children", *children_renderer);
    Gtk::TreeView::Column* children_column = _event_list_view.get_column(cols_count-1);
    children_column->add_attribute(children_renderer->property_number(), _columns->child_count);

    Gtk::CellRendererText* description_renderer = Gtk::manage(new Gtk::CellRendererText());
    description_renderer->property_ellipsize() = Pango::ELLIPSIZE_END;

    cols_count = _event_list_view.append_column("Description", *description_renderer);
    Gtk::TreeView::Column* description_column = _event_list_view.get_column(cols_count-1);
    description_column->add_attribute(description_renderer->property_text(), _columns->description);
    description_column->set_resizable();
    description_column->set_sizing(Gtk::TREE_VIEW_COLUMN_AUTOSIZE);
    description_column->set_min_width (150);

    _event_list_view.set_expander_column( *_event_list_view.get_column(cols_count-1) );

    _scrolled_window.add(_event_list_view);

    // connect EventLog callbacks
    _callback_connections[EventLog::CALLB_SELECTION_CHANGE] =
        _event_list_selection->signal_changed().connect(sigc::mem_fun(*this, &Inkscape::UI::Dialog::UndoHistory::_onListSelectionChange));

    _callback_connections[EventLog::CALLB_EXPAND] =
        _event_list_view.signal_row_expanded().connect(sigc::mem_fun(*this, &Inkscape::UI::Dialog::UndoHistory::_onExpandEvent));

    _callback_connections[EventLog::CALLB_COLLAPSE] =
        _event_list_view.signal_row_collapsed().connect(sigc::mem_fun(*this, &Inkscape::UI::Dialog::UndoHistory::_onCollapseEvent));

    _desktopChangeConn = _deskTrack.connectDesktopChanged( sigc::mem_fun(*this, &UndoHistory::setDesktop) );
    _deskTrack.connect(GTK_WIDGET(gobj()));

    // connect to be informed of document changes
    signalDocumentReplaced().connect(sigc::mem_fun(*this, &UndoHistory::_handleDocumentReplaced));

    show_all_children();

    // scroll to the selected row
    _event_list_view.set_cursor(_event_list_store->get_path(_event_log->getCurrEvent()));
}

UndoHistory::~UndoHistory()
{
    _desktopChangeConn.disconnect();
}


void UndoHistory::setDesktop(SPDesktop* desktop)
{
    Panel::setDesktop(desktop);

    EventLog *newEventLog = desktop ? desktop->event_log : nullptr;
    if ((_desktop == desktop) && (_event_log == newEventLog)) {
        // same desktop set
    }
    else
    {
        _connectDocument(desktop, desktop ? desktop->doc() : nullptr);
    }
}

void UndoHistory::_connectDocument(SPDesktop* desktop, SPDocument * /*document*/)
{
    // disconnect from prior
    if (_event_log) {
        _event_log->removeDialogConnection(&_event_list_view, &_callback_connections);
    }

    SignalBlocker blocker(&_callback_connections[EventLog::CALLB_SELECTION_CHANGE]);

    _event_list_view.unset_model();

    // connect to new EventLog/Desktop
    _desktop = desktop;
    _event_log = desktop ? desktop->event_log : nullptr;
    _document = desktop ? desktop->doc() : nullptr;
    _connectEventLog();
}

void UndoHistory::_connectEventLog()
{
    if (_event_log) {
        _event_log->add_destroy_notify_callback(this, &_handleEventLogDestroyCB);
        _event_list_store = _event_log->getEventListStore();

        _event_list_view.set_model(_event_list_store);

        _event_log->addDialogConnection(&_event_list_view, &_callback_connections);
        _event_list_view.scroll_to_row(_event_list_store->get_path(_event_list_selection->get_selected()));        
    }
}

void UndoHistory::_handleDocumentReplaced(SPDesktop* desktop, SPDocument *document)
{
    if ((desktop != _desktop) || (document != _document)) {
        _connectDocument(desktop, document);
    }
}

void *UndoHistory::_handleEventLogDestroyCB(void *data)
{
    void *result = nullptr;
    if (data) {
        UndoHistory *self = reinterpret_cast<UndoHistory*>(data);
        result = self->_handleEventLogDestroy();
    }
    return result;
}

// called *after* _event_log has been destroyed.
void *UndoHistory::_handleEventLogDestroy()
{
    if (_event_log) {
        SignalBlocker blocker(&_callback_connections[EventLog::CALLB_SELECTION_CHANGE]);

        _event_list_view.unset_model();
        _event_list_store.reset();
        _event_log = nullptr;
    }

    return nullptr;
}

void
UndoHistory::_onListSelectionChange()
{

    EventLog::const_iterator selected = _event_list_selection->get_selected();

    /* If no event is selected in the view, find the right one and select it. This happens whenever
     * a branch we're currently in is collapsed.
     */
    if (!selected) {

        EventLog::iterator curr_event = _event_log->getCurrEvent();

        if (curr_event->parent()) {

            EventLog::iterator curr_event_parent = curr_event->parent();
            EventLog::iterator last = curr_event_parent->children().end();

            _event_log->blockNotifications();
            for ( --last ; curr_event != last ; ++curr_event ) {
                DocumentUndo::redo(_document);
            }
            _event_log->blockNotifications(false);

            _event_log->setCurrEvent(curr_event);
            _event_list_selection->select(curr_event_parent);

        } else {  // this should not happen
            _event_list_selection->select(curr_event);
        }

    } else {

        EventLog::const_iterator last_selected = _event_log->getCurrEvent();

        /* Selecting a collapsed parent event is equal to selecting the last child
         * of that parent's branch.
         */

        if ( !selected->children().empty() &&
             !_event_list_view.row_expanded(_event_list_store->get_path(selected)) )
        {
            selected = selected->children().end();
            --selected;
        }

        // An event before the current one has been selected. Undo to the selected event.
        if ( _event_list_store->get_path(selected) <
             _event_list_store->get_path(last_selected) )
        {
            _event_log->blockNotifications();

            while ( selected != last_selected ) {

                DocumentUndo::undo(_document);

                if ( last_selected->parent() &&
                     last_selected == last_selected->parent()->children().begin() )
                {
                    last_selected = last_selected->parent();
                    _event_log->setCurrEventParent((EventLog::iterator)nullptr);
                } else {
                    --last_selected;
                    if ( !last_selected->children().empty() ) {
                        _event_log->setCurrEventParent(last_selected);
                        last_selected = last_selected->children().end();
                        --last_selected;
                    }
                }
            }
            _event_log->blockNotifications(false);
            _event_log->updateUndoVerbs();

        } else { // An event after the current one has been selected. Redo to the selected event.

            _event_log->blockNotifications();

            while ( selected != last_selected ) {

                DocumentUndo::redo(_document);

                if ( !last_selected->children().empty() ) {
                    _event_log->setCurrEventParent(last_selected);
                    last_selected = last_selected->children().begin();
                } else {
                    ++last_selected;
                    if ( last_selected->parent() &&
                         last_selected == last_selected->parent()->children().end() )
                    {
                        last_selected = last_selected->parent();
                        ++last_selected;
                        _event_log->setCurrEventParent((EventLog::iterator)nullptr);
                    }
                }
            }
            _event_log->blockNotifications(false);

        }

        _event_log->setCurrEvent(selected);
        _event_log->updateUndoVerbs();
    }

}

void
UndoHistory::_onExpandEvent(const Gtk::TreeModel::iterator &iter, const Gtk::TreeModel::Path &/*path*/)
{
    if ( iter == _event_list_selection->get_selected() ) {
        _event_list_selection->select(_event_log->getCurrEvent());
    }
}

void
UndoHistory::_onCollapseEvent(const Gtk::TreeModel::iterator &iter, const Gtk::TreeModel::Path &/*path*/)
{
    // Collapsing a branch we're currently in is equal to stepping to the last event in that branch
    if ( iter == _event_log->getCurrEvent() ) {
        EventLog::const_iterator curr_event_parent = _event_log->getCurrEvent();
        EventLog::const_iterator curr_event = curr_event_parent->children().begin();
        EventLog::const_iterator last = curr_event_parent->children().end();

        _event_log->blockNotifications();
        DocumentUndo::redo(_document);

        for ( --last ; curr_event != last ; ++curr_event ) {
            DocumentUndo::redo(_document);
        }
        _event_log->blockNotifications(false);

        _event_log->setCurrEvent(curr_event);
        _event_log->setCurrEventParent(curr_event_parent);
        _event_list_selection->select(curr_event_parent);
    }
}

const CellRendererInt::Filter& UndoHistory::greater_than_1 = UndoHistory::GreaterThan(1);

} // namespace Dialog
} // namespace UI
} // 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 :
