/* Copyright radare2 - 2014-2026 - pancake, ret2libc */

#include <r_core_priv.h>
#include "visual_modes.h"

enum {
	// Printable palette fields before rainbow are all color strings.
	GRAPH_PALETTE_COLORS = r_offsetof (RConsPrintablePalette, rainbow) / sizeof (char *)
};

#define GRAPH_COLOR_TAG "\x1b[777;"

typedef struct {
	RANode node;
	char *color_body;
	bool body_needs_crop;
} RANodePrivate;

typedef struct {
	RConsContext *context;
	RConsPrintablePalette saved;
	char tags[GRAPH_PALETTE_COLORS][24];
} RAGraphPalette;

R_IPI void visual_refresh(RCore *core);

static RCoreHelpMessage help_msg_visual_graph = {
	":e cmd.gprompt=agft", "show tinygraph in one side",
	"@", "toggle graph.layout between 0 and 1",
	"[]", "adjust graph.bb.maxwidth",
	"+/-/0", "zoom in/out/default",
	";", "add comment in current basic block",
	". (dot)", "center graph to the current node",
	", (comma)", "toggle graph.few",
	"^", "seek to the first bb of the function",
	"=", "toggle graph.layout",
	":cmd", "run radare command",
	"\"", "toggle graph.refs",
	"#", "toggle graph.hints",
	"/", "highlight text",
	"\\", "scroll the graph canvas to the next highlight location",
	"|", "set cmd.gprompt",
	"_", "enter hud selector",
	">", "show function callgraph (see graph.refs)",
	"<", "show program callgraph (see graph.refs)",
	"(", "reverse conditional branch of last instruction in bb",
	")", "rotate asm.emu and emu.str",
	"%", "find in disassembly (pdr~sentence) and navigate to it in graph",
	"Home/End", "go to the top/bottom of the canvas",
	"Page-UP/DOWN", "scroll canvas up/down",
	"b", "visual browse things",
	"c", "toggle graph cursor mode",
	"C", "toggle scr.colors",
	"d", "rename function",
	"D", "toggle the mixed graph+disasm mode",
	"e", "rotate graph.edges (show/hide edges)",
	"E", "rotate graph.linemode (square/diagonal lines)",
	"F", "enter flag selector",
	"g", "go/seek to given offset",
	"G", "debug trace callgraph (generated with dtc)",
	"hjkl/HJKL", "scroll canvas or node depending on graph cursor (uppercase scales with screen size)",
	"i/I", "select input / output nodes by index",
	"mK/'K", "mark/go to given key (same as in vim or r2 visual)",
	"M", "change mouse mode (scroll vertical/horizontal)",
	"n/N", "next/previous scr.nkey (function/flag..)",
	"o([A-Za-z]*)", "follow jmp/call identified by shortcut (like ;[oa])",
	"O", "toggle asm.pseudo and asm.esil",
	"p/P", "rotate graph modes (normal, display offsets, minigraph, summary)",
	"q", "back to Visual mode",
	"r", "toggle jmphints/leahints",
	"R", "change color theme or randomize colors (scr.randpal)",
	"s/S", "step / step over",
	"tab", "select next node",
	"TAB", "select previous node",
	"t/f", "follow true/false edges",
	"u/U", "undo/redo seek",
	"V", "toggle basicblock / call graphs",
	"w", "toggle between movements speed 1 and graph.scroll",
	// W            AVAILABLE
	"x/X", "jump to xref/ref",
	"y", "toggle graph.comments",
	"Y", "toggle tiny graph",
	"z", "toggle node folding",
	"Z", "toggle basic block folding",
	NULL
};

#define BORDER 3
#define BORDER_WIDTH 4
#define BORDER_HEIGHT 3
#define MARGIN_TEXT_X 2
#define MARGIN_TEXT_Y 2
#define HORIZONTAL_NODE_SPACING 4
#define VERTICAL_NODE_SPACING 2
#define MIN_NODE_WIDTH 22
#define MIN_NODE_HEIGHT BORDER_HEIGHT
#define TITLE_LEN 128
#define DEFAULT_SPEED 1
#define PAGEKEY_SPEED (h / 2)
/* 15 */
#define MINIGRAPH_NODE_TEXT_CUR "<@@@@@@>"
#define MINIGRAPH_NODE_MIN_WIDTH 12
#define MINIGRAPH_NODE_TITLE_LEN 4
#define MINIGRAPH_NODE_CENTER_X 3
#define MININODE_MIN_WIDTH 16

#define CROSSING_MATRIX_MAX_CELLS (1 << 20)

#define ZOOM_STEP 10
#define ZOOM_DEFAULT 100

#define BODY_OFFSETS 0x1
#define BODY_SUMMARY 0x2
#define BODY_COMMENTS 0x4

#define NORMALIZE_MOV(x) ((x) < 0? -1: ((x) > 0? 1: 0))

/* don't use macros for this */
#define get_anode(gn) ((gn)? (RANode *) (gn)->data: NULL)

#define graph_foreach_anode(list, it, pos, anode) \
	if (list) \
		for ((it) = (list)->head; (it) && ((pos) = (it)->data) && (pos) && ((anode) = (RANode *) (pos)->data); (it) = (it)->n)

#define graph_foreach_anode_vec(vec, it, pos, anode) \
	if (vec) \
		R_VEC_FOREACH (vec, it) \
			if (((pos) = *(it)) && ((anode) = (RANode *) (pos)->data))

struct len_pos_t {
	int len;
	int pos;
};

struct g_cb {
	RAGraph *graph;
	RANodeCallback node_cb;
	RAEdgeCallback edge_cb;
	void *data;
};

typedef struct ascii_edge_t {
	RANode *from;
	RANode *to;
	RList *x, *y;
	bool is_reversed;
	bool is_highlight;
} AEdge;

struct layer_t {
	int n_nodes;
	RGraphNode **nodes;
	int position;
	int height;
	int width;
	int gap;
};

struct agraph_refresh_data {
	RCore *core;
	RAGraph *g;
	RAnalFunction **fcn;
	bool follow_offset;
	int fs;
};

#define G(x, y) r_cons_canvas_gotoxy(g->can, x, y)
#define W(x) r_cons_canvas_write(g->can, x)
#define F(x, y, x2, y2, c) r_cons_canvas_fill(g->can, x, y, x2, y2, c)

static void ptr_ht_set(HtUP *ht, const void *k, void *v) {
	ht_up_insert (ht, (ut64)(size_t)k, v);
}

static void *ptr_ht_get(HtUP *ht, const void *k) {
	return ht_up_find (ht, (ut64)(size_t)k, NULL);
}

static RList *ptr_ht_get_rlist(HtUP *ht, const void *k) {
	return ptr_ht_get (ht, k);
}

static void ptr_ht_set_int(HtUU *ht, const void *k, int v) {
	ht_uu_insert (ht, (ut64)(size_t)k, (ut64)(st64)v);
}

static int ptr_ht_get_int(HtUU *ht, const void *k) {
	return (int)(st64)ht_uu_find (ht, (ut64)(size_t)k, NULL);
}

static void ptr_ht_rlist_free(HtUPKv *kv) {
	r_list_free ((RList *)kv->value);
}

static bool is_offset(const RAGraph *g) {
	return g->mode == R_AGRAPH_MODE_OFFSET;
}

static bool is_mini(const RAGraph *g) {
	return g->mode == R_AGRAPH_MODE_MINI;
}

static bool is_tiny(const RAGraph *g) {
	return g->is_tiny || g->mode == R_AGRAPH_MODE_TINY;
}

static bool is_summary(const RAGraph *g) {
	return g->mode == R_AGRAPH_MODE_SUMMARY;
}

static bool is_comments(const RAGraph *g) {
	return g->mode == R_AGRAPH_MODE_COMMENTS;
}

static int next_mode(int mode) {
	return (mode + 1) % R_AGRAPH_MODE_MAX;
}

static int prev_mode(int mode) {
	return (mode + R_AGRAPH_MODE_MAX - 1) % R_AGRAPH_MODE_MAX;
}

static RGraphNode *agraph_get_title(const RAGraph *g, RANode *n, bool in) {
	if (!n) {
		return NULL;
	}
	if (!R_STR_ISEMPTY (n->title)) {
		return n->gnode;
	}
	const RVecGraphNodePtr *outnodes = in? &n->gnode->in_nodes: &n->gnode->out_nodes;
	RGraphNode *gn;
	RGraphNode **iter;
	RANode *an;
	graph_foreach_anode_vec (outnodes, iter, gn, an) {
		return agraph_get_title (g, an, in);
	}
	return NULL;
}

static int mode2opts(const RAGraph *g) {
	int opts = 0;
	if (is_offset (g)) {
		opts |= BODY_OFFSETS;
	}
	if (is_comments (g)) {
		opts |= BODY_COMMENTS;
	}
	if (is_summary (g)) {
		opts |= BODY_SUMMARY;
	}
	return opts;
}

// duplicated from visual.c
static void rotateAsmemu(RCore *core) {
	const bool isEmu = r_config_get_b (core->config, "asm.emu");
	if (isEmu) {
		const bool isEmuStr = r_config_get_b (core->config, "emu.str");
		if (isEmuStr) {
			r_config_set_b (core->config, "emu.str", false);
		} else {
			r_config_set_b (core->config, "asm.emu", false);
		}
	} else {
		r_config_set_b (core->config, "emu.str", true);
	}
}

static void showcursor(RCore *core, int x) {
	RCons *cons = core->cons;
	if (!x) {
		bool wheel = r_config_get_b (core->config, "scr.wheel");
		if (wheel) {
			r_cons_enable_mouse (cons, true);
		}
	} else {
		r_cons_enable_mouse (cons, false);
	}
	r_cons_show_cursor (cons, x);
}

static char *get_title(ut64 addr) {
	return r_str_newf ("0x%" PFMT64x, addr);
}

static char *agraph_font_render_cfg(RCore *core, const char *key, const char *s) {
	const char *font = core? r_config_get (core->config, key): NULL;
	return R_STR_ISNOTEMPTY (font)? r_font_render (s, font): NULL;
}

static int agraph_refresh(struct agraph_refresh_data *grd);

static bool node_body_needs_crop(const char *body) {
	if (!r_str_is_ascii (body) || !r_str_endswith (body, "\n")) {
		return true;
	}
	if (!strchr (body, 0x1b)) {
		return false;
	}
	const size_t reset_len = sizeof (Color_RESET) - 1;
	const char *line = body, *end;
	while ((end = strchr (line, '\n'))) {
		// Canvas newlines otherwise overwrite the first color attribute of the row.
		if (end - line < reset_len || memcmp (end - reset_len, Color_RESET, reset_len)) {
			return true;
		}
		line = end + 1;
	}
	return false;
}

static void update_node_dimension(const RGraph *g, int is_mini, int zoom, int edgemode, bool callgraph, int layout) {
	const RList *nodes = r_graph_get_nodes (g);
	RGraphNode *gn;
	RListIter *it;
	RANode *n;
	graph_foreach_anode (nodes, it, gn, n) {
		if (is_mini) {
			n->h = 1;
			n->w = MINIGRAPH_NODE_MIN_WIDTH;
		} else if (n->is_mini) {
			n->h = 1;
			n->w = MININODE_MIN_WIDTH;
		} else {
			n->w = r_str_bounds (n->body, (int *)&n->h);
			((RANodePrivate *)n)->body_needs_crop = node_body_needs_crop (n->body);
			ut32 len = strlen (n->title) + MARGIN_TEXT_X;
			if (len > INT_MAX) {
				len = INT_MAX;
			}
			if (len > n->w) {
				n->w = len;
			}
			// n->w = n->w; //R_MIN (n->w, (int)len);
			n->w += BORDER_WIDTH;
			n->h += BORDER_HEIGHT;
			/* scale node by zoom */
			n->w = R_MAX (MIN_NODE_WIDTH, (n->w * zoom) / 100);
			n->h = R_MAX (MIN_NODE_HEIGHT, (n->h * zoom) / 100);

			if (edgemode == 2 && !callgraph) {
				if (!layout) {
					n->w = R_MAX (n->w, (RVecGraphNodePtr_length (&n->gnode->out_nodes) * 2 + 1) + R_EDGES_X_INC * 2);
					n->w = R_MAX (n->w, (RVecGraphNodePtr_length (&n->gnode->in_nodes) * 2 + 1) + R_EDGES_X_INC * 2);
				} else {
					n->h = R_MAX (n->h, (RVecGraphNodePtr_length (&n->gnode->out_nodes) + 1) + R_EDGES_X_INC);
					n->h = R_MAX (n->h, (RVecGraphNodePtr_length (&n->gnode->in_nodes) + 1) + R_EDGES_X_INC);
				}
			}
		}
	}
}

static void append_shortcut(const RAGraph *g, char *title, const RANode *n, int left) {
	if (!n->shortcut || left < 1) {
		return;
	}
	size_t len = strlen (title);
	if (g->can->color) {
		// XXX: do not hardcode color here
		snprintf (title + len, left, Color_YELLOW "[o%s]" Color_RESET, n->shortcut);
	} else {
		snprintf (title + len, left, "[o%s]", n->shortcut);
	}
}

static void mini_RANode_print(const RAGraph *g, const RANode *n, int cur, bool details) {
	char title[TITLE_LEN];
	int x, delta_x = 0;

	if (!G (n->x + MINIGRAPH_NODE_CENTER_X, n->y) &&
		!G (n->x + MINIGRAPH_NODE_CENTER_X + n->w, n->y)) {
		return;
	}

	x = n->x + MINIGRAPH_NODE_CENTER_X + g->can->sx;
	if (x < 0) {
		delta_x = -x;
	}
	if (!G (n->x + MINIGRAPH_NODE_CENTER_X + delta_x, n->y)) {
		return;
	}

	if (details) {
		if (cur) {
			W (&MINIGRAPH_NODE_TEXT_CUR[delta_x]);
			(void)G (-g->can->sx, -g->can->sy + 2);
			snprintf (title, sizeof (title) - 1, "[ %s ]", n->title);
			W (title);
			if (g->discroll > 0) {
				char *body = r_str_ansi_crop (n->body, 0, g->discroll, -1, -1);
				(void)G (-g->can->sx, -g->can->sy + 3);
				W (body);
				free (body);
			} else {
				(void)G (-g->can->sx, -g->can->sy + 3);
				W (n->body);
			}
		} else {
			char *str = "____";
			if (n->title) {
				int l = strlen (n->title);
				str = n->title;
				if (l > MINIGRAPH_NODE_TITLE_LEN) {
					str += l - MINIGRAPH_NODE_TITLE_LEN;
				}
			}
			if (g->can->color) {
				snprintf (title, sizeof (title) - 1, "%s__%s__", Color_RESET, str);
			} else {
				snprintf (title, sizeof (title) - 1, "__%s__", str);
			}
			append_shortcut (g, title, n, sizeof (title) - strlen (title));
			char *res = r_str_ansi_crop (title, delta_x, 0, 20, 1);
			W (res);
			free (res);
		}
	} else {
		snprintf (title, sizeof (title) - 1,
			cur? "[ %s ]": "  %s  ", n->title);
		W (title);
	}
	return;
}

static void tiny_RANode_print(const RAGraph *g, const RANode *n, int cur) {
	G (n->x, n->y);
	if (cur) {
		W ("##");
	} else {
		W ("*");
	}
}

static char *get_node_bgcolor(RCons *cons, int color, int cur) {
	if (color == -1) {
		return cur? cons->context->pal.graph_box2: cons->context->pal.graph_box;
	}
	return color
		? (color == R_ANAL_DIFF_TYPE_MATCH
				? cons->context->pal.diff_match
				: color == R_ANAL_DIFF_TYPE_UNMATCH
				? cons->context->pal.diff_unmatch
				: cons->context->pal.diff_new)
		: cons->context->pal.diff_unknown;
}

static char *get_node_color(RCons *cons, int color, int cur) {
	if (color == -1) {
		return cur? cons->context->pal.graph_box2: cons->context->pal.graph_box;
	}
	return color
		? (color == R_ANAL_DIFF_TYPE_MATCH
				? cons->context->pal.diff_match
				: color == R_ANAL_DIFF_TYPE_UNMATCH
				? cons->context->pal.diff_unmatch
				: cons->context->pal.diff_new)
		: cons->context->pal.diff_unknown;
}

static void normal_RANode_print(const RAGraph *g, const RANode *n, int cur) {
	ut32 center_x = 0, center_y = 0;
	ut32 delta_x = 0, delta_txt_x = 0;
	ut32 delta_y = 0, delta_txt_y = 0;
	char title[TITLE_LEN];
	int color = n->difftype;
	const bool showTitle = g->show_node_titles;
	const bool showBody = g->show_node_body;
	RCons *cons = g->can->cons;
	RPrint p = { .flags = R_PRINT_FLAGS_USEUTF8 };
	int dots_width = 0, dots_bytes = 0;
	const char *dots = r_print_ellipsis (&p, &dots_width, &dots_bytes);
	int x = n->x + g->can->sx;
	int y = n->y + g->can->sy;
	if (x + MARGIN_TEXT_X < 0) {
		delta_x = - (x + MARGIN_TEXT_X);
	}
	if (x + n->w < -MARGIN_TEXT_X) {
		return;
	}
	if (y < -1) {
		delta_y = R_MIN (n->h - BORDER_HEIGHT - 1, -y - MARGIN_TEXT_Y);
	}
	/* print the title */
	if (showTitle) {
		if (cur) {
			snprintf (title, sizeof (title) - 1, "[%s]", n->title);
		} else {
			char *color = g->can->color? Color_RESET: "";
			snprintf (title, sizeof (title) - 1, " %s%s ", color, n->title);
			append_shortcut (g, title, n, sizeof (title) - strlen (title));
		}
		if (g->bb_maxwidth > 0 && r_str_len_utf8_ansi (title) > g->bb_maxwidth) {
			char *pos = (char *)r_str_ansi_chrn (title, g->bb_maxwidth);
			if (pos) {
				*pos = 0;
			}
		}
		if ((delta_x < strlen (title)) && G (n->x + MARGIN_TEXT_X + delta_x, n->y + 1)) {
			char *res = r_str_ansi_crop (title, delta_x, 0, n->w - BORDER_WIDTH, 1);
			W (res);
			W (Color_RESET);
			free (res);
		}
	}

	/* print the body */
	if (g->zoom > ZOOM_DEFAULT) {
		center_x = (g->zoom - ZOOM_DEFAULT) / 10;
		center_y = (g->zoom - ZOOM_DEFAULT) / 30;
		delta_txt_x = R_MIN (delta_x, center_x);
		delta_txt_y = R_MIN (delta_y, center_y);
	}
	if (showBody) {
		if (G (n->x + MARGIN_TEXT_X + delta_x + center_x - delta_txt_x,
			n->y + MARGIN_TEXT_Y + delta_y + center_y - delta_txt_y)) {
			ut32 body_x = center_x >= delta_x? 0: delta_x - center_x;
			ut32 body_y = center_y >= delta_y? 0: delta_y - center_y;
			ut32 body_h = BORDER_HEIGHT >= n->h? 1: n->h - BORDER_HEIGHT;

			if (g->zoom < ZOOM_DEFAULT) {
				body_h--;
			}
			if (body_y + 1 <= body_h) {
				char *body = body_x || body_y || g->zoom < ZOOM_DEFAULT || ((RANodePrivate *)n)->body_needs_crop
					? r_str_ansi_crop (n->body, body_x, body_y, n->w - BORDER_WIDTH, body_h)
					: NULL;
				W (body? body: n->body);
				if (body && g->zoom < ZOOM_DEFAULT) {
					W ("\n");
				}
				free (body);
			}
			/* print some dots when the body is cropped because of zoom */
			if (R_STR_ISNOTEMPTY (n->body)) {
				if (body_y <= body_h && g->zoom < ZOOM_DEFAULT) {
					if (delta_x < dots_width) {
						if (dots_width == dots_bytes) {
							W (dots + delta_x);
						} else if (!delta_x) {
							W (dots);
						}
					}
				}
			}
		}
	}

#if 1
	// TODO: check if node is traced or not and show proper color
	// This info must be stored inside RANode* from RCore*
	const char *nc = get_node_color (cons, color, cur);
	if (g->show_node_bubble) {
		r_cons_canvas_bgfill (g->can, n->x, n->y, n->w, n->h, get_node_bgcolor (cons, color, cur));
	} else {
		r_cons_canvas_box (g->can, n->x, n->y, n->w, n->h, nc);
	}
	if (n->color) {
		r_cons_canvas_box (g->can, n->x - 1, n->y, n->w + 2, n->h, n->color);
	}
#endif
}

static bool get_crossing_order(RCons *cons, const RGraph *g, const RGraphNode *u, const RGraphNode *v, int layer, bool from_up, bool *swap) {
	if (r_cons_is_breaked (cons)) {
		return false;
	}
	const RVecGraphNodePtr *u_neighbours = from_up? r_graph_innodes (g, u): r_graph_get_neighbours (g, u);
	const RVecGraphNodePtr *v_neighbours = from_up? r_graph_innodes (g, v): r_graph_get_neighbours (g, v);
	RGraphNode *u_neighbour, *v_neighbour;
	RGraphNode **u_it, **v_it;
	const RANode *u_node, *v_node;
	ut64 u_crossings = 0, v_crossings = 0;
	graph_foreach_anode_vec (u_neighbours, u_it, u_neighbour, u_node) {
		if (from_up && u_node->layer != layer - 1) {
			continue;
		}
		graph_foreach_anode_vec (v_neighbours, v_it, v_neighbour, v_node) {
			if (from_up && v_node->layer != layer - 1) {
				continue;
			}
			if (u_node->pos_in_layer > v_node->pos_in_layer) {
				u_crossings++;
			} else if (u_node->pos_in_layer < v_node->pos_in_layer) {
				v_crossings++;
			}
		}
	}
	*swap = u_crossings > v_crossings;
	return true;
}

static int layer_sweep(RCons *cons, const RGraph *g, const struct layer_t layers[], int maxlayer, int i, int from_up) {
	int j, changed = false;
	int len = layers[i].n_nodes;

	if (len < 2) {
		return false;
	}
	if ((ut64)len * (ut64)len > CROSSING_MATRIX_MAX_CELLS) {
		return false;
	}
	if ((from_up && i == 0) || (!from_up && i == maxlayer - 1)) {
		return false;
	}

	for (j = 0; j < len - 1; j++) {
		RGraphNode *u = layers[i].nodes[j];
		RGraphNode *v = layers[i].nodes[j + 1];
		bool swap;
		if (!get_crossing_order (cons, g, u, v, i, from_up, &swap)) {
			changed = -1;
			break;
		}
		if (swap) {
			/* swap elements */
			layers[i].nodes[j] = v;
			layers[i].nodes[j + 1] = u;
			changed = true;
		}
	}

	// Keep neighbour positions stable until all comparisons have finished.
	for (j = 0; j < layers[i].n_nodes; j++) {
		RANode *n = get_anode (layers[i].nodes[j]);
		if (n) {
			n->pos_in_layer = j;
		}
	}

	return changed;
}

static int crossing_max_changes(const RAGraph *g) {
	return g->graph->n_nodes > 4096? 64: 4096;
}

static void view_cyclic_edge(const RGraphEdge *e, const RGraphVisitor *vis) {
	const RAGraph *g = (RAGraph *)vis->data;
	RGraphEdge *new_e = R_NEW0 (RGraphEdge);
	new_e->from = e->from;
	new_e->to = e->to;
	new_e->nth = e->nth;
	r_list_append (g->back_edges, new_e);
}

// wtf? long_edges leaks?
static void view_dummy(const RGraphEdge *e, const RGraphVisitor *vis) {
	const RANode *a = get_anode (e->from);
	const RANode *b = get_anode (e->to);
	RList *long_edges = (RList *)vis->data;
	if (!a || !b) {
		return;
	}
	if (R_ABS (a->layer - b->layer) > 1) {
		RGraphEdge *new_e = R_NEW0 (RGraphEdge);
		new_e->from = e->from;
		new_e->to = e->to;
		new_e->nth = e->nth;
		r_list_append (long_edges, new_e);
	}
}

/* find a set of edges that, removed, makes the graph acyclic */
/* invert the edges identified in the previous step */
static void remove_cycles(RAGraph *g) {
	RGraphVisitor cyclic_vis = {
		NULL, NULL, NULL, NULL, NULL, NULL
	};
	const RGraphEdge *e;
	const RListIter *it;

	g->back_edges = r_list_newf (free);
	cyclic_vis.back_edge = (RGraphEdgeCallback)view_cyclic_edge;
	cyclic_vis.data = g;
	r_graph_dfs (g->graph, &cyclic_vis);

	r_list_foreach (g->back_edges, it, e) {
		RANode *from = e->from? get_anode (e->from): NULL;
		RANode *to = e->to? get_anode (e->to): NULL;
		if (from && to) {
			r_agraph_del_edge (g, from, to);
			r_agraph_add_edge_at (g, to, from, e->nth);
		}
	}
}

static void add_sorted(RGraphNode *n, RGraphVisitor *vis) {
	RList *l = (RList *)vis->data;
	r_list_prepend (l, n);
}

/* assign a layer to each node of the graph.
 *
 * It visits the nodes of the graph in the topological sort, so that every time
 * you visit a node, you can be sure that you have already visited all nodes
 * that can lead to that node and thus you can easily compute the layer based
 * on the layer of these "parent" nodes. */
static void assign_layers(const RAGraph *g) {
	RGraphVisitor layer_vis = {
		NULL, NULL, NULL, NULL, NULL, NULL
	};
	const RGraphNode *gn;
	const RListIter *it;
	RANode *n;
	RList *topological_sort = r_list_new ();

	layer_vis.data = topological_sort;
	layer_vis.finish_node = (RGraphNodeCallback)add_sorted;
	r_graph_dfs (g->graph, &layer_vis);

	graph_foreach_anode (topological_sort, it, gn, n) {
		const RVecGraphNodePtr *innodes = r_graph_innodes (g->graph, gn);
		RGraphNode **it;
		RGraphNode *prev;
		RANode *preva;

		n->layer = 0;
		graph_foreach_anode_vec (innodes, it, prev, preva) {
			if (preva->layer + 1 > n->layer) {
				n->layer = preva->layer + 1;
			}
		}
	}

	r_list_free (topological_sort);
}

static ut64 graph_edge_key(const RGraphNode *from, const RGraphNode *to) {
	return ((ut64)from->idx << 32) | to->idx;
}

/* add dummy nodes when there are edges that span multiple layers */
static void create_dummy_nodes(RAGraph *g) {
	if (!g->dummy) {
		return;
	}
	RGraphVisitor dummy_vis = {
		NULL, NULL, NULL, NULL, NULL, NULL
	};
	const RListIter *it;
	const RGraphEdge *e;

	g->long_edges = r_list_newf ((RListFree)free);
	dummy_vis.data = g->long_edges;
	dummy_vis.tree_edge = (RGraphEdgeCallback)view_dummy;
	dummy_vis.fcross_edge = (RGraphEdgeCallback)view_dummy;
	r_graph_dfs (g->graph, &dummy_vis);

	SetU *back_edges = set_u_new ();
	r_list_foreach (g->back_edges, it, e) {
		if (e->from && e->to) {
			set_u_add (back_edges, graph_edge_key (e->from, e->to));
		}
	}
	r_list_foreach (g->long_edges, it, e) {
		RANode *from = get_anode (e->from);
		RANode *to = get_anode (e->to);
		int diff_layer = R_ABS (from->layer - to->layer);
		RANode *prev = get_anode (e->from);
		const bool reversed = set_u_contains (back_edges, graph_edge_key (e->to, e->from));
		int i, nth = e->nth;

		r_agraph_del_edge (g, from, to);
		for (i = 1; i < diff_layer; i++) {
			RANode *dummy = r_agraph_add_node (g, NULL, NULL, NULL);
			if (!dummy) {
				set_u_free (back_edges);
				return;
			}
			dummy->is_dummy = true;
			dummy->layer = from->layer + i;
			dummy->is_reversed = reversed;
			dummy->w = 1;
			r_agraph_add_edge_at (g, prev, dummy, nth);

			prev = dummy;
			nth = -1;
		}
		r_graph_add_edge (g->graph, prev->gnode, e->to);
	}
	set_u_free (back_edges);
}

/* create layers and assign an initial ordering of the nodes into them */
static void create_layers(RAGraph *g) {
	const RList *nodes = r_graph_get_nodes (g->graph);
	RGraphNode *gn;
	const RListIter *it;
	RANode *n;
	int i;

	/* identify max layer */
	g->n_layers = 0;
	graph_foreach_anode (nodes, it, gn, n) {
		if (n->layer > g->n_layers) {
			g->n_layers = n->layer;
		}
	}

	/* create a starting ordering of nodes for each layer */
	g->n_layers++;
	if (sizeof (struct layer_t) * g->n_layers < g->n_layers) {
		return;
	}
	g->layers = R_NEWS0 (struct layer_t, g->n_layers);

	graph_foreach_anode (nodes, it, gn, n) {
		g->layers[n->layer].n_nodes++;
	}

	for (i = 0; i < g->n_layers; i++) {
		if (sizeof (RGraphNode *) * g->layers[i].n_nodes < g->layers[i].n_nodes) {
			continue;
		}
		g->layers[i].nodes = R_NEWS0 (RGraphNode *,
			1 + g->layers[i].n_nodes);
		g->layers[i].position = 0;
	}
	graph_foreach_anode (nodes, it, gn, n) {
		n->pos_in_layer = g->layers[n->layer].position;
		g->layers[n->layer].nodes[g->layers[n->layer].position++] = gn;
	}
}

/* layer-by-layer sweep */
/* it permutes each layer, trying to find the best ordering for each layer
 * to minimize the number of crossing edges */
static void minimize_crossings(const RAGraph *g) {
	int i, cross_changed, max_changes = crossing_max_changes (g);

	RCons *cons = g->can->cons;
	do {
		cross_changed = false;
		max_changes--;

		for (i = 0; i < g->n_layers; i++) {
			int rc = layer_sweep (cons, g->graph, g->layers, g->n_layers, i, true);
			if (rc == -1) {
				return;
			}
			cross_changed |= !!rc;
		}
	} while (cross_changed && max_changes);

	max_changes = crossing_max_changes (g);

	do {
		cross_changed = false;
		max_changes--;

		for (i = g->n_layers - 1; i >= 0; i--) {
			int rc = layer_sweep (cons, g->graph, g->layers, g->n_layers, i, false);
			if (rc == -1) {
				return;
			}
			cross_changed |= !!rc;
		}
	} while (cross_changed && max_changes);
}

static ut64 dist_key(const RGraphNode *from, const RGraphNode *to) {
	return ((ut64)(from? from->idx: UT32_MAX) << 32) | (to? to->idx: UT32_MAX);
}

static bool dist_find(const RAGraph *g, const RGraphNode *from, const RGraphNode *to, int *dist) {
	bool found = false;
	if (g->dists) {
		*dist = (int)ht_uu_find (g->dists, dist_key (from, to), &found);
	}
	return found;
}

/* returns the distance between two nodes */
/* if the distance between two nodes were explicitly set, returns that;
 * otherwise calculate the distance of two nodes on the same layer */
static int dist_nodes(const RAGraph *g, const RGraphNode *a, const RGraphNode *b) {
	const RANode *aa, *ab;
	int res = 0, dist;

	if (dist_find (g, a, b, &dist)) {
		return dist;
	}

	aa = get_anode (a);
	ab = get_anode (b);
	if (aa && ab && aa->layer == ab->layer) {
		int i;

		res = aa == ab && !aa->is_reversed? HORIZONTAL_NODE_SPACING: 0;
		for (i = aa->pos_in_layer; i < ab->pos_in_layer; i++) {
			const RGraphNode *cur = g->layers[aa->layer].nodes[i];
			const RGraphNode *next = g->layers[aa->layer].nodes[i + 1];
			const RANode *anext = get_anode (next);
			const RANode *acur = get_anode (cur);
			const bool found = dist_find (g, cur, next, &dist);
			if (found) {
				res += dist;
			}

			if (acur && anext && !found) {
				int space = HORIZONTAL_NODE_SPACING;
				if (acur->is_reversed && anext->is_reversed) {
					if (!acur->is_reversed) {
						res += acur->w / 2;
					} else if (!anext->is_reversed) {
						res += anext->w / 2;
					}
					res += 1;
				} else {
					res += acur->w / 2 + anext->w / 2 + space;
				}
			}
		}
	}

	return res;
}

/* explicitly set the distance between two nodes on the same layer */
static void set_dist_nodes(const RAGraph *g, int l, int cur, int next) {
	const RGraphNode *vi, *vip;
	const RANode *avi, *avip;

	if (!g->dists) {
		return;
	}
	vi = g->layers[l].nodes[cur];
	vip = g->layers[l].nodes[next];
	avi = get_anode (vi);
	avip = get_anode (vip);

	const int dist = (avip && avi)? avip->x - avi->x: 0;
	ht_uu_update (g->dists, dist_key (vi, vip), (ut32)dist);
}

static inline int is_valid_pos(const RAGraph *g, int l, int pos) {
	return pos >= 0 && pos < g->layers[l].n_nodes;
}

/* computes the set of vertical classes in the graph */
/* if v is an original node, L (v) = { v }
 * if v is a dummy node, L (v) is the set of all the dummies node that belongs
 *      to the same long edge */
static HtUP *compute_vertical_nodes(const RAGraph *g) {
	HtUP *res = ht_up_new (NULL, ptr_ht_rlist_free, NULL);
	if (!res) {
		return NULL;
	}
	int i, j;

	for (i = 0; i < g->n_layers; i++) {
		for (j = 0; j < g->layers[i].n_nodes; j++) {
			RGraphNode *gn = g->layers[i].nodes[j];
			const RList *Ln = ptr_ht_get_rlist (res, gn);
			const RANode *an = get_anode (gn);

			if (!Ln) {
				RList *vert = r_list_new ();
				if (!vert) {
					ht_up_free (res);
					return NULL;
				}
				ptr_ht_set (res, gn, vert);
				if (an->is_dummy) {
					RGraphNode *next = gn;
					const RANode *anext = get_anode (next);

					while (anext->is_dummy) {
						r_list_append (vert, next);
						next = r_graph_nth_neighbour (g->graph, next, 0);
						if (!next) {
							break;
						}
						anext = get_anode (next);
					}
				} else {
					r_list_append (vert, gn);
				}
			}
		}
	}

	return res;
}

/* computes left or right classes, used to place dummies node */
/* classes respect three properties:
 * - v E C
 * - w E C => L (v) is a subset of C
 * - w E C, the s+ (w) exists and is not in any class yet => s+ (w) E C */
static RList **compute_classes(const RAGraph *g, HtUP *v_nodes, int is_left, int *n_classes) {
	int i, j, c;
	RList **res = R_NEWS0 (RList *, g->n_layers);
	RGraphNode *gn;
	const RListIter *it;
	RANode *n;

	graph_foreach_anode (r_graph_get_nodes (g->graph), it, gn, n) {
		n->klass = -1;
	}

	for (i = 0; i < g->n_layers; i++) {
		c = i;

		for (j = is_left? 0: g->layers[i].n_nodes - 1;
			(is_left && j < g->layers[i].n_nodes) || (!is_left && j >= 0);
			j = is_left? j + 1: j - 1) {
			const RGraphNode *gj = g->layers[i].nodes[j];
			const RANode *aj = get_anode (gj);

			if (aj->klass == -1) {
				const RList *laj = ptr_ht_get_rlist (v_nodes, gj);

				if (!res[c]) {
					res[c] = r_list_new ();
				}
				graph_foreach_anode (laj, it, gn, n) {
					r_list_append (res[c], gn);
					n->klass = c;
				}
			} else {
				c = aj->klass;
			}
		}
	}

	if (n_classes) {
		*n_classes = g->n_layers;
	}
	return res;
}

static int cmp_dist(const size_t a, const size_t b) {
	return (a < b) - (a > b);
}

static RGraphNode *get_sibling(const RAGraph *g, const RANode *n, int is_left, int is_adjust_class) {
	RGraphNode *res = NULL;
	int pos = n->pos_in_layer;

	if ((is_left && is_adjust_class) || (!is_left && !is_adjust_class)) {
		pos++;
	} else {
		pos--;
	}

	if (is_valid_pos (g, n->layer, pos)) {
		res = g->layers[n->layer].nodes[pos];
	}
	return res;
}

static int adjust_class_val(const RAGraph *g, const RGraphNode *gn, const RGraphNode *sibl, HtUU *res, int is_left) {
	if (is_left) {
		return ptr_ht_get_int (res, sibl) - ptr_ht_get_int (res, gn) - dist_nodes (g, gn, sibl);
	}
	return ptr_ht_get_int (res, gn) - ptr_ht_get_int (res, sibl) - dist_nodes (g, sibl, gn);
}

/* adjusts the position of previously placed left/right classes */
/* tries to place classes as close as possible */
static void adjust_class(const RAGraph *g, int is_left, RList **classes, HtUU *res, int c) {
	const RGraphNode *gn;
	const RListIter *it;
	const RANode *an;
	int dist = 0;
	bool is_first = true;

	graph_foreach_anode (classes[c], it, gn, an) {
		const RGraphNode *sibling = get_sibling (g, an, is_left, true);
		if (!sibling) {
			continue;
		}
		const RANode *sibl_anode = get_anode (sibling);
		if (sibl_anode->klass == c) {
			continue;
		}
		int v = adjust_class_val (g, gn, sibling, res, is_left);
		dist = is_first? v: R_MIN (dist, v);
		is_first = false;
	}

	if (is_first) {
		RList *heap = r_list_new ();

		graph_foreach_anode (classes[c], it, gn, an) {
			const RVecGraphNodePtr *neigh = r_graph_all_neighbours (g->graph, gn);
			const RGraphNode *gk;
			RGraphNode **itk;
			const RANode *ak;

			graph_foreach_anode_vec (neigh, itk, gk, ak) {
				if (ak->klass < c) {
					size_t d = (ak->x - an->x);
					if (d > 0) {
						r_list_append (heap, (void *)d);
					}
				}
			}
		}
		int len = r_list_length (heap);
		if (len == 0) {
			dist = 0;
		} else {
			r_list_sort (heap, (RListComparator)cmp_dist);
			dist = (int) (size_t)r_list_get_n (heap, len / 2);
		}
		r_list_free (heap);
	}

	graph_foreach_anode (classes[c], it, gn, an) {
		const int old_val = ptr_ht_get_int (res, gn);
		const int new_val = is_left? old_val + dist: old_val - dist;
		ptr_ht_set_int (res, gn, new_val);
	}
}

static int place_nodes_val(const RAGraph *g, const RGraphNode *gn, const RGraphNode *sibl, HtUU *res, int is_left) {
	if (is_left) {
		return ptr_ht_get_int (res, sibl) + dist_nodes (g, sibl, gn);
	}
	return ptr_ht_get_int (res, sibl) - dist_nodes (g, gn, sibl);
}

static int place_nodes_sel_p(int newval, int oldval, int is_first, int is_left) {
	if (is_first) {
		return newval;
	}
	if (is_left) {
		return R_MAX (oldval, newval);
	}
	return R_MIN (oldval, newval);
}

/* places left/right the nodes of a class */
static void place_nodes(const RAGraph *g, const RGraphNode *gn, int is_left, HtUP *v_nodes, RList **classes, HtUU *res, SetU *placed) {
	const RList *lv = ptr_ht_get_rlist (v_nodes, gn);
	int p = 0, v, is_first = true;
	const RGraphNode *gk;
	const RListIter *itk;
	const RANode *ak;

	graph_foreach_anode (lv, itk, gk, ak) {
		const RGraphNode *sibling;
		const RANode *sibl_anode;

		sibling = get_sibling (g, ak, is_left, false);
		if (!sibling) {
			continue;
		}
		sibl_anode = get_anode (sibling);
		if (ak->klass == sibl_anode->klass) {
			if (!set_u_contains (placed, (ut64)(size_t)sibling)) {
				place_nodes (g, sibling, is_left, v_nodes, classes, res, placed);
			}

			v = place_nodes_val (g, gk, sibling, res, is_left);
			p = place_nodes_sel_p (v, p, is_first, is_left);
			is_first = false;
		}
	}

	if (is_first) {
		p = is_left? 0: 50;
	}

	graph_foreach_anode (lv, itk, gk, ak) {
		ptr_ht_set_int (res, gk, p);
		set_u_add (placed, (ut64)(size_t)gk);
	}
}

/* computes the position to the left/right of all the nodes */
static HtUU *compute_pos(const RAGraph *g, int is_left, HtUP *v_nodes) {
	int n_classes, i;

	RList **classes = compute_classes (g, v_nodes, is_left, &n_classes);
	if (!classes) {
		return NULL;
	}

	HtUU *res = ht_uu_new0 ();
	SetU *placed = set_u_new ();
	if (!res || !placed) {
		ht_uu_free (res);
		set_u_free (placed);
		res = NULL;
		goto err_classes;
	}
	for (i = 0; i < n_classes; i++) {
		const RGraphNode *gn;
		const RListIter *it;

		r_list_foreach (classes[i], it, gn) {
			if (!set_u_contains (placed, (ut64)(size_t)gn)) {
				place_nodes (g, gn, is_left, v_nodes, classes, res, placed);
			}
		}

		adjust_class (g, is_left, classes, res, i);
	}

	set_u_free (placed);
err_classes:
	for (i = 0; i < n_classes; i++) {
		if (classes[i]) {
			r_list_free (classes[i]);
		}
	}
	free (classes);
	return res;
}

/* calculates position of all nodes, but in particular dummies nodes */
/* computes two different placements (called "left"/"right") and set the final
 * position of each node to the average of the values in the two placements */
static void place_dummies(const RAGraph *g) {
	const RGraphNode *gn;
	const RListIter *it;
	RANode *n;

	HtUP *vertical_nodes = compute_vertical_nodes (g);
	if (!vertical_nodes) {
		return;
	}
	HtUU *xminus = compute_pos (g, true, vertical_nodes);
	if (!xminus) {
		goto xminus_err;
	}
	HtUU *xplus = compute_pos (g, false, vertical_nodes);
	if (!xplus) {
		goto xplus_err;
	}

	const RList *nodes = r_graph_get_nodes (g->graph);
	graph_foreach_anode (nodes, it, gn, n) {
		n->x = (ptr_ht_get_int (xminus, gn) + ptr_ht_get_int (xplus, gn)) / 2;
	}

	ht_uu_free (xplus);
xplus_err:
	ht_uu_free (xminus);
xminus_err:
	ht_up_free (vertical_nodes);
}

static RGraphNode *get_right_dummy(const RAGraph *g, const RGraphNode *n) {
	const RANode *an = get_anode (n);
	if (!an) {
		return NULL;
	}
	int k, layer = an->layer;
	if (layer < 0 || layer >= g->n_layers) {
		return NULL;
	}

	int start_pos = an->pos_in_layer + 1;
	if (start_pos >= g->layers[layer].n_nodes) {
		return NULL;
	}

	for (k = start_pos; k < g->layers[layer].n_nodes; k++) {
		RGraphNode *gk = g->layers[layer].nodes[k];
		const RANode *ak = get_anode (gk);
		if (!ak) {
			break;
		}

		if (ak->is_dummy) {
			return gk;
		}
	}
	return NULL;
}

static void adjust_directions(const RAGraph *g, int i, int from_up, HtUU *D, HtUU *P) {
	const RGraphNode *vm = NULL, *wm = NULL;
	const RANode *vma = NULL, *wma = NULL;
	int j, d = from_up? 1: -1;

	if (i + d < 0 || i + d >= g->n_layers) {
		return;
	}
	for (j = 0; j < g->layers[i + d].n_nodes; j++) {
		const RGraphNode *wp, *vp = g->layers[i + d].nodes[j];
		const RANode *wpa, *vpa = get_anode (vp);

		if (!vpa || !vpa->is_dummy) {
			continue;
		}
		if (from_up) {
			const RVecGraphNodePtr *innodes = r_graph_innodes (g->graph, vp);
			RGraphNode **first = innodes? RVecGraphNodePtr_at (innodes, 0): NULL;
			wp = first? *first: NULL;
		} else {
			wp = r_graph_nth_neighbour (g->graph, vp, 0);
		}
		wpa = get_anode (wp);
		if (!wpa || !wpa->is_dummy) {
			continue;
		}
		if (vm) {
			int p = ptr_ht_get_int (P, wm);
			int k;

			for (k = wma->pos_in_layer + 1; k < wpa->pos_in_layer; k++) {
				const RGraphNode *w = g->layers[wma->layer].nodes[k];
				const RANode *aw = get_anode (w);
				if (aw && aw->is_dummy) {
					p &= ptr_ht_get_int (P, w);
				}
			}
			if (p) {
				ptr_ht_set_int (D, vm, from_up);
				for (k = vma->pos_in_layer + 1; k < vpa->pos_in_layer; k++) {
					const RGraphNode *v = g->layers[vma->layer].nodes[k];
					const RANode *av = get_anode (v);
					if (av && av->is_dummy) {
						ptr_ht_set_int (D, v, from_up);
					}
				}
			}
		}
		vm = vp;
		wm = wp;
		vma = get_anode (vm);
		wma = get_anode (wm);
	}
}

/* find a placement for a single node */
static void place_single(const RAGraph *g, int l, const RGraphNode *bm, const RGraphNode *bp, int from_up, int va) {
	const RGraphNode *gk, *v = g->layers[l].nodes[va];
	const RANode *ak;
	RANode *av = get_anode (v);
	if (!av) {
		return;
	}
	RGraphNode **itk;

	const RVecGraphNodePtr *neigh = from_up
		? r_graph_innodes (g->graph, v)
		: r_graph_get_neighbours (g->graph, v);

	int len = neigh? RVecGraphNodePtr_length (neigh): 0;
	if (len == 0) {
		return;
	}

	int sum_x = 0;
	graph_foreach_anode_vec (neigh, itk, gk, ak) {
		if (ak->is_reversed) {
			len--;
			continue;
		}
		sum_x += ak->x;
	}

	if (len == 0) {
		return;
	}
	if (av) {
		av->x = sum_x / len;
	}
	if (bm) {
		const RANode *bma = get_anode (bm);
		av->x = R_MAX (av->x, bma->x + dist_nodes (g, bm, v));
	}
	if (bp) {
		const RANode *bpa = get_anode (bp);
		av->x = R_MIN (av->x, bpa->x - dist_nodes (g, v, bp));
	}
}

static int RM_listcmp(const struct len_pos_t *a, const struct len_pos_t *b) {
	return (a->pos < b->pos) - (a->pos > b->pos);
}

static int RP_listcmp(const struct len_pos_t *a, const struct len_pos_t *b) {
	return (a->pos > b->pos) - (a->pos < b->pos);
}

static void collect_changes(const RAGraph *g, int l, const RGraphNode *b, int from_up, int s, int e, RList *list, int is_left) {
	const RGraphNode *vt = g->layers[l].nodes[e - 1];
	const RGraphNode *vtp = g->layers[l].nodes[s];
	struct len_pos_t *cx;
	int i;

	RListComparator lcmp = is_left? (RListComparator)RM_listcmp: (RListComparator)RP_listcmp;

	for (i = is_left? s: e - 1; (is_left && i < e) || (!is_left && i >= s); i = is_left? i + 1: i - 1) {
		const RGraphNode *v, *vi = g->layers[l].nodes[i];
		const RANode *av, *avi = get_anode (vi);
		const RVecGraphNodePtr *neigh;
		RGraphNode **it;
		int c = 0;

		if (!avi) {
			continue;
		}
		neigh = from_up
			? r_graph_innodes (g->graph, vi)
			: r_graph_get_neighbours (g->graph, vi);

		graph_foreach_anode_vec (neigh, it, v, av) {
			if ((is_left && av->x >= avi->x) || (!is_left && av->x <= avi->x)) {
				c++;
			} else {
				cx = R_NEW (struct len_pos_t);
				c--;
				cx->len = 2;
				cx->pos = av->x;
				if (is_left) {
					cx->pos += dist_nodes (g, vi, vt);
				} else {
					cx->pos -= dist_nodes (g, vtp, vi);
				}
				r_list_add_sorted (list, cx, lcmp);
			}
		}

		cx = R_NEW0 (struct len_pos_t);
		cx->len = c;
		cx->pos = avi->x;
		if (is_left) {
			cx->pos += dist_nodes (g, vi, vt);
		} else {
			cx->pos -= dist_nodes (g, vtp, vi);
		}
		r_list_add_sorted (list, cx, lcmp);
	}

	if (b) {
		const RANode *ab = get_anode (b);
		cx = R_NEW (struct len_pos_t);
		if (cx) {
			cx->len = is_left? INT_MAX: INT_MIN;
			cx->pos = ab->x;
			if (is_left) {
				cx->pos += dist_nodes (g, b, vt);
			} else {
				cx->pos -= dist_nodes (g, vtp, b);
			}
			r_list_add_sorted (list, cx, lcmp);
		}
	}
}

static void combine_sequences(const RAGraph *g, int l, const RGraphNode *bm, const RGraphNode *bp, int from_up, int a, int r) {
	RList *Rm = r_list_new (), *Rp = r_list_new ();
	const RGraphNode *vt, *vtp;
	RANode *at, *atp;
	int rm, rp, t, m, i;
	Rm->free = (RListFree)free;
	Rp->free = (RListFree)free;

	t = (a + r) / 2;
	vt = g->layers[l].nodes[t - 1];
	vtp = g->layers[l].nodes[t];
	at = get_anode (vt);
	atp = get_anode (vtp);

	collect_changes (g, l, bm, from_up, a, t, Rm, true);
	collect_changes (g, l, bp, from_up, t, r, Rp, false);
	rm = rp = 0;

	m = dist_nodes (g, vt, vtp);
	if (at && atp) {
		while (atp->x - at->x < m) {
			if (atp->x == at->x) {
				int step = m / 2;
				at->x -= step;
				atp->x += m - step;
			} else {
				if (rm < rp) {
					if (r_list_empty (Rm)) {
						at->x = atp->x - m;
					} else {
						struct len_pos_t *cx = (struct len_pos_t *)r_list_pop (Rm);
						rm = rm + cx->len;
						at->x = R_MAX (cx->pos, atp->x - m);
						free (cx);
					}
				} else {
					if (r_list_empty (Rp)) {
						atp->x = at->x + m;
					} else {
						struct len_pos_t *cx = (struct len_pos_t *)r_list_pop (Rp);
						rp = rp + cx->len;
						atp->x = R_MIN (cx->pos, at->x + m);
						free (cx);
					}
				}
			}
		}
	}

	r_list_free (Rm);
	r_list_free (Rp);

	for (i = t - 2; i >= a; i--) {
		const RGraphNode *gv = g->layers[l].nodes[i];
		RANode *av = get_anode (gv);
		if (av && at) {
			av->x = R_MIN (av->x, at->x - dist_nodes (g, gv, vt));
		}
	}

	for (i = t + 1; i < r; i++) {
		const RGraphNode *gv = g->layers[l].nodes[i];
		RANode *av = get_anode (gv);
		if (av && atp) {
			av->x = R_MAX (av->x, atp->x + dist_nodes (g, vtp, gv));
		}
	}
}

/* places a sequence of consecutive original nodes */
/* it tries to minimize the distance between each node in the sequence and its
 * neighbours in the "previous" layer. Those neighbours are considered as
 * "fixed". The previous layer depends on the direction used during the layers
 * traversal */
static void place_sequence(const RAGraph *g, int l, const RGraphNode *bm, const RGraphNode *bp, int from_up, int va, int vr) {
	if (vr == va + 1) {
		place_single (g, l, bm, bp, from_up, va);
	} else if (vr > va + 1) {
		int vt = (vr + va) / 2;
		place_sequence (g, l, bm, bp, from_up, va, vt);
		place_sequence (g, l, bm, bp, from_up, vt, vr);
		combine_sequences (g, l, bm, bp, from_up, va, vr);
	}
}

/* finds the placements of nodes while traversing the graph in the given
 * direction */
/* places all the sequences of consecutive original nodes in each layer. */
static void original_traverse_l(const RAGraph *g, HtUU *D, HtUU *P, int from_up) {
	int i, k, va, vr;

	for (i = from_up? 0: g->n_layers - 1;
		(from_up && i < g->n_layers) || (!from_up && i >= 0);
		i = from_up? i + 1: i - 1) {
		int j;
		const RGraphNode *bm = NULL;
		const RANode *bma = NULL;

		j = 0;
		while (j < g->layers[i].n_nodes && !bm) {
			const RGraphNode *gn = g->layers[i].nodes[j];
			const RANode *an = get_anode (gn);
			if (an && an->is_dummy) {
				va = 0;
				vr = j;
				bm = gn;
				bma = an;
			}
			j++;
		}
		if (!bm) {
			va = 0;
			vr = g->layers[i].n_nodes;
		}
		place_sequence (g, i, NULL, bm, from_up, va, vr);
		for (k = va; k < vr - 1; k++) {
			set_dist_nodes (g, i, k, k + 1);
		}
		if (is_valid_pos (g, i, vr - 1) && bm) {
			set_dist_nodes (g, i, vr - 1, bma->pos_in_layer);
		}
		while (bm) {
			const RGraphNode *bp = get_right_dummy (g, bm);
			const RANode *bpa = NULL;
			bma = get_anode (bm);

			if (!bp) {
				va = bma->pos_in_layer + 1;
				vr = g->layers[bma->layer].n_nodes;
				place_sequence (g, i, bm, NULL, from_up, va, vr);
				for (k = va; k < vr - 1; k++) {
					set_dist_nodes (g, i, k, k + 1);
				}

				if (is_valid_pos (g, i, va)) {
					set_dist_nodes (g, i, bma->pos_in_layer, va);
				}
			} else if (ptr_ht_get_int (D, bm) == from_up) {
				bpa = get_anode (bp);
				va = bma->pos_in_layer + 1;
				vr = bpa->pos_in_layer;
				place_sequence (g, i, bm, bp, from_up, va, vr);
				ptr_ht_set_int (P, bm, true);
			}
			bm = bp;
		}
		adjust_directions (g, i, from_up, D, P);
	}
}

/* computes a final position of original nodes, considering dummies nodes as
 * fixed */
/* set the node placements traversing the graph downward and then upward */
static void place_original(RAGraph *g) {
	const RList *nodes = r_graph_get_nodes (g->graph);
	const RGraphNode *gn;
	const RListIter *itn;
	const RANode *an;

	HtUU *D = ht_uu_new0 ();
	if (!D) {
		return;
	}
	HtUU *P = ht_uu_new0 ();
	if (!P) {
		ht_uu_free (D);
		return;
	}
	g->dists = ht_uu_new0 ();

	graph_foreach_anode (nodes, itn, gn, an) {
		if (!an->is_dummy) {
			continue;
		}
		const RGraphNode *right_v = get_right_dummy (g, gn);
		const RANode *right = get_anode (right_v);
		if (right_v && right) {
			ptr_ht_set_int (D, gn, 0);
			int dt_eq = right->x - an->x == dist_nodes (g, gn, right_v);
			ptr_ht_set_int (P, gn, dt_eq);
		}
	}

	original_traverse_l (g, D, P, true);
	original_traverse_l (g, D, P, false);

	ht_uu_free (g->dists);
	g->dists = NULL;
	ht_uu_free (P);
	ht_uu_free (D);
}

static void aedge_free(AEdge *e) {
	if (e) {
		r_list_free (e->x);
		r_list_free (e->y);
		free (e);
	}
}

static void ranode_free(RANode *n) {
	if (n) {
		free (((RANodePrivate *)n)->color_body);
		free (n->title);
		free (n->body);
		free (n->color);
		free (n->shortcut);
		free (n);
	}
}

static void ranode_set_shortcut(RANode *n, char *shortcut) {
	free (n->shortcut);
	n->shortcut = shortcut;
}

static void set_layer_gap(RAGraph *g) {
	int gap = 0;
	int i = 0, j = 0;
	RGraphNode **itn;
	RGraphNode *ga, *gb;
	RANode *a, *b;
	const RVecGraphNodePtr *outnodes;

	g->layers[0].gap = 0;
	for (i = 0; i < g->n_layers; i++) {
		gap = 0;
		if (i + 1 < g->n_layers) {
			g->layers[i + 1].gap = gap;
		}
		for (j = 0; j < g->layers[i].n_nodes; j++) {
			ga = g->layers[i].nodes[j];
			if (!ga) {
				continue;
			}
			a = (RANode *)ga->data;
			outnodes = &ga->out_nodes;

			if (!outnodes || !a) {
				continue;
			}
			int edge_count = RVecGraphNodePtr_length (outnodes);
			bool fanout = !g->layout && !g->is_callgraph && edge_count > 2;
			if (fanout) {
				gap += edge_count + 1;
			}
			graph_foreach_anode_vec (outnodes, itn, gb, b) {
				bool backedge = b->layer <= a->layer;
				bool needs_gap = backedge || a->is_dummy != b->is_dummy || edge_count > 1
					|| RVecGraphNodePtr_length (&gb->in_nodes) > 1;
				if (g->layout == 0) {
					needs_gap |= b->x != a->x;
				} else {
					needs_gap |= b->y != a->y || b->h != a->h;
				}
				if (needs_gap && !fanout) {
					gap++;
				}
				if (backedge) {
					g->layers[b->layer].gap++;
				}
			}
		}
		if (i + 1 < g->n_layers) {
			g->layers[i + 1].gap += gap;
		}
	}
}

static void fix_back_edge_dummy_nodes(RAGraph *g, RANode *from, RANode *to, RVecGraphNodePtr *dead) {
	RANode *v, *tmp = NULL;
	RGraphNode *gv = NULL;
	RGraphNode **it;
	int i;
	R_RETURN_IF_FAIL (g && from && to);
	const RVecGraphNodePtr *neighbours = r_graph_get_neighbours (g->graph, to->gnode);
	graph_foreach_anode_vec (neighbours, it, gv, v) {
		tmp = v;
		while (tmp->is_dummy) {
			tmp = get_anode (r_graph_nth_neighbour (g->graph, tmp->gnode, 0));
		}
		if (tmp->gnode->idx == from->gnode->idx) {
			break;
		}
		tmp = NULL;
	}
	if (tmp) {
		tmp = v;
		v = to;
		while (tmp->gnode->idx != from->gnode->idx) {
			v = tmp;
			tmp = get_anode (r_graph_nth_neighbour (g->graph, v->gnode, 0));

			i = 0;
			while (v->gnode->idx != g->layers[v->layer].nodes[i]->idx) {
				i += 1;
			}

			while (i + 1 < g->layers[v->layer].n_nodes) {
				g->layers[v->layer].nodes[i] = g->layers[v->layer].nodes[i + 1];
				i++;
			}
			g->layers[v->layer].nodes[g->layers[v->layer].n_nodes - 1] = 0;
			g->layers[v->layer].n_nodes -= 1;

			// the node list is swept once by the caller
			r_graph_node_unlink (g->graph, v->gnode);
			v->gnode->data = NULL;
			RVecGraphNodePtr_push_back (dead, &v->gnode);
			ranode_free (v);
		}
	}
}

static int get_edge_number(const RAGraph *g, RANode *src, RANode *dst, bool outgoing) {
	RGraphNode **itn;
	RGraphNode *gv;
	int cur_nth = 0;
	int nth = 0;
	RANode *v;

	if (outgoing && src->is_dummy) {
		if (src->gnode) {
			RGraphNode **first = RVecGraphNodePtr_at (&src->gnode->in_nodes, 0);
			RANode *in = first? get_anode (*first): NULL;
			if (in) {
				cur_nth = get_edge_number (g, in, src, outgoing);
			}
		}
	} else {
		const RVecGraphNodePtr *neighbours = outgoing
			? r_graph_get_neighbours (g->graph, src->gnode)
			: r_graph_innodes (g->graph, dst->gnode);
		const int exit_edges = neighbours? RVecGraphNodePtr_length (neighbours): 0;
		graph_foreach_anode_vec (neighbours, itn, gv, v) {
			cur_nth = nth;
			if (g->is_callgraph) {
				cur_nth = 0;
			} else if (exit_edges == 1) {
				cur_nth = -1;
			}
			if (outgoing && gv->idx == (dst->gnode)->idx) {
				break;
			}
			if (!outgoing && gv->idx == (src->gnode)->idx) {
				break;
			}
			nth++;
		}
	}
	return cur_nth;
}

static ut64 edge_number_key(const RANode *src, const RANode *dst) {
	return ((ut64)src->gnode->idx << 32) | (ut64)dst->gnode->idx;
}

static int get_edge_number_cached(const RAGraph *g, RANode *src, RANode *dst, bool outgoing, HtUU *cache) {
	if (!cache || !src || !dst || !src->gnode || !dst->gnode) {
		return get_edge_number (g, src, dst, outgoing);
	}
	const ut64 key = edge_number_key (src, dst);
	bool found = false;
	ut64 cached = ht_uu_find (cache, key, &found);
	if (found) {
		return (int)cached - 2;
	}
	int nth = get_edge_number (g, src, dst, outgoing);
	ht_uu_insert (cache, key, nth + 2);
	return nth;
}

static int count_edges(const RAGraph *g, RANode *src, RANode *dst) {
	return get_edge_number (g, src, dst, true);
}

static void backedge_info(RAGraph *g) {
	int i, j, k;
	int min, max;
	int inedge = 0;
	int outedge = 0;
	if (g->n_layers > ST16_MAX) {
		return;
	}

	int **arr = R_NEWS0 (int *, g->n_layers);
	if (!arr) {
		return;
	}
	for (i = 0; i < g->n_layers; i++) {
		arr[i] = R_NEWS0 (int, 2);
		if (!arr[i]) {
			goto err;
		}
	}

	for (i = 0; i < g->n_layers; i++) {
		for (j = 0; j < g->layers[i].n_nodes; j++) {
			RGraphNode *gt = g->layers[i].nodes[j];
			if (!gt) {
				continue;
			}
			RANode *t = (RANode *)gt->data;
			if (!t) {
				continue;
			}
			int tc = g->layout == 0? t->x: t->y;
			int tl = g->layout == 0? t->w: t->h;
			if (!j) {
				arr[i][0] = tc;
				arr[i][1] = tc + tl;
			}

			if (arr[i][0] > tc) {
				arr[i][0] = tc;
			}

			if (arr[i][1] < tc + tl) {
				arr[i][1] = tc + tl;
			}
		}

		for (j = 0; j < g->layers[i].n_nodes; j++) {
			RANode *a = get_anode (g->layers[i].nodes[j]);
			if (!a || a->is_dummy) {
				continue;
			}

			const RVecGraphNodePtr *neighbours = r_graph_get_neighbours (g->graph, a->gnode);
			RGraphNode *gb;
			RANode *b;
			RGraphNode **itm;

			if (i == 0) {
				const RVecGraphNodePtr *innodes = r_graph_innodes (g->graph, a->gnode);
				inedge += innodes? RVecGraphNodePtr_length (innodes): 0;
			} else if (i == g->n_layers - 1) {
				outedge += neighbours? RVecGraphNodePtr_length (neighbours): 0;
			}

			graph_foreach_anode_vec (neighbours, itm, gb, b) {
				if (b->layer > a->layer) {
					continue;
				}

				int nth = count_edges (g, a, b);
				int xinc = R_EDGES_X_INC + 2 *(nth + 1);

				int ax = g->layout == 0? a->x + xinc: a->y + (a->h / 2) + nth;
				int bx = g->layout == 0? b->x + xinc: b->y + (b->h / 2) + nth;

				if (g->layout == 0 && nth == 0 && bx > ax) {
					ax += 4;
				}

				min = arr[b->layer][0];
				max = arr[b->layer][1];
				for (k = b->layer; k <= a->layer; k++) {
					if (min > arr[k][0]) {
						min = arr[k][0];
					}

					if (max < arr[k][1]) {
						max = arr[k][1];
					}
				}

				int l = (ax - min) + (bx - min);
				int r = (max - ax) + (max - bx);

				for (k = b->layer; k <= a->layer; k++) {
					if (r < l) {
						arr[k][1] = max + 1;
					} else {
						arr[k][0] = min - 1;
					}
				}

				AEdge *e = R_NEW0 (AEdge);
				e->is_reversed = true;
				e->from = a;
				e->to = b;
				e->x = r_list_new ();
				e->y = r_list_new ();

				if (r < l) {
					r_list_append ((g->layout == 0? e->x: e->y), (void *) (size_t) (max + 1));
				} else {
					r_list_append ((g->layout == 0? e->x: e->y), (void *) (size_t) (min - 1));
				}

				r_list_append (g->edges, e);
			}
		}
	}

	// Assumption: layer layout is not changed w.r.t x-coordinate/y-coordinate for horizontal/vertical layout respectively.
	if (inedge) {
		RANode *n = (RANode *)g->layers[0].nodes[0]->data;
		AEdge *e = R_NEW0 (AEdge);
		e->is_reversed = true;
		e->from = NULL;
		e->to = NULL;
		e->x = r_list_new ();
		e->y = r_list_new ();
		if (g->layout == 0) {
			r_list_append (e->y, (void *) (size_t) (n->y - 1 - inedge));
		} else {
			r_list_append (e->x, (void *) (size_t) (n->x - 1 - inedge));
		}
		r_list_append (g->edges, e);
	}

	if (outedge) {
		RANode *n = (RANode *)g->layers[g->n_layers - 1].nodes[0]->data;
		AEdge *e = R_NEW0 (AEdge);
		e->is_reversed = true;
		e->from = NULL;
		e->to = NULL;
		e->x = r_list_new ();
		e->y = r_list_new ();
		if (g->layout == 0) {
			r_list_append (e->y, (void *) (size_t) (n->y + g->layers[g->n_layers - 1].height + 2 + outedge));
		} else {
			r_list_append (e->x, (void *) (size_t) (n->x + g->layers[g->n_layers - 1].width + 2 + outedge));
		}
		r_list_append (g->edges, e);
	}
err:
	for (i = i - 1; i >= 0; i--) {
		free (arr[i]);
	}
	free (arr);
}

/* 1) trasform the graph into a DAG
 * 2) partition the nodes in layers
 * 3) split long edges that traverse multiple layers
 * 4) reorder nodes in each layer to reduce the number of edge crossing
 * 5) assign x and y coordinates to each node
 * 6) restore the original graph, with long edges and cycles */
// turn the dummy chains left by a previous layout back into plain edges
static void remove_dummy_nodes(RAGraph *g) {
	const RListIter *it;
	RGraphNode *gn;
	RANode *a;
	RVecGraphNodePtr dummies;
	RVecGraphNodePtr_init (&dummies);
	graph_foreach_anode (r_graph_get_nodes (g->graph), it, gn, a) {
		if (a->is_dummy) {
			RVecGraphNodePtr_push_back (&dummies, &gn);
			continue;
		}
		int nth = 0;
		RGraphNode *gd;
		while ((gd = r_graph_nth_neighbour (g->graph, gn, nth))) {
			RANode *d = get_anode (gd);
			if (!d || !d->is_dummy) {
				nth++;
				continue;
			}
			RANode *b = d;
			int steps = g->graph->n_nodes;
			while (b && b->is_dummy && steps-- > 0) {
				b = get_anode (r_graph_nth_neighbour (g->graph, b->gnode, 0));
			}
			r_agraph_del_edge (g, a, d);
			if (b && !b->is_dummy) {
				r_agraph_add_edge_at (g, a, b, nth++);
			}
		}
	}
	RGraphNode **dn;
	R_VEC_FOREACH (&dummies, dn) {
		ranode_free (get_anode (*dn));
		(*dn)->data = NULL;
	}
	r_graph_del_nodes (g->graph, &dummies);
	RVecGraphNodePtr_fini (&dummies);
}

static void separate_layer_nodes(const RAGraph *g) {
	int i, j;
	for (i = 0; i < g->n_layers; i++) {
		for (j = 1; j < g->layers[i].n_nodes; j++) {
			const RANode *previous = get_anode (g->layers[i].nodes[j - 1]);
			RANode *node = get_anode (g->layers[i].nodes[j]);
			// Keep dummy edge corridors clear as well as visible node rectangles.
			int distance = previous->w - previous->w / 2 + node->w / 2 + HORIZONTAL_NODE_SPACING;
			node->x = R_MAX (node->x, previous->x + distance);
		}
	}
}

static void set_layout(RAGraph *g) {
	RCons *cons = g->can->cons;
	int i, j;

	r_list_free (g->edges);
	g->edges = r_list_newf ((RListFree)aedge_free);

	remove_dummy_nodes (g);
	remove_cycles (g);
	assign_layers (g);
	create_dummy_nodes (g);
	create_layers (g);
	minimize_crossings (g);

	if (r_cons_is_breaked (cons)) {
		r_cons_break_end (cons);
		return;
	}
	/* identify row height */
	for (i = 0; i < g->n_layers; i++) {
		int rh = 0;
		int rw = 0;
		for (j = 0; j < g->layers[i].n_nodes; j++) {
			const RANode *n = get_anode (g->layers[i].nodes[j]);
			if (n->h > rh) {
				rh = n->h;
			}
			if (n->w > rw) {
				rw = n->w;
			}
		}
		g->layers[i].height = rh;
		g->layers[i].width = rw;
	}

	for (i = 0; i < g->n_layers; i++) {
		for (j = 0; j < g->layers[i].n_nodes; j++) {
			RANode *a = (RANode *)g->layers[i].nodes[j]->data;
			if (a->is_dummy) {
				if (g->layout == 0) {
					a->h = g->layers[i].height;
				} else {
					a->w = g->layers[i].width;
				}
			}
			a->layer_height = g->layers[i].height;
			a->layer_width = g->layers[i].width;
		}
	}

	/* x-coordinate assignment: algorithm based on:
	 * A Fast Layout Algorithm for k-Level Graphs
	 * by C. Buchheim, M. Junger, S. Leipert */
	place_dummies (g);
	place_original (g);

	/* IDEA: need to put this hack because of the way algorithm is implemented.
	 * I think backedges should be restored to their original state instead of
	 * converting them to longedges and adding dummy nodes. */
	const RListIter *it;
	const RGraphEdge *e;
	RVecGraphNodePtr dead;
	RVecGraphNodePtr_init (&dead);
	r_list_foreach (g->back_edges, it, e) {
		RANode *from = e->from? get_anode (e->from): NULL;
		RANode *to = e->to? get_anode (e->to): NULL;
		fix_back_edge_dummy_nodes (g, from, to, &dead);
		r_agraph_del_edge (g, to, from);
		r_agraph_add_edge_at (g, from, to, e->nth);
	}
	r_graph_del_nodes (g->graph, &dead);
	RVecGraphNodePtr_fini (&dead);

	switch (g->layout) {
	default:
	case 0: // vertical layout
		if (!g->is_tiny) {
			separate_layer_nodes (g);
		}
		/* horizontal finalize x coordinate */
		for (i = 0; i < g->n_layers; i++) {
			for (j = 0; j < g->layers[i].n_nodes; j++) {
				RANode *n = get_anode (g->layers[i].nodes[j]);
				if (n) {
					n->x -= n->w / 2;
					if (g->is_tiny) {
						n->x /= 8;
					}
				}
			}
		}

		set_layer_gap (g);

		/* vertical align */
		int layer_y = g->layers[0].gap;
		for (i = 0; i < g->n_layers; i++) {
			int tmp_y = g->is_tiny? i: layer_y;
			for (j = 0; j < g->layers[i].n_nodes; j++) {
				RANode *n = get_anode (g->layers[i].nodes[j]);
				if (n) {
					n->y = tmp_y;
				}
			}
			if (i + 1 < g->n_layers) {
				layer_y += g->layers[i].height + g->layers[i + 1].gap + 3;
			}
		}
		break;
	/* experimental */
	case 1: // horizontal layout
		/* vertical y coordinate */
		for (i = 0; i < g->n_layers; i++) {
			int next_y = 1;
			for (j = 0; j < g->layers[i].n_nodes; j++) {
				RANode *n = get_anode (g->layers[i].nodes[j]);
				if (j) {
					next_y -= n->h + VERTICAL_NODE_SPACING;
				}
				n->y = next_y;
			}
		}

		set_layer_gap (g);

		/* horizontal align */
		int xval = g->layers[0].gap + 2;
		for (i = 0; i < g->n_layers; i++) {
			for (j = 0; j < g->layers[i].n_nodes; j++) {
				RANode *n = get_anode (g->layers[i].nodes[j]);
				n->x = xval;
			}
			if (i + 1 < g->n_layers) {
				xval += g->layers[i].width + g->layers[i + 1].gap + 3;
			}
		}
		break;
	}

	backedge_info (g);

	/* free all temporary structures used during layout */
	for (i = 0; i < g->n_layers; i++) {
		free (g->layers[i].nodes);
	}

	free (g->layers);
	r_list_free (g->long_edges);
	r_list_free (g->back_edges);
	r_cons_break_pop (cons);
}

static void graph_palette_begin(RCore *core, RAGraph *g, RAGraphPalette *palette) {
	palette->context = NULL;
	if (!g->is_interactive || !r_config_get_i (core->config, "scr.color")
			|| R_STR_ISNOTEMPTY (r_config_get (core->config, "cmd.bbgraph"))
			|| r_config_get_b (core->config, "graph.aeab")) {
		return;
	}
	RConsContext *context = core->cons->context;
	palette->saved = context->pal;
	palette->context = context;
	// Distinct tags retain separate roles even when theme colors are identical.
	size_t i;
	for (i = 0; i < GRAPH_PALETTE_COLORS; i++) {
		char **color = (char **)((ut8 *)&context->pal + i * sizeof (char *));
		if (*color && i != r_offsetof (RConsPrintablePalette, reset) / sizeof (char *)) {
			snprintf (palette->tags[i], sizeof (palette->tags[i]), GRAPH_COLOR_TAG "%um", (unsigned int)i);
			*color = palette->tags[i];
		}
	}
}

static void graph_palette_end(RAGraphPalette *palette) {
	if (palette->context) {
		palette->context->pal = palette->saved;
	}
}

static char *graph_color_body(const char *body, const RConsPrintablePalette *palette) {
	if (!body) {
		return NULL;
	}
	RStrBuf *result = r_strbuf_new (NULL);
	const char *start = body;
	const char *tag = body;
	while ((tag = strstr (tag, GRAPH_COLOR_TAG))) {
		char *end;
		unsigned long index = strtoul (tag + sizeof (GRAPH_COLOR_TAG) - 1, &end, 10);
		if (end == tag + sizeof (GRAPH_COLOR_TAG) - 1 || *end != 'm' || index >= GRAPH_PALETTE_COLORS) {
			tag++;
			continue;
		}
		const char *color = *(char *const *)((const ut8 *)palette + index * sizeof (char *));
		if (!r_strbuf_append_n (result, start, tag - start) || !r_strbuf_append (result, r_str_get (color))) {
			r_strbuf_free (result);
			return NULL;
		}
		start = tag = end + 1;
	}
	if (!r_strbuf_append (result, start)) {
		r_strbuf_free (result);
		return NULL;
	}
	return r_strbuf_drain (result);
}

static void ranode_cache_colors(RANode *node, const char *body) {
	RANodePrivate *priv = (RANodePrivate *)node;
	free (priv->color_body);
	priv->color_body = body
		? r_str_endswith (node->body, "\n") && !r_str_endswith (body, "\n")
			? r_str_newf ("%s\n", body): strdup (body)
		: NULL;
}

static RANode *agraph_add_colored_node(RAGraph *g, const char *title, const char *body, const char *color, RAGraphPalette *palette) {
	char *colored = palette->context? graph_color_body (body, &palette->saved): NULL;
	RANode *node = body && palette->context && !colored
		? NULL: r_agraph_add_node (g, title, colored? colored: body, color);
	if (node) {
		ranode_cache_colors (node, palette->context? body: NULL);
	}
	free (colored);
	return node;
}

static bool agraph_recolor_nodes(RAGraph *g, RCore *core) {
	if (!r_config_get_i (core->config, "scr.color")) {
		return true;
	}
	RGraphNode *gn;
	RANode *node;
	RListIter *iter;
	graph_foreach_anode (r_graph_get_nodes (g->graph), iter, gn, node) {
		if (!node->is_dummy && R_STR_ISNOTEMPTY (node->body) && !((RANodePrivate *)node)->color_body) {
			return false;
		}
	}
	graph_foreach_anode (r_graph_get_nodes (g->graph), iter, gn, node) {
		const char *cached = ((RANodePrivate *)node)->color_body;
		if (cached) {
			char *body = graph_color_body (cached, &core->cons->context->pal);
			if (!body) {
				return false;
			}
			free (node->body);
			node->body = body;
		}
	}
	return true;
}

static char *get_body(RCore *core, ut64 addr, int size, int opts) {
	char *body;
	int disas_size = size;
	RConfigHold *hc = r_config_hold_new (core->config);
	if (!hc) {
		return NULL;
	}
	r_config_hold (hc, "asm.lines", "asm.bytes", "asm.flags.inbytes",
		"asm.cmt.col", "asm.marks", "asm.addr", "asm.dwarf.file",
		"asm.comments", "asm.cmt.right", "asm.lines.bb", NULL);
	const bool o_comments = r_config_get_b (core->config, "graph.comments");
	const bool o_cmtright = r_config_get_b (core->config, "graph.cmtright");
	const bool o_bytes = r_config_get_b (core->config, "graph.bytes");
	bool o_flags_in_bytes = r_config_get_b (core->config, "asm.flags.inbytes");
	const bool o_graph_addr = r_config_get_b (core->config, "graph.addr");
	const int graph_bb_maxsize = r_config_get_i (core->config, "graph.bb.maxsize");
	const bool truncated = graph_bb_maxsize > 0 && disas_size > graph_bb_maxsize;
	if (truncated) {
		disas_size = graph_bb_maxsize;
	}
	int o_cursor = core->print->cur_enabled;
	r_config_set_b (core->config, "asm.dwarf.file", false);
	int mw = r_config_get_i (core->config, "graph.bb.maxwidth");
	if (opts & BODY_COMMENTS) {
		r_core_visual_toggle_decompiler_disasm (core, true, false);
		char *res = r_core_cmd_strf (core, "pD %d @ 0x%08" PFMT64x, disas_size, addr);
		res = r_str_replace (res, "; ", "", true);
		// res = r_str_replace (res, "\n", "(\n)", true);
		r_str_trim (res);
		res = r_str_trim_lines (res);
		if (truncated && res) {
			char *trunc_res = r_str_newf ("%s\n...", res);
			free (res);
			res = trunc_res;
		}
		if (mw > 0) {
			res = r_str_ansi_crop (res, 0, 0, mw, -1);
		}
		r_core_visual_toggle_decompiler_disasm (core, true, false);
		r_config_hold_restore (hc);
		r_config_hold_free (hc);
		return res;
	}

	// configure options
	if (r_config_get_b (core->config, "cfg.debug")) {
		r_config_set_b (core->config, "asm.flags.inbytes", true);
		o_flags_in_bytes = true;
	}
	r_config_set_b (core->config, "asm.lines.bb", false);
	r_config_set_b (core->config, "asm.lines", false);
	r_config_set_i (core->config, "asm.cmt.col", 0);
	r_config_set_b (core->config, "asm.marks", false);
	r_config_set_b (core->config, "asm.cmt.right", (opts & BODY_SUMMARY) || o_cmtright);
	r_config_set_b (core->config, "asm.comments", (opts & BODY_SUMMARY) || o_comments);
	r_config_set_b (core->config, "asm.bytes",
		(opts &(BODY_SUMMARY | BODY_OFFSETS)) || o_bytes || o_flags_in_bytes);
	r_config_set_b (core->config, "asm.bbmiddle", false);
	core->print->cur_enabled = false;

	const bool asm_addr = (opts & BODY_OFFSETS || opts & BODY_SUMMARY || o_graph_addr);
	r_config_set_b (core->config, "asm.addr", asm_addr);

	const bool html = r_config_get_b (core->config, "scr.html");
	r_config_set_b (core->config, "scr.html", false);

	const char *cmd = (opts & BODY_SUMMARY)? "pds": "pD";
	const char *bbcmd = r_config_get (core->config, "cmd.bbgraph");
	if (R_STR_ISNOTEMPTY (bbcmd)) {
		cmd = bbcmd;
	}
	if (r_config_get_b (core->config, "graph.aeab")) {
		body = r_core_cmd_strf (core, "%s 0x%08" PFMT64x, "aeab", addr);
	} else {
		body = r_core_cmd_strf (core, "%s %d @ 0x%08" PFMT64x, cmd, disas_size, addr);
		if (truncated && body) {
			char *trunc_body = r_str_newf ("%s\n...", body);
			free (body);
			body = trunc_body;
		}
	}
	if (mw > 0) {
		body = r_str_ansi_crop (body, 0, 0, mw, -1);
	}
	r_config_set_b (core->config, "scr.html", html);
	// r_cons_canvas_bgfill (g->can, n->x, n->y, n->w, n->h, get_node_bgcolor (color, cur));
	// body = r_str_ansi_crop (body, 0, 0, 40, 20);
	// body = r_str_ansi_resetbg (body, Color_BGRED); // color);
	// r_str_ansi_filter (body, NULL, NULL, -1);
	// restore original options
	core->print->cur_enabled = o_cursor;
	r_config_hold_restore (hc);
	r_config_hold_free (hc);
	return body;
}

static char *get_bb_body(RCore *core, RAnalBlock *b, int opts, RAnalFunction *fcn, bool emu, ut64 saved_gp, ut8 *saved_arena, int saved_arena_size) {
	if (emu) {
		core->anal->gp = saved_gp;
		if (b->parent_reg_arena) {
			r_reg_arena_poke (core->anal->reg, b->parent_reg_arena, b->parent_reg_arena_size);
			R_FREE (b->parent_reg_arena);
			ut64 gp = r_reg_getv (core->anal->reg, "gp");
			if (gp) {
				core->anal->gp = gp;
			}
		} else {
			r_reg_arena_poke (core->anal->reg, saved_arena, saved_arena_size);
		}
	}
	if (b->parent_stackptr != INT_MAX) {
		core->anal->stackptr = b->parent_stackptr;
	}
	char *body = get_body (core, b->addr, b->size, opts);
	if (b->jump != UT64_MAX) {
		if (b->jump > b->addr) {
			RAnalBlock *jumpbb = r_anal_get_block_at (b->anal, b->jump);
			if (jumpbb && r_list_contains (jumpbb->fcns, fcn)) {
				if (emu && core->anal->last_disasm_reg && !jumpbb->parent_reg_arena) {
					jumpbb->parent_reg_arena = r_reg_arena_dup (core->anal->reg, core->anal->last_disasm_reg);
				}
				if (jumpbb->parent_stackptr == INT_MAX) {
					jumpbb->parent_stackptr = core->anal->stackptr + b->stackptr;
				}
			}
		}
	}
	if (b->fail != UT64_MAX) {
		if (b->fail > b->addr) {
			RAnalBlock *failbb = r_anal_get_block_at (b->anal, b->fail);
			if (failbb && r_list_contains (failbb->fcns, fcn)) {
				if (emu && core->anal->last_disasm_reg && !failbb->parent_reg_arena) {
					failbb->parent_reg_arena = r_reg_arena_dup (core->anal->reg, core->anal->last_disasm_reg);
				}
				if (failbb->parent_stackptr == INT_MAX) {
					failbb->parent_stackptr = core->anal->stackptr + b->stackptr;
				}
			}
		}
	}
	// body = r_str_ansi_resetbg (body, Color_BGRED); // color);
	// r_cons_canvas_bgfill (g->can, n->x, n->y, n->w, n->h, get_node_bgcolor (color, cur));
	return body;
}

static int bbcmp(RAnalBlock *a, RAnalBlock *b) {
	if (a->addr < b->addr) {
		return -1;
	}
	if (a->addr > b->addr) {
		return 1;
	}
	return 0;
}

static void get_bbupdate(RAGraph *g, RCore *core, RAnalFunction *fcn) {
	RAnalBlock *bb;
	RListIter *iter;
	ut64 saved_gp = core->anal->gp;
	ut8 *saved_arena = NULL;
	int saved_arena_size = 0;
	int saved_stackptr = core->anal->stackptr;
	core->keep_asmqjmps = false;

	const bool emu = r_config_get_b (core->config, "asm.emu");
	if (emu) {
		saved_arena = r_reg_arena_peek (core->anal->reg, &saved_arena_size);
	}
	if (fcn->bbs) {
		r_list_sort (fcn->bbs, (RListComparator)bbcmp);
	}

	int shortcuts = r_config_get_b (core->config, "graph.nodejmps");
	RAGraphPalette palette;
	graph_palette_begin (core, g, &palette);
	r_list_foreach (fcn->bbs, iter, bb) {
		if (bb->addr == UT64_MAX) {
			continue;
		}
		char *body = get_bb_body (core, bb, mode2opts (g), fcn, emu, saved_gp, saved_arena, saved_arena_size);
		char *title = get_title (bb->addr);

		// r_cons_canvas_bgfill (g->can, n->x, n->y, n->w, n->h, get_node_bgcolor (color, cur));
		char *shortcut = shortcuts? r_core_add_asmqjmp (core, bb->addr): NULL;
		RANode *node = r_agraph_get_node (g, title);
		if (node) {
			char *colored = palette.context? graph_color_body (body, &palette.saved): body;
			if (!colored) {
				free (body);
				free (shortcut);
				free (title);
				continue;
			}
			ranode_set_shortcut (node, shortcut);
			shortcut = NULL;
			free (node->body);
			node->body = colored;
			ranode_cache_colors (node, palette.context? body: NULL);
			if (palette.context) {
				free (body);
			}
			R_FREE (node->color);
			if (bb->color.r || bb->color.g || bb->color.b) {
				node->color = r_cons_rgb_str (core->cons, NULL, -1, &bb->color);
			}
		} else {
			free (body);
		}
		free (shortcut);
		free (title);
		core->keep_asmqjmps = true;
	}
	graph_palette_end (&palette);

	if (emu) {
		core->anal->gp = saved_gp;
		if (saved_arena) {
			r_reg_arena_poke (core->anal->reg, saved_arena, saved_arena_size);
			R_FREE (saved_arena);
		}
	}
	core->anal->stackptr = saved_stackptr;
}

static void fold_asm_trace(RCore *core, RAGraph *g) {
	const RList *nodes = r_graph_get_nodes (g->graph);
	RGraphNode *gn;
	RListIter *it;
	RANode *n;

	RANode *curnode = get_anode (g->curnode);
	graph_foreach_anode (nodes, it, gn, n) {
		if (curnode == n) {
			n->is_mini = false;
			g->need_reload_nodes = true;
			continue;
		}
		ut64 addr = r_num_get (NULL, n->title);
		RDebugTracepointItem *tp = r_debug_trace_get (core->dbg, addr);
		n->is_mini = (tp == NULL);
	}
	g->need_update_dim = 1;
}

static void graph_node_vec_delete(RVecGraphNodePtr *vec, const RGraphNode *node) {
	size_t i;
	for (i = 0; i < RVecGraphNodePtr_length (vec); i++) {
		if (*RVecGraphNodePtr_at (vec, i) == node) {
			RVecGraphNodePtr_remove (vec, i);
			return;
		}
	}
}

static void delete_dup_edges(RAGraph *g) {
	RListIter *it;
	RGraphNode *n, *a, *b;
	r_list_foreach (g->graph->nodes, it, n) {
		size_t i, j;
		for (i = 0; i < RVecGraphNodePtr_length (&n->out_nodes); i++) {
			a = *RVecGraphNodePtr_at (&n->out_nodes, i);
			for (j = i + 1; j < RVecGraphNodePtr_length (&n->out_nodes); j++) {
				b = *RVecGraphNodePtr_at (&n->out_nodes, j);
				if (a->idx == b->idx) {
					RVecGraphNodePtr_remove (&n->out_nodes, j--);
					graph_node_vec_delete (&n->all_neighbours, b);
					graph_node_vec_delete (&b->in_nodes, n);
					graph_node_vec_delete (&b->all_neighbours, n);
					g->graph->n_edges--;
				}
			}
		}
	}
}

static inline bool cur_points_to_bb(RAnalBlock *curbb, RAnalBlock *bb) {
	if (bb->addr == curbb->addr || bb->addr == curbb->jump || bb->addr == curbb->fail) {
		return true;
	}
	if (curbb->switch_op) {
		RListIter *it;
		RAnalCaseOp *cop;
		r_list_foreach (curbb->switch_op->cases, it, cop) {
			if (cop->jump == bb->addr) {
				return true;
			}
		}
	}
	return false;
}

static bool isbbfew(RAnalBlock *curbb, RAnalBlock *bb) {
	return cur_points_to_bb (curbb, bb) || cur_points_to_bb (bb, curbb);
}

// TODO: this is very slow and this is because using sdb for the agraph. keys is totally not efficient for memory and abuses format strings in a totally unnecessary way, think hard to redesign this using optimal data structures like RVec.findSorted or SetU, etc. but agn/age/agk uses sdb to build custom graphs..
static void add_child(RCore *core, RAGraph *g, RANode *u, ut64 jump) {
	if (jump == UT64_MAX) {
		return;
	}
	char *title = get_title (jump);
	RANode *v = r_agraph_get_node (g, title);
	if (v) {
		ut64 a = r_num_get (NULL, u->title);
		char key[64];
		const char *fmt = "agraph.edge.0x%" PFMT64x "_0x%" PFMT64x ".highlight";
		snprintf (key, sizeof (key), fmt, a, jump);
		bool hl = sdb_exists (core->sdb, key);
		r_agraph_add_edge (g, u, v, hl);
	} else {
		R_LOG_WARN ("Failed to add child node 0x%" PFMT64x " to %s, child not found", jump, u? u->title: "?");
	}
	free (title);
}

/* build the RGraph inside the RAGraph g, starting from the Basic Blocks */
static int get_bbnodes(RAGraph *g, RCore *core, RAnalFunction *fcn) {
	if (!fcn) {
		return false;
	}
	RAnalBlock *bb;
	RListIter *iter;
	int shortcuts = 0;
	int ret = false;
	ut64 saved_gp = core->anal->gp;
	int saved_arena_size = 0;
	ut8 *saved_arena = NULL;
	core->keep_asmqjmps = false;
	const int saved_stackptr = core->anal->stackptr;
	const bool few = r_config_get_b (core->config, "graph.few");

	const bool emu = r_config_get_b (core->config, "asm.emu");
	if (emu) {
		saved_arena = r_reg_arena_peek (core->anal->reg, &saved_arena_size);
	}
	r_list_sort (fcn->bbs, (RListComparator)bbcmp);
	RAnalBlock *curbb = NULL;
	if (few) {
		r_list_foreach (fcn->bbs, iter, bb) {
			if (!curbb) {
				curbb = bb;
			}
			if (r_anal_block_contains (bb, core->addr)) {
				curbb = bb;
				break;
			}
		}
	}

	core->keep_asmqjmps = false;
	const bool breakable = r_list_length (fcn->bbs) > 1024;
	if (breakable) {
		r_cons_set_raw (core->cons, false);
		r_cons_break_push (core->cons, NULL, NULL);
	}
	RAGraphPalette palette;
	graph_palette_begin (core, g, &palette);
	r_list_foreach (fcn->bbs, iter, bb) {
		if (breakable && r_cons_is_breaked (core->cons)) {
			goto interrupted;
		}
		if (bb->addr == UT64_MAX) {
			continue;
		}
		if (few && !isbbfew (curbb, bb)) {
			continue;
		}
		char *body = get_bb_body (core, bb, mode2opts (g), fcn, emu, saved_gp, saved_arena, saved_arena_size);
		char *title = get_title (bb->addr);
		char *color = (bb->color.r || bb->color.g || bb->color.b)
			? r_cons_rgb_str (core->cons, NULL, -1, &bb->color)
			: NULL;

		RANode *node = agraph_add_colored_node (g, title, body, color, &palette);
		free (color);
		free (body);
		free (title);
		if (!node) {
			goto cleanup;
		}
		shortcuts = g->is_interactive? r_config_get_b (core->config, "graph.nodejmps"): false;
		if (shortcuts) {
			ranode_set_shortcut (node, r_core_add_asmqjmp (core, bb->addr));
		}
		core->keep_asmqjmps = true;
	}

	r_list_foreach (fcn->bbs, iter, bb) {
		if (bb->addr == UT64_MAX) {
			continue;
		}
		if (few && !isbbfew (curbb, bb)) {
			continue;
		}

		char *title = get_title (bb->addr);
		RANode *u = r_agraph_get_node (g, title);
		free (title);
		add_child (core, g, u, bb->jump);
		add_child (core, g, u, bb->fail);
		if (bb->switch_op) {
			RListIter *it;
			RAnalCaseOp *cop;
			r_list_foreach (bb->switch_op->cases, it, cop) {
				add_child (core, g, u, cop->jump);
			}
		}
	}
interrupted:
	if (breakable) {
		r_cons_break_end (core->cons);
		r_cons_set_raw (core->cons, true);
	}

	delete_dup_edges (g);
	ret = true;

cleanup:
	graph_palette_end (&palette);
	if (emu) {
		core->anal->gp = saved_gp;
		if (saved_arena) {
			r_reg_arena_poke (core->anal->reg, saved_arena, saved_arena_size);
			R_FREE (saved_arena);
		}
	}
	core->anal->stackptr = saved_stackptr;
	return ret;
}

/* build the RGraph inside the RAGraph g, starting from the Call Graph
 * information */
static bool get_cgnodes(RAGraph *g, RCore *core, RAnalFunction *fcn) {
	RAnalFunction *f = r_anal_get_fcn_in (core->anal, core->addr, 0);
	if (!f) {
		return false;
	}
	if (!fcn) {
		fcn = f;
	}

	r_core_seek (core, f->addr, true);

	char *title = get_title (fcn->addr);
	RANode *fcn_anode = r_agraph_add_node (g, title, "", NULL);

	free (title);
	if (!fcn_anode) {
		return false;
	}

	fcn_anode->x = 10;
	fcn_anode->y = 3;

	RVecAnalRef *refs = r_anal_function_get_refs (fcn);
	RAGraphPalette palette;
	graph_palette_begin (core, g, &palette);
	if (refs) {
		RAnalRef *ref;
		R_VEC_FOREACH (refs, ref) {
			title = get_title (ref->addr);
			if (r_agraph_get_node (g, title)) {
				continue;
			}
			free (title);

			int size = 0;
			RAnalBlock *bb = r_anal_bb_from_offset (core->anal, ref->addr);
			if (bb) {
				size = bb->size;
			}

			char *body = get_body (core, ref->addr, size, mode2opts (g));
			title = get_title (ref->addr);

			RANode *node = agraph_add_colored_node (g, title, body, NULL, &palette);
			free (title);
			free (body);
			if (!node) {
				graph_palette_end (&palette);
				RVecAnalRef_free (refs);
				return false;
			}

			node->x = 10;
			node->y = 10;

			r_agraph_add_edge (g, fcn_anode, node, false);
		}
	}

	graph_palette_end (&palette);
	RVecAnalRef_free (refs);
	return true;
}

static bool reload_nodes(RAGraph *g, RCore *core, RAnalFunction *fcn) {
	const bool is_c = g->is_callgraph;
	return is_c? get_cgnodes (g, core, fcn): get_bbnodes (g, core, fcn);
}

static void update_seek(RConsCanvas *can, RANode *n, int force) {
	if (!n) {
		return;
	}
	int x = n->x + can->sx;
	int y = n->y + can->sy;
	int w = can->w;
	int h = can->h;

	const bool doscroll = force || y < 0 || y + 5 > h || x + 5 > w || x + n->w + 5 < 0;
	if (doscroll) {
		if (n->w > w) { // too big for centering
			can->sx = -n->x;
		} else {
			can->sx = -n->x - n->w / 2 + w / 2;
		}
		if (n->h > h) { // too big for centering
			can->sy = -n->y;
		} else {
			can->sy = -n->y - n->h / 8 + h / 4;
		}
	}
}

static int is_near(const RANode *n, int x, int y, int is_next) {
	if (is_next) {
		return (n->y == y && n->x > x) || n->y > y;
	}
	return (n->y == y && n->x < x) || n->y < y;
}

/// XXX is wrong
static int is_near_h(const RANode *n, int x, int y, int is_next) {
	if (is_next) {
		return (n->x == x && n->y > y) || n->x > x;
	}
	return (n->x == x && n->y < y) || n->x < x;
}

static const RGraphNode *find_near_of(const RAGraph *g, const RGraphNode *cur, int is_next) {
	/* XXX: it's slow */
	const RList *nodes = r_graph_get_nodes (g->graph);
	const RListIter *it;
	const RGraphNode *gn, *resgn = NULL;
	const RANode *n, *acur = cur? get_anode (cur): NULL;
	const int default_v = is_next? INT_MIN: INT_MAX;
	const int start_x = acur? acur->x: default_v;
	const int start_y = acur? acur->y: default_v;

	graph_foreach_anode (nodes, it, gn, n) {
		// tab in horizontal layout is not correct, lets force vertical nextnode for now (g->layout == 0)
		bool isNear = true
			? is_near (n, start_x, start_y, is_next)
			: is_near_h (n, start_x, start_y, is_next);
		if (isNear) {
			const RANode *resn;

			if (!resgn) {
				resgn = gn;
				continue;
			}

			resn = get_anode (resgn);
			if ((is_next && resn->y > n->y) || (!is_next && resn->y < n->y)) {
				resgn = gn;
			} else if ((is_next && resn->y == n->y && resn->x > n->x) || (!is_next && resn->y == n->y && resn->x < n->x)) {
				resgn = gn;
			}
		}
	}
	if (!resgn && cur) {
		resgn = find_near_of (g, NULL, is_next);
	}
	return resgn;
}

static void update_graph_sizes(RAGraph *g) {
	RCons *cons = g->can->cons;
	RListIter *it;
	RGraphNode *gk;
	RANode *ak, *min_gn, *max_gn;
	int max_x, max_y;
	int delta_x, delta_y;
	AEdge *e;

	g->x = g->y = INT_MAX;
	max_x = max_y = INT_MIN;
	min_gn = max_gn = NULL;

	graph_foreach_anode (r_graph_get_nodes (g->graph), it, gk, ak) {
		const RVecGraphNodePtr *nd;
		int len;
		if (ak->x < g->x) {
			g->x = ak->x;
		}

		nd = r_graph_innodes (g->graph, gk);
		len = nd? RVecGraphNodePtr_length (nd) + 1: 0;
		if (ak->y - len < g->y) {
			g->y = ak->y - len;
			min_gn = ak;
		}

		if (ak->x + ak->w > max_x) {
			max_x = ak->x + ak->w;
		}

		nd = r_graph_get_neighbours (g->graph, gk);
		len = nd? RVecGraphNodePtr_length (nd) + 2: 0;
		if (ak->y + ak->h + len > max_y) {
			max_y = ak->y + ak->h + len;
			max_gn = ak;
		}
	}
	/* while calculating the graph size, take into account long edges */
	r_list_foreach (g->edges, it, e) {
		RListIter *kt;
		void *vv;
		int v;
		if (r_cons_is_breaked (cons)) {
			break;
		}
		r_list_foreach (e->x, kt, vv) {
			v = (int) (size_t)vv;
			if (v < g->x) {
				g->x = v;
			}
			if (v + 1 > max_x) {
				max_x = v + 1;
			}
		}
		r_list_foreach (e->y, kt, vv) {
			v = (int) (size_t)vv;
			if (v < g->y) {
				g->y = v;
			}
			if (v + 1 > max_y) {
				max_y = v + 1;
			}
		}
	}
	r_cons_break_pop (cons);

	if (min_gn) {
		const RVecGraphNodePtr *neigh = r_graph_innodes (g->graph, min_gn->gnode);
		if (neigh && RVecGraphNodePtr_length (neigh) > 0) {
			g->y--;
			max_y++;
		}
		if (max_gn) {
			const RVecGraphNodePtr *neigh = r_graph_get_neighbours (g->graph, min_gn->gnode);
			if (neigh && RVecGraphNodePtr_length (neigh) > 0) {
				max_y++;
			}
		}
	}

	if (g->x != INT_MAX && g->y != INT_MAX) {
		g->w = max_x - g->x;
		if (g->title) {
			size_t len = strlen (g->title);
			if (len > INT_MAX) {
				g->w = INT_MAX;
			}
			if ((int)len > g->w) {
				g->w = len;
			}
		}
		g->h = max_y - g->y;
	} else {
		g->x = g->y = 0;
		g->w = g->h = 0;
	}

	sdb_num_set (g->db, "agraph.w", g->w, 0);
	sdb_num_set (g->db, "agraph.h", g->h, 0);
	/* delta_x, delta_y are needed to make every other x,y coordinates
	 * unsigned, so that we can use sdb_num_ API */
	delta_x = g->x < 0? -g->x: 0;
	delta_y = g->y < 0? -g->y: 0;
	sdb_num_set (g->db, "agraph.delta_x", delta_x, 0);
	sdb_num_set (g->db, "agraph.delta_y", delta_y, 0);
}

R_API void r_agraph_set_curnode(RAGraph *g, RANode *a) {
	if (!a) {
		return;
	}
	g->curnode = a->gnode;
	if (a->title) {
		sdb_set (g->db, "agraph.curnode", a->title, 0);
		if (g->on_curnode_change) {
			g->on_curnode_change (a, g->on_curnode_change_data);
		}
	}
}

static ut64 rebase(RAGraph *g, int v) {
	return g->x < 0? -g->x + v: v;
}

static void agraph_set_layout(RAGraph *g) {
	RListIter *it;
	RGraphNode *n;
	RANode *a;

	set_layout (g);

	update_graph_sizes (g);
	graph_foreach_anode (r_graph_get_nodes (g->graph), it, n, a) {
		if (a->is_dummy) {
			continue;
		}
		sdb_num_setf (g->db, rebase (g, a->x), 0, "agraph.nodes.%s.x", a->title);
		sdb_num_setf (g->db, rebase (g, a->y), 0, "agraph.nodes.%s.y", a->title);
		sdb_num_setf (g->db, a->w, 0, "agraph.nodes.%s.w", a->title);
		sdb_num_setf (g->db, a->h, 0, "agraph.nodes.%s.h", a->title);
	}
}

/* set the willing to center the screen on a particular node */
static void agraph_update_seek(RAGraph *g, RANode *n, bool force) {
	g->update_seek_on = n;
	g->force_update_seek = force;
}

static void agraph_print_node(const RAGraph *g, RANode *n) {
	if (n->is_dummy) {
		return;
	}
	const int cur = g->curnode && get_anode (g->curnode) == n;
	const bool isMini = is_mini (g);
	if (g->is_tiny) {
		tiny_RANode_print (g, n, cur);
	} else if (isMini || n->is_mini) {
		mini_RANode_print (g, n, cur, isMini);
	} else {
		normal_RANode_print (g, n, cur);
	}
}

static void agraph_print_nodes(const RAGraph *g) {
	const RList *nodes = r_graph_get_nodes (g->graph);
	RGraphNode *gn;
	RListIter *it;
	RANode *n;

	graph_foreach_anode (nodes, it, gn, n) {
		if (gn != g->curnode) {
			agraph_print_node (g, n);
		}
	}

	/* draw current node now to make it appear on top */
	if (g->curnode) {
		agraph_print_node (g, get_anode (g->curnode));
	}
}

struct tmplayer {
	int layer;
	int edgectr;
	int revedgectr;
	int minx;
	int maxx;
};
struct tmpbackedgeinfo {
	int ax;
	int ay;
	int bx;
	int by;
	int edgectr;
	int fromlayer;
	int tolayer;
	int edge;
	RCanvasLineStyle style;
};

typedef enum {
	EDGE_DRAW_LINE,
	EDGE_DRAW_SQUARE,
	EDGE_DRAW_BACK
} EdgeDrawKind;

typedef struct {
	EdgeDrawKind kind;
	bool isvert;
	int edge;
	int x, y, x2, y2;
	int bend, xbend, ybend;
	int minx, miny, maxx, maxy;
	RCanvasLineStyle style;
} EdgeDraw;

typedef struct {
	ut64 from;
	ut64 to;
	bool titled;
	const char *ansicolor;
} EdgeHighlight;

R_VEC_TYPE (RVecEdgeDraw, EdgeDraw);
R_VEC_TYPE (RVecEdgeHighlight, EdgeHighlight);
R_VEC_TYPE (RVecEdgeRouteKey, ut64);

typedef struct r_agraph_edge_routes_t {
	RVecEdgeRouteKey key;
	RVecEdgeRouteKey scratch;
	RVecEdgeHighlight edges;
	RVecEdgeDraw draws;
	bool valid;
} EdgeRoutes;

static void edge_routes_free(EdgeRoutes *routes) {
	if (routes) {
		RVecEdgeRouteKey_fini (&routes->key);
		RVecEdgeRouteKey_fini (&routes->scratch);
		RVecEdgeHighlight_fini (&routes->edges);
		RVecEdgeDraw_fini (&routes->draws);
		free (routes);
	}
}

static inline bool edge_key_push(RVecEdgeRouteKey *key, ut64 value) {
	ut64 *entry = RVecEdgeRouteKey_emplace_back (key);
	if (!entry) {
		return false;
	}
	*entry = value;
	return true;
}

static inline ut64 edge_key_pair(int a, int b) {
	return ((ut64)(ut32)a << 32) | (ut32)b;
}

// every input of the edge routing, so routes are reused only while it is unchanged
static bool edge_routes_key(const RAGraph *g, RVecEdgeRouteKey *key) {
	const RList *nodes = r_graph_get_nodes (g->graph);
	RGraphNode *gn, **itn;
	RListIter *it;
	RANode *a;
	RVecEdgeRouteKey_clear (key);
	if (!edge_key_push (key, edge_key_pair (g->layout, g->zoom))
			|| !edge_key_push (key, edge_key_pair (g->is_callgraph, g->hints))
			|| !edge_key_push (key, edge_key_pair (g->edgemode, g->can->linemode))) {
		return false;
	}
	graph_foreach_anode (nodes, it, gn, a) {
		if (!edge_key_push (key, (ut64)(size_t)a)
				|| !edge_key_push (key, edge_key_pair (gn->idx, a->is_dummy))
				|| !edge_key_push (key, edge_key_pair (a->x, a->y))
				|| !edge_key_push (key, edge_key_pair (a->w, a->h))
				|| !edge_key_push (key, edge_key_pair (a->layer, a->layer_height))
				|| !edge_key_push (key, edge_key_pair (a->layer_width, RVecGraphNodePtr_length (&gn->out_nodes)))) {
			return false;
		}
		R_VEC_FOREACH (&gn->out_nodes, itn) {
			if (!edge_key_push (key, (*itn)->idx)) {
				return false;
			}
		}
		if (!edge_key_push (key, RVecGraphNodePtr_length (&gn->in_nodes))) {
			return false;
		}
		R_VEC_FOREACH (&gn->in_nodes, itn) {
			if (!edge_key_push (key, (*itn)->idx)) {
				return false;
			}
		}
	}
	return true;
}

static bool edge_routes_key_eq(const RVecEdgeRouteKey *a, const RVecEdgeRouteKey *b) {
	const size_t len = RVecEdgeRouteKey_length (a);
	return len == RVecEdgeRouteKey_length (b) && (!len || !memcmp (a->_start, b->_start, len * sizeof (ut64)));
}

static bool edge_routes_add(EdgeRoutes *routes, const RConsCanvas *c, EdgeDrawKind kind, int edge, int x, int y, int x2, int y2, RCanvasLineStyle *style, int bend, int xbend, int ybend, bool isvert) {
	EdgeDraw *d = RVecEdgeDraw_emplace_back (&routes->draws);
	if (!d) {
		return false;
	}
	// conservative bounds of every cell the canvas line functions can touch
	int xs[] = { x, x2, x, x, x2 };
	int ys[] = { y, y2, y, y, y2, y2 + 2 };
	// unused bends must not extend the bounds back to the origin
	if (c->linemode && kind != EDGE_DRAW_LINE) {
		if (isvert) {
			ys[3] = y + bend + 2;
			if (kind == EDGE_DRAW_BACK) {
				xs[2] = xbend;
				ys[4] = y2 - ybend - 1;
			}
		} else {
			xs[3] = x + bend + 3;
			if (kind == EDGE_DRAW_BACK) {
				ys[2] = xbend;
				xs[4] = x2 - ybend - 1;
			}
		}
	}
	int i;
	*d = (EdgeDraw){ kind, isvert, edge, x, y, x2, y2, bend, xbend, ybend, INT_MAX, INT_MAX, INT_MIN, INT_MIN, *style };
	for (i = 0; i < R_ARRAY_SIZE (xs); i++) {
		d->minx = R_MIN (d->minx, xs[i]);
		d->maxx = R_MAX (d->maxx, xs[i]);
	}
	for (i = 0; i < R_ARRAY_SIZE (ys); i++) {
		d->miny = R_MIN (d->miny, ys[i]);
		d->maxy = R_MAX (d->maxy, ys[i]);
	}
	d->minx -= 2;
	d->miny -= 2;
	d->maxx += 2;
	d->maxy += 2;
	// diagonal lines reset the shared style of the following segments
	if (!c->linemode && (x == x2 || y == y2)) {
		style->dot_style = DOT_STYLE_NORMAL;
	}
	return true;
}

static bool edge_draw_visible(const RConsCanvas *c, const EdgeDraw *d) {
	return d->maxx >= -c->sx && d->minx < c->w - c->sx
		&& d->maxy >= -c->sy && d->miny < c->h - c->sy;
}

static void edge_routes_draw(RAGraph *g, EdgeRoutes *routes) {
	RConsCanvas *c = g->can;
	RCons *cons = c->cons;
	const char *ansicolor = NULL;
	EdgeHighlight *eh;
	R_VEC_FOREACH (&routes->edges, eh) {
		if (eh->titled) {
			r_strf_var (k, 64, "agraph.edge.0x%" PFMT64x "_0x%" PFMT64x ".highlight", eh->from, eh->to);
			ansicolor = sdb_exists (g->db, k)? Color_BYELLOW: NULL; // it's CYAN for graphviz
		}
		eh->ansicolor = ansicolor;
	}
	const EdgeDraw *last = RVecEdgeDraw_last (&routes->draws);
	EdgeDraw *d;
	R_VEC_FOREACH (&routes->draws, d) {
		if (r_cons_is_breaked (cons)) {
			break;
		}
		// the last draw leaves its color in the canvas attribute
		if (d != last && !edge_draw_visible (c, d)) {
			continue;
		}
		RCanvasLineStyle style = d->style;
		style.ansicolor = RVecEdgeHighlight_at (&routes->edges, d->edge)->ansicolor;
		switch (d->kind) {
		case EDGE_DRAW_LINE:
			r_cons_canvas_line (c, d->x, d->y, d->x2, d->y2, &style);
			break;
		case EDGE_DRAW_SQUARE:
			r_cons_canvas_line_square_defined (c, d->x, d->y, d->x2, d->y2, &style, d->bend, d->isvert);
			break;
		case EDGE_DRAW_BACK:
			r_cons_canvas_line_back_edge (c, d->x, d->y, d->x2, d->y2, &style, d->bend, d->xbend, d->ybend, d->isvert);
			break;
		}
	}
}

static int tmplayercmp(const void *a, const void *b) {
	return ((struct tmplayer *)a)->layer > ((struct tmplayer *)b)->layer;
}

static void agraph_print_edges_simple(RAGraph *g) {
	RCanvasLineStyle style = { 0 };
	RANode *n, *n2;
	RGraphNode *gn, *gn2;
	RListIter *iter;
	RGraphNode **iter2;
	const RList *nodes = r_graph_get_nodes (g->graph);
	graph_foreach_anode (nodes, iter, gn, n) {
		graph_foreach_anode_vec (&n->gnode->out_nodes, iter2, gn2, n2) {
			int sx = n->w / 2;
			int sy = n->h;
			int sx2 = n2->w / 2;
			if (g->is_tiny) {
				sx = 0;
				sy = 0;
				sx2 = 0;
			}
			// TODO: better alignments here
			r_cons_canvas_line (g->can,
				n->x + sx, n->y + sy,
				n2->x + sx2, n2->y, &style);

			if (n2->is_dummy) {
				r_cons_canvas_line (g->can,
					n2->x + sx2, n2->y - 1,
					n2->x + sx2, n2->y + n2->h, &style);
			}
		}
	}
}

static int first_x_cmp(RGraphNode *const *ga, RGraphNode *const *gb) {
	RANode *a = (RANode *)(*ga)->data;
	RANode *b = (RANode *)(*gb)->data;
	if (b->y < a->y) {
		return -1;
	}
	if (b->y > a->y) {
		return 1;
	}
	if (a->x < b->x) {
		return 1;
	}
	if (a->x > b->x) {
		return -1;
	}
	return 0;
}

static bool edge_routes_build(RAGraph *g, EdgeRoutes *routes) {
	RCons *cons = g->can->cons;
	bool complete = false;
	int out_nth, in_nth, bendpoint;
	RGraphNode **itn;
	RListIter *itm, *ito;
	RCanvasLineStyle style = { 0 };
	const RList *nodes = r_graph_get_nodes (g->graph);
	RGraphNode *ga;
	RANode *a;

	RList *lyr = r_list_new ();
	RList *bckedges = r_list_new ();
	HtUU *out_edge_numbers = ht_uu_new0 ();
	HtUU *in_edge_numbers = ht_uu_new0 ();
	struct tmplayer *tl, *tm;

	graph_foreach_anode (nodes, itm, ga, a) {
		const RGraphNode *gb;
		RANode *b;
		RVecGraphNodePtr *neighbours = &ga->out_nodes;
		int ax, ay, bx, by, a_x_inc, b_x_inc;
		tl = tm = NULL;
		if (r_cons_is_breaked (cons)) {
			break;
		}

		r_list_foreach (lyr, ito, tl) {
			if (tl->layer == a->layer) {
				tm = tl;
				if (g->layout == 0) { // vertical layout
					if (tm->minx > a->x) {
						tm->minx = a->x;
					}
					if (tm->maxx < a->x + a->w) {
						tm->maxx = a->x + a->w;
					}
				} else {
					if (tm->minx > a->y) {
						tm->minx = a->y;
					}
					if (tm->maxx < a->y + a->h) {
						tm->maxx = a->y + a->h;
					}
				}
				break;
			}
		}

		if (!tm) {
			tm = R_NEW0 (struct tmplayer);
			tm->layer = a->layer;
			tm->edgectr = 0;
			tm->revedgectr = 0;
			if (g->layout == 0) { // vertical layout
				tm->minx = a->x;
				tm->maxx = a->x + a->w;
			} else {
				tm->minx = a->y;
				tm->maxx = a->y + a->h;
			}
			r_list_add_sorted (lyr, tm, tmplayercmp);
		}

		bool many = RVecGraphNodePtr_length (neighbours) > 2;
		int bendpoint_base = tm->edgectr;

		if (many && !g->is_callgraph) {
			RVecGraphNodePtr_sort (neighbours, first_x_cmp);
		}

		graph_foreach_anode_vec (neighbours, itn, gb, b) {
			const int edge = RVecEdgeHighlight_length (&routes->edges);
			EdgeHighlight *eh = RVecEdgeHighlight_emplace_back (&routes->edges);
			if (!eh) {
				goto cleanup;
			}
			*eh = (EdgeHighlight){ 0 };
			if (!R_STR_ISEMPTY (a->title) && !R_STR_ISEMPTY (b->title)) {
				eh->titled = true;
				eh->from = r_num_get (NULL, a->title);
				eh->to = r_num_get (NULL, b->title);
			}
			out_nth = get_edge_number_cached (g, a, b, true, out_edge_numbers);
			in_nth = get_edge_number_cached (g, a, b, false, in_edge_numbers);

			bool parent_many = false;
			if (a->is_dummy) {
				RGraphNode **first = RVecGraphNodePtr_at (&ga->in_nodes, 0);
				RANode *in = first? get_anode (*first): NULL;
				while (in && in->is_dummy) {
					first = RVecGraphNodePtr_at (&in->gnode->in_nodes, 0);
					in = first? get_anode (*first): NULL;
				}
				if (in && in->gnode) {
					parent_many = RVecGraphNodePtr_length (&in->gnode->out_nodes) > 2;
				} else {
					parent_many = false;
				}
			}

			style.dot_style = DOT_STYLE_NORMAL;
			if (many || parent_many) {
				style.color = LINE_UNCJMP;
			} else {
				switch (out_nth) {
				case 0:
					style.color = LINE_TRUE;
					style.dot_style = DOT_STYLE_CONDITIONAL;
					break;
				case 1:
					style.color = LINE_FALSE;
					style.dot_style = DOT_STYLE_CONDITIONAL;
					break;
				case -1:
					style.color = LINE_UNCJMP;
					break;
				default:
					style.color = LINE_NONE;
					break;
				}
			}
			switch (g->layout) {
			case 0:
			default:
				style.symbol = (!g->hints || a->is_dummy)? LINE_NOSYM_VERT: style.color;
				if (a->y + a->h > b->y) {
					style.dot_style = DOT_STYLE_BACKEDGE;
				}

				a_x_inc = R_EDGES_X_INC + 2 *(out_nth + 1);
				b_x_inc = R_EDGES_X_INC + 2 *(in_nth + 1);

				bx = b->is_dummy? b->x: (b->x + b_x_inc);
				ay = a->y + a->h;
				by = b->y - 1;

				if (many && !g->is_callgraph) {
					int t = R_EDGES_X_INC + 2 *(RVecGraphNodePtr_length (neighbours) + 1);
					ax = a->is_dummy? a->x: (a->x + a->w / 2 + (t / 2 - a_x_inc));
					bendpoint = bendpoint_base + (bx < ax? RVecGraphNodePtr_length (neighbours) - out_nth: out_nth);
				} else {
					ax = a->is_dummy? a->x: (a->x + a_x_inc);
					bendpoint = tm->edgectr;
				}

				if (!a->is_dummy && out_nth == 0 && bx > ax) {
					ax += (many && !g->is_callgraph)? 0: 4;
				}
				if (a->h < a->layer_height) {
					if (!edge_routes_add (routes, g->can, EDGE_DRAW_LINE, edge, ax, ay, ax, ay + a->layer_height - a->h, &style, 0, 0, 0, false)) {
						goto cleanup;
					}
					ay = a->y + a->layer_height;
					style.symbol = LINE_NOSYM_VERT;
				}
				if (by >= ay) {
					if (!edge_routes_add (routes, g->can, EDGE_DRAW_SQUARE, edge, ax, ay, bx, by, &style, bendpoint, 0, 0, true)) {
						goto cleanup;
					}
				} else {
					struct tmpbackedgeinfo *tmp = calloc (1, sizeof (struct tmpbackedgeinfo));
					tmp->ax = ax;
					tmp->bx = bx;
					tmp->ay = ay;
					tmp->by = by;
					tmp->edgectr = bendpoint;
					tmp->fromlayer = a->layer;
					tmp->tolayer = b->layer;
					tmp->edge = edge;
					tmp->style = style;
					r_list_append (bckedges, tmp);
				}
				if (b->is_dummy) {
					style.symbol = LINE_NOSYM_VERT;
					if (!edge_routes_add (routes, g->can, EDGE_DRAW_LINE, edge, bx, by, bx, b->y + b->h, &style, 0, 0, 0, false)) {
						goto cleanup;
					}
				}
				if (ax != bx || b->layer <= a->layer || (!a->is_dummy && b->is_dummy) || (a->is_dummy && !b->is_dummy)) {
					if (tm) {
						tm->edgectr++;
					}
				}
				break;
			case 1:
				style.symbol = (!g->hints || a->is_dummy)? LINE_NOSYM_HORIZ: style.color;
				if (a->x + a->w > b->x) {
					style.dot_style = DOT_STYLE_BACKEDGE;
				}

				ax = a->x;
				if (g->zoom > 0) {
					ax += a->w;
				} else {
					ax++;
				}
				ay = a->y;
				if (!a->is_dummy && g->zoom > 0) {
					ay += R_EDGES_X_INC + out_nth;
				}
				bx = b->x - 1;
				by = b->y;
				if (!b->is_dummy && g->zoom > 0) {
					by += R_EDGES_X_INC + in_nth;
				}

				if (a->w < a->layer_width) {
					if (!edge_routes_add (routes, g->can, EDGE_DRAW_SQUARE, edge, ax, ay, a->x + a->layer_width, ay, &style, 0, 0, 0, false)) {
						goto cleanup;
					}
					ax = a->x;
					if (g->zoom > 1) {
						ax += a->layer_width;
					} else {
						ax += 1;
					}
					style.symbol = LINE_NOSYM_HORIZ;
				}
				if (bx >= ax) {
					if (!edge_routes_add (routes, g->can, EDGE_DRAW_SQUARE, edge, ax, ay, bx, by, &style, tm->edgectr, 0, 0, false)) {
						goto cleanup;
					}
				} else {
					struct tmpbackedgeinfo *tmp = calloc (1, sizeof (struct tmpbackedgeinfo));
					if (tmp) {
						tmp->ax = ax;
						tmp->bx = bx;
						tmp->ay = ay;
						tmp->by = by;
						tmp->edgectr = tm->edgectr;
						tmp->fromlayer = a->layer;
						tmp->tolayer = b->layer;
						tmp->edge = edge;
						tmp->style = style;
						r_list_append (bckedges, tmp);
					}
				}
				if (b->is_dummy) {
					style.symbol = LINE_NOSYM_HORIZ;
					if (!edge_routes_add (routes, g->can, EDGE_DRAW_SQUARE, edge, bx, by, bx + b->layer_width, by, &style, 0, 0, 0, false)) {
						goto cleanup;
					}
				}
				if (ay != by || b->layer <= a->layer || (!a->is_dummy && b->is_dummy) || (a->is_dummy && !b->is_dummy)) {
					tm->edgectr += 1;
				}
				break;
			}
		}
		if (!g->layout && many && !g->is_callgraph) {
			// Reserve the whole fanout band before routing another node in this layer.
			tm->edgectr = bendpoint_base + RVecGraphNodePtr_length (neighbours) + 1;
		}
	}

	struct tmpbackedgeinfo *temp;
	r_list_foreach (bckedges, itm, temp) {
		int leftlen, rightlen;
		int minx = 0, maxx = 0;
		struct tmplayer *tt = NULL;
		tl = r_list_get_n (lyr, temp->fromlayer);
		if (r_cons_is_breaked (cons)) {
			break;
		}

		r_list_foreach (lyr, ito, tl) {
			if (tl->layer <= temp->tolayer) {
				tt = tl;
				minx = tl->minx;
				maxx = tl->maxx;
				continue;
			}
			minx = minx < tl->minx? minx: tl->minx;
			maxx = maxx > tl->maxx? maxx: tl->maxx;
			if (tl->layer >= temp->fromlayer) {
				break;
			}
		}

		if (tt) {
			tt->revedgectr += 1;
		}
		if (g->layout == 0) {
			leftlen = (temp->ax - minx) + (temp->bx - minx);
			rightlen = (maxx - temp->ax) + (maxx - temp->bx);
		} else {
			leftlen = (temp->ay - minx) + (temp->by - minx);
			rightlen = (maxx - temp->ay) + (maxx - temp->by);
		}

		if (tt) {
			int arg = (rightlen < leftlen)? maxx + 1: minx - 1;
			if (!edge_routes_add (routes, g->can, EDGE_DRAW_BACK, temp->edge, temp->ax, temp->ay, temp->bx, temp->by,
					&temp->style, temp->edgectr, arg, tt->revedgectr, !g->layout)) {
				goto cleanup;
			}
		}

		r_list_foreach (lyr, ito, tl) {
			if (tl->layer < temp->tolayer) {
				continue;
			}
			if (rightlen < leftlen) {
				tl->maxx = maxx + 1;
			} else {
				tl->minx = minx - 1;
			}
			if (tl->layer >= temp->fromlayer) {
				break;
			}
		}
	}

	complete = true;
cleanup:
	r_list_foreach (lyr, ito, tl) {
		free (tl);
	}

	r_list_foreach (bckedges, ito, tl) {
		free (tl);
	}

	r_list_free (lyr);
	r_list_free (bckedges);
	ht_uu_free (out_edge_numbers);
	ht_uu_free (in_edge_numbers);
	return complete && !r_cons_is_breaked (cons);
}

static void agraph_print_edges(RAGraph *g) {
	if (!g->edgemode) {
		return;
	}
	if (g->edgemode == 1) {
		agraph_print_edges_simple (g);
		return;
	}
	if (!g->edge_routes) {
		g->edge_routes = R_NEW0 (EdgeRoutes);
	}
	EdgeRoutes *routes = g->edge_routes;
	if (!edge_routes_key (g, &routes->scratch)) {
		routes->valid = false;
		goto beach;
	}
	if (!routes->valid || !edge_routes_key_eq (&routes->scratch, &routes->key)) {
		RVecEdgeDraw_clear (&routes->draws);
		RVecEdgeHighlight_clear (&routes->edges);
		// building sorts the neighbours, so take the key afterwards
		routes->valid = edge_routes_build (g, routes) && edge_routes_key (g, &routes->key);
	}
	edge_routes_draw (g, routes);
beach:
	r_cons_break_pop (g->can->cons);
}

static void agraph_toggle_callgraph(RAGraph *g) {
	g->is_callgraph = !g->is_callgraph;
	g->need_reload_nodes = true;
	g->force_update_seek = true;
}

static void agraph_set_zoom(RAGraph *g, int v) {
	if (v >= -10) {
		g->is_tiny = false;
		if (v == 0) {
			g->mode = R_AGRAPH_MODE_MINI;
		} else if (v < 0) {
			g->mode = R_AGRAPH_MODE_TINY;
			g->is_tiny = true;
		} else {
			g->mode = R_AGRAPH_MODE_NORMAL;
		}
		const int K = 920;
		if (g->zoom < v) {
			g->can->sy = (g->can->sy * K) / 1000;
		} else {
			g->can->sy = (g->can->sy * 1000) / K;
		}
		g->zoom = v;
		g->need_update_dim = true;
		g->need_set_layout = true;
	}
}

/* reload all the info in the nodes, depending on the type of the graph
 *(callgraph, CFG, etc.), set the default layout for these nodes and center
 * the screen on the selected one */
static bool agraph_reload_nodes(RAGraph *g, RCore *core, RAnalFunction *fcn) {
	if (g->is_handmade) {
		return true;
	}
	r_agraph_reset (g);
	return reload_nodes (g, core, fcn);
}

static void follow_nth(RAGraph *g, int nth) {
	const RGraphNode *cn = r_graph_nth_neighbour (g->graph, g->curnode, nth);
	RANode *a = get_anode (cn);

	while (a && a->is_dummy) {
		cn = r_graph_nth_neighbour (g->graph, a->gnode, 0);
		a = get_anode (cn);
	}
	if (a) {
		r_agraph_set_curnode (g, a);
	}
}

static void move_current_node(RAGraph *g, int xdiff, int ydiff) {
	RANode *n = get_anode (g->curnode);
	if (n) {
		if (is_tiny (g)) {
			xdiff = NORMALIZE_MOV (xdiff);
			ydiff = NORMALIZE_MOV (ydiff);
		}
		n->x += xdiff;
		n->y += ydiff;
	}
}

static void agraph_toggle_tiny(RAGraph *g) {
	g->is_tiny = !g->is_tiny;
	g->need_update_dim = 1;
	RCons *cons = g->can->cons;
	agraph_refresh (cons->event_data);
	agraph_set_layout ((RAGraph *)g);
}

static void agraph_toggle_mini(RAGraph *g) {
	RANode *n = get_anode (g->curnode);
	if (n) {
		n->is_mini = !n->is_mini;
	}
	g->need_update_dim = 1;
	RCons *cons = g->can->cons;
	agraph_refresh (cons->event_data);
	agraph_set_layout ((RAGraph *)g);
}

static void agraph_follow_innodes(RCore *core, RAGraph *g, bool in) {
	int count = 0;
	RGraphNode **iter;
	RANode *an = get_anode (g->curnode);
	if (!an) {
		return;
	}
	const RVecGraphNodePtr *list = in? &an->gnode->in_nodes: &an->gnode->out_nodes;
	int nth = -1;
	if (RVecGraphNodePtr_length (list) == 0) {
		return;
	}
	RCons *cons = core->cons;
	r_cons_gotoxy (cons, 0, 2);
	r_cons_printf (cons, in? "Input nodes:\n": "Output nodes:\n");
	RList *options = r_list_newf (NULL);
	RGraphNode *gn;
	R_VEC_FOREACH (list, iter) {
		gn = *iter;
		RANode *an = get_anode (gn);
		RGraphNode *gnn = agraph_get_title (g, an, in);
		if (gnn) {
			RANode *nnn = gnn->data;
			RANode *o;
			RListIter *iter2;
			// avoid dupes
			r_list_foreach (options, iter2, o) {
				if (!strcmp (o->title, nnn->title)) {
					continue;
				}
			}
			r_cons_printf (cons, "%d %s\n", count, nnn->title);
			r_list_append (options, nnn);
			count++;
		}
	}
	r_cons_flush (core->cons);
	if (RVecGraphNodePtr_length (list) == 1) {
		nth = 0;
	} else if (RVecGraphNodePtr_length (list) < 10) {
		// just 1 key
		r_cons_set_raw (cons, true);
		char ch = r_cons_readchar (cons);
		if (ch >= '0' && ch <= '9') {
			nth = ch - '0';
		}
	} else {
		r_cons_show_cursor (core->cons, true);
		r_cons_enable_mouse (core->cons, false);
		char *nth_string = r_cons_input (core->cons, "index> ");
		nth = atoi (nth_string);
		if (nth == 0 && *nth_string != '0') {
			nth = -1;
		}
		free (nth_string);
	}
	if (nth != -1) {
		RANode *selected_node = r_list_get_n (options, nth);
		r_agraph_set_curnode (g, selected_node);
	}
	r_list_free (options);
	agraph_update_seek (g, get_anode (g->curnode), false);
}

static void agraph_follow_true(RAGraph *g) {
	follow_nth (g, 0);
	agraph_update_seek (g, get_anode (g->curnode), false);
}

static void agraph_follow_false(RAGraph *g) {
	follow_nth (g, 1);
	agraph_update_seek (g, get_anode (g->curnode), false);
}

/* seek the next node in visual order */
static void agraph_next_node(RAGraph *g) {
	RANode *a = get_anode (find_near_of (g, g->curnode, true));
	while (a && a->is_dummy) {
		a = get_anode (find_near_of (g, a->gnode, true));
	}
	r_agraph_set_curnode (g, a);
	agraph_update_seek (g, get_anode (g->curnode), false);
}

/* seek the previous node in visual order */
static void agraph_prev_node(RAGraph *g) {
	RANode *a = get_anode (find_near_of (g, g->curnode, false));
	while (a && a->is_dummy) {
		a = get_anode (find_near_of (g, a->gnode, false));
	}
	r_agraph_set_curnode (g, a);
	agraph_update_seek (g, get_anode (g->curnode), false);
}

static void agraph_update_title(RCore *core, RAGraph *g, RAnalFunction *fcn) {
	RANode *a = get_anode (g->curnode);
	if (!a) {
		return;
	}
	char *sig = r_core_cmd_str (core, "afcf");
	char *new_title = r_str_newf (
		"%s[0x%08" PFMT64x "]> %s # %s ",
		core->visual.graphCursor? "(cursor)": "",
		fcn->addr, a->title, sig);
	r_agraph_set_title (g, new_title);
	free (new_title);
	free (sig);
}

/* look for any change in the state of the graph and update what's necessary */
static bool check_changes(RAGraph *g, bool is_interactive, RCore *core, RAnalFunction *fcn) {
	int oldpos[2] = {
		0, 0
	};
	if (g->update_seek_on && core && r_config_get_b (core->config, "graph.few")) {
		g->need_reload_nodes = true;
	}
	if (g->need_reload_nodes && core) {
		if (!g->update_seek_on && !g->force_update_seek) {
			// save scroll here
			oldpos[0] = g->can->sx;
			oldpos[1] = g->can->sy;
		}
		if (!agraph_reload_nodes (g, core, fcn)) {
			return false;
		}
	}
	if (core && core->config) {
		if (r_config_get_b (core->config, "graph.trace")) {
			// fold all bbs not traced
			fold_asm_trace (core, g);
		}
	}
	g->bb_maxwidth = core? r_config_get_i (core->config, "graph.bb.maxwidth"): 0;
	if (g->need_update_dim || g->need_reload_nodes || !is_interactive) {
		update_node_dimension (g->graph, is_mini (g), g->zoom, g->edgemode, g->is_callgraph, g->layout);
	}
	if (g->need_set_layout || g->need_reload_nodes || !is_interactive) {
		agraph_set_layout (g);
	}
	if (core) {
		ut64 off = r_anal_get_bbaddr (core->anal, core->addr);
		if (off != UT64_MAX) {
			char *title = get_title (off);
			RANode *cur_anode = get_anode (g->curnode);
			if (fcn && ((is_interactive && !cur_anode) || (cur_anode && strcmp (cur_anode->title, title)))) {
				g->update_seek_on = r_agraph_get_node (g, title);
				if (g->update_seek_on) {
					r_agraph_set_curnode (g, g->update_seek_on);
					g->force_update_seek = true;
				}
			}
			free (title);
		}
		g->can->color = r_config_get_i (core->config, "scr.color");
		g->hints = r_config_get_b (core->config, "graph.hints");
	}
	if (g->update_seek_on || g->force_update_seek) {
		RANode *n = g->update_seek_on;
		if (!n && g->curnode) {
			n = get_anode (g->curnode);
		}
		if (n) {
			update_seek (g->can, n, g->force_update_seek);
		}
	}
	if (fcn) {
		agraph_update_title (core, g, fcn);
	}
	if (oldpos[0] || oldpos[1]) {
		g->can->sx = oldpos[0];
		g->can->sy = oldpos[1];
	}
	g->need_reload_nodes = false;
	g->need_update_dim = false;
	g->need_set_layout = false;
	g->update_seek_on = NULL;
	g->force_update_seek = false;
	return true;
}

static int agraph_print(RCore *core, RAGraph *g, bool is_interactive, bool do_print, RAnalFunction *fcn) {
	RCons *cons = g->can->cons;
	int h = 24;
	int w = do_print? r_cons_get_size (cons, &h): 80;
	if (w < 1 || h < 1) {
		// we cannot determine terminal size, lets use some default values
		w = 80;
		h = 24;
	}
	bool ret = check_changes (g, is_interactive, core, fcn);
	if (!ret) {
		return false;
	}

	if (is_interactive) {
		r_cons_clear00 (cons);
	} else {
		/* TODO: limit to screen size when the output is not redirected to file */
		update_graph_sizes (g);
	}

	h = is_interactive? h: g->h + 1;
	w = is_interactive? w: g->w + 2;
	if (!r_cons_canvas_resize (g->can, w, h)) {
		return false;
	}
	if (!is_interactive) {
		g->can->sx = -g->x;
		g->can->sy = -g->y - 1;
	}
	if (g->is_dis) {
		(void)G (-g->can->sx + 1, -g->can->sy + 2);
		bool scr_utf8 = r_config_get_b (core->config, "scr.utf8");
		bool asm_bytes = r_config_get_b (core->config, "asm.bytes");
		bool asm_cmt_right = r_config_get_b (core->config, "asm.cmt.right");
		r_config_set_b (core->config, "scr.utf8", false);
		r_config_set_b (core->config, "asm.bytes", false);
		r_config_set_b (core->config, "asm.cmt.right", false);
		char *str = r_core_cmd_str (core, "pd $r");
		if (str) {
			W (str);
			free (str);
		}
		r_config_set_b (core->config, "scr.utf8", scr_utf8);
		r_config_set_b (core->config, "asm.bytes", asm_bytes);
		r_config_set_b (core->config, "asm.cmt.right", asm_cmt_right);
	}
	if (R_STR_ISNOTEMPTY (g->title)) {
		g->can->sy++;
	}
	agraph_print_edges (g);
	agraph_print_nodes (g);
	if (R_STR_ISNOTEMPTY (g->title)) {
		g->can->sy--;
	}
	/* print the graph title */
	(void)G (-g->can->sx, -g->can->sy);
	if (g->title) {
		char *rendered_title = NULL;
		const char *title = g->title;
		if (!g->is_tiny) {
			rendered_title = agraph_font_render_cfg (core, "scr.font.prompt", g->title);
			title = rendered_title? rendered_title: g->title;
			int color = core? r_config_get_i (core->config, "scr.color"): 0;
			if (do_print && color > 0) {
				const char *kolor = cons->context->pal.prompt;
				r_cons_gotoxy (cons, 0, 0);
				r_cons_print (cons, kolor? kolor: Color_WHITE);
				r_cons_print (cons, title);
				r_cons_print (cons, Color_RESET "\r");
			} else {
				W (title); // canvas write is always black/white
			}
		}
		if (is_interactive) {
			const int title_len = r_str_len_utf8_ansi (title);
			r_cons_canvas_fill (g->can, -g->can->sx + title_len,
				-g->can->sy, w - title_len, 1, ' ');
		}
		free (rendered_title);
	}

	g->can->flags = r_cons_canvas_flags (cons);
	if (do_print) {
		r_cons_canvas_print_region (g->can);
	}

	if (is_interactive) {
		r_cons_newline (cons);
		const char *cmdv = r_config_get (core->config, "cmd.gprompt");
		bool mustFlush = false;
		r_cons_visual_flush (cons);
		if (!strcmp (cmdv, ".dr*")) {
			cmdv = ".dr*;dr=@e:hex.cols=`?v $w*3`";
		}
		if (R_STR_ISNOTEMPTY (cmdv)) {
			r_cons_gotoxy (cons, 0, 2);
			r_cons_print (cons, Color_RESET);
			r_core_cmd0 (core, cmdv);
			mustFlush = true;
		}
		if (core && core->scr_gadgets) {
			r_core_call (core, "pg");
		}
		if (mustFlush) {
			r_cons_flush (cons);
		}
		if (r_config_get_b (core->config, "graph.mini")) { // minigraph
			int h, w = r_cons_get_size (cons, &h);
			r_cons_push (cons);
			g->can->h *= 4;
			RConsCanvas *ocan = g->can;
			g->can = r_cons_canvas_new (cons, w * 2, h * 4, -2);
			g->can->sx = ocan->sx;
			g->can->sy = ocan->sy;
			g->can->color = 0;
			g->can->linemode = ocan->linemode;
			agraph_print_edges (g);
			agraph_print_nodes (g);
			r_cons_canvas_print_region (g->can);
			g->can = ocan;
			char *s = r_cons_drain (cons, NULL);
			r_cons_pop (cons);
			if (s) {
				cmd_agfb3 (core, s, w - 40, 2);
				free (s);
			}
			g->can->h /= 4;
			r_cons_flush (cons);
		}
	}

	return true;
}

static void check_function_modified(RCore *core, RAnalFunction *fcn) {
	if (r_anal_function_was_modified (fcn)) {
		if (r_config_get_b (core->config, "anal.onchange") || r_cons_yesno (core->cons, 'y', "Function was modified. Reanalyze? (Y/n)")) {
			r_anal_function_update_analysis (fcn);
		}
	}
}

static int agraph_refresh(struct agraph_refresh_data *grd) {
	if (!grd) {
		return 0;
	}
	RCore *core = grd->core;
	RCons *cons = core->cons;
	cons->event_data = grd;
	RAGraph *g = grd->g;
	RAnalFunction *f = NULL;
	RAnalFunction **fcn = grd->fcn;
	if (!fcn) {
		return agraph_print (core, g, grd->fs, true, NULL);
	}
	// allow to change the current function during debugging
	if (g->is_instep && r_config_get_b (core->config, "cfg.debug")) {
		// seek only when the graph node changes
		const char *pc = r_reg_alias_getname (core->dbg->reg, R_REG_ALIAS_PC);
		ut64 addr = r_reg_getv (core->dbg->reg, pc);
		RANode *acur = get_anode (g->curnode);

		addr = r_anal_get_bbaddr (core->anal, addr);
		char *title = get_title (addr);
		if (!acur || strcmp (acur->title, title)) {
			r_core_call (core, "sr PC");
		}
		free (title);
		g->is_instep = false;
	}

	if (grd->follow_offset) {
		if (r_io_is_valid_offset (core->io, core->addr, 0)) {
			f = r_anal_get_fcn_in (core->anal, core->addr, 0);
			if (!f) {
				if (!g->is_dis) {
					if (!r_cons_yesno (cons, 'y', "\rNo function at 0x%08" PFMT64x ". Define it here (Y/n)? ", core->addr)) {
						return 0;
					}
					r_core_call (core, "af");
				}
				f = r_anal_get_fcn_in (core->anal, core->addr, 0);
				g->need_reload_nodes = true;
			}
			if (f && fcn && f != *fcn) {
				*fcn = f;
				check_function_modified (core, *fcn);
				g->need_reload_nodes = true;
				if (!core->visual.coming_from_vmark) {
					g->force_update_seek = true;
					core->visual.coming_from_vmark = false;
				}
			}
		} else {
			// TODO: maybe go back to avoid seeking from graph view to an scary place?
			r_cons_message (core->cons, "This is not a valid offset\n");
			r_cons_flush (core->cons);
		}
	}

	int res = agraph_print (core, g, grd->fs, true, *fcn);

	if (r_config_get_b (core->config, "scr.scrollbar")) {
		r_core_print_scrollbar (core);
	}

	return res;
}

static void agraph_refresh_queued(struct agraph_refresh_data *grd) {
	agraph_refresh (grd);
}

static void agraph_set_need_reload_nodes(struct agraph_refresh_data *grd) {
	grd->g->need_reload_nodes = true;
}

static void agraph_toggle_speed(RAGraph *g, RCore *core) {
	const int alt = r_config_get_b (core->config, "graph.scroll");
	g->movspeed = g->movspeed == DEFAULT_SPEED? alt: DEFAULT_SPEED;
}

static int agraph_scroll_step(int size, int speed) {
	double scale = 2.0 + log2 (R_MAX (size, 20) / 20.0);
	if (size > 80) {
		scale += (size - 80) / 40.0;
	}
	return speed * scale;
}

static void agraph_init(RAGraph *g) {
	g->is_callgraph = false;
	g->is_instep = false;
	g->need_reload_nodes = true;
	g->show_node_titles = true;
	g->show_node_body = true;
	g->force_update_seek = true;
	g->graph = r_graph_new ();
	g->nodes = ht_pp_new0 ();
	g->edgemode = 2;
	g->zoom = ZOOM_DEFAULT;
	g->hints = 1;
	g->movspeed = DEFAULT_SPEED;
	g->db = sdb_new0 ();
	RVecAGraphLocation_init (&g->ghits.word_list);
}

static void graphNodeMove(RAGraph *g, int dir, int speed) {
	int delta = (dir == 'k')? -1: 1;
	if (dir == 'H') {
		return;
	}
	if (dir == 'h' || dir == 'l') {
		// horizontal scroll
		if (is_mini (g)) {
			g->discroll = 0;
		} else {
			int delta = (dir == 'l')? 1: -1;
			move_current_node (g, speed * delta, 0);
		}
	} else {
		// vertical scroll
		if (is_mini (g)) {
			g->discroll += (delta * speed);
		} else if (g->is_dis) {
			// XXX core is null here cant work
			// RCore *core = NULL;
			// r_core_cmdf (core, "so %d", (delta * 4) * speed);
		} else {
			move_current_node (g, 0, delta * speed);
		}
	}
}

static void agraph_free_nodes(RAGraph *g) {
	if (!g) {
		return;
	}
	RListIter *it;
	RGraphNode *n;
	RANode *a;

	graph_foreach_anode (r_graph_get_nodes (g->graph), it, n, a) {
		ranode_free (a);
	}
	if (g->edge_routes) {
		g->edge_routes->valid = false;
	}

	ht_pp_free (g->nodes);
}

static void sdb_set_enc(Sdb *db, const char *key, const char *v, ut32 cas) {
	char *estr = sdb_encode ((const void *)v, -1);
	sdb_set (db, key, estr, cas);
	free (estr);
}

static void agraph_sdb_init(const RAGraph *g) {
	RConsCanvas *canvas = g->can;
	RConsContext *ctx = canvas->cons->context;
	sdb_bool_set (g->db, "agraph.is_callgraph", g->is_callgraph, 0);
	sdb_set_enc (g->db, "agraph.color_box", ctx->pal.graph_box, 0);
	sdb_set_enc (g->db, "agraph.color_box2", ctx->pal.graph_box2, 0);
	sdb_set_enc (g->db, "agraph.color_box3", ctx->pal.graph_box3, 0);
	sdb_set_enc (g->db, "agraph.color_true", ctx->pal.graph_true, 0);
	sdb_set_enc (g->db, "agraph.color_false", ctx->pal.graph_false, 0);
}

R_API Sdb *r_agraph_get_sdb(RAGraph *g) {
	g->need_update_dim = true;
	g->need_set_layout = true;
	(void)check_changes (g, false, NULL, NULL);
	return g->db;
}

R_API void r_agraph_print(RAGraph *g, void *_core) {
	RCore *core = (RCore *)_core;
	g->can->flags = 0;
	agraph_print (core, g, false, true, NULL);
	if (g->graph->n_nodes > 0) {
		r_cons_newline (g->can->cons);
	}
}

R_API char *r_agraph_tostring(RAGraph *g, void *_core) {
	R_RETURN_VAL_IF_FAIL (g && g->can, NULL);
	RCore *core = (RCore *)_core;
	g->can->flags = 0;
	if (!agraph_print (core, g, false, false, NULL)) {
		return NULL;
	}
	return r_cons_canvas_tostring (g->can);
}

R_API void r_agraph_print_json(RAGraph *g, PJ *pj) {
	R_RETURN_IF_FAIL (g && pj);
	RList *nodes = g->graph->nodes;
	RListIter *it;
	RGraphNode *node = NULL;
	r_list_foreach (nodes, it, node) {
		RANode *anode = (RANode *)node->data;
		char *label = strdup (anode->body);
		pj_o (pj);
		pj_ki (pj, "id", anode->gnode->idx);
		pj_ks (pj, "title", anode->title);
		pj_ks (pj, "body", label);
		pj_k (pj, "out_nodes");
		pj_a (pj);
		RGraphNode **vit;
		R_VEC_FOREACH (&anode->gnode->out_nodes, vit) {
			pj_i (pj, (*vit)->idx);
		}
		pj_end (pj);
		pj_end (pj);
		free (label);
	}
}

R_API void r_agraph_set_title(RAGraph *g, const char *title) {
	free (g->title);
	g->title = title? strdup (title): NULL;
	sdb_set (g->db, "agraph.title", g->title, 0);
}

static bool agraph_node_is_indexed(const RAGraph *g, const RANode *n) {
	return g && n && n->title && ht_pp_find (g->nodes, n->title, NULL) == n;
}

R_API RANode *r_agraph_add_node(const RAGraph *g, const char *title, const char *body, const char *color) {
	R_RETURN_VAL_IF_FAIL (g, NULL);
	RANode *res = title? r_agraph_get_node (g, title): NULL;
	if (res) {
		return res;
	}
	RANodePrivate *priv = R_NEW0 (RANodePrivate);
	res = &priv->node;

	res->title = title? r_str_trunc_ellipsis (title, 255): strdup ("");
	res->body = (body && strlen (body))? r_str_endswith (body, "\n")? strdup (body): r_str_newf ("%s\n", body): strdup (""); // XXX
	res->layer = -1;
	res->pos_in_layer = -1;
	res->is_dummy = false;
	res->is_reversed = false;
	res->klass = -1;
	res->color = color? strdup (color): NULL;
	res->difftype = -1;
	res->gnode = r_graph_add_node (g->graph, res);
	if (title && !ht_pp_insert (g->nodes, res->title, res)) {
		r_graph_del_node (g->graph, res->gnode);
		ranode_free (res);
		return NULL;
	}
	if (title) {
		char *s, *estr, *b;
		size_t len;
		sdb_array_add (g->db, "agraph.nodes", res->title, 0);
		b = strdup (res->body);
		len = strlen (b);
		if (len > 0 && b[len - 1] == '\n') {
			b[len - 1] = '\0';
		}
		estr = sdb_encode ((const void *)b, -1);
		s = r_str_newf ("base64:%s", estr);
		free (estr);
		free (b);
		sdb_setf (g->db, s, 0, "agraph.nodes.%s.body", res->title);
		free (s);
	}
	return res;
}

R_API bool r_agraph_del_node(const RAGraph *g, const char *title) {
	R_RETURN_VAL_IF_FAIL (g && title, false);
	r_strf_buffer (128);
	char *title_trunc = r_str_trunc_ellipsis (title, 255);
	RANode *an, *res = r_agraph_get_node (g, title_trunc);
	free (title_trunc);
	RGraphNode *gn;

	if (!res) {
		return false;
	}
	ht_pp_delete (g->nodes, res->title);
	sdb_array_remove (g->db, "agraph.nodes", res->title, 0);
	if (g->edge_routes) {
		g->edge_routes->valid = false;
	}
	sdb_setf (g->db, NULL, 0, "agraph.nodes.%s", res->title);
	sdb_setf (g->db, NULL, 0, "agraph.nodes.%s.body", res->title);
	sdb_setf (g->db, NULL, 0, "agraph.nodes.%s.x", res->title);
	sdb_setf (g->db, NULL, 0, "agraph.nodes.%s.y", res->title);
	sdb_setf (g->db, NULL, 0, "agraph.nodes.%s.w", res->title);
	sdb_setf (g->db, NULL, 0, "agraph.nodes.%s.h", res->title);
	sdb_setf (g->db, NULL, 0, "agraph.nodes.%s.neighbours", res->title);

	const RVecGraphNodePtr *innodes = r_graph_innodes (g->graph, res->gnode);
	RGraphNode **vit;
	graph_foreach_anode_vec (innodes, vit, gn, an) {
		if (agraph_node_is_indexed (g, an)) {
			const char *key = r_strf ("agraph.nodes.%s.neighbours", an->title);
			sdb_array_remove (g->db, key, res->title, 0);
		}
	}

	r_graph_del_node (g->graph, res->gnode);
	res->gnode = NULL;

	ranode_free (res);
	return true;
}

static bool user_node_cb(void *user, const void *k UNUSED, const void *v) {
	struct g_cb *u = user;
	RANode *n = (RANode *)v;
	RANodeCallback cb = u->node_cb;
	void *user_data = u->data;
	if (n) {
		cb (n, user_data);
	}
	return true;
}

static bool user_edge_cb(void *user, const void *k UNUSED, const void *v) {
	struct g_cb *u = user;
	RAEdgeCallback cb = u->edge_cb;
	RAGraph *g = u->graph;
	void *user_data = u->data;
	RANode *n = (RANode *)v;
	RANode *an;
	if (!n) {
		return false;
	}
	const RVecGraphNodePtr *neigh = r_graph_get_neighbours (g->graph, n->gnode);
	RGraphNode **it;
	RGraphNode *gn;

	graph_foreach_anode_vec (neigh, it, gn, an) {
		cb (n, an, user_data);
	}
	return true;
}

R_API void r_agraph_foreach(RAGraph *g, RANodeCallback cb, void *user) {
	struct g_cb u = {
		.node_cb = cb,
		.data = user
	};
	ht_pp_foreach (g->nodes, user_node_cb, &u);
}

R_API void r_agraph_foreach_edge(RAGraph *g, RAEdgeCallback cb, void *user) {
	struct g_cb u = {
		.graph = g,
		.edge_cb = cb,
		.data = user
	};
	ht_pp_foreach (g->nodes, user_edge_cb, &u);
}

R_API RANode *r_agraph_get_first_node(const RAGraph *g) {
	const RList *l = r_graph_get_nodes (g->graph);
	RGraphNode *rgn = r_list_first (l);
	return get_anode (rgn);
}

R_API RANode *r_agraph_get_node(const RAGraph *g, const char *title) {
	R_RETURN_VAL_IF_FAIL (g, NULL);
	if (!title) {
		return NULL;
	}
	char *title_trunc = r_str_trunc_ellipsis (title, 255);
	RANode *node = ht_pp_find (g->nodes, title_trunc, NULL);
	free (title_trunc);
	return node;
}

R_API void r_agraph_add_edge(const RAGraph *g, RANode *a, RANode *b, bool highlight) {
	R_RETURN_IF_FAIL (g && a && b);
	r_graph_add_edge (g->graph, a->gnode, b->gnode);
	if (highlight) {
		ut64 aa = r_num_get (NULL, a->title);
		ut64 bb = r_num_get (NULL, b->title);
		sdb_setf (g->db, "true", 0,
			"agraph.edge.0x%" PFMT64x "_0x%" PFMT64x ".highlight", aa, bb);
	}
	if (agraph_node_is_indexed (g, a) && agraph_node_is_indexed (g, b)) {
		char *k = r_str_newf ("agraph.nodes.%s.neighbours", a->title);
		sdb_array_add (g->db, k, b->title, 0);
		free (k);
	}
}

R_API void r_agraph_add_edge_at(const RAGraph *g, RANode *a, RANode *b, int nth) {
	R_RETURN_IF_FAIL (g && a && b);
	if (agraph_node_is_indexed (g, a) && agraph_node_is_indexed (g, b)) {
		char *k = r_str_newf ("agraph.nodes.%s.neighbours", a->title);
		sdb_array_insert (g->db, k, nth, b->title, 0);
		free (k);
	}
	r_graph_add_edge_at (g->graph, a->gnode, b->gnode, nth);
}

R_API void r_agraph_del_edge(const RAGraph *g, RANode *a, RANode *b) {
	R_RETURN_IF_FAIL (g && a && b);
	if (agraph_node_is_indexed (g, a) && agraph_node_is_indexed (g, b)) {
		char *k = r_str_newf ("agraph.nodes.%s.neighbours", a->title);
		sdb_array_remove (g->db, k, b->title, 0);
		free (k);
	}
	r_graph_del_edge (g->graph, a->gnode, b->gnode);
}

R_API void r_agraph_reset(RAGraph *g) {
	R_RETURN_IF_FAIL (g);
	agraph_free_nodes (g);
	r_graph_reset (g->graph);
	r_agraph_set_title (g, NULL);
	sdb_reset (g->db);
	if (g->edges) {
		r_list_purge (g->edges);
	}
	g->nodes = ht_pp_new0 ();
	g->update_seek_on = NULL;
	g->need_reload_nodes = false;
	g->need_set_layout = true;
	g->need_update_dim = true;
	g->x = g->y = g->w = g->h = 0;
	agraph_sdb_init (g);
	g->curnode = NULL;
}

R_API void r_agraph_free(RAGraph *g) {
	if (g) {
		agraph_free_nodes (g);
		r_graph_free (g->graph);
		r_list_free (g->edges);
		edge_routes_free (g->edge_routes);
		r_agraph_set_title (g, NULL);
		sdb_free (g->db);
		r_cons_canvas_free (g->can);
		free (g);
	}
}

R_API RAGraph *r_agraph_new(RConsCanvas *can) {
	RAGraph *g = R_NEW0 (RAGraph);
	g->can = can;
	g->dummy = true;
	agraph_init (g);
	agraph_sdb_init (g);
	return g;
}

static void visual_offset(RCore *core, RAGraph *g) {
	char buf[256];
	int rows;
	r_cons_get_size (core->cons, &rows);
	r_cons_gotoxy (core->cons, 0, rows);
	r_cons_flush (core->cons);
	core->cons->line->prompt_type = R_LINE_PROMPT_OFFSET;
	r_line_set_hist_callback (core->cons->line, &r_line_hist_offset_up, &r_line_hist_offset_down);
	r_line_set_prompt (core->cons->line, "[offset]> ");
	strcpy (buf, "s ");
	if (r_cons_fgets (core->cons, buf + 2, sizeof (buf) - 2, 0, NULL) > 0) {
		if (buf[2] == '.') {
			buf[1] = '.';
		}
		r_core_cmd0 (core, buf);
		r_line_set_hist_callback (core->cons->line, &r_line_hist_cmd_up, &r_line_hist_cmd_down);
	}
	core->cons->line->prompt_type = R_LINE_PROMPT_DEFAULT;
}

R_API void r_core_visual_find(RCore *core, RAGraph *g) {
	int offset = 0;
	int offset_max = 0;
	int rows;
	char buf[256];

	const bool asm_addr = r_config_get_b (core->config, "asm.addr");
	const bool asm_lines = r_config_get_b (core->config, "asm.lines");

	core->cons->line->prompt_type = R_LINE_PROMPT_OFFSET;
	r_line_set_prompt (core->cons->line, "[find]> ");

	RCons *cons = core->cons;
	while (1) {
		r_cons_get_size (cons, &rows);
		r_cons_gotoxy (cons, 0, rows - 1);
		r_cons_flush (cons);
		printf (Color_RESET);
		fflush (stdout);
		r_cons_flush (cons);

		r_cons_fgets (cons, buf, sizeof (buf), 0, NULL);

		if (buf[0] == '\0') {
			break;
		}

	find_next:
		if (cons->line->state.contents != NULL && strcmp (buf, cons->line->state.contents) == 0) {
			offset += 1;
			if (offset >= offset_max) {
				offset = 0;
			}
		} else {
			offset = 0;
		}

		r_config_set_b (core->config, "asm.addr", 1);
		r_config_set_b (core->config, "asm.lines", 0);

		if (offset == 0) {
			free (cons->line->state.contents);
			cons->line->state.contents = strdup (buf);

			char *lines_nbr = r_core_cmd_strf (core, "pdr~%s~^0x~?", buf);
			offset_max = atoi (lines_nbr);
			free (lines_nbr);
		}

		// TODO: filter `buf` to avoid command injection here
		char *line = r_core_cmd_strf (core, "pdr~%s~^0x:%d", buf, offset);
		r_str_trim (line);
		ut64 addr = r_num_get (core->num, line);
		if (addr > 0) {
			r_core_seek (core, addr, true);
			r_config_set (core->config, "scr.highlight", buf);
		} else {
			r_config_set (core->config, "scr.highlight", "");
		}

		r_config_set_b (core->config, "asm.addr", asm_addr);
		r_config_set_b (core->config, "asm.lines", asm_lines);
		if (g) {
			agraph_refresh (cons->event_data);
		} else {
			visual_refresh (core);
		}
		r_cons_clear_line (cons, false, false);
		r_cons_printf (cons, Color_RESET);
		if (addr > 0) {
			r_cons_gotoxy (cons, 0, 0);
			r_cons_printf (cons, "[find]> match '%s'", line);
			R_LOG_INFO ("Found (%d/%d). Press 'n' for next, 'N' for prev, 'q' for quit, any other key to continue", offset + 1, offset_max);
		} else {
			R_LOG_ERROR ("Text '%s' not found. Press 'q' for quit, any other key to conitnue", buf);
		}
		r_cons_flush (cons);

		free (line);

		char c = r_cons_readchar (cons);
		if (addr > 0) {
			if (c == ';') {
				char buf[256];
				r_line_set_prompt (cons->line, "[comment]> ");
				if (r_cons_fgets (cons, buf, sizeof (buf), 0, NULL) > 0) {
					r_core_cmdf (core, "'CC %s", buf);
				}
				r_line_set_prompt (cons->line, "[find]> ");
				if (g) {
					g->need_reload_nodes = true;
				}
			}
			if (c == ':') {
				cons->event_resize = (RConsEvent)agraph_set_need_reload_nodes;
				r_core_visual_prompt_input (core);
				if (g) {
					g->can->flags = r_cons_canvas_flags (cons);
				}
				r_cons_set_raw (cons, true);
				cons->event_resize = (RConsEvent)agraph_refresh_queued;
			}
			if (c == 'n' || c == 'j') {
				goto find_next;
			}
			if (c == 'N' || c == 'k') {
				offset -= 2;
				if (offset < -1) {
					offset = offset_max - 2;
				}
				goto find_next;
			}
		}
		if (c == 'q') {
			break;
		}
		if (g) {
			agraph_refresh (cons->event_data);
		} else {
			visual_refresh (core);
		}
	}

	free (cons->line->state.contents);
	cons->line->state.contents = NULL;
	r_config_set (core->config, "scr.highlight", "");
	cons->line->prompt_type = R_LINE_PROMPT_DEFAULT;
	r_config_set_b (core->config, "asm.addr", asm_addr);
	r_config_set_b (core->config, "asm.lines", asm_lines);
}

static void goto_asmqjmps(RAGraph *g, RCore *core) {
	const char *h = "[Fast goto call/jmp]> ";
	char obuf[R_CORE_ASMQJMPS_LEN_LETTERS + 1];
	int rows, i = 0;
	bool cont;
	RCons *cons = core->cons;

	r_cons_get_size (cons, &rows);
	r_cons_gotoxy (cons, 0, rows);
	r_cons_clear_line (cons, false, false);
	r_cons_print (cons, Color_RESET);
	r_cons_print (cons, h);
	r_cons_flush (cons);

	do {
		r_cons_set_raw (cons, true);
		char ch = r_cons_readchar (cons);
		obuf[i++] = ch;
		r_cons_write (cons, &ch, 1);
		cont = isalpha (ch & 0xff) && !islower (ch & 0xff);
	} while (i < R_CORE_ASMQJMPS_LEN_LETTERS && cont);
	r_cons_flush (cons);

	obuf[i] = '\0';
	ut64 addr = r_core_get_asmqjmps (core, obuf);
	if (addr != UT64_MAX) {
		char *title = get_title (addr);
		RANode *addr_node = r_agraph_get_node (g, title);
		if (addr_node) {
			r_agraph_set_curnode (g, addr_node);
			r_core_seek (core, addr, false);
			agraph_update_seek (g, addr_node, true);
		} else {
			r_io_sundo_push (core->io, core->addr, 0);
			r_core_seek (core, addr, false);
		}
		free (title);
	}
}

static void seek_to_node(RANode *n, RCore *core) {
	ut64 off = r_anal_get_bbaddr (core->anal, core->addr);
	char *title = get_title (off);
	if (title && strcmp (title, n->title)) {
		const char *err = NULL;
		ut64 addr = r_num_get_err (core->num, n->title, &err);
		if (!err && addr != core->addr) {
			r_io_sundo_push (core->io, core->addr, 0);
			r_core_seek (core, addr, true);
		}
	}
	free (title);
}

static void graph_single_step_in(RCore *core, RAGraph *g) {
	if (r_config_get_b (core->config, "cfg.debug")) {
		if (core->print->cur_enabled) {
			// dcu 0xaddr
			r_core_cmdf (core, "dcu 0x%08" PFMT64x, core->addr + core->print->cur);
			core->print->cur_enabled = 0;
		} else {
			r_core_cmd (core, "ds", 0);
			r_core_cmd (core, ".dr*", 0);
		}
	} else {
		r_core_cmd (core, "aes", 0);
		r_core_cmd (core, ".ar*", 0);
	}
	g->is_instep = true;
	g->need_reload_nodes = true;
}

static void graph_single_step_over(RCore *core, RAGraph *g) {
	if (r_config_get_b (core->config, "cfg.debug")) {
		if (core->print->cur_enabled) {
			r_core_cmd (core, "dcr", 0);
			core->print->cur_enabled = 0;
		} else {
			r_core_cmd (core, "dso", 0);
			r_core_cmd (core, ".dr*", 0);
		}
	} else {
		r_core_cmd (core, "aeso", 0);
		r_core_cmd (core, ".ar*", 0);
	}
	g->is_instep = true;
	g->need_reload_nodes = true;
}

static void graph_breakpoint(RCore *core) {
	r_core_cmd (core, "dbs $$", 0);
}

static void graph_continue(RCore *core) {
	r_core_cmd (core, "dc", 0);
}

static void rotateColor(RCore *core) {
	int color = r_config_get_i (core->config, "scr.color");
	if (++color > 3) {
		color = 0;
	}
	r_config_set_i (core->config, "scr.color", color);
}

// dupe in visual.c
static bool toggle_bb(RCore *core, ut64 addr) {
	RAnalFunction *fcn = r_anal_get_fcn_in (core->anal, addr, R_ANAL_FCN_TYPE_NULL);
	if (fcn) {
		RAnalBlock *bb = r_anal_function_bbget_in (core->anal, fcn, addr);
		if (bb) {
			bb->folded = !bb->folded;
		} else {
			R_WARN_IF_REACHED ();
		}
		return true;
	}
	return false;
}

static char *get_graph_string(RCore *core, RAGraph *g) {
	int c = r_config_get_i (core->config, "scr.color");
	bool u = r_config_get_b (core->config, "scr.utf8");
	r_config_set_i (core->config, "scr.color", 0);
	r_config_set_b (core->config, "scr.utf8", false);
	r_core_visual_graph (core, g, NULL, false);
	char *s = strdup (r_cons_get_buffer (core->cons, NULL));
	r_cons_reset (core->cons);
	r_config_set_i (core->config, "scr.color", c);
	r_config_set_b (core->config, "scr.utf8", u);
	return s;
}

static void nextword(RCore *core, RAGraph *g, const char *word) {
	R_RETURN_IF_FAIL (core && core->graph && g && g->can && word);
	if (R_STR_ISEMPTY (word)) {
		return;
	}
	RAGraphHits *gh = &g->ghits;
	RConsCanvas *can = g->can;
	if (RVecAGraphLocation_length (&gh->word_list) && gh->old_word && !strcmp (word, gh->old_word)) {
		if (gh->word_nth >= RVecAGraphLocation_length (&gh->word_list)) {
			gh->word_nth = 0;
		}

		RAGraphLocation *pos = RVecAGraphLocation_at (&gh->word_list, gh->word_nth);
		gh->word_nth++;
		if (pos) {
			can->sx = -pos->x + can->w / 2;
			can->sy = -pos->y + can->h / 2;
		}
		return;
	} else {
		RVecAGraphLocation_clear (&gh->word_list);
	}
	char *s = get_graph_string (core, g);
	r_cons_clear00 (core->cons);
	r_cons_flush (core->cons);
	const size_t MAX_COUNT = 4096;
	const char *a = NULL;
	size_t count = 0;
	int x = 0, y = 0;
	for (count = 0; count < MAX_COUNT; count++) {
		a = r_str_str_xy (s, word, a, &x, &y);
		if (!a) {
			break;
		}
		RAGraphLocation *pos = RVecAGraphLocation_emplace_back (&gh->word_list);
		pos->x = x + g->x;
		pos->y = y + g->y;
	}
	free (gh->old_word);
	gh->old_word = strdup (word);
	free (s);
	if (a || count > 0) {
		nextword (core, g, word);
	}
}

R_API bool r_core_visual_graph(RCore *core, RAGraph *g, RAnalFunction *_fcn, int mode) {
	bool is_interactive = mode == 1 || mode == 3;
	if (is_interactive && !r_cons_is_interactive (core->cons)) {
		R_LOG_ERROR ("Interactive graph mode requires 'e scr.interactive=true'");
		return false;
	}
	r_cons_set_raw (core->cons, true);
	int o_asmqjmps_letter = core->is_asmqjmps_letter;
	int o_vmode = core->vmode;
	int exit_graph = false, is_error = false;
	bool must_update_seek = false;
	struct agraph_refresh_data *grd;
	int okey, key;
	RAnalFunction *fcn = NULL;
	const char *key_s;
	RConsCanvas *o_can = NULL;
	bool graph_allocated = false;
	int movspeed;
	int ret, invscroll;
	RConfigHold *hc = r_config_hold_new (core->config);
	if (!hc) {
		return false;
	}
	r_config_hold (hc, "asm.pseudo", "asm.esil", "asm.cmt.right", NULL);

	int h, w = r_cons_get_size (core->cons, &h);
	RConsCanvas *can = r_cons_canvas_new (core->cons, w, h, -2);
	if (!can) {
		w = 80;
		h = 25;
		can = r_cons_canvas_new (core->cons, w, h, -2);
		if (!can) {
			R_LOG_ERROR ("Cannot create RCons.canvas context. Invalid screen "
				"size? See scr.cols + scr.rows");
			r_config_hold_free (hc);
			return false;
		}
	}
	can->linemode = r_config_get_b (core->config, "graph.linemode");
	can->color = r_config_get_i (core->config, "scr.color");

	if (!g) {
		graph_allocated = true;
		fcn = _fcn? _fcn: r_anal_get_fcn_in (core->anal, core->addr, 0);
		if (!fcn) {
			R_LOG_ERROR ("No function to graph");
			r_config_hold_restore (hc);
			r_config_hold_free (hc);
			r_cons_canvas_free (can);
			return false;
		}
		check_function_modified (core, fcn);
		g = r_agraph_new (can);
		if (!g) {
			r_cons_canvas_free (can);
			r_config_hold_restore (hc);
			r_config_hold_free (hc);
			return false;
		}
		g->is_tiny = mode == 2;
		g->layout = r_config_get_i (core->config, "graph.layout");
		g->dummy = r_config_get_i (core->config, "graph.dummy");
		g->show_node_titles = r_config_get_i (core->config, "graph.ntitles");
	} else {
		o_can = g->can;
	}

	g->can = can;
	g->movspeed = r_config_get_i (core->config, "graph.scroll");
	const int graph_zoom = r_config_get_i (core->config, "graph.zoom");
	if (graph_zoom) {
		agraph_set_zoom (g, graph_zoom);
	}
	g->show_node_titles = r_config_get_i (core->config, "graph.ntitles");
	g->show_node_body = r_config_get_b (core->config, "graph.body");
	g->show_node_bubble = r_config_get_i (core->config, "graph.bubble");
	g->on_curnode_change = (RANodeCallback)seek_to_node;
	g->on_curnode_change_data = core;
	g->edgemode = r_config_get_i (core->config, "graph.edges");
	g->hints = r_config_get_b (core->config, "graph.hints");
	g->is_interactive = is_interactive;
	bool asm_comments = r_config_get_b (core->config, "asm.comments");
	r_config_set_b (core->config, "asm.comments", r_config_get_b (core->config, "graph.comments"));

	/* we want letters as shortcuts for call/jmps */
	core->is_asmqjmps_letter = true;
	core->vmode = true;

	grd = R_NEW0 (struct agraph_refresh_data);
	grd->g = g;
	grd->fs = is_interactive == 1;
	grd->core = core;
	grd->follow_offset = _fcn == NULL;
	grd->fcn = fcn? &fcn: NULL;
	ret = agraph_refresh (grd);
	RCoreGraphCapture *capture = ((RCorePriv *)core->priv)->graph_capture;
	if (ret && !is_interactive && capture) {
		capture->cb (g, capture->user);
	}
	if (!ret || is_interactive != 1) {
		r_cons_newline (core->cons);
		exit_graph = true;
		is_error = !ret;
	}
	if (!exit_graph) {
		fcn = r_anal_get_fcn_in (core->anal, core->addr, 0);
	}

	core->cons->event_resize = NULL; // avoid running old event with new data
	core->cons->event_data = grd;
	core->cons->event_resize = (RConsEvent)agraph_refresh_queued;

	r_cons_break_push (core->cons, NULL, NULL);
	if (mode == 3) { // XXX wrong usage or buggy RGraph.domTree ()
		// dominance tree here
		const RList *l = r_graph_get_nodes (g->graph);
		RGraphNode *root = r_list_first (l);
		if (root) {
			RGraph *dg = r_graph_dom_tree (g->graph, root);
			if (dg) {
				// XXX double free - r_graph_free (g->graph);
				g->graph = dg;
			} else {
				R_LOG_WARN ("Cannot compute the dominance tree");
				r_sys_sleep (1);
			}
		}
	}

	while (!exit_graph && !is_error && !r_cons_is_breaked (core->cons)) {
		w = r_cons_get_size (core->cons, &h);
		invscroll = r_config_get_b (core->config, "graph.invscroll");
		ret = agraph_refresh (grd);

		if (!ret) {
			is_error = true;
			break;
		}
		showcursor (core, false);

		// r_core_graph_inputhandle ()
		r_cons_set_raw (core->cons, true);
		okey = r_cons_readchar (core->cons);
		key = r_cons_arrow_to_hjkl (core->cons, okey);

		if (core->cons->mouse_event) {
			int x, y;
			if (r_cons_get_click (core->cons, &x, &y)) {
				int ax = x - g->can->sx;
				int ay = y - g->can->sy;
				const RList *nodes = r_graph_get_nodes (g->graph);
				RListIter *it;
				RGraphNode *gn;
				RANode *n;
				graph_foreach_anode (nodes, it, gn, n) {
					if (ax >= n->x && ax < n->x + n->w && ay >= n->y && ay < n->y + n->h + 2) {
						r_agraph_set_curnode (g, n);
						break;
					}
				}
			}
			if (core->cons->drag_event) {
				// dragging pans the canvas 1:1 with the pointer
				movspeed = 1;
			} else {
				movspeed = r_config_get_i (core->config, "scr.wheel.speed");
				switch (key) {
				case 'h':
				case 'j':
				case 'k':
				case 'l':
					switch (core->visual.mousemode) {
					case 0: break;
					case 1: key = key == 'k'? 'h': 'l'; break;
					case 2: key = key == 'k'? 'J': 'K'; break;
					case 3: key = key == 'k'? 'L': 'H'; break;
					}
					break;
				}
			}
		} else {
			movspeed = g->movspeed;
		}
		const char *cmd;
		switch (key) {
		case '-':
			agraph_set_zoom (g, g->zoom - ZOOM_STEP);
			g->force_update_seek = true;
			break;
		case '+':
			agraph_set_zoom (g, g->zoom + ZOOM_STEP);
			g->force_update_seek = true;
			break;
		case '0':
			agraph_set_zoom (g, ZOOM_DEFAULT);
			agraph_update_seek (g, get_anode (g->curnode), true);
			// update scroll (with minor shift)
			break;
			// Those hardcoded keys are useful only for aegi, should add subcommand of ag to set key actions
		case '1':
			r_core_cmd0 (core, "so;.aeg*");
			break;
		case '2':
			r_core_cmd0 (core, "so -1;.aeg*");
			break;
		case '=': // TODO: edit
			{
				showcursor (core, true);
				const char *cmd = r_config_get (core->config, "cmd.gprompt");
				r_line_set_prompt (core->cons->line, "cmd.gprompt> ");
				core->cons->line->state.contents = strdup (cmd);
				const char *buf = r_line_readline (core->cons);
				core->cons->line->state.contents = NULL;
				r_config_set (core->config, "cmd.gprompt", buf);
				showcursor (core, false);
			}
			break;
		case '|':
			{
				int e = r_config_get_i (core->config, "graph.layout");
				if (++e > 1) {
					e = 0;
				}
				r_config_set_i (core->config, "graph.layout", e);
				g->layout = r_config_get_i (core->config, "graph.layout");
				g->need_update_dim = true;
				g->need_set_layout = true;
			}
			g->discroll = 0;
			agraph_update_seek (g, get_anode (g->curnode), true);
			break;
		case 'e':
			{
				int e = r_config_get_i (core->config, "graph.edges");
				e++;
				if (e > 2) {
					e = 0;
				}
				r_config_set_i (core->config, "graph.edges", e);
				g->edgemode = e;
				g->need_update_dim = true;
			}
			break;
		case '\\':
			nextword (core, g, r_config_get (core->config, "scr.highlight"));
			break;
		case 'b':
			r_core_visual_browse (core, "");
			break;
		case 'E':
			{
				int e = r_config_get_i (core->config, "graph.linemode");
				e--;
				if (e < 0) {
					e = 1;
				}
				r_config_set_i (core->config, "graph.linemode", e);
				g->can->linemode = e;
			}
			break;
		case 13:
			agraph_update_seek (g, get_anode (g->curnode), true);
			must_update_seek = true;
			exit_graph = true;
			break;
		case '>':
			if (fcn && r_cons_yesno (core->cons, 'y', "Compute function callgraph? (Y/n)")) {
				r_core_cmd0 (core, "ag-;.agc* @$FB;.axfg @$FB;aggi");
			}
			break;
		case '<':
			// r_core_visual_refs (core, true, false);
			if (fcn) {
				r_core_cmd0 (core, "ag-;.axtg $FB;aggi");
			}
			break;
		case 'G':
			r_core_cmd0 (core, "ag-;.dtg*;aggi");
			break;
		case 'V':
			if (fcn) {
				agraph_toggle_callgraph (g);
			}
			break;
		case 'Z':
			if (okey == 27) { // shift-tab
				agraph_prev_node (g);
			} else {
				RANode *n = get_anode (g->curnode);
				if (n) {
					ut64 addr = r_num_get (NULL, n->title);
					toggle_bb (core, addr);
					g->need_reload_nodes = true;
				}
			}
			break;
		case 's':
			if (!fcn) {
				break;
			}
			key_s = r_config_get (core->config, "key.s");
			if (key_s && *key_s) {
				r_core_cmd0 (core, key_s);
			} else {
				graph_single_step_in (core, g);
			}
			g->discroll = 0;
			agraph_update_seek (g, get_anode (g->curnode), true);
			break;
		case 'S':
			if (fcn) {
				graph_single_step_over (core, g);
			}
			break;
		case 'x':
		case 'X':
			{
				if (!fcn) {
					break;
				}
				ut64 old_off = core->addr;
				ut64 off = r_anal_get_bbaddr (core->anal, core->addr);
				r_core_seek (core, off, false);
			if ((key == 'x' && !r_core_visual_refs (core, true, true)) ||
				(key == 'X' && !r_core_visual_refs (core, false, true))) {
					r_core_seek (core, old_off, false);
				}
				break;
			}
		case '@': // tab
			r_config_set_i (core->config, "graph.layout",
				r_config_get_i (core->config, "graph.layout")? 0: 1);
			g->discroll = 0;
			g->layout = r_config_get_i (core->config, "graph.layout");
			g->need_reload_nodes = true;
			agraph_update_seek (g, get_anode (g->curnode), true);
			break;
		case '%':
			showcursor (core, true);
			r_core_visual_find (core, g);
			showcursor (core, false);
			break;
		case 9: // tab
			agraph_next_node (g);
			g->discroll = 0;
			break;
		case '?':
			r_cons_clear00 (core->cons);
			RStrBuf *rsb = r_strbuf_new ("");
			r_core_visual_append_help (core, rsb, "Visual Graph Mode (VV) Help", help_msg_visual_graph);
			ret = r_cons_less_str (core->cons, r_strbuf_get (rsb), "?");
			r_strbuf_free (rsb);
			break;
		case '"':
			r_config_toggle (core->config, "graph.refs");
			break;
		case '#':
			if (g->mode == R_AGRAPH_MODE_COMMENTS) {
				g->mode = R_AGRAPH_MODE_NORMAL;
			} else {
				g->mode = R_AGRAPH_MODE_COMMENTS;
			}
			g->need_reload_nodes = true;
			g->discroll = 0;
			agraph_update_seek (g, get_anode (g->curnode), true);
			// r_config_toggle (core->config, "graph.hints");
			break;
		case 'p':
			g->mode = next_mode (g->mode);
			g->need_reload_nodes = true;
			agraph_update_seek (g, get_anode (g->curnode), true);
			break;
		case 'P':
			if (!fcn) {
				break;
			}
			g->mode = prev_mode (g->mode);
			g->need_reload_nodes = true;
			agraph_update_seek (g, get_anode (g->curnode), true);
			break;
		case 'o':
			goto_asmqjmps (g, core);
			break;
		case 'g':
			showcursor (core, true);
			visual_offset (core, g);
			showcursor (core, false);
			break;
		case 'O':
			if (!fcn) {
				break;
			}
			core->visual.disMode = (core->visual.disMode + 1) % 3;
			applyDisMode (core);
			g->need_reload_nodes = true;
			break;
		case 'u':
			{
				RIOUndos *undo = r_io_sundo (core->io, core->addr);
				if (undo) {
					r_core_seek (core, undo->off, false);
				} else {
					R_LOG_ERROR ("Cannot undo");
				}
				if (r_config_get_i (core->config, "graph.few")) {
					g->need_reload_nodes = true;
				}
				break;
			}
		case 'U':
			{
				RIOUndos *undo = r_io_sundo_redo (core->io);
				if (undo) {
					r_core_seek (core, undo->off, false);
				} else {
					R_LOG_ERROR ("Cannot redo");
				}
				if (r_config_get_i (core->config, "graph.few")) {
					g->need_reload_nodes = true;
				}
				break;
			}
		case 'r':
			fcn = r_anal_get_fcn_in (core->anal, core->addr, 0);
			if (fcn) {
				g->layout = r_config_get_i (core->config, "graph.layout");
				g->need_reload_nodes = true;
			}
			// TODO: toggle shortcut hotkeys
			if (r_config_get_b (core->config, "asm.hint.call")) {
				r_core_call (core, "e!asm.hint.call");
				r_core_call (core, "e!asm.hint.jmp");
			} else if (r_config_get_b (core->config, "asm.hint.jmp")) {
				r_core_call (core, "e!asm.hint.jmp");
				r_core_call (core, "e!asm.hint.lea");
			} else if (r_config_get_b (core->config, "asm.hint.lea")) {
				r_core_call (core, "e!asm.hint.lea");
				r_core_call (core, "e!asm.hint.call");
			}
			break;
		case 'R':
			if (r_config_get_b (core->config, "scr.randpal")) {
				r_core_call (core, "ecr");
			} else {
				r_core_call (core, "ecn");
			}
			g->edgemode = r_config_get_i (core->config, "graph.edges");
			if (!agraph_recolor_nodes (g, core)) {
				fcn = r_anal_get_fcn_in (core->anal, core->addr, 0);
				if (fcn) {
					get_bbupdate (g, core, fcn);
				}
			}
			break;
		case '$':
			r_core_cmd (core, "dr PC=$$", 0);
			r_core_cmd (core, "sr PC", 0);
			g->need_reload_nodes = true;
			break;
		case '!':
			r_core_panels_root (core, core->panels_root);
			break;
		case 'y':
			r_config_toggle (core->config, "graph.comments");
			g->need_reload_nodes = true;
			break;
		case '[':
			if (core->print->cur_enabled) {
				int scrcols = r_config_get_i (core->config, "hex.cols");
				if (scrcols > 1) {
					r_config_set_i (core->config, "hex.cols", scrcols - 1);
				}
			} else {
				int n = r_config_get_i (core->config, "graph.bb.maxwidth");
				n -= 5;
				if (n < 20) {
					n = 0;
				}
				r_config_set_i (core->config, "graph.bb.maxwidth", n);
				g->need_reload_nodes = true;
				g->bb_maxwidth = n;
			}
			break;
		case ']':
			if (core->print->cur_enabled) {
				int scrcols = r_config_get_i (core->config, "hex.cols");
				r_config_set_i (core->config, "hex.cols", scrcols + 1);
			} else {
				int n = r_config_get_i (core->config, "graph.bb.maxwidth");
				if (n < 1) {
					n = 20;
				} else {
					n += 5;
				}
				r_config_set_i (core->config, "graph.bb.maxwidth", n);
				g->need_reload_nodes = true;
				g->bb_maxwidth = n;
			}
			break;
		case ';':
			fcn = r_anal_get_fcn_in (core->anal, core->addr, 0);
			if (fcn) {
				showcursor (core, true);
				char buf[256];
				r_line_set_prompt (core->cons->line, "[comment]> ");
				if (r_cons_fgets (core->cons, buf, sizeof (buf), 0, NULL) > 0) {
					r_core_cmdf (core, "'CC %s", buf);
				}
				g->need_reload_nodes = true;
				showcursor (core, false);
			}
			break;
		case 'C':
			rotateColor (core);
			break;
		case 'm':
			// mark current x/y + offset
			{
				r_cons_gotoxy (core->cons, 0, 0);
				r_cons_printf (core->cons, R_CONS_CLEAR_LINE "Set shortcut key for 0x%" PFMT64x "\n", core->addr);
				r_cons_flush (core->cons);
				int ch = r_cons_readchar (core->cons);
				if (ch > 0) {
					r_core_vmark_set (core, ch, core->addr, can->sx, can->sy);
				}
			}
			break;
		case '\'':
			// go to given mark
			{
				int ch = r_core_vmark_select (core, false);
				if (ch >= 0) {
					r_core_vmark_seek (core, ch, g);
					core->visual.coming_from_vmark = true;
				}
			}
			break;
		case 'M':
			core->visual.mousemode--;
			if (core->visual.mousemode < 0) {
				core->visual.mousemode = 3;
			}
			break;
		case '(':
			fcn = r_anal_get_fcn_in (core->anal, core->addr, 0);
			if (fcn) {
				r_core_cmd0 (core, "wao recj@B:-1");
				g->need_reload_nodes = true;
			}
			break;
		case ')':
			fcn = r_anal_get_fcn_in (core->anal, core->addr, 0);
			if (fcn) {
				rotateAsmemu (core);
				g->need_reload_nodes = true;
			}
			break;
		case 'd':
			{
				showcursor (core, true);
				r_core_visual_define (core, "", 0);
				RAnalFunction *fcn = r_anal_get_fcn_in (core->anal, core->addr, 0);
				if (fcn) {
					get_bbupdate (g, core, fcn);
				}
				showcursor (core, false);
			}
			break;
		case 'D':
			g->is_dis = !g->is_dis;
			break;
		case 'n':
			r_core_seek_next (core, r_config_get (core->config, "scr.nkey"));
			break;
		case 'N':
			r_core_seek_previous (core, r_config_get (core->config, "scr.nkey"));
			break;
		case 'Y':
			agraph_toggle_tiny (g);
			agraph_update_seek (g, get_anode (g->curnode), true);
			break;
		case 'z':
			agraph_toggle_mini (g);
			g->discroll = 0;
			agraph_update_seek (g, get_anode (g->curnode), true);
			break;
		case 'v':
			r_core_visual_anal (core, NULL);
			break;
		case 'J':
			// copypaste from 'j'
			if (core->visual.graphCursor) {
				int speed = (okey == 27)? PAGEKEY_SPEED: movspeed;
				graphNodeMove (g, 'j', speed * 2);
			} else {
				can->sy -= agraph_scroll_step (h, movspeed) *(invscroll? -1: 1);
			}
			break;
		case 'K':
			if (core->visual.graphCursor) {
				int speed = (okey == 27)? PAGEKEY_SPEED: movspeed;
				graphNodeMove (g, 'k', speed * 2);
			} else {
				can->sy += agraph_scroll_step (h, movspeed) *(invscroll? -1: 1);
			}
			break;
		case 'H':
			if (core->visual.graphCursor) {
				// move node canvas faster
				graphNodeMove (g, 'h', movspeed * 2);
			} else {
				// scroll canvas faster
				if (okey == 27) {
					// handle home key
					const RGraphNode *gn = find_near_of (g, NULL, true);
					g->update_seek_on = get_anode (gn);
				} else {
					can->sx += agraph_scroll_step (w, movspeed) *(invscroll? -1: 1);
				}
			}
			break;
		case 'L':
			if (core->visual.graphCursor) {
				graphNodeMove (g, 'l', movspeed * 2);
			} else {
				can->sx -= agraph_scroll_step (w, movspeed) *(invscroll? -1: 1);
			}
			break;
		case 'c':
			core->visual.graphCursor = !core->visual.graphCursor;
			break;
		case 'j':
			if (g->is_dis) {
				r_core_call (core, "so 1");
			} else {
				if (core->visual.graphCursor) {
					int speed = (okey == 27)? PAGEKEY_SPEED: movspeed;
					graphNodeMove (g, 'j', speed);
				} else {
					// scroll canvas
					can->sy -= movspeed *(invscroll? -1: 1);
				}
			}
			break;
		case 'k':
			if (g->is_dis) {
				r_core_call (core, "so -1");
			} else {
				if (core->visual.graphCursor) {
					int speed = (okey == 27)? PAGEKEY_SPEED: movspeed;
					graphNodeMove (g, 'k', speed);
				} else {
					// scroll canvas
					can->sy += movspeed *(invscroll? -1: 1);
				}
			}
			break;
		case 'l':
			if (core->visual.graphCursor) {
				int speed = (okey == 27)? PAGEKEY_SPEED: movspeed;
				graphNodeMove (g, 'l', speed);
			} else {
				can->sx -= movspeed *(invscroll? -1: 1);
			}
			break;
		case 'h':
			if (core->visual.graphCursor) {
				int speed = (okey == 27)? PAGEKEY_SPEED: movspeed;
				graphNodeMove (g, 'h', speed);
			} else {
				can->sx += movspeed *(invscroll? -1: 1);
			}
			break;
		case '^':
			{
				RAnalFunction *fcn = r_anal_get_fcn_in (core->anal, core->addr, 0);
				if (fcn) {
					r_core_seek (core, fcn->addr, false);
				}
			}
			agraph_update_seek (g, get_anode (g->curnode), true);
			break;
		case ',':
			r_config_toggle (core->config, "graph.few");
			g->need_reload_nodes = true;
			agraph_update_seek (g, get_anode (g->curnode), true);
			break;
		case '.':
			g->discroll = 0;
			agraph_update_seek (g, get_anode (g->curnode), true);
			break;
		case 'i':
			agraph_follow_innodes (core, g, true);
			break;
		case 'I':
			agraph_follow_innodes (core, g, false);
			break;
		case 't':
			agraph_follow_true (g);
			break;
		case 'T':
			// XXX WIP	agraph_merge_child (g, 0);
			break;
		case 'f':
			agraph_follow_false (g);
			break;
		case 'F':
			if (okey == 27) {
				// handle end key
				const RGraphNode *gn = find_near_of (g, NULL, false);
				g->update_seek_on = get_anode (gn);
			} else {
				// agraph_merge_child (g, 1);
				r_core_visual_trackflags (core);
			}
			break;
		case '/':
			showcursor (core, true);
			r_core_cmd0 (core, "?i highlight;e scr.highlight=`yp`");
			showcursor (core, false);
			break;
		case ':':
			core->cons->event_resize = (RConsEvent)agraph_set_need_reload_nodes;
			r_core_visual_prompt_input (core);
			g->can->flags = r_cons_canvas_flags (core->cons);
			r_cons_set_raw (core->cons, true);
			core->cons->event_resize = (RConsEvent)agraph_refresh_queued;
			break;
		case '&':
			{
				RIOUndos *undo = r_io_sundo (core->io, core->addr);
				if (undo) {
					r_io_sundo_redo (core->io);
					sdb_setf (g->db, "true", 0,
						"agraph.edge.0x%" PFMT64x "_0x%" PFMT64x ".highlight", undo->off, core->addr);
				}
			}
			break;
		case 'w':
			agraph_toggle_speed (g, core);
			break;
		case '_':
			r_core_visual_hudstuff (core);
			break;
		case R_CONS_KEY_F1:
			cmd = r_config_get (core->config, "key.f1");
			if (cmd && *cmd) {
				(void)r_core_cmd0 (core, cmd);
				g->need_reload_nodes = true;
			}
			break;
		case R_CONS_KEY_F2:
			cmd = r_config_get (core->config, "key.f2");
			if (R_STR_ISNOTEMPTY (cmd)) {
				r_core_cmd0 (core, cmd);
				g->need_reload_nodes = true;
			} else {
				graph_breakpoint (core);
			}
			break;
		case R_CONS_KEY_F3:
			cmd = r_config_get (core->config, "key.f3");
			if (R_STR_ISNOTEMPTY (cmd)) {
				r_core_cmd0 (core, cmd);
				g->need_reload_nodes = true;
			}
			break;
		case R_CONS_KEY_F4:
			cmd = r_config_get (core->config, "key.f4");
			if (R_STR_ISNOTEMPTY (cmd)) {
				r_core_cmd0 (core, cmd);
				g->need_reload_nodes = true;
			}
			break;
		case R_CONS_KEY_F5:
			cmd = r_config_get (core->config, "key.f5");
			if (R_STR_ISNOTEMPTY (cmd)) {
				r_core_cmd0 (core, cmd);
				g->need_reload_nodes = true;
			}
			break;
		case R_CONS_KEY_F6:
			cmd = r_config_get (core->config, "key.f6");
			if (cmd && *cmd) {
				(void)r_core_cmd0 (core, cmd);
				g->need_reload_nodes = true;
			}
			break;
		case R_CONS_KEY_F7:
			cmd = r_config_get (core->config, "key.f7");
			if (R_STR_ISNOTEMPTY (cmd)) {
				(void)r_core_cmd0 (core, cmd);
				g->need_reload_nodes = true;
			} else {
				graph_single_step_in (core, g);
			}
			break;
		case R_CONS_KEY_F8:
			cmd = r_config_get (core->config, "key.f8");
			if (R_STR_ISNOTEMPTY (cmd)) {
				(void)r_core_cmd0 (core, cmd);
				g->need_reload_nodes = true;
			} else {
				graph_single_step_over (core, g);
			}
			break;
		case R_CONS_KEY_F9:
			cmd = r_config_get (core->config, "key.f9");
			if (R_STR_ISNOTEMPTY (cmd)) {
				(void)r_core_cmd0 (core, cmd);
				g->need_reload_nodes = true;
			} else {
				graph_continue (core);
			}
			break;
		case R_CONS_KEY_F10:
			cmd = r_config_get (core->config, "key.f10");
			if (R_STR_ISNOTEMPTY (cmd)) {
				(void)r_core_cmd0 (core, cmd);
				g->need_reload_nodes = true;
			}
			break;
		case R_CONS_KEY_F11:
			cmd = r_config_get (core->config, "key.f11");
			if (R_STR_ISNOTEMPTY (cmd)) {
				(void)r_core_cmd0 (core, cmd);
				g->need_reload_nodes = true;
			}
			break;
		case R_CONS_KEY_F12:
			cmd = r_config_get (core->config, "key.f12");
			if (R_STR_ISNOTEMPTY (cmd)) {
				(void)r_core_cmd0 (core, cmd);
				g->need_reload_nodes = true;
			}
			break;
		case -1: // EOF
		case ' ':
		case 'Q':
		case 'q':
			if (g->is_callgraph) {
				agraph_toggle_callgraph (g);
			} else {
				exit_graph = true;
			}
			break;
		case 27: // ESC
			if (r_cons_readchar (core->cons) == 91) {
				if (r_cons_readchar (core->cons) == 90) {
					agraph_prev_node (g);
				}
			}
			break;
		default:
			break;
		}
	}
	RVecAGraphLocation_fini (&g->ghits.word_list);
	r_cons_break_pop (core->cons);
	r_config_set_b (core->config, "asm.comments", asm_comments);
	core->cons->event_resize = NULL;
	core->cons->event_data = NULL;
	core->vmode = o_vmode;
	core->is_asmqjmps_letter = o_asmqjmps_letter;
	core->keep_asmqjmps = false;

	free (grd);
	if (graph_allocated) {
		r_agraph_free (g);
	} else {
		g->can = o_can;
	}
	r_config_hold_restore (hc);
	r_config_hold_free (hc);
	if (must_update_seek) {
		return true; // -1
	}
	return !is_error;
}

R_API RAGraph *r_agraph_new_from_graph(void *_core, const RGraph *graph, RAGraphTransitionCBs *cbs, void *user) {
	R_RETURN_VAL_IF_FAIL (graph && cbs && cbs->get_title && cbs->get_body, NULL);

	RCore *core = (RCore *)_core;
	RConsCanvas *canvas = r_cons_canvas_new (r_core_get_cons (core), 1, 1, -2);
	RAGraph *result_agraph = r_agraph_new (canvas);
	if (!result_agraph) {
		return NULL;
	}
	result_agraph->need_reload_nodes = false;
	// Cache lookup to build edges
	HtPPOptions pointer_options = { 0 };
	HtPP /*<RGraphNode *node, RANode *anode>*/ *hashmap = ht_pp_new_opt (&pointer_options);

	if (!hashmap) {
		r_agraph_free (result_agraph);
		return NULL;
	}
	// List of the new RANodes
	RListIter *iter;
	RGraphNode *node;
	// Traverse the list, create new ANode for each Node
	r_list_foreach (graph->nodes, iter, node) {
		char *title = cbs->get_title (node->data, user);
		char *body = cbs->get_body (node->data, user);
		RANode *a_node = r_agraph_add_node (result_agraph, title, body, NULL);
		R_FREE (title);
		R_FREE (body);
		if (!a_node) {
			goto failure;
		}
		ht_pp_insert (hashmap, node, a_node);
	}

	// Traverse the nodes again, now build up the edges
	r_list_foreach (graph->nodes, iter, node) {
		RANode *a_node = ht_pp_find (hashmap, node, NULL);
		if (!a_node) {
			goto failure; // shouldn't happen in correct graph state
		}

		RGraphNode **neighbour_iter;
		R_VEC_FOREACH (&node->in_nodes, neighbour_iter) {
			RGraphNode *neighbour = *neighbour_iter;
			RANode *a_neighbour = ht_pp_find (hashmap, neighbour, NULL);
			if (!a_neighbour) {
				goto failure;
			}
			r_agraph_add_edge (result_agraph, a_neighbour, a_node, false);
		}
	}

	ht_pp_free (hashmap);
	return result_agraph;
failure:
	ht_pp_free (hashmap);
	r_agraph_free (result_agraph);
	return NULL;
}
