// SPDX-FileCopyrightText: 2021 RizinOrg <info@rizin.re>
// SPDX-FileCopyrightText: 2021 deroad <wargio@libero.it>
// SPDX-License-Identifier: LGPL-3.0-only

#include <rz_cons.h>

#define Color_RANGE    Color_BBLUE
#define Color_INSERT   Color_BGREEN
#define Color_DELETE   Color_BRED
#define Color_BGINSERT "\x1b[48;5;22m"
#define Color_BGDELETE "\x1b[48;5;52m"

#define RZ_DIFF_DEFAULT_N_GROUPS_BYTES 8

#define FAST_MOD2(x, y)      ((x) & (y - 1))
#define FAST_MOD64(x)        FAST_MOD2(x, 64)
#define DIFF_COLOR(prefix)   (prefix == '+' ? Color_INSERT : (prefix == '-' ? Color_DELETE : ""))
#define DIFF_BGCOLOR(prefix) (prefix == '+' ? Color_BGINSERT : (prefix == '-' ? Color_BGDELETE : ""))

static inline ut32 count_newlines(RzDiff *diff, const void *array, st32 beg, st32 end) {
	RzDiffMethodElemAt elem_at = diff->methods.elem_at;
	RzDiffMethodStringify stringify = diff->methods.stringify;
	int len = 0;
	ut32 count = 0;
	const char *p;
	const void *elem;
	RzStrBuf tmp;

	for (st32 i = beg; i < end; ++i) {
		rz_strbuf_init(&tmp);
		elem = elem_at(array, i);
		stringify(elem, &tmp);
		len = rz_strbuf_length(&tmp);
		p = rz_strbuf_get(&tmp);
		if (len > 0 && p[len - 1] == '\n') {
			count++;
		}
		rz_strbuf_fini(&tmp);
	}
	return count;
}

static inline void diff_unified_append_ranges(RzList /*<RzDiffOp *>*/ *opcodes, RzStrBuf *sb, bool color) {
	const char *color_beg = color ? Color_RANGE : "";
	const char *color_end = color ? Color_RESET : "";

	RzDiffOp *first = rz_list_first_val(opcodes);
	RzDiffOp *last = rz_list_last_val(opcodes);
	st32 a_len = last->a_end - first->a_beg;
	st32 b_len = last->b_end - first->b_beg;

	rz_strbuf_appendf(sb, "%s@@ -%d,%d +%d,%d @@%s\n", color_beg, first->a_beg + 1, a_len, first->b_beg + 1, b_len, color_end);
}

static inline void diff_unified_json_ranges(RzList /*<RzDiffOp *>*/ *opcodes, PJ *pj) {
	RzDiffOp *first = rz_list_first_val(opcodes);
	RzDiffOp *last = rz_list_last_val(opcodes);
	st32 a_len = last->a_end - first->a_beg;
	st32 b_len = last->b_end - first->b_beg;

	pj_ka(pj, "from");
	pj_N(pj, first->a_beg + 1);
	pj_N(pj, a_len);
	pj_end(pj);

	pj_ka(pj, "to");
	pj_N(pj, first->b_beg + 1);
	pj_N(pj, b_len);
	pj_end(pj);
}

static inline void diff_unified_append_data(RzDiff *diff, const void *array, st32 beg, st32 end, RzStrBuf *sb, char prefix, bool color) {
	RzDiffMethodElemAt elem_at = diff->methods.elem_at;
	RzDiffMethodStringify stringify = diff->methods.stringify;
	int len = 0;
	ut32 count = 0;
	const char *p;
	const void *elem;
	RzStrBuf tmp;
	bool newline = false;
	bool is_bytes = DIFF_IS_BYTES_METHOD(diff->methods);

	if (beg < 0) {
		beg = 0;
	}

	const char *bcol = color ? DIFF_COLOR(prefix) : "";
	const char *ecol = color ? (Color_RESET) : "";

	rz_strbuf_appendf(sb, "%s%c", bcol, prefix);
	for (st32 i = beg; i < end; ++i) {
		if (newline || (is_bytes && count > 0 && !FAST_MOD64(count))) {
			rz_strbuf_appendf(sb, "%s\n%s%c", ecol, bcol, prefix);
			newline = false;
		}
		rz_strbuf_init(&tmp);
		elem = elem_at(array, i);
		stringify(elem, &tmp);
		len = rz_strbuf_length(&tmp);
		p = rz_strbuf_get(&tmp);
		count += len;
		if (len > 0 && p[len - 1] == '\n') {
			len--;
			newline = true;
		}
		rz_strbuf_append_n(sb, p, len);
		rz_strbuf_fini(&tmp);
	}
	rz_strbuf_appendf(sb, "%s\n", ecol);
}

// Assumes that color is true, diffing lines, op->type is RZ_DIFF_OP_REPLACE
// and that the number of inserted lines is equal to the number of deleted
// lines.
static inline void diff_unified_lines_hl(RzDiff *diff, RzDiffOp *op, RzStrBuf *sb, char del_prefix, char ins_prefix) {
	RzDiffMethodElemAt elem_at = diff->methods.elem_at;
	RzDiffMethodStringify stringify = diff->methods.stringify;
	int len = 0;
	const char *p;
	const void *elem;
	RzStrBuf tmp, tmp2;
	const char *ecol = Color_RESET;
	const char *ebgcol = Color_RESET_BG;

	const void *a_array = diff->a;
	st32 a_beg = op->a_beg;
	if (a_beg < 0) {
		a_beg = 0;
	}
	st32 a_end = op->a_end;

	const void *b_array = diff->b;
	st32 b_beg = op->b_beg;
	if (b_beg < 0) {
		b_beg = 0;
	}
	st32 b_end = op->b_end;

	ut32 num_nl = count_newlines(diff, a_array, a_beg, a_end);
	// + 1 just in case there's no nl at end
	ut32 num_bounds = num_nl + 1;
	st32 *char_bounds = malloc(sizeof(st32) * 2 * num_bounds);
	if (!char_bounds) {
		return;
	}
	for (ut32 i = 0; i < num_bounds; i++) {
		char_bounds[i * 2] = char_bounds[i * 2 + 1] = -1;
	}

	// Fill char_bounds array
	ut32 bounds_idx = 0;
	bool newline = false;
	st32 i = a_beg;
	st32 j = b_beg;
	for (; i < a_end; ++i) {
		if (newline) {
			bounds_idx++;
			newline = false;
		}
		rz_strbuf_init(&tmp);
		elem = elem_at(a_array, i);
		stringify(elem, &tmp);
		len = rz_strbuf_length(&tmp);
		p = rz_strbuf_get(&tmp);
		if (len > 0 && p[len - 1] == '\n') {
			len--;
			newline = true;
		}

		int len_b = 0;
		const void *elem_b;
		const char *p_b;

		for (; j < b_end; ++j) {
			rz_strbuf_init(&tmp2);
			elem_b = elem_at(b_array, j);
			stringify(elem_b, &tmp2);
			len_b = rz_strbuf_length(&tmp2);
			p_b = rz_strbuf_get(&tmp2);
			if (len_b > 0 && p_b[len_b - 1] == '\n') {
				len_b--;
			}

			if (len && len_b && (p[0] == p_b[0] || p[len - 1] == p_b[len_b - 1])) {
				// Get left bound.
				st32 left = 0;
				for (; left < RZ_MIN(len, len_b) && p[left] == p_b[left]; left++)
					;
				char_bounds[bounds_idx * 2] = left;
				// Get right bound (offset). "- left" chops off
				// the left portion that has already matched.
				st32 right = 0;
				for (; right < RZ_MIN(len, len_b) - left && p[len - 1 - right] == p_b[len_b - 1 - right];
					right++)
					;
				char_bounds[bounds_idx * 2 + 1] = right;
			}

			rz_strbuf_fini(&tmp2);
			++j;
			break;
		}
		rz_strbuf_fini(&tmp);
	}

	// Show deleted lines
	char prefix = del_prefix;
	const char *bcol = DIFF_COLOR(prefix);
	const char *bbgcol = DIFF_BGCOLOR(prefix);
	newline = false;
	bounds_idx = 0;

	rz_strbuf_appendf(sb, "%s%c", bcol, prefix);
	for (st32 i = a_beg; i < a_end; ++i) {
		if (newline) {
			rz_strbuf_appendf(sb, "%s\n%s%c", ecol, bcol, prefix);
			newline = false;
			bounds_idx++;
		}
		rz_strbuf_init(&tmp);
		elem = elem_at(a_array, i);
		stringify(elem, &tmp);
		len = rz_strbuf_length(&tmp);
		p = rz_strbuf_get(&tmp);
		if (len > 0 && p[len - 1] == '\n') {
			len--;
			newline = true;
		}
		st32 left = char_bounds[bounds_idx * 2];
		st32 right = char_bounds[bounds_idx * 2 + 1];
		if (left < 0 || right < 0 || len - right < left) {
			rz_strbuf_append_n(sb, p, len);
		} else {
			rz_strbuf_append_n(sb, p, left);
			rz_strbuf_append(sb, bbgcol);
			rz_strbuf_append_n(sb, p + left, len - right - left);
			rz_strbuf_append(sb, ebgcol);
			rz_strbuf_append_n(sb, p + len - right, right);
		}
		rz_strbuf_fini(&tmp);
	}
	rz_strbuf_appendf(sb, "%s\n", ecol);

	// Show inserted lines
	prefix = ins_prefix;
	bcol = DIFF_COLOR(prefix);
	bbgcol = DIFF_BGCOLOR(prefix);
	newline = false;
	bounds_idx = 0;

	rz_strbuf_appendf(sb, "%s%c", bcol, prefix);
	for (st32 i = b_beg; i < b_end; ++i) {
		if (newline) {
			rz_strbuf_appendf(sb, "%s\n%s%c", ecol, bcol, prefix);
			newline = false;
			bounds_idx++;
		}
		rz_strbuf_init(&tmp);
		elem = elem_at(b_array, i);
		stringify(elem, &tmp);
		len = rz_strbuf_length(&tmp);
		p = rz_strbuf_get(&tmp);
		if (len > 0 && p[len - 1] == '\n') {
			len--;
			newline = true;
		}
		st32 left = char_bounds[bounds_idx * 2];
		st32 right = char_bounds[bounds_idx * 2 + 1];
		if (left < 0 || right < 0 || len - right < left) {
			rz_strbuf_append_n(sb, p, len);
		} else {
			rz_strbuf_append_n(sb, p, left);
			rz_strbuf_append(sb, bbgcol);
			rz_strbuf_append_n(sb, p + left, len - right - left);
			rz_strbuf_append(sb, ebgcol);
			rz_strbuf_append_n(sb, p + len - right, right);
		}
		rz_strbuf_fini(&tmp);
	}
	rz_strbuf_appendf(sb, "%s\n", ecol);
	free(char_bounds);
}

static inline void diff_unified_json_data(RzDiff *diff, const void *array, st32 beg, st32 end, PJ *pj, const char *op) {
	RzDiffMethodElemAt elem_at = diff->methods.elem_at;
	RzDiffMethodStringify stringify = diff->methods.stringify;
	int len = 0;
	ut32 count = 0;
	const char *p;
	const void *elem;
	RzStrBuf tmp;
	bool newline = false;
	bool is_bytes = DIFF_IS_BYTES_METHOD(diff->methods);

	if (beg < 0) {
		beg = 0;
	}

	pj_o(pj);
	pj_ks(pj, "op", op);
	rz_strbuf_init(&tmp);
	for (st32 i = beg; i < end; ++i) {
		if (newline || (is_bytes && count > 0 && !FAST_MOD64(count))) {
			pj_ks(pj, "value", rz_strbuf_get(&tmp));
			pj_end(pj);

			rz_strbuf_fini(&tmp);
			rz_strbuf_init(&tmp);

			pj_o(pj);
			pj_ks(pj, "op", op);
			newline = false;
		}
		elem = elem_at(array, i);
		stringify(elem, &tmp);
		len = rz_strbuf_length(&tmp);
		p = rz_strbuf_get(&tmp);
		count += len;
		if (len > 0 && p[len - 1] == '\n') {
			newline = true;
		}
	}
	pj_ks(pj, "value", rz_strbuf_get(&tmp));
	pj_end(pj);
	rz_strbuf_fini(&tmp);
}

/**
 * \brief Produces a diff output with A and B inputs presented immediately adjacent to each other.
 *
 * Produces a diff output with A and B inputs presented immediately adjacent to each other.
 * It begins with range information and is immediately followed with the line additions,
 * line deletions, and any number of the contextual lines.
 * */
RZ_API RZ_OWN char *rz_diff_unified_text(RZ_NONNULL RzDiff *diff, RZ_NULLABLE const char *from, RZ_NULLABLE const char *to, bool show_time, bool color) {
	rz_return_val_if_fail(diff && diff->methods.elem_at && diff->methods.stringify, NULL);
	RzStrBuf *sb = NULL;
	RzList *groups = NULL;
	RzList *opcodes = NULL;
	RzDiffOp *op = NULL;
	RzListIter *itg = NULL;
	RzListIter *ito = NULL;

	if (!from) {
		from = "/original";
	}
	if (!to) {
		to = "/modified";
	}
	sb = rz_strbuf_new("");
	if (!sb) {
		RZ_LOG_ERROR("rz_diff_unified: cannot allocate sb\n");
		goto rz_diff_unified_text_fail;
	}

	if (show_time) {
		char *time = rz_time_date_now_to_string();
		rz_strbuf_appendf(sb, "--- %s %s\n+++ %s %s\n", from, (time ? time : ""), to, (time ? time : ""));
		free(time);
	} else {
		rz_strbuf_appendf(sb, "--- %s\n+++ %s\n", from, to);
	}

	bool is_lines = DIFF_IS_LINES_METHOD(diff->methods);
	bool is_bytes = DIFF_IS_BYTES_METHOD(diff->methods);

	groups = rz_diff_opcodes_grouped_new(diff, is_bytes ? RZ_DIFF_DEFAULT_N_GROUPS_BYTES : RZ_DIFF_DEFAULT_N_GROUPS);
	if (!groups) {
		goto rz_diff_unified_text_fail;
	}

	rz_list_foreach (groups, itg, opcodes) {
		if (rz_list_length(opcodes) < 1) {
			continue;
		}
		diff_unified_append_ranges(opcodes, sb, color);
		rz_list_foreach (opcodes, ito, op) {
			switch (op->type) {
			case RZ_DIFF_OP_DELETE:
				diff_unified_append_data(diff, diff->a, op->a_beg, op->a_end, sb, '-', color);
				break;
			case RZ_DIFF_OP_EQUAL:
				diff_unified_append_data(diff, diff->a, op->a_beg, op->a_end, sb, ' ', color);
				break;
			case RZ_DIFF_OP_INSERT:
				diff_unified_append_data(diff, diff->b, op->b_beg, op->b_end, sb, '+', color);
				break;
			default: // RZ_DIFF_OP_REPLACE
				if (!color || !is_lines ||
					count_newlines(diff, diff->a, op->a_beg, op->a_end) !=
						count_newlines(diff, diff->b, op->b_beg, op->b_end)) {
					diff_unified_append_data(diff, diff->a, op->a_beg, op->a_end, sb, '-', color);
					diff_unified_append_data(diff, diff->b, op->b_beg, op->b_end, sb, '+', color);
				} else {
					diff_unified_lines_hl(diff, op, sb, '-', '+');
				}
				break;
			}
		}
	}

	rz_list_free(groups);
	return rz_strbuf_drain(sb);

rz_diff_unified_text_fail:
	rz_strbuf_free(sb);
	rz_list_free(groups);
	return NULL;
}

/**
 * \brief Produces a diff output to convert A in B in a JSON format.
 *
 * Produces a diff output with A and B inputs and contains the operations required
 * to convert A in B and the values to remove, insert or keep.
 * */
RZ_API RZ_OWN PJ *rz_diff_unified_json(RZ_NONNULL RzDiff *diff, RZ_NULLABLE const char *from, RZ_NULLABLE const char *to, bool show_time) {
	rz_return_val_if_fail(diff && diff->methods.elem_at && diff->methods.stringify, NULL);
	PJ *pj = NULL;
	RzList *groups = NULL;
	RzList *opcodes = NULL;
	RzDiffOp *op = NULL;
	RzListIter *itg = NULL;
	RzListIter *ito = NULL;

	if (!from) {
		from = "/original";
	}
	if (!to) {
		to = "/modified";
	}

	pj = pj_new();
	if (!pj) {
		RZ_LOG_ERROR("rz_diff_unified: failed to allocate json\n");
		goto rz_diff_unified_json_fail;
	}
	pj_o(pj);

	if (show_time) {
		char *time = rz_time_date_now_to_string();
		if (!time) {
			RZ_LOG_ERROR("rz_diff_unified: failed to allocate timestamp\n");
			goto rz_diff_unified_json_fail;
		}
		pj_ks(pj, "timestamp", time);
		free(time);
	}

	pj_ks(pj, "from", from);
	pj_ks(pj, "to", to);

	groups = rz_diff_opcodes_grouped_new(diff, RZ_DIFF_DEFAULT_N_GROUPS);
	if (!groups) {
		goto rz_diff_unified_json_fail;
	}

	pj_ka(pj, "diff");
	rz_list_foreach (groups, itg, opcodes) {
		if (rz_list_length(opcodes) < 1) {
			continue;
		}
		pj_o(pj);
		diff_unified_json_ranges(opcodes, pj);
		pj_ka(pj, "ops");
		rz_list_foreach (opcodes, ito, op) {
			if (op->type == RZ_DIFF_OP_EQUAL) {
				diff_unified_json_data(diff, diff->a, op->a_beg, op->a_end, pj, "equal");
				continue;
			}
			if (op->type == RZ_DIFF_OP_DELETE || op->type == RZ_DIFF_OP_REPLACE) {
				diff_unified_json_data(diff, diff->a, op->a_beg, op->a_end, pj, "delete");
			}
			if (op->type == RZ_DIFF_OP_INSERT || op->type == RZ_DIFF_OP_REPLACE) {
				diff_unified_json_data(diff, diff->b, op->b_beg, op->b_end, pj, "insert");
			}
		}
		pj_end(pj);
		pj_end(pj);
	}
	pj_end(pj);
	pj_end(pj);

	rz_list_free(groups);
	return pj;

rz_diff_unified_json_fail:
	pj_free(pj);
	rz_list_free(groups);
	return NULL;
}
