// SPDX-FileCopyrightText: 2021-2026 RizinOrg <info@rizin.re>
// SPDX-FileCopyrightText: 2021-2026 deroad <deroad@kumo.xn--q9jyb4c>
// SPDX-License-Identifier: LGPL-3.0-only

#include <rz_core.h>
#include <rz_io.h>
#include <rz_bin.h>
#include <rz_diff.h>
#include <rz_util.h>
#include <rz_main.h>

#define RZ_DIFF_DISTANCE_MAX_SIZE (1024 * 1024) // 1 miB

#define SAFE_STR_DEF(x, y) (x ? x : y)
#define SAFE_STR(x)        (x ? x : "")
#define IF_STRCMP_S(ret, x, y) \
	do { \
		if ((x) != (y) && (!(x) || !(y))) { \
			return !(y) ? 1 : -1; \
		} else if ((x) && (y) && (ret = strcmp((x), (y)))) { \
			return ret; \
		} \
	} while (0)

typedef enum {
	DIFF_MODE_STANDARD = 0,
	DIFF_MODE_JSON,
	DIFF_MODE_QUIET,
} DiffMode;

typedef enum {
	DIFF_DISTANCE_UNKNOWN = 0,
	DIFF_DISTANCE_MYERS,
	DIFF_DISTANCE_LEVENSHTEIN,
	DIFF_DISTANCE_LCS_ROLLING,
	DIFF_DISTANCE_SSDEEP,
} DiffDistance;

typedef enum {
	DIFF_TYPE_UNKNOWN = 0,
	DIFF_TYPE_BYTES,
	DIFF_TYPE_CLASSES,
	DIFF_TYPE_COMMAND,
	DIFF_TYPE_ENTRIES,
	DIFF_TYPE_FIELDS,
	DIFF_TYPE_FUNCTIONS,
	DIFF_TYPE_IMPORTS,
	DIFF_TYPE_LIBRARIES,
	DIFF_TYPE_LINES,
	DIFF_TYPE_SECTIONS,
	DIFF_TYPE_STRINGS,
	DIFF_TYPE_SYMBOLS,
	DIFF_TYPE_PLOTDIFF,
} DiffType;

typedef enum {
	DIFF_OPT_UNKNOWN = 0,
	DIFF_OPT_ERROR,
	DIFF_OPT_HELP,
	DIFF_OPT_USAGE,
	DIFF_OPT_VERSION,
	DIFF_OPT_DISTANCE,
	DIFF_OPT_UNIFIED,
	DIFF_OPT_GRAPH,
	DIFF_OPT_HEX_VISUAL,
} DiffOption;

typedef struct diff_screen_t {
	int width;
	int height;
} DiffScreen;

typedef struct diff_context_t {
	DiffType type;
	DiffMode mode;
	DiffOption option;
	DiffDistance distance;
	ut32 arch_bits;
	bool compare_addresses;
	bool show_time;
	bool colors;
	bool analyze_all;
	bool command_line;
	bool verbose;
	const char *architecture;
	const char *input_a;
	const char *input_b;
	const char *file_a;
	const char *file_b;
	const char *theme_name;
	DiffScreen screen;
	RzList /*<char *>*/ *evars;
} DiffContext;

typedef struct diff_io_t {
	const char *filename;
	ut64 filesize;
	RzIO *io;
} DiffIO;

typedef struct diff_file_t {
	/* const */
	RzBinFile *file;
	RzBinPlugin *plugin;
	/* to free */
	RzBin *bin;
	DiffIO *dio;
} DiffFile;

typedef struct diff_function_t {
	char *name;
	int bits;
	ut64 address;
	int n_instructions;
} DiffFunction;

typedef struct diff_colors_t {
	const char *number;
	const char *match;
	const char *unmatch;
	const char *legenda;
	const char *reset;
} DiffColors;

typedef struct diff_hex_view_t {
	char *line;
	ut8 *buffer_a;
	ut8 *buffer_b;
	ut64 size_a;
	ut64 size_b;
	ut64 address_a;
	ut64 address_b;
	DiffIO *io_a;
	DiffIO *io_b;
	bool column_descr;
	DiffColors colors;
	RzConsCanvas *canvas;
	DiffScreen screen;
} DiffHexView;

#define rz_diff_error(f, ...) \
	RZ_LOG_ERROR("rz-diff: error, " f, ##__VA_ARGS__)

#define rz_diff_error_ret(fail, f, ...) \
	RZ_LOG_ERROR("rz-diff: error, " f, ##__VA_ARGS__); \
	return (fail)

#define rz_diff_error_opt(x, o, f, ...) \
	(x)->option = o; \
	RZ_LOG_ERROR("rz-diff: error, " f, ##__VA_ARGS__); \
	return

#define rz_diff_ctx_set(x, k, v) (x)->k = (v)

#define rz_diff_set_def(x, d, v) \
	do { \
		if ((x) != (d)) { \
			RZ_LOG_ERROR("rz-diff: error, invalid combination of arguments for '-%c' (expected " #d " but found something else)\n", c); \
			return; \
		} \
		(x) = (v); \
	} while (0)

#define rz_diff_ctx_set_def(x, k, d, v) \
	do { \
		if ((x)->k != (d)) { \
			rz_diff_error_opt(x, DIFF_OPT_UNKNOWN, "invalid combination of arguments for '-%c' (expected " #d " but found something else)\n", c); \
		} \
		(x)->k = (v); \
	} while (0)

#define rz_diff_ctx_set_val(x, k, d, v) \
	do { \
		if ((x)->k != (d)) { \
			rz_diff_error_opt(x, DIFF_OPT_UNKNOWN, "invalid combination of arguments for '-%c' (expected " #d " but found something else)\n", c); \
		} \
		(x)->k = (v); \
	} while (0)

#define rz_diff_ctx_set_unsigned(x, k, i) \
	do { \
		(x)->k = strtoull((i), NULL, 0); \
		if ((x)->k < 1) { \
			rz_diff_error_opt(x, DIFF_OPT_UNKNOWN, "argument must be > 0\n"); \
		} \
	} while (0)

#define rz_diff_ctx_add_evar(x, o) \
	do { \
		char *copy = rz_str_dup(o); \
		if (!copy || !rz_list_append((x)->evars, copy)) { \
			free(copy); \
			rz_diff_error_opt(x, DIFF_OPT_ERROR, "cannot add evar '%s' to list\n", o); \
		} \
	} while (0)

#define rz_diff_ctx_set_dist(x, t) rz_diff_ctx_set_def(x, distance, DIFF_DISTANCE_UNKNOWN, t)
#define rz_diff_ctx_set_type(x, t) rz_diff_ctx_set_def(x, type, DIFF_TYPE_UNKNOWN, t)
#define rz_diff_ctx_set_mode(x, m) rz_diff_ctx_set_def(x, mode, DIFF_MODE_STANDARD, m)
#define rz_diff_ctx_set_opt(x, o)  rz_diff_ctx_set_def(x, option, DIFF_OPT_UNKNOWN, o)

static void rz_diff_show_help(bool usage_only) {
	printf("%s%s%s", Color_CYAN, "Usage: ", Color_RESET);
	printf("rz-diff [options] <file0> <file1>\n");
	if (usage_only) {
		return;
	}
	const char *options[] = {
		// clang-format off
		"-a",       "arch",         "Specify architecture plugin to use (x86, arm, ..)",
		"-b",       "bits",         "Specify register size for arch (16 (thumb), 32, 64, ..)",
		"-d",       "myers",        "Compute edit distance using Eugene W. Myers' O(ND) algorithm (no substitution)",
		"-d",       "leven",        "Compute edit distance using Levenshtein O(N^2) algorithm (with substitution)",
		"-d",       "lcs-roll",     "Compute edit distance using Myers+FastCDC with chunk size " RZ_STR_DEF(RZ_DIFF_LCS_ROLL_DEFAULT_BLOCK_SIZE) " (-0 to change chunk size)",
		"-d",       "ssdeep",       "Compute edit distance using Context triggered piecewise hashing comparison",
		"-i",       "",             "Use command line arguments instead of files (only for -d)",
		"-H",       "",             "Hexadecimal visual mode",
		"-h",       "",             "Show this help",
		"-j",       "",             "JSON output",
		"-q",       "",             "Quiet output",
		"-v",       "",             "Show version information",
		"-V",       "",             "Be more verbose (stderr output)",
		"-K",       "theme",        "Set a give color theme (see rizin 'eco' command)",
		"-e",       "k=v",          "Set an evaluable config variable",
		"-A",       "",             "Compare virtual and physical addresses",
		"-B",       "",             "Run 'aaa' when loading the bin",
		"-C",       "",             "Disable colors",
		"-T",       "",             "Show timestamp information",
		"-S",       "WxH",          "Set the width and height of the terminal for visual mode",
		"-0",       "cmd",          "Input for file0 when option -t 'commands' is given.",
		"",         "",             "The same value will be set for file1, if -1 is not set.",
		"-1",       "cmd",          "Input for file1 when option -t 'commands' is given.",
		"-t",       "bytes",        "Compare raw bytes in the files (only for small files)",
		"-t",       "lines",        "Compare text files",
		"-t",       "functions",    "Compare functions found in the files",
		"",         "",             "optional -0 <fcn name|offset> to compare only one function",
		"-t",       "classes",      "Compare classes found in the files",
		"-t",       "command",      "Compare command output returned when executed in both files",
		"",         "",             "requires -0 <cmd> and -1 <cmd> is optional",
		"-t",       "entries",      "Compare entries found in the files",
		"-t",       "fields",       "Compare fields found in the files",
		"-t",       "graphs",       "Compare 2 functions and outputs in graphviz/dot format",
		"",         "",             "requires -0 <fcn name|offset> and -1 <fcn name|offset> is optional",
		"-t",       "imports",      "Compare imports found in the files",
		"-t",       "libraries",    "Compare libraries found in the files",
		"-t",       "sections",     "Compare sections found in the files",
		"-t",       "strings",      "Compare strings found in the files",
		"-t",       "symbols",      "Compare symbols found in the files",
		// clang-format on
	};
	rz_print_colored_help(options, RZ_ARRAY_SIZE(options), false);
	printf(
		"Palette colors can be changed by adding the following lines inside the $HOME/.rizinrc file\n"
		"  ec diff.unknown blue  | offset color\n"
		"  ec diff.match   green | match color\n"
		"  ec diff.unmatch red   | mismatch color\n"
		"Environment variables\n"
		"  RZ_COLOR              | enables/disables colors support\n");
}

static bool rz_diff_is_file(const char *file) {
	if (IS_NULLSTR(file)) {
		rz_diff_error_ret(false, "cannot open a file without a name.\n");
	}
	if (rz_file_is_directory(file)) {
		rz_diff_error_ret(false, "cannot open directories (%s).\n", file);
	}
	return true;
}

static bool diff_env_get_bool(const char *key, bool def_value) {
	bool value = def_value;
	char *tmp = rz_sys_getenv(key);
	if (RZ_STR_ISNOTEMPTY(tmp)) {
		value = rz_num_get(NULL, tmp) != 0;
	}
	free(tmp);
	return value;
}

static void rz_diff_parse_arguments(int argc, const char **argv, DiffContext *ctx) {
	const char *type = NULL;
	const char *algorithm = NULL;
	const char *screen = NULL;
	memset((void *)ctx, 0, sizeof(DiffContext));
	ctx->colors = diff_env_get_bool("RZ_COLOR", true);
	ctx->evars = rz_list_newf(free);

	if (!ctx->evars) {
		rz_diff_error_opt(ctx, DIFF_OPT_ERROR, "cannot allocate list for evars");
		return;
	}

	RzGetopt opt;
	int c;
	rz_getopt_init(&opt, argc, argv, "hHjqvViABCTa:b:K:e:d:t:0:1:S:");
	while ((c = rz_getopt_next(&opt)) != -1) {
		switch (c) {
		case '0': rz_diff_ctx_set_def(ctx, input_a, NULL, opt.arg); break;
		case '1': rz_diff_ctx_set_def(ctx, input_b, NULL, opt.arg); break;
		case 'A': rz_diff_ctx_set_def(ctx, compare_addresses, false, true); break;
		case 'B': rz_diff_ctx_set_def(ctx, analyze_all, false, true); break;
		case 'C': rz_diff_ctx_set_def(ctx, colors, ctx->colors, false); break;
		case 'T': rz_diff_ctx_set_def(ctx, show_time, false, true); break;
		case 'a': rz_diff_ctx_set_def(ctx, architecture, NULL, opt.arg); break;
		case 'b': rz_diff_ctx_set_unsigned(ctx, arch_bits, opt.arg); break;
		case 'd': rz_diff_set_def(algorithm, NULL, opt.arg); break;
		case 'h': rz_diff_ctx_set_opt(ctx, DIFF_OPT_HELP); break;
		case 'i': rz_diff_ctx_set_def(ctx, command_line, false, true); break;
		case 'j': rz_diff_ctx_set_mode(ctx, DIFF_MODE_JSON); break;
		case 'q': rz_diff_ctx_set_mode(ctx, DIFF_MODE_QUIET); break;
		case 't': rz_diff_set_def(type, NULL, opt.arg); break;
		case 'v': rz_diff_ctx_set_opt(ctx, DIFF_OPT_VERSION); break;
		case 'V': rz_diff_ctx_set_def(ctx, verbose, false, true); break;
		case 'S': rz_diff_set_def(screen, NULL, opt.arg); break;
		case 'H': rz_diff_ctx_set_opt(ctx, DIFF_OPT_HEX_VISUAL); break;
		case 'e': rz_diff_ctx_add_evar(ctx, opt.arg); break;
		case 'K': rz_diff_set_def(ctx->theme_name, NULL, opt.arg); break;
		default:
			rz_diff_error_opt(ctx, DIFF_OPT_ERROR, "unknown flag '%c'\n", c);
		}
	}

	if (ctx->option == DIFF_OPT_HELP ||
		ctx->option == DIFF_OPT_VERSION) {
		return;
	}

	if (opt.ind >= argc || (argc - opt.ind) != 2) {
		rz_diff_error_opt(ctx, DIFF_OPT_USAGE, "expected 2 files but got %d.\n", (argc - opt.ind));
	}

	ctx->file_a = argv[opt.ind + 0];
	ctx->file_b = argv[opt.ind + 1];

	if (!rz_diff_is_file(ctx->file_a) ||
		!rz_diff_is_file(ctx->file_b)) {
		ctx->option = DIFF_OPT_USAGE;
		return;
	}

	if (algorithm) {
		if (type) {
			rz_diff_error_opt(ctx, DIFF_OPT_ERROR, "option -t argument is not compatible with -d.\n");
		} else if (ctx->show_time) {
			rz_diff_error_opt(ctx, DIFF_OPT_ERROR, "option -T argument is not compatible with -d.\n");
		}

		rz_diff_ctx_set_opt(ctx, DIFF_OPT_DISTANCE);
		if (!strcmp(algorithm, "myers")) {
			rz_diff_ctx_set_dist(ctx, DIFF_DISTANCE_MYERS);
		} else if (!strcmp(algorithm, "leven")) {
			rz_diff_ctx_set_dist(ctx, DIFF_DISTANCE_LEVENSHTEIN);
		} else if (!strcmp(algorithm, "lcs-roll")) {
			rz_diff_ctx_set_dist(ctx, DIFF_DISTANCE_LCS_ROLLING);
		} else if (!strcmp(algorithm, "ssdeep")) {
			rz_diff_ctx_set_dist(ctx, DIFF_DISTANCE_SSDEEP);
		} else {
			rz_diff_error_opt(ctx, DIFF_OPT_ERROR, "option -d argument '%s' is not a recognized algorithm.\n", algorithm);
		}
	} else if (type) {
		rz_diff_ctx_set_opt(ctx, DIFF_OPT_UNIFIED);

		if (ctx->command_line) {
			rz_diff_error_opt(ctx, DIFF_OPT_ERROR, "option -i is not supported with -t flag.\n");
		}

		if (!strcmp(type, "bytes")) {
			rz_diff_ctx_set_type(ctx, DIFF_TYPE_BYTES);
		} else if (!strcmp(type, "lines")) {
			rz_diff_ctx_set_type(ctx, DIFF_TYPE_LINES);
		} else if (!strcmp(type, "functions")) {
			if (ctx->input_b) {
				rz_diff_error_opt(ctx, DIFF_OPT_ERROR, "option -t '%s' does not support -1.\n", type);
			}
			ctx->option = DIFF_OPT_GRAPH;
			rz_diff_ctx_set_type(ctx, DIFF_TYPE_FUNCTIONS);
		} else if (!strcmp(type, "classes")) {
			rz_diff_ctx_set_type(ctx, DIFF_TYPE_CLASSES);
		} else if (!strcmp(type, "command")) {
			if (!ctx->input_a) {
				rz_diff_error_opt(ctx, DIFF_OPT_ERROR, "option -t '%s' requires -0 <command>.\n", type);
			}
			if (!ctx->input_b) {
				ctx->input_b = ctx->input_a;
			}
			rz_diff_ctx_set_type(ctx, DIFF_TYPE_COMMAND);
		} else if (!strcmp(type, "entries")) {
			rz_diff_ctx_set_type(ctx, DIFF_TYPE_ENTRIES);
		} else if (!strcmp(type, "fields")) {
			rz_diff_ctx_set_type(ctx, DIFF_TYPE_FIELDS);
		} else if (!strcmp(type, "graphs")) {
			if (!ctx->input_a) {
				rz_diff_error_opt(ctx, DIFF_OPT_ERROR, "option -t '%s' requires -0 <fcn name|address>.\n", type);
			} else if (ctx->input_a && !ctx->input_b) {
				ctx->input_b = ctx->input_a;
			}
			ctx->option = DIFF_OPT_GRAPH;
			rz_diff_ctx_set_type(ctx, DIFF_TYPE_PLOTDIFF);
		} else if (!strcmp(type, "imports")) {
			rz_diff_ctx_set_type(ctx, DIFF_TYPE_IMPORTS);
		} else if (!strcmp(type, "libraries")) {
			rz_diff_ctx_set_type(ctx, DIFF_TYPE_LIBRARIES);
		} else if (!strcmp(type, "sections")) {
			rz_diff_ctx_set_type(ctx, DIFF_TYPE_SECTIONS);
		} else if (!strcmp(type, "strings")) {
			rz_diff_ctx_set_type(ctx, DIFF_TYPE_STRINGS);
		} else if (!strcmp(type, "symbols")) {
			rz_diff_ctx_set_type(ctx, DIFF_TYPE_SYMBOLS);
		} else {
			rz_diff_error_opt(ctx, DIFF_OPT_ERROR, "option -t argument '%s' is not a recognized type.\n", type);
		}
	} else if (screen) {
		const char *hp = NULL;
		if (!(hp = strchr(screen, 'x'))) {
			rz_diff_error_opt(ctx, DIFF_OPT_USAGE, "invalid format for -S; example 120x20 where width=120 and height=20.\n");
		}
		ut64 height = strtoull(screen, NULL, 0);
		ut64 width = strtoull(hp + 1, NULL, 0);
		if (width < 1 || width > 0xFFFF || height < 1 || height > 0xFFFF) {
			rz_diff_error_opt(ctx, DIFF_OPT_USAGE, "invalid format for -S; example 120x20 where width=120 and height=20.\n");
		} else if (width < 20 || height < 120) {
			rz_diff_error_opt(ctx, DIFF_OPT_USAGE, "Min width=120, Min height=20.\n");
		}
		ctx->screen.width = (int)width;
		ctx->screen.height = (int)height;
	} else if (ctx->option == DIFF_OPT_UNKNOWN) {
		rz_diff_error_opt(ctx, DIFF_OPT_USAGE, "option -t or -d is required to be specified.\n");
	}
}

static void rz_diff_get_colors(DiffColors *dcolors, RzConsContext *ctx, bool colors) {
	dcolors->number = colors ? ctx->pal.diff_unknown : "";
	dcolors->match = colors ? ctx->pal.diff_match : "";
	dcolors->unmatch = colors ? ctx->pal.diff_unmatch : Color_INVERT;
	dcolors->legenda = colors ? ctx->pal.comment : "";
	dcolors->reset = Color_RESET;
}

static DiffIO *rz_diff_io_open(const char *file) {
	RzIODesc *desc = NULL;
	RzIO *io = NULL;
	DiffIO *dio = NULL;

	dio = RZ_NEW0(DiffIO);
	if (!dio) {
		rz_diff_error("cannot allocate diff io\n");
		goto rz_diff_io_open_end;
	}

	io = rz_io_new();
	if (!io) {
		rz_diff_error("cannot allocate io\n");
		goto rz_diff_io_open_end;
	}

	desc = rz_io_open_nomap(io, file, RZ_PERM_R, 0);
	if (!desc) {
		rz_diff_error("cannot open file '%s'\n", file);
		goto rz_diff_io_open_end;
	}

	dio->filename = file;
	dio->filesize = rz_io_desc_size(desc);
	dio->io = io;
	return dio;

rz_diff_io_open_end:
	rz_io_desc_close(desc);
	rz_io_free(io);
	free(dio);
	return NULL;
}

static void rz_diff_io_close(DiffIO *dio) {
	if (!dio) {
		return;
	}
	rz_io_desc_close(dio->io->desc);
	rz_io_free(dio->io);
	free(dio);
}

/* This is terrible because can eat a lot of memory */
static ut8 *rz_diff_slurp_file(const char *file, size_t *size) {
	ut8 *buffer = NULL;
	ssize_t read = 0;
	DiffIO *dio = NULL;

	dio = rz_diff_io_open(file);
	if (!dio) {
		goto rz_diff_slurp_file_end;
	}

	buffer = malloc(dio->filesize + 1);
	if (!buffer) {
		rz_diff_error("cannot allocate buffer\n");
		goto rz_diff_slurp_file_end;
	}
	buffer[dio->filesize] = 0;

	read = rz_io_pread_at(dio->io, 0, buffer, dio->filesize);
	if (read != dio->filesize) {
		free(buffer);
		buffer = NULL;
		rz_diff_error("cannot read buffer correctly\n");
		goto rz_diff_slurp_file_end;
	}

	*size = dio->filesize;

rz_diff_slurp_file_end:
	rz_diff_io_close(dio);
	return buffer;
}

static ut32 check_distance_max_size(ut32 size, const char *algo, const char *name) {
	const ut32 max_size = RZ_DIFF_DISTANCE_MAX_SIZE;
	if (size <= max_size) {
		return size;
	}

	RZ_LOG_WARN("diff: length of buffer %s exceeds the limit size for %s distance (size: %u > max: %u); buffer length has been changed.\n", name, algo, size, max_size);
	return max_size;
}

static bool rz_diff_calculate_distance(DiffContext *ctx) {
	size_t a_size = 0;
	size_t b_size = 0;
	ut8 *a_buffer = NULL;
	ut8 *b_buffer = NULL;
	ut32 distance = 0;
	st32 chunk_size = 0;
	double similarity = 0.0;

	if (ctx->command_line) {
		if (!(a_buffer = (ut8 *)rz_str_dup(ctx->file_a))) {
			goto rz_diff_calculate_distance_bad;
		}
		a_size = strlen((const char *)a_buffer);
		if (!(b_buffer = (ut8 *)rz_str_dup(ctx->file_b))) {
			goto rz_diff_calculate_distance_bad;
		}
		b_size = strlen((const char *)b_buffer);
	} else {
		if (!(a_buffer = rz_diff_slurp_file(ctx->file_a, &a_size))) {
			goto rz_diff_calculate_distance_bad;
		}
		if (!(b_buffer = rz_diff_slurp_file(ctx->file_b, &b_size))) {
			goto rz_diff_calculate_distance_bad;
		}
	}

	switch (ctx->distance) {
	case DIFF_DISTANCE_MYERS:
		a_size = check_distance_max_size(a_size, "myers", "file0");
		b_size = check_distance_max_size(b_size, "myers", "file1");
		if (!rz_diff_myers_distance(a_buffer, a_size, b_buffer, b_size, &distance, &similarity)) {
			rz_diff_error("failed to calculate distance with myers algorithm\n");
			goto rz_diff_calculate_distance_bad;
		}
		break;
	case DIFF_DISTANCE_LEVENSHTEIN:
		a_size = check_distance_max_size(a_size, "levenshtein", "file0");
		b_size = check_distance_max_size(b_size, "levenshtein", "file1");
		if (!rz_diff_levenshtein_distance(a_buffer, a_size, b_buffer, b_size, &distance, &similarity)) {
			rz_diff_error("failed to calculate distance with levenshtein algorithm\n");
			goto rz_diff_calculate_distance_bad;
		}
		break;
	case DIFF_DISTANCE_LCS_ROLLING:
		chunk_size = RZ_DIFF_LCS_ROLL_DEFAULT_BLOCK_SIZE;
		if (ctx->input_a) {
			chunk_size = atoi(ctx->input_a);
			if (chunk_size < 1) {
				rz_diff_error("invalid to chunk size (%d)\n", chunk_size);
				goto rz_diff_calculate_distance_bad;
			}
		}
		if (!rz_diff_lcs_rolling_distance(a_buffer, a_size, b_buffer, b_size, chunk_size, &distance, &similarity)) {
			rz_diff_error("failed to calculate distance with lcs rolling algorithm with chunk size %d\n", chunk_size);
			goto rz_diff_calculate_distance_bad;
		}
		break;
	case DIFF_DISTANCE_SSDEEP:
		if ((similarity = rz_hash_ssdeep_compare((const char *)a_buffer, (const char *)b_buffer)) < 0) {
			rz_diff_error("failed to calculate distance with ssdeep compare algorithm\n");
			goto rz_diff_calculate_distance_bad;
		}
		break;
	default:
		rz_diff_error("unknown distance algorithm\n");
		goto rz_diff_calculate_distance_bad;
	}

	if (ctx->mode == DIFF_MODE_JSON) {
		PJ *pj = pj_new();
		if (!pj) {
			rz_diff_error("failed to allocate json\n");
			goto rz_diff_calculate_distance_bad;
		}
		pj_o(pj);
		pj_kd(pj, "similarity", similarity);
		if (ctx->distance != DIFF_DISTANCE_SSDEEP) {
			pj_kn(pj, "distance", distance);
		}
		if (ctx->distance == DIFF_DISTANCE_LCS_ROLLING) {
			pj_kn(pj, "chunk_size", chunk_size);
		}
		pj_end(pj);
		printf("%s\n", pj_string(pj));
		pj_free(pj);
	} else if (ctx->mode == DIFF_MODE_QUIET) {
		printf("%.3f\n", similarity);
		if (ctx->distance != DIFF_DISTANCE_SSDEEP) {
			printf("%d\n", distance);
		}
	} else {
		// DIFF_MODE_STANDARD
		printf("similarity: %.3f\n", similarity);
		if (ctx->distance != DIFF_DISTANCE_SSDEEP) {
			printf("distance: %u\n", distance);
		}
		if (ctx->distance == DIFF_DISTANCE_LCS_ROLLING) {
			printf("chunksize: %d\n", chunk_size);
		}
	}
	free(a_buffer);
	free(b_buffer);
	return true;

rz_diff_calculate_distance_bad:
	free(a_buffer);
	free(b_buffer);
	return false;
}

static inline RzBinFile *core_get_file(RzCoreFile *cfile) {
	return rz_pvector_at(&cfile->binfiles, 0);
}

static RzCoreFile *rz_diff_load_file_with_core(const char *filename, const char *architecture, ut32 arch_bits, RzList /*<char *>*/ *evars, bool colors, bool verbose) {
	RzCore *core = NULL;
	RzCoreFile *cfile = NULL;
	RzBinFile *bfile = NULL;
	RzListIter *it;
	char *config;

	if (verbose) {
		fprintf(stderr, "rz-diff: loading file '%s'\n", filename);
	}

	core = rz_core_new();
	if (!core) {
		rz_diff_error("cannot allocate core\n");
		goto rz_diff_load_file_with_core_fail;
	}
	rz_core_loadlibs(core, RZ_CORE_LOADLIBS_ALL);

	rz_config_set_i(core->config, "scr.color", colors ? 1 : 0);
	rz_config_set_b(core->config, "scr.interactive", false);
	rz_config_set_b(core->config, "cfg.debug", false);
	rz_config_set_b(core->config, "scr.prompt", false);
	core->print->scr_prompt = false;
	cfile = rz_core_file_open(core, filename, 0, 0);
	if (!cfile) {
		rz_diff_error("cannot open file '%s'\n", filename);
		goto rz_diff_load_file_with_core_fail;
	}

	if (!rz_core_bin_load(core, NULL, UT64_MAX)) {
		rz_diff_error("cannot load file '%s'\n", filename);
		goto rz_diff_load_file_with_core_fail;
	}

	if (!rz_core_bin_update_arch_bits(core)) {
		rz_diff_error("cannot set architecture with bits\n");
		goto rz_diff_load_file_with_core_fail;
	}

	bfile = core_get_file(cfile);
	if (!bfile) {
		rz_diff_error("cannot get architecture with bits\n");
		goto rz_diff_load_file_with_core_fail;
	}

	if (rz_pvector_empty(bfile->o->maps)) {
		rz_config_set_i(core->config, "io.va", false);
	}

	if (architecture) {
		rz_config_set(core->config, "asm.arch", architecture);
	}

	if (arch_bits) {
		rz_config_set_i(core->config, "asm.bits", arch_bits);
	}

	rz_list_foreach (evars, it, config) {
		rz_config_eval(core->config, config);
	}

	return cfile;

rz_diff_load_file_with_core_fail:
	rz_core_free(core);
	return NULL;
}

static bool rz_diff_file_open(DiffFile *dfile, const char *filename) {
	memset((void *)dfile, 0, sizeof(DiffFile));
	RzBinOptions opt = { 0 };
	RzBinFile *file = NULL;
	RzBin *bin = NULL;
	DiffIO *dio = NULL;

	dio = rz_diff_io_open(filename);
	if (!dio) {
		goto rz_diff_file_open_bad;
	}

	bin = rz_bin_new();
	if (!bin) {
		rz_diff_error("cannot allocate bin\n");
		goto rz_diff_file_open_bad;
	}

	rz_io_bind(dio->io, &bin->iob);

	// TODO: no RzConfig ???
	rz_bin_options_init(&opt, dio->io->desc->fd, 0, 0, false);
	opt.obj_opts.elf_load_sections = true;
	opt.obj_opts.elf_checks_sections = true;
	opt.obj_opts.elf_checks_segments = true;
	opt.sz = rz_io_desc_size(dio->io->desc);

	file = rz_bin_open_io(bin, &opt);
	if (!file) {
		rz_diff_error("cannot open bin file via io\n");
		goto rz_diff_file_open_bad;
	}

	dfile->plugin = rz_bin_file_cur_plugin(file);
	dfile->file = file;
	dfile->bin = bin;
	dfile->dio = dio;
	return true;

rz_diff_file_open_bad:
	rz_bin_free(bin);
	rz_diff_io_close(dio);
	return false;
}

static void rz_diff_file_close(DiffFile *file) {
	// plugin and file are freed by rz_bin_free
	rz_bin_free(file->bin);
	rz_diff_io_close(file->dio);
}

#define rz_diff_file_get(df, n) ((df)->file->o->n)

/**************************************** rzpvector ***************************************/

static const void *rz_diff_pvector_elem_at(const RzPVector /*<void *>*/ *array, ut32 index) {
	return rz_pvector_at(array, index);
}

/**************************************** imports ***************************************/

static ut32 import_hash(const RzBinImport *elem) {
	ut32 hash = rz_diff_hash_data((const ut8 *)elem->name, strlen(elem->name));
	hash ^= rz_diff_hash_data((const ut8 *)elem->bind, strlen(SAFE_STR(elem->bind)));
	hash ^= rz_diff_hash_data((const ut8 *)elem->type, strlen(SAFE_STR(elem->type)));
	return hash;
}

static void import_stringify(const RzBinImport *elem, RzStrBuf *sb) {
	rz_strbuf_setf(sb, "%-7s %-7s %s\n", SAFE_STR_DEF(elem->bind, RZ_BIN_BIND_UNKNOWN_STR),
		SAFE_STR_DEF(elem->type, RZ_BIN_TYPE_UNKNOWN_STR), elem->name);
}

static int import_compare(const RzBinImport *a, const RzBinImport *b) {
	int ret;
	IF_STRCMP_S(ret, a->name, b->name);
	IF_STRCMP_S(ret, a->bind, b->bind);
	IF_STRCMP_S(ret, a->type, b->type);
	return 0;
}

static int import_compare_vec(const RzBinImport *a, const RzBinImport *b, void *user) {
	return import_compare(a, b);
}

static RzDiff *rz_diff_imports_new(DiffFile *dfile_a, DiffFile *dfile_b) {
	RzPVector *vec_a = NULL;
	RzPVector *vec_b = NULL;

	vec_a = rz_diff_file_get(dfile_a, imports);
	if (!vec_a) {
		rz_diff_error_ret(NULL, "cannot get imports from '%s'\n", dfile_a->dio->filename);
	}

	vec_b = rz_diff_file_get(dfile_b, imports);
	if (!vec_b) {
		rz_diff_error_ret(NULL, "cannot get imports from '%s'\n", dfile_b->dio->filename);
	}

	rz_pvector_sort(vec_a, (RzPVectorComparator)import_compare_vec, NULL);
	rz_pvector_sort(vec_b, (RzPVectorComparator)import_compare_vec, NULL);

	RzDiffMethods methods = {
		.elem_at = (RzDiffMethodElemAt)rz_diff_pvector_elem_at,
		.elem_hash = (RzDiffMethodElemHash)import_hash,
		.compare = (RzDiffMethodCompare)import_compare,
		.stringify = (RzDiffMethodStringify)import_stringify,
		.ignore = NULL,
	};

	return rz_diff_generic_new(vec_a, rz_pvector_len(vec_a), vec_b, rz_pvector_len(vec_b), &methods);
}

/**************************************** symbols ***************************************/

static ut32 symbol_hash_addr(const RzBinSymbol *elem) {
	ut32 hash = rz_diff_hash_data((const ut8 *)elem->name, elem->name ? strlen(elem->name) : 0);
	hash ^= rz_diff_hash_data((const ut8 *)elem->dname, elem->dname ? strlen(elem->dname) : 0);
	hash ^= rz_diff_hash_data((const ut8 *)elem->libname, elem->libname ? strlen(elem->libname) : 0);
	hash ^= rz_diff_hash_data((const ut8 *)elem->classname, elem->classname ? strlen(elem->classname) : 0);
	hash ^= (ut32)(elem->vaddr >> 32);
	hash ^= (ut32)elem->vaddr;
	hash ^= (ut32)(elem->paddr >> 32);
	hash ^= (ut32)elem->paddr;
	return hash;
}

static int symbol_compare_addr(const RzBinSymbol *a, const RzBinSymbol *b) {
	st64 ret;
	IF_STRCMP_S(ret, a->classname, b->classname);
	IF_STRCMP_S(ret, a->libname, b->libname);
	IF_STRCMP_S(ret, a->dname, b->dname);
	IF_STRCMP_S(ret, a->name, b->name);
	ret = ((st64)b->paddr) - ((st64)a->paddr);
	if (ret) {
		return ret;
	}
	return ((st64)b->vaddr) - ((st64)a->vaddr);
}

static void symbol_stringify_addr(const RzBinSymbol *elem, RzStrBuf *sb) {
	rz_strbuf_setf(sb, "virt: 0x%016" PFMT64x " phys: 0x%016" PFMT64x " %s %s %s\n", elem->vaddr, elem->paddr, elem->libname, elem->classname, elem->name);
}

static ut32 symbol_hash(const RzBinSymbol *elem) {
	return rz_diff_hash_data((const ut8 *)elem->name, strlen(elem->name));
}

static int symbol_compare(const RzBinSymbol *a, const RzBinSymbol *b) {
	int ret;
	IF_STRCMP_S(ret, a->name, b->name);
	return 0;
}

static int symbol_compare_vec(const RzBinSymbol *a, const RzBinSymbol *b, void *user) {
	return symbol_compare(a, b);
}

static void symbol_stringify(const RzBinSymbol *elem, RzStrBuf *sb) {
	rz_strbuf_setf(sb, "%s %s %s\n", elem->libname, elem->classname, elem->name);
}

static RzDiff *rz_diff_symbols_new(DiffFile *dfile_a, DiffFile *dfile_b, bool compare_addr) {
	RzPVector *vec_a = NULL;
	RzPVector *vec_b = NULL;

	vec_a = rz_diff_file_get(dfile_a, symbols);
	if (!vec_a) {
		rz_diff_error_ret(NULL, "cannot get symbols from '%s'\n", dfile_a->dio->filename);
	}

	vec_b = rz_diff_file_get(dfile_b, symbols);
	if (!vec_b) {
		rz_diff_error_ret(NULL, "cannot get symbols from '%s'\n", dfile_b->dio->filename);
	}

	rz_pvector_sort(vec_a, (RzPVectorComparator)symbol_compare_vec, NULL);
	rz_pvector_sort(vec_b, (RzPVectorComparator)symbol_compare_vec, NULL);

	RzDiffMethods methods = {
		.elem_at = (RzDiffMethodElemAt)rz_diff_pvector_elem_at,
		.elem_hash = (RzDiffMethodElemHash)(compare_addr ? symbol_hash_addr : symbol_hash),
		.compare = (RzDiffMethodCompare)(compare_addr ? symbol_compare_addr : symbol_compare),
		.stringify = (RzDiffMethodStringify)(compare_addr ? symbol_stringify_addr : symbol_stringify),
		.ignore = NULL,
	};

	return rz_diff_generic_new(vec_a, rz_pvector_len(vec_a), vec_b, rz_pvector_len(vec_b), &methods);
}

/**************************************** strings ***************************************/

static ut32 string_hash_addr(const RzBinString *elem) {
	ut32 hash = rz_diff_hash_data((const ut8 *)elem->string, elem->size);
	hash ^= (ut32)(elem->vaddr >> 32);
	hash ^= (ut32)elem->vaddr;
	hash ^= (ut32)(elem->paddr >> 32);
	hash ^= (ut32)elem->paddr;
	return hash;
}

static int string_compare_addr(const RzBinString *a, const RzBinString *b) {
	st64 ret;
	ret = ((st64)b->size) - ((st64)a->size);
	if (ret) {
		return ret;
	}
	IF_STRCMP_S(ret, a->string, b->string);
	ret = ((st64)b->paddr) - ((st64)a->paddr);
	if (ret) {
		return ret;
	}
	return ((st64)b->vaddr) - ((st64)a->vaddr);
}

static void string_stringify_addr(const RzBinString *elem, RzStrBuf *sb) {
	rz_strbuf_setf(sb, "virt: 0x%016" PFMT64x " phys: 0x%016" PFMT64x " %s\n", elem->vaddr, elem->paddr, elem->string);
}

static ut32 string_hash(const RzBinString *elem) {
	return rz_diff_hash_data((const ut8 *)elem->string, elem->size);
}

static int string_compare(const RzBinString *a, const RzBinString *b) {
	st64 ret;
	ret = ((st64)b->size) - ((st64)a->size);
	if (ret) {
		return ret;
	}
	IF_STRCMP_S(ret, a->string, b->string);
	return 0;
}

static void string_stringify(const RzBinString *elem, RzStrBuf *sb) {
	rz_strbuf_setf(sb, "%s\n", elem->string);
}

static int string_compare_vec(const RzBinString *a, const RzBinString *b, void *user) {
	return string_compare(a, b);
}

static RzDiff *rz_diff_strings_new(DiffFile *dfile_a, DiffFile *dfile_b, bool compare_addr) {
	RzPVector *vec_a = NULL;
	RzPVector *vec_b = NULL;

	vec_a = (RzPVector *)rz_bin_object_get_strings(dfile_a->file->o);
	if (!vec_a) {
		rz_diff_error_ret(NULL, "cannot get strings from '%s'\n", dfile_a->dio->filename);
	}

	vec_b = (RzPVector *)rz_bin_object_get_strings(dfile_b->file->o);
	if (!vec_b) {
		rz_diff_error_ret(NULL, "cannot get strings from '%s'\n", dfile_b->dio->filename);
	}

	rz_pvector_sort(vec_a, (RzPVectorComparator)string_compare_vec, NULL);
	rz_pvector_sort(vec_b, (RzPVectorComparator)string_compare_vec, NULL);

	RzDiffMethods methods = {
		.elem_at = (RzDiffMethodElemAt)rz_diff_pvector_elem_at,
		.elem_hash = (RzDiffMethodElemHash)(compare_addr ? string_hash_addr : string_hash),
		.compare = (RzDiffMethodCompare)(compare_addr ? string_compare_addr : string_compare),
		.stringify = (RzDiffMethodStringify)(compare_addr ? string_stringify_addr : string_stringify),
		.ignore = NULL,
	};

	return rz_diff_generic_new(vec_a, rz_pvector_len(vec_a), vec_b, rz_pvector_len(vec_b), &methods);
}

/**************************************** classes ***************************************/

static ut32 class_hash_addr(const RzBinClass *elem) {
	ut32 hash = rz_diff_hash_data((const ut8 *)elem->name, strlen(elem->name));
	hash ^= rz_diff_hash_data((const ut8 *)elem->super, strlen(elem->super));
	hash ^= (ut32)(elem->addr >> 32);
	hash ^= (ut32)elem->addr;
	return hash;
}

static int class_compare_addr(const RzBinClass *a, const RzBinClass *b) {
	int ret;
	IF_STRCMP_S(ret, a->super, b->super);
	IF_STRCMP_S(ret, a->name, b->name);
	return a->addr - b->addr;
}

static void class_stringify_addr(const RzBinClass *elem, RzStrBuf *sb) {
	rz_strbuf_setf(sb, "0x%016" PFMT64x " %s %s\n", elem->addr, SAFE_STR(elem->super), elem->name);
}

static ut32 class_hash(const RzBinClass *elem) {
	ut32 hash = rz_diff_hash_data((const ut8 *)elem->name, strlen(elem->name));
	hash ^= rz_diff_hash_data((const ut8 *)elem->super, strlen(elem->super));
	return hash;
}

static int class_compare(const RzBinClass *a, const RzBinClass *b) {
	int ret;
	IF_STRCMP_S(ret, a->super, b->super);
	IF_STRCMP_S(ret, a->name, b->name);
	return 0;
}

static int class_compare_vec(const RzBinClass *a, const RzBinClass *b, void *user) {
	return class_compare(a, b);
}

static void class_stringify(const RzBinClass *elem, RzStrBuf *sb) {
	rz_strbuf_setf(sb, "%s %s\n", SAFE_STR(elem->super), elem->name);
}

static RzDiff *rz_diff_classes_new(DiffFile *dfile_a, DiffFile *dfile_b, bool compare_addr) {
	RzPVector *vec_a = NULL;
	RzPVector *vec_b = NULL;

	vec_a = rz_diff_file_get(dfile_a, classes);
	if (!vec_a) {
		rz_diff_error_ret(NULL, "cannot get classes from '%s'\n", dfile_a->dio->filename);
	}

	vec_b = rz_diff_file_get(dfile_b, classes);
	if (!vec_b) {
		rz_diff_error_ret(NULL, "cannot get classes from '%s'\n", dfile_b->dio->filename);
	}

	rz_pvector_sort(vec_a, (RzPVectorComparator)class_compare_vec, NULL);
	rz_pvector_sort(vec_b, (RzPVectorComparator)class_compare_vec, NULL);

	RzDiffMethods methods = {
		.elem_at = (RzDiffMethodElemAt)rz_diff_pvector_elem_at,
		.elem_hash = (RzDiffMethodElemHash)(compare_addr ? class_hash_addr : class_hash),
		.compare = (RzDiffMethodCompare)(compare_addr ? class_compare_addr : class_compare),
		.stringify = (RzDiffMethodStringify)(compare_addr ? class_stringify_addr : class_stringify),
		.ignore = NULL,
	};

	return rz_diff_generic_new(vec_a, rz_pvector_len(vec_a), vec_b, rz_pvector_len(vec_b), &methods);
}

/**************************************** entries ***************************************/

static ut32 entry_hash(const RzBinAddr *elem) {
	ut32 hash = rz_diff_hash_data((const ut8 *)"entry", strlen("entry"));
	hash ^= (ut32)(elem->vaddr >> 32);
	hash ^= (ut32)elem->vaddr;
	hash ^= (ut32)(elem->paddr >> 32);
	hash ^= (ut32)elem->paddr;
	hash ^= (ut32)elem->type;
	return hash;
}

static int entry_compare(const RzBinAddr *a, const RzBinAddr *b, void *user) {
	st64 ret;
	ret = ((st64)b->paddr) - ((st64)a->paddr);
	if (ret) {
		return ret;
	}
	ret = ((st64)b->vaddr) - ((st64)a->vaddr);
	if (ret) {
		return ret;
	}
	return b->type - a->type;
}

static void entry_stringify(const RzBinAddr *elem, RzStrBuf *sb) {
	const char *name = NULL;
	switch (elem->type) {
	case RZ_BIN_ENTRY_TYPE_PROGRAM:
		name = "program";
		break;
	case RZ_BIN_ENTRY_TYPE_MAIN:
		name = "main";
		break;
	case RZ_BIN_ENTRY_TYPE_INIT:
		name = "init";
		break;
	case RZ_BIN_ENTRY_TYPE_FINI:
		name = "fini";
		break;
	case RZ_BIN_ENTRY_TYPE_TLS:
		name = "tls";
		break;
	case RZ_BIN_ENTRY_TYPE_PREINIT:
		name = "preinit";
		break;
	default:
		name = "unknown";
		break;
	}
	rz_strbuf_setf(sb, "virt: 0x%016" PFMT64x " phys: 0x%016" PFMT64x " entry %s\n", elem->vaddr, elem->paddr, name);
}

static RzDiff *rz_diff_entries_new(DiffFile *dfile_a, DiffFile *dfile_b) {
	RzPVector *vec_a = NULL;
	RzPVector *vec_b = NULL;

	vec_a = rz_diff_file_get(dfile_a, entries);
	if (!vec_a) {
		rz_diff_error_ret(NULL, "cannot get entries from '%s'\n", dfile_a->dio->filename);
	}

	vec_b = rz_diff_file_get(dfile_b, entries);
	if (!vec_b) {
		rz_diff_error_ret(NULL, "cannot get entries from '%s'\n", dfile_b->dio->filename);
	}

	rz_pvector_sort(vec_a, (RzPVectorComparator)entry_compare, NULL);
	rz_pvector_sort(vec_b, (RzPVectorComparator)entry_compare, NULL);

	RzDiffMethods methods = {
		.elem_at = (RzDiffMethodElemAt)rz_diff_pvector_elem_at,
		.elem_hash = (RzDiffMethodElemHash)entry_hash,
		.compare = (RzDiffMethodCompare)entry_compare,
		.stringify = (RzDiffMethodStringify)entry_stringify,
		.ignore = NULL,
	};

	return rz_diff_generic_new(vec_a, rz_pvector_len(vec_a), vec_b, rz_pvector_len(vec_b), &methods);
}

/**************************************** libraries ***************************************/

static ut32 libs_hash(const char *elem) {
	return rz_diff_hash_data((const ut8 *)elem, strlen(elem));
}

static int libs_compare(const char *a, const char *b) {
	int ret;
	IF_STRCMP_S(ret, a, b);
	return 0;
}

static int libs_compare_vec(const char *a, const char *b, void *user) {
	return libs_compare(a, b);
}

static void libs_stringify(const char *elem, RzStrBuf *sb) {
	rz_strbuf_setf(sb, "%s\n", SAFE_STR(elem));
}

static RzDiff *rz_diff_libraries_new(DiffFile *dfile_a, DiffFile *dfile_b) {
	RzPVector *vec_a = NULL;
	RzPVector *vec_b = NULL;

	vec_a = rz_diff_file_get(dfile_a, libs);
	if (!vec_a) {
		rz_diff_error_ret(NULL, "cannot get libraries from '%s'\n", dfile_a->dio->filename);
	}

	vec_b = rz_diff_file_get(dfile_b, libs);
	if (!vec_b) {
		rz_diff_error_ret(NULL, "cannot get libraries from '%s'\n", dfile_b->dio->filename);
	}

	rz_pvector_sort(vec_a, (RzPVectorComparator)libs_compare_vec, NULL);
	rz_pvector_sort(vec_b, (RzPVectorComparator)libs_compare_vec, NULL);

	RzDiffMethods methods = {
		.elem_at = (RzDiffMethodElemAt)rz_diff_pvector_elem_at,
		.elem_hash = (RzDiffMethodElemHash)libs_hash,
		.compare = (RzDiffMethodCompare)libs_compare,
		.stringify = (RzDiffMethodStringify)libs_stringify,
		.ignore = NULL,
	};

	return rz_diff_generic_new(vec_a, rz_pvector_len(vec_a), vec_b, rz_pvector_len(vec_b), &methods);
}

/**************************************** sections ***************************************/

static ut32 section_hash_addr(const RzBinSection *elem) {
	ut32 hash = rz_diff_hash_data((const ut8 *)elem->name, strlen(elem->name));
	hash ^= elem->perm;
	hash ^= (ut32)(elem->align >> 32);
	hash ^= (ut32)elem->align;
	hash ^= (ut32)(elem->vsize >> 32);
	hash ^= (ut32)elem->vsize;
	hash ^= (ut32)(elem->size >> 32);
	hash ^= (ut32)elem->size;
	hash ^= (ut32)(elem->vaddr >> 32);
	hash ^= (ut32)elem->vaddr;
	hash ^= (ut32)(elem->paddr >> 32);
	hash ^= (ut32)elem->paddr;
	return hash;
}

static int section_compare_addr(const RzBinSection *a, const RzBinSection *b) {
	st64 ret;
	IF_STRCMP_S(ret, a->name, b->name);
	ret = ((st64)b->size) - ((st64)a->size);
	if (ret) {
		return ret;
	}
	ret = ((st64)b->vsize) - ((st64)a->vsize);
	if (ret) {
		return ret;
	}
	ret = ((st64)b->paddr) - ((st64)a->paddr);
	if (ret) {
		return ret;
	}
	ret = ((st64)b->vaddr) - ((st64)a->vaddr);
	if (ret) {
		return ret;
	}
	ret = ((st64)b->perm) - ((st64)a->perm);
	if (ret) {
		return ret;
	}
	return ((st64)b->align) - ((st64)a->align);
}

static void section_stringify_addr(const RzBinSection *elem, RzStrBuf *sb) {
	char perm[5];

	perm[0] = elem->perm & RZ_PERM_SHAR ? 's' : '-';
	perm[1] = elem->perm & RZ_PERM_R ? 'r' : '-';
	perm[2] = elem->perm & RZ_PERM_W ? 'w' : '-';
	perm[3] = elem->perm & RZ_PERM_X ? 'x' : '-';
	perm[4] = 0;

	rz_strbuf_setf(sb, "virt: 0x%016" PFMT64x ":0x%04" PFMT64x " phys: 0x%016" PFMT64x ":0x%04" PFMT64x " align: 0x%08" PFMT64x " %s %s\n",
		elem->vaddr, elem->vsize, elem->paddr, elem->size, elem->align, perm, elem->name);
}

static ut32 section_hash(const RzBinSection *elem) {
	ut32 hash = rz_diff_hash_data((const ut8 *)elem->name, strlen(elem->name));
	hash ^= elem->perm;
	hash ^= (ut32)(elem->align >> 32);
	hash ^= (ut32)elem->align;
	return hash;
}

static int section_compare(const RzBinSection *a, const RzBinSection *b) {
	st64 ret;
	IF_STRCMP_S(ret, a->name, b->name);
	ret = ((st64)b->perm) - ((st64)a->perm);
	if (ret) {
		return ret;
	}
	return ((st64)b->align) - ((st64)a->align);
}

static void section_stringify(const RzBinSection *elem, RzStrBuf *sb) {
	char perm[5];

	perm[0] = elem->perm & RZ_PERM_SHAR ? 's' : '-';
	perm[1] = elem->perm & RZ_PERM_R ? 'r' : '-';
	perm[2] = elem->perm & RZ_PERM_W ? 'w' : '-';
	perm[3] = elem->perm & RZ_PERM_X ? 'x' : '-';
	perm[4] = 0;

	rz_strbuf_setf(sb, "align: 0x%08" PFMT64x " %s %s\n", elem->align, perm, elem->name);
}

static RzDiff *rz_diff_sections_new(DiffFile *dfile_a, DiffFile *dfile_b, bool compare_addr) {
	RzPVector *vec_a = NULL;
	RzPVector *vec_b = NULL;

	vec_a = rz_diff_file_get(dfile_a, sections);
	if (!vec_a) {
		rz_diff_error_ret(NULL, "cannot get sections from '%s'\n", dfile_a->dio->filename);
	}

	vec_b = rz_diff_file_get(dfile_b, sections);
	if (!vec_b) {
		rz_diff_error_ret(NULL, "cannot get sections from '%s'\n", dfile_b->dio->filename);
	}

	rz_pvector_sort(vec_a, (RzPVectorComparator)section_compare, NULL);
	rz_pvector_sort(vec_b, (RzPVectorComparator)section_compare, NULL);

	RzDiffMethods methods = {
		.elem_at = (RzDiffMethodElemAt)rz_diff_pvector_elem_at,
		.elem_hash = (RzDiffMethodElemHash)(compare_addr ? section_hash_addr : section_hash),
		.compare = (RzDiffMethodCompare)(compare_addr ? section_compare_addr : section_compare),
		.stringify = (RzDiffMethodStringify)(compare_addr ? section_stringify_addr : section_stringify),
		.ignore = NULL,
	};

	return rz_diff_generic_new(vec_a, rz_pvector_len(vec_a), vec_b, rz_pvector_len(vec_b), &methods);
}

/**************************************** fields ***************************************/

static ut32 field_hash_addr(const RzBinField *elem) {
	ut32 hash = rz_diff_hash_data((const ut8 *)elem->name, strlen(elem->name));
	hash ^= rz_diff_hash_data((const ut8 *)elem->type, strlen(SAFE_STR(elem->type)));
	hash ^= (ut32)(elem->paddr >> 32);
	hash ^= (ut32)elem->paddr;
	hash ^= (ut32)(elem->vaddr >> 32);
	hash ^= (ut32)elem->vaddr;
	return hash;
}

static int field_compare_addr(const RzBinField *a, const RzBinField *b) {
	st64 ret;
	IF_STRCMP_S(ret, a->name, b->name);
	IF_STRCMP_S(ret, a->type, b->type);
	ret = ((st64)b->size) - ((st64)a->size);
	if (ret) {
		return ret;
	}
	ret = ((st64)b->paddr) - ((st64)a->paddr);
	if (ret) {
		return ret;
	}
	return ((st64)b->vaddr) - ((st64)a->vaddr);
}

static void field_stringify_addr(const RzBinField *elem, RzStrBuf *sb) {
	rz_strbuf_setf(sb, "virt: 0x%016" PFMT64x " phys: 0x%016" PFMT64x " %-8s %s\n",
		elem->vaddr, elem->paddr, SAFE_STR(elem->type), elem->name);
}

static ut32 field_hash(const RzBinField *elem) {
	ut32 hash = rz_diff_hash_data((const ut8 *)elem->name, strlen(elem->name));
	hash ^= rz_diff_hash_data((const ut8 *)elem->type, strlen(SAFE_STR(elem->type)));
	return hash;
}

static int field_compare(const RzBinField *a, const RzBinField *b) {
	int ret;
	IF_STRCMP_S(ret, a->name, b->name);
	IF_STRCMP_S(ret, a->type, b->type);
	return 0;
}

static int field_compare_addr_vec(const RzBinField *a, const RzBinField *b, void *user) {
	return field_compare_addr(a, b);
}

static int field_compare_vec(const RzBinField *a, const RzBinField *b, void *user) {
	return field_compare(a, b);
}

static void field_stringify(const RzBinField *elem, RzStrBuf *sb) {
	rz_strbuf_setf(sb, "%s %s\n", SAFE_STR(elem->type), elem->name);
}

static RzDiff *rz_diff_fields_new(DiffFile *dfile_a, DiffFile *dfile_b, bool compare_addr) {
	RzPVector *vec_a = NULL;
	RzPVector *vec_b = NULL;

	vec_a = rz_diff_file_get(dfile_a, fields);
	if (!vec_a) {
		rz_diff_error_ret(NULL, "cannot get fields from '%s'\n", dfile_a->dio->filename);
	}

	vec_b = rz_diff_file_get(dfile_b, fields);
	if (!vec_b) {
		rz_diff_error_ret(NULL, "cannot get fields from '%s'\n", dfile_b->dio->filename);
	}

	rz_pvector_sort(vec_a, (RzPVectorComparator)(compare_addr ? field_compare_addr_vec : field_compare_vec), NULL);
	rz_pvector_sort(vec_b, (RzPVectorComparator)(compare_addr ? field_compare_addr_vec : field_compare_vec), NULL);

	RzDiffMethods methods = {
		.elem_at = (RzDiffMethodElemAt)rz_diff_pvector_elem_at,
		.elem_hash = (RzDiffMethodElemHash)(compare_addr ? field_hash_addr : field_hash),
		.compare = (RzDiffMethodCompare)(compare_addr ? field_compare_addr : field_compare),
		.stringify = (RzDiffMethodStringify)(compare_addr ? field_stringify_addr : field_stringify),
		.ignore = NULL,
	};

	return rz_diff_generic_new(vec_a, rz_pvector_len(vec_a), vec_b, rz_pvector_len(vec_b), &methods);
}

/**************************************** commands ***************************************/

static char *execute_command(const char *command, const char *filename, DiffContext *ctx) {
	RzCoreFile *cfile = rz_diff_load_file_with_core(filename, ctx->architecture, ctx->arch_bits, ctx->evars, ctx->colors, ctx->verbose);
	if (!cfile) {
		return NULL;
	}

	if (ctx->analyze_all) {
		if (ctx->verbose) {
			fprintf(stderr, "rz-diff: analysing file '%s'\n", filename);
		}
		if (!rz_core_analysis_everything(cfile->core, false, NULL)) {
			rz_diff_error("cannot analyze binary '%s'\n", filename);
		}
	}

	char *output = rz_core_cmd_str(cfile->core, command);
	rz_core_free(cfile->core);
	return output;
}

static RzDiff *rz_diff_command_new(DiffContext *ctx) {
	char *output_a = NULL;
	char *output_b = NULL;

	output_a = execute_command(ctx->input_a, ctx->file_a, ctx);
	if (!output_a) {
		rz_diff_error_ret(NULL, "cannot execute command '%s' on file '%s'\n", ctx->input_a, ctx->file_a);
	}

	output_b = execute_command(ctx->input_b, ctx->file_b, ctx);
	if (!output_b) {
		free(output_a);
		rz_diff_error_ret(NULL, "cannot execute command '%s' on file '%s'\n", ctx->input_b, ctx->file_b);
	}

	RzDiff *diff = rz_diff_lines_new(output_a, output_b, NULL);
	free(output_a);
	free(output_b);
	return diff;
}

/**************************************** unified ***************************************/

static bool rz_diff_unified_files(DiffContext *ctx) {
	size_t a_size = 0;
	size_t b_size = 0;
	ut8 *a_buffer = NULL;
	ut8 *b_buffer = NULL;
	DiffFile dfile_a = { 0 };
	DiffFile dfile_b = { 0 };
	RzDiff *diff = NULL;
	bool result = false;

	if (ctx->type == DIFF_TYPE_BYTES ||
		ctx->type == DIFF_TYPE_LINES) {
		if (!(a_buffer = rz_diff_slurp_file(ctx->file_a, &a_size))) {
			goto rz_diff_unified_files_bad;
		}

		if (!(b_buffer = rz_diff_slurp_file(ctx->file_b, &b_size))) {
			goto rz_diff_unified_files_bad;
		}
	} else if (ctx->type != DIFF_TYPE_FUNCTIONS && ctx->type != DIFF_TYPE_COMMAND) {
		if (!rz_diff_file_open(&dfile_a, ctx->file_a)) {
			goto rz_diff_unified_files_bad;
		}
		if (!rz_diff_file_open(&dfile_b, ctx->file_b)) {
			goto rz_diff_unified_files_bad;
		}
	}

	switch (ctx->type) {
	case DIFF_TYPE_BYTES:
		diff = rz_diff_bytes_new(a_buffer, a_size, b_buffer, b_size);
		break;
	case DIFF_TYPE_CLASSES:
		diff = rz_diff_classes_new(&dfile_a, &dfile_b, ctx->compare_addresses);
		break;
	case DIFF_TYPE_COMMAND:
		diff = rz_diff_command_new(ctx);
		break;
	case DIFF_TYPE_ENTRIES:
		diff = rz_diff_entries_new(&dfile_a, &dfile_b);
		break;
	case DIFF_TYPE_FIELDS:
		diff = rz_diff_fields_new(&dfile_a, &dfile_b, ctx->compare_addresses);
		break;
	case DIFF_TYPE_IMPORTS:
		diff = rz_diff_imports_new(&dfile_a, &dfile_b);
		break;
	case DIFF_TYPE_LIBRARIES:
		diff = rz_diff_libraries_new(&dfile_a, &dfile_b);
		break;
	case DIFF_TYPE_LINES:
		diff = rz_diff_lines_new((const char *)a_buffer, (const char *)b_buffer, NULL);
		break;
	case DIFF_TYPE_SECTIONS:
		diff = rz_diff_sections_new(&dfile_a, &dfile_b, ctx->compare_addresses);
		break;
	case DIFF_TYPE_STRINGS:
		diff = rz_diff_strings_new(&dfile_a, &dfile_b, ctx->compare_addresses);
		break;
	case DIFF_TYPE_SYMBOLS:
		diff = rz_diff_symbols_new(&dfile_a, &dfile_b, ctx->compare_addresses);
		break;
	default:
		rz_diff_error("unknown type\n");
		goto rz_diff_unified_files_bad;
	}

	if (!diff) {
		goto rz_diff_unified_files_bad;
	}

	if (ctx->mode == DIFF_MODE_JSON) {
		PJ *pj = rz_diff_unified_json(diff, ctx->file_a, ctx->file_b, ctx->show_time);
		if (!pj) {
			goto rz_diff_unified_files_bad;
		}
		printf("%s\n", pj_string(pj));
		pj_free(pj);
	} else {
		// DIFF_MODE_STANDARD & DIFF_MODE_QUIET
		char *result = rz_diff_unified_text(diff, ctx->file_a, ctx->file_b, ctx->show_time, ctx->colors);
		if (!result) {
			goto rz_diff_unified_files_bad;
		}
		puts(result);
		free(result);
	}

	result = true;

rz_diff_unified_files_bad:
	rz_diff_free(diff);
	rz_diff_file_close(&dfile_a);
	rz_diff_file_close(&dfile_b);
	free(a_buffer);
	free(b_buffer);
	return result;
}

/**************************************** graphs ***************************************/

static const char *get_config_or_default(RzCore *core, const char *key, const char *def_value) {
	const char *value = rz_config_get(core->config, key);
	if (RZ_STR_ISEMPTY(value)) {
		return def_value;
	}
	return value;
}

static void graphviz_dot_header(RzCore *core_a) {
	const char *font = get_config_or_default(core_a, "graph.font", "Courier");
	const char *gv_edge = get_config_or_default(core_a, "graph.gv.edge", "arrowhead=\"normal\"");
	const char *gv_node = get_config_or_default(core_a, "graph.gv.node", "fillcolor=gray style=filled shape=box");
	const char *gv_spline = get_config_or_default(core_a, "graph.gv.spline", "splines=\"ortho\"");
	rz_cons_printf("digraph code {\n"
		       "\tgraph [bgcolor=azure fontsize=8 fontname=\"%s\" %s];\n"
		       "\tnode [%s];\n"
		       "\tedge [%s];\n",
		font, gv_spline, gv_node, gv_edge);
}

static void print_color_node(RzCore *core, RzAnalysisBlock *bbi) {
	bool color_current = rz_config_get_b(core->config, "graph.gv.current");
	bool current = rz_analysis_block_contains(bbi, core->offset);
	if (current && color_current) {
		rz_cons_printf("\t\"0x%08" PFMT64x "\" ", bbi->addr);
		rz_cons_printf("\t[fillcolor=gray style=filled shape=box];\n");
	}
}

static char *basic_block_opcodes(RzCore *core, RzAnalysisBlock *bbi) {
	char *opcodes = NULL;
	RzConfigHold *hc = NULL;
	ut8 *block = NULL;

	hc = rz_config_hold_new2(core->config, "scr.color", "scr.utf8", "asm.offset", "asm.lines", "asm.cmt.right", "asm.lines.fcn", "asm.bytes", "asm.comments", NULL);
	if (!hc) {
		return NULL;
	}
	rz_config_set_i(core->config, "scr.utf8", 0);
	rz_config_set_i(core->config, "asm.offset", 0);
	rz_config_set_i(core->config, "asm.lines", 0);
	rz_config_set_i(core->config, "asm.cmt.right", 0);
	rz_config_set_i(core->config, "asm.lines.fcn", 0);
	rz_config_set_i(core->config, "asm.bytes", 0);
	rz_config_set_i(core->config, "asm.comments", 0);
	rz_config_set_i(core->config, "scr.color", COLOR_MODE_DISABLED);

	rz_cons_push();
	RzAnalysisBlock *b = rz_analysis_find_most_relevant_block_in(core->analysis, bbi->addr);
	if (!b) {
		RZ_LOG_ERROR("Cannot find function at 0x%08" PFMT64x "\n", bbi->addr);
		goto exit;
	}

	block = malloc(b->size + 1);
	if (!block) {
		RZ_LOG_ERROR("Cannot allocate buffer\n");
		goto exit;
	}

	rz_io_read_at_mapped(core->io, b->addr, block, b->size);
	RzCoreDisasmOptions disasm_options = {
		.cbytes = 2,
	};
	rz_core_print_disasm(core, b->addr, block, b->size, 9999, NULL, &disasm_options);
	rz_cons_filter();
	const char *retstr = rz_str_get(rz_cons_get_buffer());
	opcodes = rz_str_dup(retstr);
exit:
	rz_cons_pop();
	rz_cons_echo(NULL);
	free(block);
	rz_config_hold_restore(hc);
	rz_config_hold_free(hc);
	return opcodes;
}

static const char *pair_color(RzAnalysisMatchPair *pair) {
	if (pair->similarity >= 1.0) {
		return "lightgray";
	} else if (pair->similarity >= RZ_ANALYSIS_SIMILARITY_THRESHOLD) {
		return "yellow";
	}
	return "red";
}

static void graphviz_dot_nodes(RzCore *core_a, RzAnalysisFunction *fcn_a, RzCore *core_b, RzAnalysisFunction *fcn_b, RzAnalysisMatchResult *result) {
	char addr_a[32], addr_b[32];

	RzAnalysisMatchPair *pair = NULL;
	RzAnalysisBlock *bbi = NULL;
	RzListIter *iter = NULL;
	const char *font = get_config_or_default(core_a, "graph.font", "Courier");

	rz_strf(addr_a, "0x%08" PFMT64x, fcn_a->addr);
	rz_strf(addr_b, "0x%08" PFMT64x, fcn_b->addr);

	const char *norig = fcn_a->name ? fcn_a->name : addr_a;
	const char *nmodi = fcn_b->name ? fcn_b->name : addr_b;

	// we add all the matching basic block first
	rz_list_foreach (result->matches, iter, pair) {
		bbi = (RzAnalysisBlock *)pair->pair_a;

		const char *fillcolor = pair_color(pair);
		char *original = basic_block_opcodes(core_a, bbi);
		if (!original) {
			break;
		}

		if (pair->similarity >= RZ_ANALYSIS_SIMILARITY_THRESHOLD) {
			// if they are similar then we diff the opcodes.

			char *modified = basic_block_opcodes(core_b, (RzAnalysisBlock *)pair->pair_b);
			if (modified && strcmp(original, modified) != 0) {
				RzDiff *dff = rz_diff_lines_new(original, modified, NULL);
				char *diffstr = rz_diff_unified_text(dff, norig, nmodi, false, false);
				rz_diff_free(dff);
				free(modified);

				rz_str_replace_char(diffstr, '"', '\'');
				diffstr = rz_str_replace(diffstr, "\n", "\\l", 1);
				rz_cons_printf("\t\"0x%08" PFMT64x "\" [fillcolor=\"%s\","
					       "color=\"black\", fontname=\"%s\","
					       " label=\"%s\", URL=\"%s/0x%08" PFMT64x "\"]\n",
					bbi->addr, fillcolor, font, diffstr, fcn_a->name,
					bbi->addr);
				free(diffstr);
				free(original);
				continue;
			}

			// sometimes the mismatch is on a call value
			// but the output is actually the same due sym./imp.
			// thus we ignore the similarity check and consider this
			// as a perfect match.
			free(modified);
		}

		rz_str_replace_char(original, '"', '\'');
		original = rz_str_replace(original, "\n", "\\l", 1);
		rz_cons_printf("\t\"0x%08" PFMT64x "\" [fillcolor=\"%s\","
			       "color=\"black\", fontname=\"%s\","
			       " label=\"%s\", URL=\"%s/0x%08" PFMT64x "\"]\n",
			bbi->addr, fillcolor, font, original, fcn_a->name, bbi->addr);
		free(original);
	}

	// we then add all the unmatched basic blocks
	rz_list_foreach (result->unmatch_a, iter, bbi) {
		char *opcodes = basic_block_opcodes(core_a, bbi);
		if (!opcodes) {
			break;
		}

		rz_str_replace_char(opcodes, '"', '\'');
		opcodes = rz_str_replace(opcodes, "\n", "\\l", 1);
		rz_cons_printf("\t\"0x%08" PFMT64x "\" [fillcolor=\"white\","
			       "color=\"black\", fontname=\"%s\","
			       " label=\"%s\", URL=\"%s/0x%08" PFMT64x "\"]\n",
			bbi->addr, font, opcodes, fcn_a->name, bbi->addr);
		free(opcodes);
	}
}

#define PAL_JUMP "#0037da"
#define PAL_FAIL "#c50f1f"
#define PAL_TRUE "#13a10e"
static void graphviz_dot_edges(RzCore *core, RzAnalysisFunction *fcn) {
	RzAnalysisBlock *bbi;
	void **iter;

	rz_pvector_foreach (fcn->bbs, iter) {
		bbi = (RzAnalysisBlock *)*iter;
		if (bbi->jump != UT64_MAX) {
			rz_cons_printf("\t\"0x%08" PFMT64x "\" -> \"0x%08" PFMT64x "\" [color=\"%s\"];\n",
				bbi->addr, bbi->jump,
				bbi->fail != UT64_MAX ? PAL_TRUE : PAL_JUMP);
			print_color_node(core, bbi);
		}
		if (bbi->fail != UT64_MAX) {
			rz_cons_printf("\t\"0x%08" PFMT64x "\" -> \"0x%08" PFMT64x "\" [color=\"" PAL_FAIL "\"];\n",
				bbi->addr, bbi->fail);
			print_color_node(core, bbi);
		}
		if (bbi->switch_op) {
			RzAnalysisCaseOp *caseop;
			RzListIter *iter2;

			if (bbi->fail != UT64_MAX) {
				rz_cons_printf("\t\"0x%08" PFMT64x "\" -> \"0x%08" PFMT64x "\" [color=\"" PAL_FAIL "\"];\n",
					bbi->addr, bbi->fail);
				print_color_node(core, bbi);
			}
			rz_list_foreach (bbi->switch_op->cases, iter2, caseop) {
				rz_cons_printf("\t\"0x%08" PFMT64x "\" -> \"0x%08" PFMT64x "\" [color2=\"" PAL_FAIL "\"];\n",
					caseop->addr, caseop->jump);
				print_color_node(core, bbi);
			}
		}
	}
}
#undef PAL_JUMP
#undef PAL_FAIL
#undef PAL_TRUE

static void graphviz_dot_graph(RzCore *core_a, RzAnalysisFunction *fcn_a, RzCore *core_b, RzAnalysisFunction *fcn_b, RzAnalysisMatchResult *result) {
	graphviz_dot_header(core_a);
	if (fcn_a->bbs) {
		graphviz_dot_nodes(core_a, fcn_a, core_b, fcn_b, result);
		graphviz_dot_edges(core_a, fcn_a);
	} else {
		rz_cons_printf("\t\"0x%08" PFMT64x "\";\n", fcn_a->addr);
	}
	rz_cons_printf("}\n");
}

static void graph_basic_block_json(const char *name, RzAnalysisBlock *bbi, PJ *pj) {
	pj_ko(pj, name); // "<name>": { -- object begin
	pj_kn(pj, "address", bbi->addr);
	if (bbi->jump != UT64_MAX) {
		pj_kn(pj, "jump", bbi->jump);
	}
	if (bbi->fail != UT64_MAX) {
		pj_kn(pj, "fail", bbi->fail);
	}
	if (!bbi->switch_op) {
		pj_end(pj); // } -- object end
		return;
	}

	RzAnalysisCaseOp *caseop;
	RzListIter *iter;
	pj_ka(pj, "switch"); // [ -- "switch" begin
	rz_list_foreach (bbi->switch_op->cases, iter, caseop) {
		pj_o(pj); // { -- caseop object begin
		pj_kn(pj, "address", caseop->addr);
		pj_kn(pj, "jump", caseop->jump);
		pj_end(pj); // } -- caseop object end
	}
	pj_end(pj); // ] -- "switch" end
	pj_end(pj); // } -- object end
}

static void diff_graph_result_as_json(RzCore *core_a, RzAnalysisFunction *fcn_a, RzCore *core_b, RzAnalysisFunction *fcn_b, RzAnalysisMatchResult *result, PJ *pj) {
	RzAnalysisMatchPair *pair = NULL;
	RzAnalysisBlock *bbi_a = NULL, *bbi_b = NULL;
	RzListIter *iter = NULL;
	char *opcodes_a = NULL, *opcodes_b = NULL;
	char addr_a[32], addr_b[32];

	rz_strf(addr_a, "0x%08" PFMT64x, fcn_a->addr);
	rz_strf(addr_b, "0x%08" PFMT64x, fcn_b->addr);

	const char *norig = fcn_a->name ? fcn_a->name : addr_a;
	const char *nmodi = fcn_b->name ? fcn_b->name : addr_b;

	pj_ka(pj, "result"); // "result": { -- array object begin

	// we add all the matching basic block first
	rz_list_foreach (result->matches, iter, pair) {
		bbi_a = (RzAnalysisBlock *)pair->pair_a;
		bbi_b = (RzAnalysisBlock *)pair->pair_b;

		opcodes_a = basic_block_opcodes(core_a, bbi_a);
		if (!opcodes_a) {
			break;
		}

		pj_o(pj); // { -- array object begin

		pj_ko(pj, "pair"); // "pair": {
		graph_basic_block_json("source", bbi_a, pj);
		graph_basic_block_json("match", bbi_b, pj);
		pj_end(pj); // } -- "pair" end

		pj_ko(pj, "similarity"); // "similarity": {
		pj_ks(pj, "type", RZ_ANALYSIS_SIMILARITY_TYPE_STR(pair->similarity));
		pj_kd(pj, "score", pair->similarity);
		pj_end(pj); // } -- "similarity" end

		if (pair->similarity >= RZ_ANALYSIS_SIMILARITY_THRESHOLD) {
			// if they are similar then we diff the opcodes.

			opcodes_b = basic_block_opcodes(core_b, bbi_b);
			if (opcodes_b && strcmp(opcodes_a, opcodes_b) != 0) {
				RzDiff *dff = rz_diff_lines_new(opcodes_a, opcodes_b, NULL);
				char *diffstr = rz_diff_unified_text(dff, norig, nmodi, false, false);
				rz_diff_free(dff);
				if (diffstr) {
					free(opcodes_a);
					opcodes_a = diffstr;
				}
			}

			// sometimes the mismatch is on a call value
			// but the output is actually the same due sym./imp.
			// thus we ignore the similarity check and consider this
			// as a perfect match.
			free(opcodes_b);
		}

		pj_ks(pj, "opcodes", opcodes_a);
		pj_end(pj); // } -- array object end
		free(opcodes_a);
	}

	// we then add all the unmatched basic blocks
	// first from the source function
	rz_list_foreach (result->unmatch_a, iter, bbi_a) {
		opcodes_a = basic_block_opcodes(core_a, bbi_a);
		if (!opcodes_a) {
			break;
		}

		pj_o(pj); // { -- array object begin

		pj_ko(pj, "pair"); // "pair": {
		graph_basic_block_json("source", bbi_a, pj);
		pj_knull(pj, "match");
		pj_end(pj); // } -- "pair" end

		pj_ko(pj, "similarity"); // "similarity": {
		pj_ks(pj, "type", RZ_ANALYSIS_SIMILARITY_UNLIKE_STR);
		pj_kd(pj, "score", 0.0);
		pj_end(pj); // } -- "similarity" end

		pj_ks(pj, "opcodes", opcodes_a);
		pj_end(pj); // } -- array object end
		free(opcodes_a);
	}

	// then from the match function
	rz_list_foreach (result->unmatch_b, iter, bbi_b) {
		opcodes_b = basic_block_opcodes(core_b, bbi_b);
		if (!opcodes_b) {
			break;
		}

		pj_o(pj); // { -- array object begin

		pj_ko(pj, "pair"); // "pair": {
		pj_knull(pj, "source");
		graph_basic_block_json("match", bbi_b, pj);
		pj_end(pj); // } -- "pair" end

		pj_ko(pj, "similarity"); // "similarity": {
		pj_ks(pj, "type", RZ_ANALYSIS_SIMILARITY_UNLIKE_STR);
		pj_kd(pj, "score", 0.0);
		pj_end(pj); // } -- "similarity" end

		pj_ks(pj, "opcodes", opcodes_b);
		pj_end(pj); // } -- array object end
		free(opcodes_b);
	}

	pj_end(pj); // } -- result object end
}

static void diff_graph_function_detail_as_json(const char *object_name, RzAnalysisFunction *fcn, PJ *pj) {
	pj_ko(pj, object_name); // "<object_name>: {" -- object begin

	char *fcn_name = rz_str_escape_utf8_for_json(fcn->name, -1);
	pj_ks(pj, "name", rz_str_get_null(fcn_name));
	free(fcn_name);
	pj_kn(pj, "offset", fcn->addr);
	pj_ki(pj, "ninstr", fcn->ninstr);
	pj_kn(pj, "nargs", rz_analysis_arg_count(fcn));
	pj_kn(pj, "nlocals", rz_analysis_var_local_count(fcn));
	pj_kn(pj, "size", rz_analysis_function_linear_size(fcn));
	pj_ki(pj, "stack", fcn->maxstack);
	pj_ks(pj, "type", rz_analysis_fcntype_tostring(fcn->type));

	pj_end(pj); // } -- <object_name> object end
}

static void diff_graph_as_json(RzCore *core_a, RzAnalysisFunction *fcn_a, RzCore *core_b, RzAnalysisFunction *fcn_b, RzAnalysisMatchResult *result, PJ *pj) {
	pj_o(pj);
	diff_graph_function_detail_as_json("source", fcn_a, pj);
	diff_graph_function_detail_as_json("match", fcn_a, pj);
	diff_graph_result_as_json(core_a, fcn_a, core_b, fcn_b, result, pj);
	pj_end(pj);
}

static bool diff_progess_status(const size_t n_left, const size_t n_matches, void *user) {
	rz_cons_clear_line(stderr);
	fprintf(stderr, "rz-diff: to check %" PFMTSZu " | matches %" PFMTSZu "\r", n_left, n_matches);
	return !rz_cons_is_breaked();
}

static bool diff_check_ctrl_c(const size_t n_left, const size_t n_matches, void *user) {
	return !rz_cons_is_breaked();
}

static RzAnalysisFunction *find_best_matching_function(RzAnalysis *analysis_a, RzAnalysis *analysis_b, RzAnalysisFunction *find, bool verbose) {
	RzAnalysisMatchPair *pair = NULL;
	RzAnalysisFunction *match = NULL;
	RzAnalysisMatchResult *result = NULL;
	RzAnalysisMatchOpt opts = { 0 };
	RzList *list_a = rz_list_new();
	if (!list_a || !rz_list_append(list_a, find)) {
		RZ_LOG_ERROR("rz-diff: cannot allocate and initialize RzList for function search\n");
		goto fail;
	}

	opts.callback = verbose ? diff_progess_status : diff_check_ctrl_c;
	opts.analysis_a = analysis_a;
	opts.analysis_b = analysis_b;

	RzList *fcns_b = rz_analysis_function_list(analysis_b);
	result = rz_analysis_match_functions(list_a, fcns_b, &opts);
	if (result && rz_list_length(result->matches) > 0) {
		pair = (RzAnalysisMatchPair *)rz_list_first_val(result->matches);
		match = (RzAnalysisFunction *)pair->pair_b;
	}

fail:
	rz_analysis_match_result_free(result);
	rz_list_free(list_a);
	return match;
}

static int compareBlocks(const RzAnalysisBlock *a, const RzAnalysisBlock *b, void *user) {
	return (a && b && a->addr && b->addr ? (a->addr > b->addr) - (a->addr < b->addr) : 0);
}

static int comparePairBlocks(const RzAnalysisMatchPair *ma, const RzAnalysisMatchPair *mb, void *user) {
	const RzAnalysisBlock *a = ma->pair_a;
	const RzAnalysisBlock *b = mb->pair_a;
	return compareBlocks(a, b, user);
}

/**
 * \brief Generate a json or graphviz dot output of the graph and its data.
 *
 * Each node that doesn't match 100% with the other function will include
 * a unified diff of the assembly of the same basic block.
 * */
static void core_show_function_diff(RzCore *core_a, ut64 addr_a, RzCore *core_b, ut64 addr_b, DiffMode mode, bool verbose) {
	rz_return_if_fail(core_a && core_b);

	PJ *pj = NULL;
	RzAnalysisFunction *fcn_b = NULL, *fcn_a = NULL;
	RzAnalysisMatchResult *result = NULL;
	RzAnalysisMatchOpt opts = { 0 };

	// find function
	fcn_a = rz_analysis_get_function_at(core_a->analysis, addr_a);
	if (!fcn_a) {
		RZ_LOG_ERROR("rz-diff: cannot get function at 0x%" PFMT64x "\n", addr_a);
		return;
	}

	if (addr_b == UT64_MAX) {
		// find matching function on core B
		fcn_b = find_best_matching_function(core_a->analysis, core_b->analysis, fcn_a, verbose);
		if (!fcn_b) {
			RZ_LOG_ERROR("rz-diff: cannot find best matching function for function at 0x%" PFMT64x "\n", addr_a);
			return;
		}
	} else {
		fcn_b = rz_analysis_get_function_at(core_b->analysis, addr_b);
		if (!fcn_b) {
			RZ_LOG_ERROR("rz-diff: cannot get function at 0x%" PFMT64x "\n", addr_b);
			return;
		}
	}

	opts.callback = verbose ? diff_progess_status : diff_check_ctrl_c;
	opts.analysis_a = core_a->analysis;
	opts.analysis_b = core_b->analysis;

	// calculate all the matches between the basic blocks of the 2 functions.
	result = rz_analysis_match_basic_blocks(fcn_a, fcn_b, &opts);
	if (!result) {
		RZ_LOG_ERROR("rz-diff: cannot calculate matching basic blocks for function at 0x%" PFMT64x "\n", addr_a);
		return;
	}

	rz_list_sort(result->matches, (RzListComparator)comparePairBlocks, NULL);
	rz_list_sort(result->unmatch_a, (RzListComparator)compareBlocks, NULL);
	rz_list_sort(result->unmatch_b, (RzListComparator)compareBlocks, NULL);

	switch (mode) {
	case DIFF_MODE_JSON:
		pj = pj_new();
		if (!pj) {
			RZ_LOG_ERROR("rz-diff: cannot allocate json structure for function matching\n");
			rz_analysis_match_result_free(result);
			return;
		}

		diff_graph_as_json(core_a, fcn_a, core_b, fcn_b, result, pj);
		rz_cons_printf("%s\n", pj_string(pj));
		pj_free(pj);
		break;
	default:
		graphviz_dot_graph(core_a, fcn_a, core_b, fcn_b, result);
		break;
	}
	rz_cons_flush();

	rz_analysis_match_result_free(result);
}

static void diff_function_as_json(const char *obj_name, RzAnalysisFunction *fcn, PJ *pj) {
	if (!fcn) {
		pj_knull(pj, obj_name);
		return;
	}
	pj_ko(pj, obj_name); // "<obj_name>": { -- object end
	pj_ks(pj, "name", fcn->name);
	pj_kn(pj, "addr", fcn->addr);
	pj_kn(pj, "size", rz_analysis_function_realsize(fcn));
	pj_end(pj); // } -- <obj_name> object end
}

static void diff_similarity_as_json(RzAnalysisFunction *fcn_a, RzAnalysisFunction *fcn_b, double similarity, PJ *pj) {
	pj_o(pj); // { -- match object begin
	pj_kd(pj, "similarity", similarity);
	pj_ks(pj, "type", RZ_ANALYSIS_SIMILARITY_TYPE_STR(similarity));
	diff_function_as_json("original", fcn_a, pj);
	diff_function_as_json("modified", fcn_b, pj);
	pj_end(pj); // } -- match object end
}

static void diff_similarity_as_table(RzAnalysisFunction *fcn_a, RzAnalysisFunction *fcn_b, double similarity, bool no_name, RzTable *table) {
	char tmp[128];
	const char *type_s = NULL;
	const char *type_n = NULL;

	ut64 size_a = fcn_a ? rz_analysis_function_realsize(fcn_a) : 0;
	ut64 size_b = fcn_b ? rz_analysis_function_realsize(fcn_b) : 0;

	if (similarity > 0.0 && fcn_a && fcn_b) {
		type_n = tmp;
		if (similarity >= 1.0) {
			rz_strf(tmp, "%.6f", similarity);
			type_s = "COMPLETE";
		} else if (similarity >= RZ_ANALYSIS_SIMILARITY_THRESHOLD) {
			rz_strf(tmp, "%.6f", similarity);
			type_s = "PARTIAL";
		} else {
			rz_strf(tmp, "%.4f", similarity);
			type_s = "UNLIKE";
		}
	} else {
		type_n = "0.000000";
		type_s = "UNLIKE";
	}

	if (no_name) {
		if (fcn_a && fcn_b) {
			rz_table_add_rowf(table, "nXssXn", size_a, fcn_a->addr, type_s, type_n, fcn_b->addr, fcn_b->addr, size_b);
		} else if (fcn_a) {
			rz_table_add_rowf(table, "nXssXn", size_a, fcn_a->addr, type_s, type_n, UT64_MAX, size_b);
		} else {
			rz_table_add_rowf(table, "nXssXn", size_a, UT64_MAX, type_s, type_n, size_b, fcn_b->addr);
		}
		return;
	}

	// with names
	if (fcn_a && fcn_b) {
		rz_table_add_rowf(table, "snXssXns", fcn_a->name, size_a, fcn_a->addr, type_s, type_n, fcn_b->addr, size_b, fcn_b->name);
	} else if (fcn_a) {
		rz_table_add_rowf(table, "snXssXns", fcn_a->name, size_a, fcn_a->addr, type_s, type_n, UT64_MAX, size_b, "");
	} else {
		rz_table_add_rowf(table, "snXssXns", "", size_a, UT64_MAX, type_s, type_n, fcn_b->addr, size_b, fcn_b->name);
	}
}

static int comparePairFunctions(const RzAnalysisMatchPair *ma, const RzAnalysisMatchPair *mb, void *user) {
	const RzAnalysisFunction *a = ma->pair_a;
	const RzAnalysisFunction *b = mb->pair_a;
	return (a && b && a->addr && b->addr ? (a->addr > b->addr) - (a->addr < b->addr) : 0);
}

static const char *diff_table_color_selector(const char *value, const char *column, const size_t column_n, const RzTableColumnType type, void *user) {
	if (!column || !value) {
		return NULL;
	}
	const RzConsContext *ctx = (const RzConsContext *)user;
	if (RZ_STR_EQ(column, "type")) {
		if (RZ_STR_EQ(value, "COMPLETE")) {
			return ctx->pal.diff_match;
		} else if (RZ_STR_EQ(value, "PARTIAL")) {
			return ctx->pal.diff_unmatch;
		}
		return ctx->pal.diff_unknown;
	}
	if (RZ_STR_EQ(column, "similarity")) {
		return ctx->pal.cjmp;
	} else if (rz_str_startswith(column, "size")) {
		return ctx->pal.num;
	} else if (rz_str_startswith(column, "addr")) {
		return ctx->pal.offset;
	}
	return ctx->pal.label;
}

/**
 * \brief Performs function matching and shows the result in a table.
 *
 * Takes 2 cores and tries to match all the functions with eachother;
 * Then the scores are shown in a table (when in quiet mode, the table
 * is headerless)
 * */
static void core_diff_show(RzCore *core_a, RzCore *core_b, const char *addr_a, DiffMode mode, bool verbose) {
	rz_return_if_fail(core_a && core_b);

	char *output = NULL;
	RzList *fcns_a = NULL, *fcns_b = NULL;
	PJ *pj = NULL;
	RzTable *table = NULL;
	RzAnalysisMatchResult *result = NULL;
	RzAnalysisMatchPair *pair = NULL;
	RzAnalysisMatchOpt opts = { 0 };
	RzAnalysisFunction *fcn_a = NULL, *fcn_b = NULL;
	RzListIter *iter = NULL;
	bool color = false, no_name = false;
	if (RZ_STR_ISEMPTY(addr_a)) {
		fcns_a = rz_list_clone(rz_analysis_function_list(core_a->analysis));
	} else {
		fcns_a = rz_list_new();
		if (!fcns_a) {
			RZ_LOG_ERROR("rz-diff: failed to create new list.\n");
			goto fail;
		}
		ut64 addr = rz_num_get(core_a->num, addr_a);
		RzAnalysisFunction *fcn = rz_analysis_get_function_at(core_a->analysis, addr);
		if (!fcn) {
			RZ_LOG_ERROR("rz-diff: failed to find %s in file0.\n", addr_a);
			goto fail;
		}
		rz_list_append(fcns_a, fcn);
	}
	if (rz_list_empty(fcns_a)) {
		RZ_LOG_ERROR("rz-diff: No functions found in file0.\n");
		goto fail;
	}

	fcns_b = rz_list_clone(rz_analysis_function_list(core_b->analysis));
	if (rz_list_empty(fcns_b)) {
		RZ_LOG_ERROR("rz-diff: No functions found in file1.\n");
		goto fail;
	}

	opts.callback = verbose ? diff_progess_status : diff_check_ctrl_c;
	opts.analysis_a = core_a->analysis;
	opts.analysis_b = core_b->analysis;

	// calculate all the matches between the functions of the 2 different core files.
	result = rz_analysis_match_functions(fcns_a, fcns_b, &opts);
	if (!result) {
		RZ_LOG_ERROR("rz-diff: cannot perform matching functions search\n");
		goto fail;
	}

	rz_list_sort(result->matches, (RzListComparator)comparePairFunctions, NULL);
	rz_list_sort(result->unmatch_a, rz_analysis_get_column_sort(core_a->analysis), NULL);
	rz_list_sort(result->unmatch_b, rz_analysis_get_column_sort(core_b->analysis), NULL);

	if (mode == DIFF_MODE_JSON) {
		pj = pj_new();
		if (!pj) {
			RZ_LOG_ERROR("rz-diff: cannot allocate json structure for function matching\n");
			goto fail;
		}
		pj_a(pj); // [ -- list of pairs begin
	} else {
		color = rz_config_get_i(core_a->config, "scr.color") > 0 || rz_config_get_i(core_b->config, "scr.color") > 0;
		no_name = rz_config_get_b(core_a->config, "diff.bare") || rz_config_get_b(core_b->config, "diff.bare");

		table = rz_table_new();
		if (!table) {
			RZ_LOG_ERROR("rz-diff: cannot allocate table structure for function matching\n");
			goto fail;
		}

		if (no_name) {
			rz_table_set_columnsf(table, "nXssXn", "size0", "addr0", "type", "similarity", "addr1", "size1");
		} else {
			rz_table_set_columnsf(table, "snXssXns", "name0", "size0", "addr0", "type", "similarity", "addr1", "size1", "name1");
		}
	}

	// first the matching functions.
	rz_list_foreach (result->matches, iter, pair) {
		fcn_a = (RzAnalysisFunction *)pair->pair_a;
		fcn_b = (RzAnalysisFunction *)pair->pair_b;
		if (fcn_a->type != RZ_ANALYSIS_FCN_TYPE_FCN && fcn_a->type != RZ_ANALYSIS_FCN_TYPE_SYM) {
			continue;
		}
		if (mode == DIFF_MODE_JSON) {
			diff_similarity_as_json(fcn_a, fcn_b, pair->similarity, pj);
		} else {
			diff_similarity_as_table(fcn_a, fcn_b, pair->similarity, no_name, table);
		}
	}

	// then the unmatched functions from list A.
	rz_list_foreach (result->unmatch_a, iter, fcn_a) {
		if (fcn_a->type != RZ_ANALYSIS_FCN_TYPE_FCN && fcn_a->type != RZ_ANALYSIS_FCN_TYPE_SYM) {
			continue;
		}
		if (mode == DIFF_MODE_JSON) {
			diff_similarity_as_json(fcn_a, NULL, 0.0, pj);
		} else {
			diff_similarity_as_table(fcn_a, NULL, 0.0, no_name, table);
		}
	}

	// then the unmatched functions from list B.
	rz_list_foreach (result->unmatch_b, iter, fcn_b) {
		if (fcn_b->type != RZ_ANALYSIS_FCN_TYPE_FCN && fcn_b->type != RZ_ANALYSIS_FCN_TYPE_SYM) {
			continue;
		}
		if (mode == DIFF_MODE_JSON) {
			diff_similarity_as_json(NULL, fcn_b, 0.0, pj);
		} else {
			diff_similarity_as_table(NULL, fcn_b, 0.0, no_name, table);
		}
	}

	if (table) {
		if (color) {
			rz_table_set_color_selector(table, diff_table_color_selector, core_a->cons->context);
		}
		if (core_a->cons->use_utf8_curvy) {
			rz_table_set_char_mode(table, RZ_TABLE_CHAR_MODE_UTF8_CURVY);
		} else if (core_a->cons->use_utf8) {
			rz_table_set_char_mode(table, RZ_TABLE_CHAR_MODE_UTF8);
		}
	}

	switch (mode) {
	case DIFF_MODE_JSON:
		pj_end(pj); // ] -- list of pairs end
		output = pj_drain(pj);
		rz_cons_printf("%s\n", output);
		pj = NULL;
		break;
	case DIFF_MODE_STANDARD:
		output = rz_table_tofancystring(table);
		rz_cons_printf("%s", output);
		break;
	default: // DIFF_MODE_QUIET
		rz_table_align(table, 0, RZ_TABLE_ALIGN_RIGHT);
		rz_table_show_header(table, false);
		output = rz_table_tosimplestring(table);
		rz_cons_printf("%s", output);
		break;
	}

	rz_cons_flush();

fail:
	free(output);
	rz_table_free(table);
	rz_analysis_match_result_free(result);
	rz_list_free(fcns_a);
	rz_list_free(fcns_b);
}

static bool convert_offset_from_input(RzCore *core, const char *input, ut64 *offset) {
	if (rz_num_is_valid_input(NULL, input)) {
		*offset = rz_num_get_input_value(NULL, input);
		return true;
	}

	RzFlagItem *fi = rz_flag_get(core->flags, input);
	if (fi) {
		*offset = fi->offset;
		return true;
	}

	return false;
}

static bool rz_diff_graphs_files(DiffContext *ctx) {
	bool success = false;
	RzCoreFile *a = NULL;
	RzCoreFile *b = NULL;

	a = rz_diff_load_file_with_core(ctx->file_a, ctx->architecture, ctx->arch_bits, ctx->evars, ctx->colors, ctx->verbose);
	if (!a) {
		goto rz_diff_graphs_files_bad;
	}

	b = rz_diff_load_file_with_core(ctx->file_b, ctx->architecture, ctx->arch_bits, ctx->evars, ctx->colors, ctx->verbose);
	if (!b) {
		goto rz_diff_graphs_files_bad;
	}

	if (ctx->type == DIFF_TYPE_PLOTDIFF) {
		ut64 address_a = UT64_MAX;
		ut64 address_b = UT64_MAX;

		if (!convert_offset_from_input(a->core, ctx->input_a, &address_a)) {
			rz_diff_error("cannot convert '%s' into an offset\n", ctx->input_a);
			goto rz_diff_graphs_files_bad;
		}

		if (!convert_offset_from_input(b->core, ctx->input_b, &address_b)) {
			rz_diff_error("cannot convert '%s' into an offset\n", ctx->input_b);
			goto rz_diff_graphs_files_bad;
		}

		if (ctx->analyze_all) {
			if (ctx->verbose) {
				fprintf(stderr, "rz-diff: analysing file '%s'\n", ctx->file_a);
			}
			if (!rz_core_analysis_everything(a->core, false, NULL)) {
				rz_diff_error("cannot analyze binary '%s'\n", ctx->file_a);
				goto rz_diff_graphs_files_bad;
			}
			if (ctx->verbose) {
				fprintf(stderr, "rz-diff: analysing file '%s'\n", ctx->file_b);
			}
			if (!rz_core_analysis_everything(b->core, false, NULL)) {
				rz_diff_error("cannot analyze binary '%s'\n", ctx->file_b);
				goto rz_diff_graphs_files_bad;
			}
		} else {
			bool analyze_recursively = rz_config_get_i(a->core->config, "analysis.calls");
			if (!rz_core_analysis_function_add(a->core, NULL, address_a, analyze_recursively)) {
				rz_diff_error("cannot find function at '%s' in '%s' \n", ctx->input_a, ctx->file_a);
				goto rz_diff_graphs_files_bad;
			}
			if (!rz_core_analysis_function_add(b->core, NULL, address_b, analyze_recursively)) {
				rz_diff_error("cannot find function at '%s' in '%s' \n", ctx->input_b, ctx->file_b);
				goto rz_diff_graphs_files_bad;
			}
		}
		if (ctx->verbose) {
			fprintf(stderr, "rz-diff: start diffing.\n");
		}
		core_show_function_diff(a->core, address_a, b->core, address_b, ctx->mode, ctx->verbose);
	} else {
		if (ctx->verbose) {
			fprintf(stderr, "rz-diff: analysing file '%s'\n", ctx->file_a);
		}
		if (!rz_core_analysis_everything(a->core, false, NULL)) {
			rz_diff_error("cannot analyze binary '%s'\n", ctx->file_a);
			goto rz_diff_graphs_files_bad;
		}
		if (ctx->verbose) {
			fprintf(stderr, "rz-diff: analysing file '%s'\n", ctx->file_b);
		}
		if (!rz_core_analysis_everything(b->core, false, NULL)) {
			rz_diff_error("cannot analyze binary '%s'\n", ctx->file_b);
			goto rz_diff_graphs_files_bad;
		}
		if (ctx->verbose) {
			fprintf(stderr, "rz-diff: start diffing.\n");
		}
		if (ctx->colors) {
			rz_core_theme_load(a->core, ctx->theme_name ? ctx->theme_name : "default");
		}
		core_diff_show(a->core, b->core, ctx->input_a, ctx->mode, ctx->verbose);
	}

	success = true;

rz_diff_graphs_files_bad:
	rz_core_free(a ? a->core : NULL);
	rz_core_free(b ? b->core : NULL);
	return success;
}

/********************************************************************************/

typedef enum diff_hex_len_t {
	DIFF_HEX_8 = 58,
	DIFF_HEX_16 = 90,
	DIFF_HEX_32 = 154,
} DiffHexLen;

static inline int diff_hexdump_partial(DiffHexView *hview, int hexlen, int lp, int lsize, const ut8 *bytes_a, const ut8 *bytes_b, ut64 address_a, ut64 address_b, ut64 size_a, ut64 size_b, ut64 pos, ssize_t read_a, ssize_t read_b, ssize_t skip_a, ssize_t skip_b) {
	const char *number = hview->colors.number;
	const char *match = hview->colors.match;
	const char *unmatch = hview->colors.unmatch;
	const char *reset = hview->colors.reset;
	ssize_t i;
	char *line = hview->line;

#define P(x)                (IS_PRINTABLE(x) ? x : '.')
#define printline(fmt, ...) snprintf(line + lp, RZ_MAX(lsize - lp, 0), fmt, ##__VA_ARGS__)
	// write to buffer fileA offset + hex bytes
	lp += printline("%s0x%016" PFMT64x "%s | ", number, address_a + pos, reset);
	for (i = 0; i < hexlen && i < read_a; ++i) {
		if (pos + i >= size_a || pos + i < skip_a) {
			// if the byte is outside the range [0 - fileA size) then do not write any hex
			memset(line + lp, ' ', 3);
			lp += 3;
		} else if (i < read_b && pos + i >= skip_b) {
			// if the byte is inside the range [0 - fileA size) check bytes_b for match/mismatch
			const char *color = bytes_a[pos + i] == bytes_b[pos + i] ? match : unmatch;
			lp += printline("%s%02x%s ", color, bytes_a[pos + i], reset);
		} else {
			// if the byte is inside the range [0 - fileA size) but address_b
			// is outside [0 - fileB size) then is a mismatch
			lp += printline("%s%02x%s ", unmatch, bytes_a[pos + i], reset);
		}
	}
	if (i < hexlen) {
		// fill any missing space to have fileA bytes aligned
		memset(line + lp, ' ', (hexlen - i) * 3);
		lp += (hexlen - i) * 3;
	}

	// print now printable chars of the printed hex values
	lp += printline(" | ");
	for (i = 0; i < hexlen && i < read_a; ++i) {
		if (pos + i >= size_a || pos + i < skip_a) {
			// if the byte is outside the range [0 - fileA size) then do not write any hex
			line[lp] = ' ';
			lp++;
		} else if (i < read_b && pos + i >= skip_b) {
			// if the byte is inside the range [0 - fileA size) check bytes_b for match/mismatch
			const char *color = bytes_a[pos + i] == bytes_b[pos + i] ? match : unmatch;
			lp += printline("%s%c%s", color, P(bytes_a[pos + i]), reset);
		} else {
			// if the byte is inside the range [0 - fileA size) but address_b
			// is outside [0 - fileB size) then is a mismatch
			lp += printline("%s%c%s", unmatch, P(bytes_a[pos + i]), reset);
		}
	}
	if (i < hexlen) {
		// fill any missing space to have fileA bytes aligned
		memset(line + lp, ' ', (hexlen - i));
		lp += (hexlen - i);
	}
	return lp;
#undef printline
#undef P
}

static inline void diff_hexdump_line(DiffHexView *hview, DiffHexLen hlen, ut64 pos, ssize_t read_a, ssize_t read_b, ssize_t skip_a, ssize_t skip_b) {
	int width = hview->screen.width;
	int height = hview->screen.height;
	char *line = hview->line;
	const ut8 *buffer_a = hview->buffer_a;
	const ut8 *buffer_b = hview->buffer_b;
	ut64 address_a = hview->address_a;
	ut64 address_b = hview->address_b;
	int lp = 0;
	int lsize = width * height;
	int hexlen = 0;

	switch (hlen) {
	case DIFF_HEX_16:
		hexlen = 16;
		break;
	case DIFF_HEX_32:
		hexlen = 32;
		break;
	default:
		hexlen = 8;
		break;
	}

#define printline(fmt, ...) snprintf(line + lp, RZ_MAX(lsize - lp, 0), fmt, ##__VA_ARGS__)
	lp = diff_hexdump_partial(hview, hexlen, 0, lsize, buffer_a, buffer_b, address_a, address_b, hview->size_a, hview->size_b, pos, read_a, read_b, skip_a, skip_b);
	lp += printline(" | ");
	lp = diff_hexdump_partial(hview, hexlen, lp, lsize, buffer_b, buffer_a, address_b, address_a, hview->size_b, hview->size_a, pos, read_b, read_a, skip_b, skip_a);
	lp += printline(" |");
#undef printline
}

static inline int len_draw_hexdump(DiffHexView *hview) {
	int width = hview->screen.width;
	if (width >= (DIFF_HEX_32 * 2)) {
		return DIFF_HEX_32;
	} else if (width >= (DIFF_HEX_16 * 2)) {
		return DIFF_HEX_16;
	}
	return DIFF_HEX_8;
}

static inline int seek_min_shift(DiffHexView *hview) {
	int width = hview->screen.width;
	if (width >= (DIFF_HEX_32 * 2)) {
		return 5;
	} else if (width >= (DIFF_HEX_16 * 2)) {
		return 4;
	}
	return 3;
}

static inline int seek_min_value(DiffHexView *hview) {
	int width = hview->screen.width;
	if (width >= (DIFF_HEX_32 * 2)) {
		return 32;
	} else if (width >= (DIFF_HEX_16 * 2)) {
		return 16;
	}
	return 8;
}

static inline int offset_len(DiffHexView *hview) {
	ut64 filesize = RZ_MAX(hview->io_a->filesize, hview->io_b->filesize);
	if (filesize > UT32_MAX) {
		return 16;
	} else if (filesize > UT16_MAX) {
		return 8;
	}
	return 4;
}

static bool rz_diff_draw_tui(DiffHexView *hview, bool show_help) {
	ssize_t read_a = 0, read_b = 0;
	char *line = hview->line;
	int shift = 8, offlen = 16, xpos = 0;
	int width = hview->screen.width;
	int height = hview->screen.height;
	int lsize = width * height;
	DiffHexLen hlen = 0;
	DiffIO *io_a = hview->io_a;
	DiffIO *io_b = hview->io_b;
	ut64 filesize_a = hview->io_a->filesize;
	ut64 filesize_b = hview->io_b->filesize;
	ut64 max_rows = height - 2;
	ut64 skip_a = 0;
	ut64 skip_b = 0;
	RzConsCanvas *canvas = hview->canvas;
	const char *reset = hview->colors.reset;
	const char *legenda = hview->colors.legenda;
	const char *toolbar = NULL;
	bool utf8 = rz_cons_singleton()->use_utf8;
	const char *arrow_up = utf8 ? RUNE_ARROW_UP " " : "/\\";
	const char *arrow_down = utf8 ? RUNE_ARROW_DOWN " " : "\\/";
	const char *arrow_right = utf8 ? RUNE_ARROW_RIGHT " " : "> ";
	const char *arrow_left = utf8 ? RUNE_ARROW_LEFT " " : "< ";

	if (!line || !hview->buffer_a || !hview->buffer_b) {
		return false;
	}

	offlen = offset_len(hview);
	hlen = len_draw_hexdump(hview);
	xpos = RZ_MAX((width / 2) - hlen, 0);

	const char *p = NULL;
	const char *file_a = io_a->filename;
	const char *file_b = io_b->filename;

	p = io_a->filename;
	while ((p = strstr(file_a, RZ_SYS_DIR))) {
		file_a = p + 1;
	}

	p = io_b->filename;
	while ((p = strstr(file_b, RZ_SYS_DIR))) {
		file_b = p + 1;
	}

	if (hview->column_descr) {
		max_rows--;
	}

	if (hview->address_a > hview->size_a && (hview->address_a + hview->size_a) < hview->size_a) {
		// underflow
		ut64 size = hview->address_a + hview->size_a;
		ut64 offset_p = hview->size_a - size;
		read_a = rz_io_pread_at(io_a->io, 0, hview->buffer_a + offset_p, size);
		if (read_a > 0) {
			// include also excluded bytes from underflow
			read_a += offset_p;
			skip_a = offset_p;
		}
	} else {
		read_a = rz_io_pread_at(io_a->io, hview->address_a, hview->buffer_a, hview->size_a);
	}

	if (hview->address_b > hview->size_b && (hview->address_b + hview->size_b) < hview->size_b) {
		// underflow
		ut64 size = hview->address_b + hview->size_b;
		ut64 offset_p = hview->size_b - size;
		read_b = rz_io_pread_at(io_b->io, 0, hview->buffer_b + offset_p, size);
		if (read_b > 0) {
			// include also excluded bytes from underflow
			read_b += offset_p;
			skip_b = offset_p;
		}
	} else {
		read_b = rz_io_pread_at(io_b->io, hview->address_b, hview->buffer_b, hview->size_b);
	}

	rz_cons_goto_origin_reset();
	rz_cons_clear();
	rz_cons_canvas_clear(canvas);
	shift = seek_min_shift(hview);
	for (ut64 h = 0, pos = 0; h < max_rows; ++h) {
		// draw hexadecimal values
		pos = h << shift;
		diff_hexdump_line(hview, hlen, pos, read_a - pos, read_b - pos, skip_a, skip_b);
		rz_cons_canvas_gotoxy(canvas, xpos, h + (hview->column_descr ? 2 : 1));
		rz_cons_canvas_write(canvas, line);
	}

	switch (len_draw_hexdump(hview)) {
	case DIFF_HEX_32: toolbar = " 0  1  2  3  4  5  6  7  8  9  A  B  C  D  E  F 10 11 12 13 14 15 16 17 18 19 1A 1B 1C 1D 1E 1F"; break;
	case DIFF_HEX_16: toolbar = " 0  1  2  3  4  5  6  7  8  9  A  B  C  D  E  F"; break;
	default: toolbar = " 0  1  2  3  4  5  6  7"; break;
	}

	rz_cons_canvas_box(canvas, 0, 0, width, height - 1, reset);
	if (hview->column_descr) {
		rz_cons_canvas_gotoxy(canvas, xpos + 21, 1);
		rz_cons_canvas_write(canvas, toolbar);
		rz_cons_canvas_gotoxy(canvas, xpos, 0);
	} else {
		rz_cons_canvas_gotoxy(canvas, xpos, 0);
	}
	snprintf(line, lsize, " [%*" PFMT64x "]( %.42s )", offlen, filesize_a, file_a);
	rz_cons_canvas_write(canvas, line);

	if (hview->column_descr) {
		rz_cons_canvas_gotoxy(canvas, xpos + hlen + 22, 1);
		rz_cons_canvas_write(canvas, toolbar);
		rz_cons_canvas_gotoxy(canvas, xpos + hlen, 0);
	} else {
		rz_cons_canvas_gotoxy(canvas, xpos + hlen, 0);
	}
	snprintf(line, lsize, " [%*" PFMT64x "]( %.42s )", offlen, filesize_b, file_b);
	rz_cons_canvas_write(canvas, line);

	// clang-format off
	toolbar = " "
		"%s1 2%s -/+0x%x | "
		"%sZ A%s file0 +/-1 | "
		"%sC D%s file1 +/-1 | "
		"%sG B%s end/begin | "
		"%sN M%s next/prev | "
		"%s%s%s%s +/-%u | "
		"%s%s%s%s +/-1 | "
		"%s:%s seek";
	snprintf(line, lsize, toolbar
			, legenda, reset, (1 << shift) * max_rows
			, legenda, reset
			, legenda, reset
			, legenda, reset
			, legenda, reset
			, legenda, arrow_down, arrow_up, reset, 1 << shift
			, legenda, arrow_left, arrow_right, reset
			, legenda, reset);
	// clang-format on

	rz_cons_canvas_gotoxy(canvas, 0, height);
	rz_cons_canvas_write(canvas, line);

	if (show_help) {
		rz_cons_canvas_fill(canvas, 4, 2, 56, 16, ' ');
		rz_cons_canvas_box(canvas, 4, 2, 56, 16, legenda);

		snprintf(line, lsize, "%sHelp page%s\n", legenda, reset);
		rz_cons_canvas_gotoxy(canvas, 6, 3);
		rz_cons_canvas_write(canvas, line);

		snprintf(line, lsize, "%s1 2%s   increase/decrease the offsets by 0x%x\n", legenda, reset, (1 << shift) * (height - 2));
		rz_cons_canvas_gotoxy(canvas, 6, 5);
		rz_cons_canvas_write(canvas, line);

		snprintf(line, lsize, "%sZ A%s   increase/decrease the offset of the file0 by 1\n", legenda, reset);
		rz_cons_canvas_gotoxy(canvas, 6, 6);
		rz_cons_canvas_write(canvas, line);

		snprintf(line, lsize, "%sC D%s   increase/decrease the offset of the file1 by 1\n", legenda, reset);
		rz_cons_canvas_gotoxy(canvas, 6, 7);
		rz_cons_canvas_write(canvas, line);

		snprintf(line, lsize, "%sN M%s   next/previous difference\n", legenda, reset);
		rz_cons_canvas_gotoxy(canvas, 6, 8);
		rz_cons_canvas_write(canvas, line);

		snprintf(line, lsize, "%sG B%s   seek to end/begin\n", legenda, reset);
		rz_cons_canvas_gotoxy(canvas, 6, 9);
		rz_cons_canvas_write(canvas, line);

		snprintf(line, lsize, "%s9%s     sets both offsets to a common value\n", legenda, reset);
		rz_cons_canvas_gotoxy(canvas, 6, 10);
		rz_cons_canvas_write(canvas, line);

		snprintf(line, lsize, "%s0%s     shows/hides the column legenda\n", legenda, reset);
		rz_cons_canvas_gotoxy(canvas, 6, 11);
		rz_cons_canvas_write(canvas, line);

		snprintf(line, lsize, "%s%s %s%s increase/decrease both offsets by %u\n", legenda, arrow_down, arrow_up, reset, 1 << shift);
		rz_cons_canvas_gotoxy(canvas, 6, 12);
		rz_cons_canvas_write(canvas, line);

		snprintf(line, lsize, "%s%s %s%s increase/decrease both offsets by 1\n", legenda, arrow_left, arrow_right, reset);
		rz_cons_canvas_gotoxy(canvas, 6, 13);
		rz_cons_canvas_write(canvas, line);

		snprintf(line, lsize, "%s:%s     seek at offset (relative via +-)\n", legenda, reset);
		rz_cons_canvas_gotoxy(canvas, 6, 14);
		rz_cons_canvas_write(canvas, line);

		snprintf(line, lsize, "%s3%s     file0 seek at offset (relative via +-)\n", legenda, reset);
		rz_cons_canvas_gotoxy(canvas, 6, 15);
		rz_cons_canvas_write(canvas, line);

		snprintf(line, lsize, "%s4%s     file1 seek at offset (relative via +-)\n", legenda, reset);
		rz_cons_canvas_gotoxy(canvas, 6, 16);
		rz_cons_canvas_write(canvas, line);
	}

	rz_cons_canvas_print(canvas);
	rz_cons_flush();

	// allow to refresh the terminal
	// before printing again the ui
	rz_sys_usleep(200);
	return true;
}

static char *visual_prompt(DiffHexView *hview, const char *prompt) {
	char buf[1024];
	rz_cons_gotoxy(0, hview->screen.height);
	rz_cons_clear_line(stdout);
	rz_cons_printf("%s%s ", hview->colors.reset, prompt);
	rz_line_set_prompt(rz_cons_singleton()->line, ":> ");
	rz_cons_flush();
	rz_cons_fgets(buf, sizeof(buf), 0, NULL);
	if (*buf) {
		return rz_str_dup(buf);
	}
	return NULL;
}

static void prompt_offset_and_seek(DiffHexView *hview, ut64 minseek) {
	char *value = visual_prompt(hview, " you can input an absolute offset or a relative offset by adding the prefix + or -\n offset");
	if (value) {
		const char *p = rz_str_trim_head_ro(value);
		if (!IS_DIGIT(*p) && *p != '-' && *p != '+') {
			free(value);
			return;
		}
		st64 number = strtoll((*p == '+' || *p == '-') ? p + 1 : p, NULL, 0);
		if (*p == '-') {
			hview->address_a -= number;
			hview->address_b -= number;
		} else if (*p == '+') {
			hview->address_a += number;
			hview->address_b += number;
		} else {
			hview->address_a = number;
			hview->address_b = number;
		}
	}
	free(value);
}

static void prompt_offset_and_seek_file(DiffHexView *hview, ut64 minseek, bool is_file0) {
	char *value = visual_prompt(hview, " you can input an absolute offset or a relative offset by adding the prefix + or -\n offset");
	if (value) {
		const char *p = rz_str_trim_head_ro(value);
		if (!IS_DIGIT(*p) && *p != '-' && *p != '+') {
			free(value);
			return;
		}
		st64 number = strtoll((*p == '+' || *p == '-') ? p + 1 : p, NULL, 0);
		if (*p == '-') {
			if (is_file0) {
				hview->address_a -= number;
			} else {
				hview->address_b -= number;
			}
		} else if (*p == '+') {
			if (is_file0) {
				hview->address_a += number;
			} else {
				hview->address_b += number;
			}
		} else {
			if (is_file0) {
				hview->address_a = number;
			} else {
				hview->address_b = number;
			}
		}
	}
	free(value);
}

static void find_next_diff(DiffHexView *hview, ut64 seek) {
	if (!hview->buffer_a || !hview->buffer_b) {
		return;
	}

	DiffIO *io_a = hview->io_a;
	DiffIO *io_b = hview->io_b;
	ssize_t read_a = 0, read_b = 0;
	ssize_t minread = 0, minseek = 0;
	ut64 address_a = hview->address_a + seek;
	ut64 address_b = hview->address_b + seek;
	ut64 minsize = RZ_MIN(hview->size_a, hview->size_b);
	if (RZ_MIN(io_a->filesize, io_b->filesize) < seek) {
		hview->address_a = 0;
		hview->address_b = 0;
		return;
	}
	do {
		read_a = rz_io_pread_at(io_a->io, address_a, hview->buffer_a, minsize);
		read_b = rz_io_pread_at(io_b->io, address_b, hview->buffer_b, minsize);
		if (read_a < 1 || read_b < 1) {
			break;
		}
		minread = RZ_MIN(read_a, read_b);
		if (minread != minsize && !memcmp(hview->buffer_a, hview->buffer_b, minread)) {
			address_a += RZ_MAX(minread - seek, 0);
			address_b += RZ_MAX(minread - seek, 0);
			break;
		} else if (minread == minsize && !memcmp(hview->buffer_a, hview->buffer_b, minsize)) {
			address_a += minsize;
			address_b += minsize;
			continue;
		}
		minread = RZ_MIN(minsize, minread);
		minseek = RZ_MIN(seek, minread);
		for (ssize_t i = 0; i < minread; i += minseek) {
			if (memcmp(&hview->buffer_a[i], &hview->buffer_b[i], minseek)) {
				hview->address_a = address_a;
				hview->address_b = address_b;
				return;
			}
			address_a += minseek;
			address_b += minseek;
		}
	} while (1);

	if (address_a >= io_a->filesize) {
		address_a = io_a->filesize - seek;
	}

	if (address_b >= io_b->filesize) {
		address_b = io_b->filesize - seek;
	}

	hview->address_a = address_a;
	hview->address_b = address_b;
}

static void find_prev_diff(DiffHexView *hview, ut64 seek) {
	if (!hview->buffer_a || !hview->buffer_b) {
		return;
	}

	DiffIO *io_a = hview->io_a;
	DiffIO *io_b = hview->io_b;
	ssize_t read_a = 0, read_b = 0;
	st64 address_a = hview->address_a;
	st64 address_b = hview->address_b;

	do {
		address_a -= seek;
		address_b -= seek;
		if (address_a < 0) {
			address_a = 0;
		}
		if (address_b < 0) {
			address_b = 0;
		}
		read_a = rz_io_pread_at(io_a->io, address_a, hview->buffer_a, seek);
		read_b = rz_io_pread_at(io_b->io, address_b, hview->buffer_b, seek);
		if (read_a < 1 || read_b < 1) {
			break;
		}
		if (memcmp(hview->buffer_a, hview->buffer_b, seek)) {
			break;
		}
		if (address_a == 0 || address_b == 0) {
			break;
		}
	} while (1);
	hview->address_a = RZ_MAX(address_a, 0);
	hview->address_b = RZ_MAX(address_b, 0);
}

static void rz_diff_resize_buffer(DiffHexView *hview) {
	int height, width = rz_cons_get_size(&height);

	ut64 size_a = ((st64)(width / 2) * (height - 2));
	ut64 size_b = ((st64)(width / 2) * (height - 2));
	st64 video_size = width;
	video_size *= height;

	// build the replacement view state first so resize is like all-or-nothing.
	char *new_line = malloc(video_size);
	ut8 *new_buf_a = malloc(size_a);
	ut8 *new_buf_b = malloc(size_b);
	if (!new_line || !new_buf_a || !new_buf_b) {
		free(new_line);
		free(new_buf_a);
		free(new_buf_b);
		return;
	}

	RzConsCanvas *new_canvas = rz_cons_canvas_new(width, height);
	if (!new_canvas) {
		free(new_line);
		free(new_buf_a);
		free(new_buf_b);
		return;
	}

	new_canvas->color = true;
	new_canvas->linemode = 1;

	// swap only after every replacement alloc succeeded.
	free(hview->line);
	free(hview->buffer_a);
	free(hview->buffer_b);
	rz_cons_canvas_free(hview->canvas);
	hview->line = new_line;
	hview->buffer_a = new_buf_a;
	hview->buffer_b = new_buf_b;
	hview->size_a = size_a;
	hview->size_b = size_b;
	hview->screen.width = width;
	hview->screen.height = height;
	hview->canvas = new_canvas;

	rz_diff_draw_tui(hview, false);
}

static bool rz_diff_hex_visual(DiffContext *ctx) {
	RzCons *console = NULL;
	DiffIO *io_a = NULL;
	DiffIO *io_b = NULL;
	RzConsCanvas *canvas = NULL;
	DiffHexView hview;
	bool draw_visual = true;
	bool show_help = false;
	int read, pressed;
	int height = ctx->screen.width;
	int width = ctx->screen.height;
	ut64 size_a = 0;
	ut64 size_b = 0;

	hview.line = NULL;
	hview.buffer_a = NULL;
	hview.buffer_b = NULL;

	RzCore *core = rz_core_new();
	if (!core) {
		rz_diff_error("cannot allocate core\n");
		goto rz_diff_hex_visual_fail;
	}

	rz_core_parse_rizinrc(core);

	console = rz_cons_singleton();
	if (!console) {
		rz_diff_error("cannot get console.\n");
		goto rz_diff_hex_visual_fail;
	}

	rz_cons_set_interactive(false);

	io_a = rz_diff_io_open(ctx->file_a);
	if (!io_a) {
		goto rz_diff_hex_visual_fail;
	}

	io_b = rz_diff_io_open(ctx->file_b);
	if (!io_b) {
		goto rz_diff_hex_visual_fail;
	}

	if (width < 1 && height < 1) {
		width = rz_cons_get_size(&height);
		if (width < 1 && height < 1) {
			rz_diff_error("invalid screen size; use -S WxH to define the sizes.\n");
			goto rz_diff_hex_visual_fail;
		}
	}

	canvas = rz_cons_canvas_new(width, height);
	if (!canvas) {
		rz_diff_error("cannot allocate canvas. try to use -S WxH to define the sizes.\n");
		goto rz_diff_hex_visual_fail;
	}

	size_a = ((width / 2) * (height - 2));
	size_b = ((width / 2) * (height - 2));

	canvas->color = true;
	canvas->linemode = 1;

	st64 video_size = width;
	video_size *= height;
	hview.line = malloc(video_size);
	if (!hview.line) {
		rz_diff_error("cannot allocate line buffer.\n");
		goto rz_diff_hex_visual_fail;
	}
	hview.buffer_a = malloc(size_a);
	if (!hview.buffer_a) {
		rz_diff_error("cannot allocate buffer for %s.\n", io_a->filename);
		goto rz_diff_hex_visual_fail;
	}
	hview.buffer_b = malloc(size_b);
	if (!hview.buffer_b) {
		rz_diff_error("cannot allocate buffer for %s.\n", io_b->filename);
		goto rz_diff_hex_visual_fail;
	}

	hview.size_a = size_a;
	hview.size_b = size_b;
	hview.io_a = io_a;
	hview.io_b = io_b;
	hview.canvas = canvas;
	hview.screen.width = width;
	hview.screen.height = height;
	hview.address_a = 0;
	hview.address_b = 0;
	hview.column_descr = true;
	rz_diff_get_colors(&hview.colors, console->context, ctx->colors);

	rz_cons_show_cursor(false);
	rz_cons_enable_mouse(false);

	console->event_data = &hview;
	console->event_resize = (RzConsEvent)rz_diff_resize_buffer;

	int seekmin = 0;
	while (draw_visual && !rz_cons_is_breaked()) {
		if (!rz_diff_draw_tui(&hview, show_help)) {
			break;
		}
		seekmin = seek_min_value(&hview);
		read = rz_cons_readchar();
		pressed = rz_cons_arrow_to_hjkl(read);

		if (show_help && (pressed == 'q' || pressed == 'Q')) {
			// allow to close the help without closing the util
			pressed = 0;
		}

		show_help = false;
		switch (pressed) {
		case '0':
			hview.column_descr = !hview.column_descr;
			break;
		case '?':
			show_help = true;
			break;
		case ':':
			prompt_offset_and_seek(&hview, seekmin);
			break;
		case '3':
			prompt_offset_and_seek_file(&hview, seekmin, true);
			break;
		case '4':
			prompt_offset_and_seek_file(&hview, seekmin, false);
			break;
		case '9':
			hview.address_a = hview.address_b = RZ_MIN(hview.address_a, hview.address_b);
			break;
		case 'G':
		case 'g':
			hview.address_a = io_a->filesize - seekmin;
			hview.address_b = io_b->filesize - seekmin;
			break;
		case 'B':
		case 'b':
			hview.address_a = 0;
			hview.address_b = 0;
			break;
		case 'A':
		case 'a':
			hview.address_a--;
			break;
		case 'N':
		case 'n':
			find_next_diff(&hview, seekmin);
			break;
		case 'M':
		case 'm':
			find_prev_diff(&hview, seekmin);
			break;
		case 'Z':
		case 'z':
			hview.address_a++;
			break;
		case 'D':
		case 'd':
			hview.address_b--;
			break;
		case 'C':
		case 'c':
			hview.address_b++;
			break;
		/* ARROWS */
		case '1':
			hview.address_a -= (seekmin * (height - 2));
			hview.address_b -= (seekmin * (height - 2));
			break;
		case '2':
			hview.address_a += (seekmin * (height - 2));
			hview.address_b += (seekmin * (height - 2));
			break;
		case 'K':
		case 'k':
			hview.address_a -= seekmin;
			hview.address_b -= seekmin;
			break;
		case 'J':
		case 'j':
			hview.address_a += seekmin;
			hview.address_b += seekmin;
			break;
		case 'L':
		case 'l':
			hview.address_a--;
			hview.address_b--;
			break;
		case 'H':
		case 'h':
			hview.address_a++;
			hview.address_b++;
			break;
		case -1: // EOF
		case 'Q':
		case 'q':
			draw_visual = false;
		default:
			break;
		}
	}
	canvas = hview.canvas;
	console->event_data = NULL;
	console->event_resize = NULL;

	rz_cons_show_cursor(true);
	rz_cons_goto_origin_reset();
	rz_cons_clear();
	rz_cons_print(Color_RESET_TERMINAL);
	rz_cons_flush();

rz_diff_hex_visual_fail:
	free(hview.line);
	free(hview.buffer_a);
	free(hview.buffer_b);
	rz_cons_canvas_free(canvas);
	rz_diff_io_close(io_a);
	rz_diff_io_close(io_b);
	rz_core_free(core);
	rz_cons_free();
	return true;
}

RZ_API int rz_main_rz_diff(int argc, const char **argv) {
	bool success = false;
	DiffContext ctx;
	rz_diff_parse_arguments(argc, argv, &ctx);

	switch (ctx.option) {
	case DIFF_OPT_DISTANCE:
		success = rz_diff_calculate_distance(&ctx);
		break;
	case DIFF_OPT_UNIFIED:
		success = rz_diff_unified_files(&ctx);
		break;
	case DIFF_OPT_GRAPH:
		success = rz_diff_graphs_files(&ctx);
		break;
	case DIFF_OPT_HEX_VISUAL:
		success = rz_diff_hex_visual(&ctx);
		break;
	case DIFF_OPT_VERSION: {
		RzPath *sys_path = rz_path_new();
		if (!sys_path) {
			rz_diff_show_help(false);
			break;
		}
		rz_main_version_print(sys_path, "rz-diff");
		rz_path_free(sys_path);
		success = true;
		break;
	}
	case DIFF_OPT_USAGE:
		rz_diff_show_help(true);
		break;
	case DIFF_OPT_ERROR:
		break;
	case DIFF_OPT_HELP:
		success = true;
		// fallthrough
	default:
		rz_diff_show_help(false);
		break;
	}

	rz_list_free(ctx.evars);
	return success ? 0 : 1;
}
