// SPDX-License-Identifier: GPL-2.0-or-later
/**
 * Specialization of GtkTreeView for the XML tree view
 *
 * Authors:
 *   MenTaLguY <mental@rydia.net>
 *
 * Copyright (C) 2002 MenTaLguY
 *
 * Released under GNU GPL v2+, read the file 'COPYING' for more information.
 */

#include <cstring>
#include <gmodule.h>

#include "xml/node-event-vector.h"
#include "sp-xmlview-tree.h"

namespace {
struct NodeData {
	SPXMLViewTree * tree;
	GtkTreeRowReference  *rowref;
	Inkscape::XML::Node * repr;
    bool expanded = false; //< true if tree view has been expanded to this node
    bool dragging = false;

    NodeData(SPXMLViewTree *tree, GtkTreeIter *node, Inkscape::XML::Node *repr);
    ~NodeData();
};

// currently dragged node
Inkscape::XML::Node *dragging_repr = nullptr;
} // namespace

enum { STORE_TEXT_COL = 0, STORE_DATA_COL, STORE_N_COLS };

static void sp_xmlview_tree_destroy(GtkWidget * object);

static NodeData *sp_xmlview_tree_node_get_data(GtkTreeModel *model, GtkTreeIter *iter);

static void add_node(SPXMLViewTree *tree, GtkTreeIter *parent, GtkTreeIter *before, Inkscape::XML::Node *repr);

static void element_child_added (Inkscape::XML::Node * repr, Inkscape::XML::Node * child, Inkscape::XML::Node * ref, gpointer data);
static void element_attr_changed (Inkscape::XML::Node * repr, const gchar * key, const gchar * old_value, const gchar * new_value, bool is_interactive, gpointer data);
static void element_child_removed (Inkscape::XML::Node * repr, Inkscape::XML::Node * child, Inkscape::XML::Node * ref, gpointer data);
static void element_order_changed (Inkscape::XML::Node * repr, Inkscape::XML::Node * child, Inkscape::XML::Node * oldref, Inkscape::XML::Node * newref, gpointer data);
static void element_name_changed (Inkscape::XML::Node* repr, gchar const* oldname, gchar const* newname, gpointer data);
static void element_attr_or_name_change_update(Inkscape::XML::Node* repr, NodeData* data);

static void text_content_changed (Inkscape::XML::Node * repr, const gchar * old_content, const gchar * new_content, gpointer data);
static void comment_content_changed (Inkscape::XML::Node * repr, const gchar * old_content, const gchar * new_content, gpointer data);
static void pi_content_changed (Inkscape::XML::Node * repr, const gchar * old_content, const gchar * new_content, gpointer data);

static gboolean ref_to_sibling (NodeData *node, Inkscape::XML::Node * ref, GtkTreeIter *);
static gboolean repr_to_child (NodeData *node, Inkscape::XML::Node * repr, GtkTreeIter *);
static GtkTreeRowReference *tree_iter_to_ref(SPXMLViewTree *, GtkTreeIter *);
static gboolean tree_ref_to_iter (SPXMLViewTree * tree, GtkTreeIter* iter, GtkTreeRowReference  *ref);

static gboolean search_equal_func(GtkTreeModel *, gint column, const gchar *key, GtkTreeIter *, gpointer search_data);
static gboolean foreach_func(GtkTreeModel *, GtkTreePath *, GtkTreeIter *, gpointer user_data);

static void on_row_changed(GtkTreeModel *, GtkTreePath *, GtkTreeIter *, gpointer user_data);
static void on_drag_begin(GtkWidget *, GdkDragContext *, gpointer userdata);
static void on_drag_end(GtkWidget *, GdkDragContext *, gpointer userdata);
static gboolean do_drag_motion(GtkWidget *, GdkDragContext *, gint x, gint y, guint time, gpointer user_data);

static const Inkscape::XML::NodeEventVector element_repr_events = {
        element_child_added,
        element_child_removed,
        element_attr_changed,
        nullptr, /* content_changed */
        element_order_changed,
        element_name_changed
};

static const Inkscape::XML::NodeEventVector text_repr_events = {
        nullptr, /* child_added */
        nullptr, /* child_removed */
        nullptr, /* attr_changed */
        text_content_changed,
        nullptr  /* order_changed */,
        nullptr  /* element_name_changed */
};

static const Inkscape::XML::NodeEventVector comment_repr_events = {
        nullptr, /* child_added */
        nullptr, /* child_removed */
        nullptr, /* attr_changed */
        comment_content_changed,
        nullptr  /* order_changed */,
        nullptr  /* element_name_changed */
};

static const Inkscape::XML::NodeEventVector pi_repr_events = {
        nullptr, /* child_added */
        nullptr, /* child_removed */
        nullptr, /* attr_changed */
        pi_content_changed,
        nullptr  /* order_changed */,
        nullptr  /* element_name_changed */
};

/**
 * Get an iterator to the first child of `data`
 * @param data handle which references a row
 * @param[out] child_iter On success: valid iterator to first child
 * @return False if the node has no children
 */
static bool get_first_child(NodeData *data, GtkTreeIter *child_iter)
{
    GtkTreeIter iter;
    return tree_ref_to_iter(data->tree, &iter, data->rowref) &&
           gtk_tree_model_iter_children(GTK_TREE_MODEL(data->tree->store), child_iter, &iter);
}

/**
 * @param iter First dummy row on that level
 * @pre all rows on the same level are dummies
 * @pre iter is valid
 * @post iter is invalid
 * @post level is empty
 */
static void remove_dummy_rows(GtkTreeStore *store, GtkTreeIter *iter)
{
    do {
        g_assert(nullptr == sp_xmlview_tree_node_get_data(GTK_TREE_MODEL(store), iter));
        gtk_tree_store_remove(store, iter);
    } while (gtk_tree_store_iter_is_valid(store, iter));
}

static gboolean on_test_expand_row( //
    GtkTreeView *tree_view,         //
    GtkTreeIter *iter,              //
    GtkTreePath *path,              //
    gpointer)
{
    auto tree = SP_XMLVIEW_TREE(tree_view);
    auto model = GTK_TREE_MODEL(tree->store);

    GtkTreeIter childiter;
    bool has_children = gtk_tree_model_iter_children(model, &childiter, iter);
    g_assert(has_children);

    if (sp_xmlview_tree_node_get_repr(model, &childiter) == nullptr) {
        NodeData *data = sp_xmlview_tree_node_get_data(model, iter);

        remove_dummy_rows(tree->store, &childiter);

        // insert real rows
        data->expanded = true;
        sp_repr_synthesize_events(data->repr, &element_repr_events, data);
    }

    return false;
}

GtkWidget *sp_xmlview_tree_new(Inkscape::XML::Node * repr, void * /*factory*/, void * /*data*/)
{
    SPXMLViewTree *tree = SP_XMLVIEW_TREE(g_object_new (SP_TYPE_XMLVIEW_TREE, nullptr));

    gtk_tree_view_set_headers_visible (GTK_TREE_VIEW(tree), FALSE);
    gtk_tree_view_set_reorderable (GTK_TREE_VIEW(tree), TRUE);
    gtk_tree_view_set_enable_search (GTK_TREE_VIEW(tree), TRUE);
    gtk_tree_view_set_search_equal_func (GTK_TREE_VIEW(tree), search_equal_func, nullptr, nullptr);

    GtkCellRenderer *renderer = gtk_cell_renderer_text_new ();
    GtkTreeViewColumn *column = gtk_tree_view_column_new_with_attributes ("", renderer, "text", STORE_TEXT_COL, nullptr);
    gtk_tree_view_append_column (GTK_TREE_VIEW (tree), column);
    gtk_cell_renderer_set_padding (renderer, 2, 0);
    gtk_tree_view_column_set_sizing (column, GTK_TREE_VIEW_COLUMN_AUTOSIZE);

    sp_xmlview_tree_set_repr (tree, repr);

    g_signal_connect(GTK_TREE_VIEW(tree), "drag-begin", G_CALLBACK(on_drag_begin), tree);
    g_signal_connect(GTK_TREE_VIEW(tree), "drag-end", G_CALLBACK(on_drag_end), tree);
    g_signal_connect(GTK_TREE_VIEW(tree), "drag-motion",  G_CALLBACK(do_drag_motion), tree);
    g_signal_connect(GTK_TREE_VIEW(tree), "test-expand-row", G_CALLBACK(on_test_expand_row), nullptr);

    return GTK_WIDGET(tree);
}

G_DEFINE_TYPE(SPXMLViewTree, sp_xmlview_tree, GTK_TYPE_TREE_VIEW);

void sp_xmlview_tree_class_init(SPXMLViewTreeClass * klass)
{
    auto widget_class = GTK_WIDGET_CLASS(klass);
    widget_class->destroy = sp_xmlview_tree_destroy;
    
    // Signal for when a tree drag and drop has completed
    g_signal_new (  "tree_move",
        G_TYPE_FROM_CLASS(klass),
        G_SIGNAL_RUN_FIRST,
        0,
        nullptr, nullptr,
        g_cclosure_marshal_VOID__UINT,
        G_TYPE_NONE, 1,
        G_TYPE_UINT);
}

void
sp_xmlview_tree_init (SPXMLViewTree * tree)
{
	tree->repr = nullptr;
	tree->blocked = 0;
}

void sp_xmlview_tree_destroy(GtkWidget * object)
{
	SPXMLViewTree * tree = SP_XMLVIEW_TREE (object);

	sp_xmlview_tree_set_repr (tree, nullptr);

	GTK_WIDGET_CLASS(sp_xmlview_tree_parent_class)->destroy (object);
}

/*
 * Add a new row to the tree
 */
void
add_node (SPXMLViewTree * tree, GtkTreeIter *parent, GtkTreeIter *before, Inkscape::XML::Node * repr)
{
	const Inkscape::XML::NodeEventVector * vec;

	g_assert (tree != nullptr);

    if (before && !gtk_tree_store_iter_is_valid(tree->store, before)) {
        before = nullptr;
    }

	GtkTreeIter iter;
    gtk_tree_store_insert_before (tree->store, &iter, parent, before);

    if (!gtk_tree_store_iter_is_valid(tree->store, &iter)) {
        return;
    }

    if (!repr) {
        // no need to store any data
        return;
    }

    auto data = new NodeData(tree, &iter, repr);

    g_assert (data != nullptr);

    gtk_tree_store_set(tree->store, &iter, STORE_DATA_COL, data, -1);

	if ( repr->type() == Inkscape::XML::NodeType::TEXT_NODE ) {
		vec = &text_repr_events;
	} else if ( repr->type() == Inkscape::XML::NodeType::COMMENT_NODE ) {
		vec = &comment_repr_events;
	} else if ( repr->type() == Inkscape::XML::NodeType::PI_NODE ) {
		vec = &pi_repr_events;
	} else if ( repr->type() == Inkscape::XML::NodeType::ELEMENT_NODE ) {
		vec = &element_repr_events;
	} else {
		vec = nullptr;
	}

	if (vec) {
		/* cheat a little to get the text updated on nodes without id */
        if (repr->type() == Inkscape::XML::NodeType::ELEMENT_NODE && repr->attribute("id") == nullptr) {
			element_attr_changed (repr, "id", nullptr, nullptr, false, data);
		}
		sp_repr_add_listener (repr, vec, data);
		sp_repr_synthesize_events (repr, vec, data);
	}
}

static gboolean remove_all_listeners(GtkTreeModel *model, GtkTreePath *, GtkTreeIter *iter, gpointer)
{
    NodeData *data = sp_xmlview_tree_node_get_data(model, iter);
    delete data;
    return false;
}

NodeData::NodeData(SPXMLViewTree *tree, GtkTreeIter *iter, Inkscape::XML::Node *repr)
    : tree(tree)
    , rowref(tree_iter_to_ref(tree, iter))
    , repr(repr)
{
    if (repr) {
        Inkscape::GC::anchor(repr);
    }
}

NodeData::~NodeData()
{
    if (repr) {
        sp_repr_remove_listener_by_data(repr, this);
        Inkscape::GC::release(repr);
    }
    gtk_tree_row_reference_free(rowref);
}

void element_child_added (Inkscape::XML::Node * /*repr*/, Inkscape::XML::Node * child, Inkscape::XML::Node * ref, gpointer ptr)
{
    NodeData *data = static_cast<NodeData *>(ptr);
    GtkTreeIter before;

    if (data->tree->blocked) return;

    if (!ref_to_sibling (data, ref, &before)) {
        return;
    }

    GtkTreeIter data_iter;
    tree_ref_to_iter(data->tree, &data_iter,  data->rowref);

    if (!data->expanded) {
        auto model = GTK_TREE_MODEL(data->tree->store);
        GtkTreeIter childiter;
        if (!gtk_tree_model_iter_children(model, &childiter, &data_iter)) {
            // no children yet, add a dummy
            child = nullptr;
        } else if (sp_xmlview_tree_node_get_repr(model, &childiter) == nullptr) {
            // already has a dummy child
            return;
        }
    }

    add_node (data->tree, &data_iter, &before, child);
}

void element_attr_changed(
        Inkscape::XML::Node* repr, gchar const* key,
        gchar const* /*old_value*/, gchar const* /*new_value*/, bool /*is_interactive*/,
        gpointer ptr)
{
    if (0 != strcmp (key, "id") && 0 != strcmp (key, "inkscape:label"))
        return;
    element_attr_or_name_change_update(repr, static_cast<NodeData*>(ptr));
}

void element_name_changed(
        Inkscape::XML::Node* repr,
        gchar const* /*oldname*/, gchar const* /*newname*/, gpointer ptr)
{
    element_attr_or_name_change_update(repr, static_cast<NodeData*>(ptr));
}

void element_attr_or_name_change_update(Inkscape::XML::Node* repr, NodeData* data)
{
    if (data->tree->blocked) {
        return;
    }

    gchar const* node_name = repr->name();
    gchar const* id_value = repr->attribute("id");
    gchar const* label_value = repr->attribute("inkscape:label");
    gchar* display_text;

    if (id_value && label_value) {
        display_text = g_strdup_printf ("<%s id=\"%s\" inkscape:label=\"%s\">", node_name, id_value, label_value);
    } else if (id_value) {
        display_text = g_strdup_printf ("<%s id=\"%s\">", node_name, id_value);
    } else if (label_value) {
        display_text = g_strdup_printf ("<%s inkscape:label=\"%s\">", node_name, label_value);
    } else {
        display_text = g_strdup_printf ("<%s>", node_name);
    }

    GtkTreeIter iter;
    if (tree_ref_to_iter(data->tree, &iter,  data->rowref)) {
        gtk_tree_store_set (GTK_TREE_STORE(data->tree->store), &iter, STORE_TEXT_COL, display_text, -1);
    }

    g_free(display_text);
}

void element_child_removed(Inkscape::XML::Node *repr, Inkscape::XML::Node *child, Inkscape::XML::Node * /*ref*/,
                           gpointer ptr)
{
    NodeData *data = static_cast<NodeData *>(ptr);

    if (data->tree->blocked) return;

    GtkTreeIter iter;
    if (repr_to_child(data, child, &iter)) {
        delete sp_xmlview_tree_node_get_data(GTK_TREE_MODEL(data->tree->store), &iter);
        gtk_tree_store_remove(data->tree->store, &iter);
    } else if (!repr->firstChild() && get_first_child(data, &iter)) {
        // remove dummy when all children gone
        remove_dummy_rows(data->tree->store, &iter);
    } else {
        return;
    }

#ifndef GTK_ISSUE_2510_IS_FIXED
    // https://gitlab.gnome.org/GNOME/gtk/issues/2510
    gtk_tree_selection_unselect_all(gtk_tree_view_get_selection(GTK_TREE_VIEW(data->tree)));
#endif
}

void element_order_changed(Inkscape::XML::Node * /*repr*/, Inkscape::XML::Node * child, Inkscape::XML::Node * /*oldref*/, Inkscape::XML::Node * newref, gpointer ptr)
{
    NodeData *data = static_cast<NodeData *>(ptr);
    GtkTreeIter before, node;

    if (data->tree->blocked) return;

    ref_to_sibling (data, newref, &before);
    repr_to_child (data, child, &node);

    if (gtk_tree_store_iter_is_valid(data->tree->store, &before)) {
        gtk_tree_store_move_before (data->tree->store, &node, &before);
    } else {
        repr_to_child (data, newref, &before);
        gtk_tree_store_move_after (data->tree->store, &node, &before);
    }
}

/**
 * Truncate `val` to `maxlen` unicode characters and replace newlines and tabs
 * with placeholder symbols. The string is modified in place.
 * @param[in,out] val String in UTF-8 encoding
 */
static void sp_remove_newlines_and_tabs(std::string &val, size_t const maxlen = 200)
{
    if (g_utf8_strlen(val.data(), maxlen * 2) > maxlen) {
        size_t newlen = g_utf8_offset_to_pointer(val.data(), maxlen - 3) - val.data();
        val.resize(newlen);
        val.append("…");
    }

    struct
    {
        const char *query;
        const char *replacement;
    } replacements[] = {
        {"\r\n", "⏎"},
        {"\n", "⏎"},
        {"\t", "⇥"},
    };

    for (auto const &item : replacements) {
        for (size_t pos = 0; (pos = val.find(item.query, pos)) != std::string::npos;) {
            val.replace(pos, strlen(item.query), item.replacement);
        }
    }
}

void text_content_changed(Inkscape::XML::Node * /*repr*/, const gchar * /*old_content*/, const gchar * new_content, gpointer ptr)
{
    NodeData *data = static_cast<NodeData *>(ptr);

    if (data->tree->blocked) return;

    auto nolinecontent = std::string("\"").append(new_content).append("\"");
    sp_remove_newlines_and_tabs(nolinecontent);

    GtkTreeIter iter;
    if (tree_ref_to_iter(data->tree, &iter,  data->rowref)) {
        gtk_tree_store_set(GTK_TREE_STORE(data->tree->store), &iter, STORE_TEXT_COL, nolinecontent.c_str(), -1);
    }
}

void comment_content_changed(Inkscape::XML::Node * /*repr*/, const gchar * /*old_content*/, const gchar *new_content, gpointer ptr)
{
    NodeData *data = static_cast<NodeData*>(ptr);

    if (data->tree->blocked) return;

    auto nolinecontent = std::string("<!--").append(new_content).append("-->");
    sp_remove_newlines_and_tabs(nolinecontent);

    GtkTreeIter iter;
    if (tree_ref_to_iter(data->tree, &iter,  data->rowref)) {
        gtk_tree_store_set(GTK_TREE_STORE(data->tree->store), &iter, STORE_TEXT_COL, nolinecontent.c_str(), -1);
    }
}

void pi_content_changed(Inkscape::XML::Node *repr, const gchar * /*old_content*/, const gchar *new_content, gpointer ptr)
{
    NodeData *data = static_cast<NodeData *>(ptr);

    if (data->tree->blocked) return;

    auto nolinecontent = std::string("<?").append(repr->name()).append(" ").append(new_content).append("?>");
    sp_remove_newlines_and_tabs(nolinecontent);

    GtkTreeIter iter;
    if (tree_ref_to_iter(data->tree, &iter,  data->rowref)) {
        gtk_tree_store_set(GTK_TREE_STORE(data->tree->store), &iter, STORE_TEXT_COL, nolinecontent.c_str(), -1);
    }
}

/*
 * Save the source path on drag start, will need it in on_row_changed() when moving a row
 */
void on_drag_begin(GtkWidget *, GdkDragContext *, gpointer userdata)
{
    SPXMLViewTree *tree = static_cast<SPXMLViewTree *>(userdata);
    if (!tree) {
        return;
    }

    GtkTreeModel *model = nullptr;
    GtkTreeIter iter;
    GtkTreeSelection *selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(tree));
    if (gtk_tree_selection_get_selected(selection, &model, &iter)) {
        NodeData *data = sp_xmlview_tree_node_get_data(model, &iter);
        if (data) {
            data->dragging = true;
            dragging_repr = data->repr;
        }
    }
}

/**
 * Finalize what happened in `on_row_changed` and clean up what was set up in `on_drag_begin`
 */
void on_drag_end(GtkWidget *, GdkDragContext *, gpointer userdata)
{
    if (!dragging_repr)
        return;

    auto tree = static_cast<SPXMLViewTree *>(userdata);
    auto selection = gtk_tree_view_get_selection(GTK_TREE_VIEW(tree));
    bool failed = false;

    GtkTreeIter iter;
    if (sp_xmlview_tree_get_repr_node(tree, dragging_repr, &iter)) {
        NodeData *data = sp_xmlview_tree_node_get_data(GTK_TREE_MODEL(tree->store), &iter);

        if (data && data->dragging) {
            // dragging flag was not cleared in `on_row_changed`, this indicates a failed drag
            data->dragging = false;
            failed = true;
        } else {
            // Reselect the dragged row
            gtk_tree_selection_select_iter(selection, &iter);
        }
    } else {
#ifndef GTK_ISSUE_2510_IS_FIXED
        // https://gitlab.gnome.org/GNOME/gtk/issues/2510
        gtk_tree_selection_unselect_all(selection);
#endif
    }

    dragging_repr = nullptr;

    if (!failed) {
        // Signal that a drag and drop has completed successfully
        g_signal_emit_by_name(G_OBJECT(tree), "tree_move", GUINT_TO_POINTER(1));
    }
}

/*
 * Main drag & drop function
 * Get the old and new paths, and change the Inkscape::XML::Node repr's
 */
void on_row_changed(GtkTreeModel *tree_model, GtkTreePath *path, GtkTreeIter *iter, gpointer user_data)
{
    NodeData *data = sp_xmlview_tree_node_get_data(tree_model, iter);

    if (!data || !data->dragging) {
        return;
    }
    data->dragging = false;

    SPXMLViewTree *tree = SP_XMLVIEW_TREE(user_data);

    gtk_tree_row_reference_free(data->rowref);
    data->rowref = tree_iter_to_ref(tree, iter);

    GtkTreeIter new_parent;
    if (!gtk_tree_model_iter_parent(tree_model, &new_parent, iter)) {
        //No parent of drop location
        return;
    }

    Inkscape::XML::Node *repr = sp_xmlview_tree_node_get_repr(tree_model, iter);
    Inkscape::XML::Node *before_repr = nullptr;

    // Find the sibling node before iter
    GtkTreeIter before_iter = *iter;
    if (gtk_tree_model_iter_previous(tree_model, &before_iter)) {
        before_repr = sp_xmlview_tree_node_get_repr(tree_model, &before_iter);
    }

    // Drop onto oneself causes assert in changeOrder() below, ignore
    if (repr == before_repr)
        return;

    auto repr_old_parent = repr->parent();
    auto repr_new_parent = sp_xmlview_tree_node_get_repr(tree_model, &new_parent);

    tree->blocked++;

    if (repr_old_parent == repr_new_parent) {
        repr_old_parent->changeOrder(repr, before_repr);
    } else {
        repr_old_parent->removeChild(repr);
        repr_new_parent->addChild(repr, before_repr);
    }

    NodeData *data_new_parent = sp_xmlview_tree_node_get_data(tree_model, &new_parent);
    if (data_new_parent && data_new_parent->expanded) {
        // Reselect the dragged row in `on_drag_end` instead of here, because of
        // https://gitlab.gnome.org/GNOME/gtk/-/issues/2510
    } else {
        // convert to dummy node
        delete data;
        gtk_tree_store_set(tree->store, iter, STORE_DATA_COL, nullptr, -1);
    }

    tree->blocked--;
}

/*
 * Set iter to ref or node data's child with the same repr or first child
 */
gboolean ref_to_sibling (NodeData *data, Inkscape::XML::Node *repr, GtkTreeIter *iter)
{
	if (repr) {
		if (!repr_to_child (data, repr, iter)) {
            return false;
        }
        gtk_tree_model_iter_next (GTK_TREE_MODEL(data->tree->store), iter);
	} else {
	    GtkTreeIter data_iter;
	    if (!tree_ref_to_iter(data->tree, &data_iter,  data->rowref)) {
	        return false;
	    }
	    gtk_tree_model_iter_children(GTK_TREE_MODEL(data->tree->store), iter, &data_iter);
	}
	return true;
}

/*
 * Set iter to the node data's child with the same repr
 */
gboolean repr_to_child (NodeData *data, Inkscape::XML::Node * repr, GtkTreeIter *iter)
{
    GtkTreeIter data_iter;
    GtkTreeModel *model = GTK_TREE_MODEL(data->tree->store);
    gboolean valid = false;

    if (!tree_ref_to_iter(data->tree, &data_iter, data->rowref)) {
        return false;
    }

    /*
     * The node we are looking for is likely to be the last one, so check it first.
     */
    gint n_children = gtk_tree_model_iter_n_children (model, &data_iter);
    if (n_children > 1) {
        valid = gtk_tree_model_iter_nth_child (model, iter, &data_iter, n_children-1);
        if (valid && sp_xmlview_tree_node_get_repr (model, iter) == repr) {
            //g_message("repr_to_child hit %d", n_children);
            return valid;
        }
    }

    valid = gtk_tree_model_iter_children(model, iter, &data_iter);
    while (valid && sp_xmlview_tree_node_get_repr (model, iter) != repr) {
        valid = gtk_tree_model_iter_next(model, iter);
	}

    return valid;
}

/*
 * Get a matching GtkTreeRowReference for a GtkTreeIter
 */
GtkTreeRowReference  *tree_iter_to_ref (SPXMLViewTree * tree, GtkTreeIter* iter)
{
    GtkTreePath* path = gtk_tree_model_get_path(GTK_TREE_MODEL(tree->store), iter);
    GtkTreeRowReference  *ref = gtk_tree_row_reference_new(GTK_TREE_MODEL(tree->store), path);
    gtk_tree_path_free(path);

    return ref;
}

/*
 * Get a matching GtkTreeIter for a GtkTreeRowReference
 */
gboolean tree_ref_to_iter (SPXMLViewTree * tree, GtkTreeIter* iter, GtkTreeRowReference  *ref)
{
    GtkTreePath* path = gtk_tree_row_reference_get_path(ref);
    if (!path) {
        return false;
    }
    gboolean const valid = //
        gtk_tree_model_get_iter(GTK_TREE_MODEL(tree->store), iter, path);
    gtk_tree_path_free(path);

    return valid;
}

/*
 * Disable drag and drop target on : root node and non-element nodes
 */
gboolean do_drag_motion(GtkWidget *widget, GdkDragContext *context, gint x, gint y, guint time, gpointer user_data)
{
    GtkTreePath *path = nullptr;
    GtkTreeViewDropPosition pos;
    gtk_tree_view_get_dest_row_at_pos (GTK_TREE_VIEW(widget), x, y, &path, &pos);

    int action = 0;

    if (!dragging_repr) {
        goto finally;
    }

    if (path) {
        SPXMLViewTree *tree = SP_XMLVIEW_TREE(user_data);
        GtkTreeIter iter;
        gtk_tree_model_get_iter(GTK_TREE_MODEL(tree->store), &iter, path);
        auto repr = sp_xmlview_tree_node_get_repr(GTK_TREE_MODEL(tree->store), &iter);

        bool const drop_into = pos != GTK_TREE_VIEW_DROP_BEFORE && //
                               pos != GTK_TREE_VIEW_DROP_AFTER;

        // 0. don't drop on self (also handled by on_row_changed but nice to not have drop highlight for it)
        if (repr == dragging_repr) {
            goto finally;
        }

        // 1. only xml elements can have children
        if (drop_into && repr->type() != Inkscape::XML::NodeType::ELEMENT_NODE) {
            goto finally;
        }

        // 3. elements must be at least children of the root <svg:svg> element
        if (gtk_tree_path_get_depth(path) < 2) {
            goto finally;
        }

        // 4. drag node specific limitations
        {
            // nodes which can't be re-parented (because the document holds pointers to them which must stay valid)
            static GQuark const CODE_sodipodi_namedview = g_quark_from_static_string("sodipodi:namedview");
            static GQuark const CODE_svg_defs = g_quark_from_static_string("svg:defs");

            bool const no_reparenting = dragging_repr->code() == CODE_sodipodi_namedview || //
                                        dragging_repr->code() == CODE_svg_defs;

            if (no_reparenting && (drop_into || dragging_repr->parent() != repr->parent())) {
                goto finally;
            }
        }

        action = GDK_ACTION_MOVE;
    }

finally:
    if (action == 0) {
        // remove drop highlight
        gtk_tree_view_set_drag_dest_row(GTK_TREE_VIEW(widget), nullptr, pos /* ignored */);
    }

    gtk_tree_path_free(path);
    gdk_drag_status (context, (GdkDragAction)action, time);

    return (action == 0);
}

/*
 * Set the tree selection and scroll to the row with the given repr
 */
void
sp_xmlview_tree_set_repr (SPXMLViewTree * tree, Inkscape::XML::Node * repr)
{
    if ( tree->repr == repr ) return;

    if (tree->store) {
        gtk_tree_view_set_model(GTK_TREE_VIEW(tree), nullptr);
        gtk_tree_model_foreach(GTK_TREE_MODEL(tree->store), remove_all_listeners, nullptr);
        g_object_unref(tree->store);
        tree->store = nullptr;
    }

    if (tree->repr) {
        Inkscape::GC::release(tree->repr);
    }
    tree->repr = repr;
    if (repr) {
        tree->store = gtk_tree_store_new(STORE_N_COLS, G_TYPE_STRING, G_TYPE_POINTER);

        Inkscape::GC::anchor(repr);
        add_node(tree, nullptr, nullptr, repr);

        // Set the tree model here, after all data is inserted
        gtk_tree_view_set_model (GTK_TREE_VIEW(tree), GTK_TREE_MODEL(tree->store));
        g_signal_connect(G_OBJECT(tree->store), "row-changed", G_CALLBACK(on_row_changed), tree);

        GtkTreePath *path = gtk_tree_path_new_from_indices(0, -1);
        gtk_tree_view_expand_to_path (GTK_TREE_VIEW(tree), path);
        gtk_tree_view_scroll_to_cell (GTK_TREE_VIEW(tree), path, nullptr, true, 0.5, 0.0);
        gtk_tree_path_free(path);
    }
}

/*
 * Return the node data at a given GtkTreeIter position
 */
NodeData *sp_xmlview_tree_node_get_data(GtkTreeModel *model, GtkTreeIter *iter)
{
    NodeData *data = nullptr;
    gtk_tree_model_get(model, iter, STORE_DATA_COL, &data, -1);
    return data;
}

/*
 * Return the repr at a given GtkTreeIter position
 */
Inkscape::XML::Node *
sp_xmlview_tree_node_get_repr (GtkTreeModel *model, GtkTreeIter * iter)
{
    NodeData *data = sp_xmlview_tree_node_get_data(model, iter);
    return data ? data->repr : nullptr;
}

struct IterByReprData {
    const Inkscape::XML::Node *repr; //< in
    GtkTreeIter *iter;               //< out
};

/*
 * Find a GtkTreeIter position in the tree by repr
 * @return True if the node was found
 */
gboolean
sp_xmlview_tree_get_repr_node (SPXMLViewTree * tree, Inkscape::XML::Node * repr, GtkTreeIter *iter)
{
    iter->stamp = 0; // invalidate iterator
    IterByReprData funcdata = { repr, iter };
    gtk_tree_model_foreach(GTK_TREE_MODEL(tree->store), foreach_func, &funcdata);
    return iter->stamp != 0;
}

gboolean foreach_func(GtkTreeModel *model, GtkTreePath * /*path*/, GtkTreeIter *iter, gpointer user_data)
{
    auto funcdata = static_cast<IterByReprData *>(user_data);
    if (sp_xmlview_tree_node_get_repr(model, iter) == funcdata->repr) {
        *funcdata->iter = *iter;
        return TRUE;
    }

    return FALSE;
}

/*
 * Callback function for string searches in the tree
 * Return a match on any substring
 */
gboolean search_equal_func(GtkTreeModel *model, gint /*column*/, const gchar *key, GtkTreeIter *iter, gpointer /*search_data*/)
{
    gchar *text = nullptr;
    gtk_tree_model_get(model, iter, STORE_TEXT_COL, &text, -1);

    gboolean match = (strstr(text, key) != nullptr);

    g_free(text);

    return !match;
}

/*
  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=4:softtabstop=4:fileencoding=utf-8 :
