/**
 *
 * This is an implementation of Sentence Break Buffer
 * It is still in development and could contain bugs
 * If you will find bugs in SBS, you could try to create an
 * issue in the forked repository:
 *
 * https://github.com/maxkoryukov/ccextractor/issues
 * Only SBS-related bugs!
 *
 * IMPORTANT: SBS is color-blind, and color tags mislead SBS.
 * Please, use `-sbs` option with `-nofc` (actual for CCE 0.8.5)
 */

#include "ccx_common_platform.h"
#include "ccx_encoders_common.h"
#include "lib_ccx.h"
#include "ocr.h"
#include "utility.h"

// #define DEBUG_SBS
// #define ENABLE_OCR

#ifdef DEBUG_SBS
#define LOG_DEBUG(...) fprintf(stdout, __VA_ARGS__)
#else
#define LOG_DEBUG ;
#endif

#ifdef ENABLE_SHARING
#include "ccx_share.h"
#endif //ENABLE_SHARING


//---------------------------
// BEGIN of #BUG639
// HACK: this is workaround for https://github.com/CCExtractor/ccextractor/issues/639
// short: the outside function, called encode_sub changes ENCODER_CONTEXT when it required..
// as result SBS loses it internal state...

// #BUG639
typedef struct {

	unsigned char * buffer; /// Storage for sentence-split buffer
	size_t handled_len; /// The length of the string in the SBS-buffer, already handled, but preserved for DUP-detection.

	//ccx_sbs_utf8_character *sbs_newblock;
	LLONG time_from; // Used by the split-by-sentence code to know when the current block starts...
	LLONG time_trim; // ... and ends
	size_t capacity;

} sbs_context_t;

// DO NOT USE IT DIRECTLY!
// The only one exceptions:
// 1. init_sbs_context
sbs_context_t * ____sbs_context = NULL;

/**
 * Initializes SBS settings for encoder context
 */

void sbs_reset_context() {
	if (NULL != ____sbs_context) {
		free(____sbs_context);
		____sbs_context = NULL;
	}
}

//void init_encoder_sbs(struct encoder_ctx * ctx, const int splitbysentence);
sbs_context_t * sbs_init_context() {

	LOG_DEBUG("SBS: init_sbs_context\n\
____sbs_context:   [%p]\n\
",
		____sbs_context
	);

	if (NULL == ____sbs_context) {

		LOG_DEBUG("SBS: init_sbs_context: INIT\n");

		____sbs_context = malloc(sizeof(sbs_context_t));
		____sbs_context->time_from = -1;
		____sbs_context->time_trim = -1;
		____sbs_context->capacity = 16;
		____sbs_context->buffer = malloc(____sbs_context->capacity * sizeof(unsigned char));
		____sbs_context->buffer[0] = 0;
		____sbs_context->handled_len = 0;
	}

	LOG_DEBUG("SBS: init_sbs_context: DONE\n\
____sbs_context:   [%p]\n\
",
		____sbs_context
	);

	return ____sbs_context;
}

// END of #BUG639
//---------------------------

void sbs_str_autofix(unsigned char * str)
{
	LOG_DEBUG("SBS: sbs_str_autofix\n\
\t old str:  [%s]\n\
",
		str
	);

	int i;
	int j;

	i = 0;
	j = 0;

	// replace all whitespaces with spaces:
	while (str[i] != 0)
	{

		if (isspace(str[i])) {
			// \n
			// \t
			// \r
			// <WHITESPACES>
			// =>
			// <SPACE>
			while (isspace(str[i])) {
				i++;
			}

			if (j > 0) {
				str[j] = ' ';
				j++;
			}

		} else if (
			str[i] == '|'
			&& (i==0 || isspace(str[i-1]))
			&& (str[i+1] == 0 || isspace(str[i+1]) || str[i+1]=='\'')
		) {
			// <SPACE>|'
			// <SPACE>|<SPACE>
			// =>
			// <SPACE>I'
			str[j] = 'I';
			i++;
			j++;
		} else {
			str[j] = str[i];
			i++;
			j++;
		}
	}

	str[j] = 0;

	LOG_DEBUG("SBS: sbs_str_autofix\n\
\t old str:  [%s]\n\
",
		str
	);
}

int sbs_is_pointer_on_sentence_breaker(char * start, char * current)
{
	char c = *current;
	char n = *(current + 1);
	char p = *(current - 1);

	if (0 == c) n = 0;
	if (current == start) p = 0;

	if (0 == c) return 1;

	if ('.' == c
		|| '!' == c
		|| '?' == c
	)
	{
		if ('.' == n
			|| '!' == n
			|| '?' == n
		)
		{
			return 0;
		}

		return 1;
	}

	return 0;
}

int sbs_char_equal_CI(const char A, const char B) {
	char a = tolower(A);
	char b = tolower(B);
	if (a > b) return 1;
	if (a < b) return -1;
	return 0;
}

char * sbs_find_insert_point_partial(char * old_tail, const char * new_start, size_t n, const int maxerr, int *errcount) {

	const int PARTIAL_CHANGE_LENGTH_MIN = 7;
	const int FEW_ERRORS_RATE = 10;

#ifdef DEBUG_SBS
	char fmtbuf[20000];
#endif

	int few_errors = maxerr/FEW_ERRORS_RATE;

	size_t len_r = n/2;
	size_t len_l = n - len_r;

	int dist_l = -1;
	int dist_r = -1;
	int partial_shift;

	int i; // top level indexer for strings

	dist_l = levenshtein_dist_char(old_tail, new_start, len_l, len_l);
	dist_r = levenshtein_dist_char(old_tail + len_l, new_start + len_l, len_r, len_r);

	*errcount = dist_r + dist_l;

	if (dist_l + dist_r > maxerr) {
// #ifdef DEBUG_SBS
// 		sprintf(fmtbuf, "SBS: sbs_find_insert_point_partial: compare\n\
// \tnot EQ:          [TRUE]\n\
// \tmaxerr:          [%%d]\n\
// \tL buffer:          [%%.%zus]\n\
// \tL string:          [%%.%zus]\n\
// \tL dist_l:          [%%d]\n\
// \tR buffer:          [%%.%zus]\n\
// \tR string:          [%%.%zus]\n\
// \tR dist_r:          [%%d]\n\
// ",
// 			len_l,
// 			len_l,
// 			len_r,
// 			len_r
// 		);
// 		LOG_DEBUG(fmtbuf,
// 			maxerr,
// 			old_tail,
// 			new_start,
// 			dist_l,
// 			old_tail + len_l,
// 			new_start + len_l,
// 			dist_r
// 		);
// #endif
		return NULL;
	};

	if (
		dist_r <= few_errors               // right part almost the same
		&& n > PARTIAL_CHANGE_LENGTH_MIN   // the sentence is long enough for analysis
	) {
// 		LOG_DEBUG("SBS: sbs_find_insert_point_partial: LEFT CHANGED,\n\tbuf:[%s]\n\tstr:[%s]\n\
// \tmaxerr:[%d]\n\
// \tdist_l:[%d]\n\
// \tdist_r:[%d]\n\
// ",
// 			old_tail,
// 			new_start,
// 			maxerr,
// 			dist_l,
// 			dist_r
// 		);
		// searching for first mismatched symbol at the end of buf
		// This is a naive implementation of error detection
		//
		// Will travel from the end of string to the beginning, and
		// compare OLD value (from buffer) and new value (new STR)
		partial_shift = 0;
		while (
			old_tail[partial_shift] != 0
			&& old_tail[partial_shift+1] != 0
			&& (0 != sbs_char_equal_CI(old_tail[partial_shift], new_start[partial_shift]))
		) {
			partial_shift += 1;
		}

		// LOG_DEBUG("SBS: sbs_find_insert_point_partial: PARTIAL SHIFT, [%d]\n",
		// 	partial_shift
		// );

		return old_tail + partial_shift;
	}

// #ifdef DEBUG_SBS

// 		sprintf(fmtbuf, "SBS: sbs_find_insert_point_partial: REPLACE ENTIRE TAIL !!\n\
// \tmaxerr:          [%%d]\n\
// \tL buffer:        [%%.%zus]\n\
// \tL string:        [%%.%zus]\n\
// \tL dist_l:        [%%d]\n\
// \tR buffer:        [%%.%zus]\n\
// \tR string:        [%%.%zus]\n\
// \tR dist_r:        [%%d]\n\
// ",
// 			len_l,
// 			len_l,
// 			len_r,
// 			len_r
// 		);
// 		LOG_DEBUG(fmtbuf,
// 			maxerr,
// 			old_tail,
// 			new_start,
// 			dist_l,
// 			old_tail + len_l,
// 			new_start + len_l,
// 			dist_r
// 		);
// #endif

	return old_tail;
}

char * sbs_find_insert_point(char * buf, const char * str, int * ilen) {

	int maxerr;
	size_t buf_len;
	unsigned char * buffer_tail;
	const unsigned char * prefix = str;

	int             cur_err = 0;
	unsigned char * cur_ptr;
	int             cur_len;

	int             best_err;
	unsigned char * best_ptr;
	int             best_len;

	buf_len = strlen(buf);
	cur_len = strlen(str);

	if (buf_len < cur_len)
		cur_len = buf_len;

	// init errcounter with value, greater than possible amount of errors in string
	best_err = cur_len + 1;
	best_ptr = NULL;
	best_len = 0;

	while (cur_len > 0)
	{
		maxerr = cur_len / 5;
		buffer_tail = buf + buf_len - cur_len;

		cur_ptr = sbs_find_insert_point_partial(buffer_tail, prefix, cur_len, maxerr, &cur_err);
		if (NULL != cur_ptr)
		{
			if ((cur_len-cur_err) >= (best_len-best_err)) {
				best_err = cur_err;
				best_len = cur_len;
				best_ptr = cur_ptr;
			}
		}
		cur_len--;
	}

	*ilen = best_len;
	return best_ptr;
}

void sbs_strcpy_without_dup(const unsigned char * str, sbs_context_t * context)
{
	int intersect_len;
	unsigned char * buffer_insert_point;

	unsigned long sbs_len;

	sbs_len = strlen(context->buffer);


	LOG_DEBUG("SBS: sbs_strcpy_without_dup: going to append, looking for common part\n\
\tbuffer:          [%p][%s]\n\
\tstring:          [%s]\n\
",
		context->buffer,
		context->buffer,
		str
	);

	buffer_insert_point = sbs_find_insert_point(context->buffer, str, &intersect_len);

	LOG_DEBUG("SBS: sbs_strcpy_without_dup: analyze search results\n\
\t buffer:         [%s]\n\
\t string:         [%s]\n\
\t insert point: ->[%s]\n\
\t intersection len[%4d]\n\
\t sbslen          [%4ld]\n\
\t handled len     [%4zu]\n\
",
		context->buffer,
		str,
		buffer_insert_point,
		intersect_len,
		sbs_len,
		context->handled_len
	);

	if (intersect_len > 0)
	{
		// there is a common part (suffix of old sentence equals to prefix of new str)
		//
		// remove dup from buffer
		// we will use an appropriate part from the new string

		//context->buffer[sbs_len-intersect_len] = 0;

		LOG_DEBUG("SBS: sbs_strcpy_without_dup: cut buffer by insert point\n");
		*buffer_insert_point = 0;
	}

	// check, that new string does not contain data, from
	// already handled sentence:
	if ( (sbs_len - intersect_len) >= context->handled_len)
	{
		// there is no intersection.
		// It is time to clean the buffer. Excepting the last uncomplete sentence
		LOG_DEBUG("SBS: sbs_strcpy_without_dup: DROP parsed part\n\
\t buffer:         [%s]\n\
\t handled len     [%4zu]\n\
\t new start at  ->[%s]\n\
",
			context->buffer,
			context->handled_len,
			context->buffer + context->handled_len
		);

		// skip leading whitespaces:
		int skip_ws = context->handled_len;
		while (isspace(context->buffer[skip_ws])) {
			skip_ws ++;
		}

		strcpy(context->buffer, context->buffer + skip_ws);
		context->handled_len = 0;
	}

	sbs_len = strlen(context->buffer);

	if (
		!isspace(str[0])                // not a space char in the beginning of new str
		//&& context->handled_len >0  // buffer is not empty (there is uncomplete sentence)
		&& sbs_len > 0  // buffer is not empty (there is uncomplete sentence)
		&& !isspace(context->buffer[sbs_len-1])  // not a space char at the end of existing buf
	)
	{
		strcat(context->buffer, " ");
	}

	strcat(context->buffer, str);
}

/**
 * Appends the function to the sentence buffer, and returns a list of full sentences (if there are any), or NULL
 *
 * @param  str       Partial (or full) sub to append.
 * @param  time_from Starting timestamp
 * @param  time_trim Ending timestamp
 * @param  context   Encoder context
 * @return           New <struct cc_subtitle *> subtitle, or NULL, if <str> doesn't contain the ending part of the sentence. If there are more than one sentence, the remaining sentences will be chained using <result->next> reference.
 */
struct cc_subtitle * sbs_append_string(unsigned char * str, const LLONG time_from, const LLONG time_trim, sbs_context_t * context)
{
	struct cc_subtitle * resub;
	struct cc_subtitle * tmpsub;

	unsigned char * bp_current;
	unsigned char * bp_last_break;
	unsigned char * sbs_undone_start;

	int required_capacity;
	int new_capacity;

	LLONG alphanum_total;
	LLONG alphanum_cur;

	LLONG anychar_total;
	LLONG anychar_cur;

	LLONG duration;
	LLONG available_time;
	int use_alphanum_counters;

	if (! str)
		return NULL;

	LOG_DEBUG("SBS: sbs_append_string: after sbs init:\n\
\tsbs ptr:   [%p]\n\
",
		context
	);

	LOG_DEBUG("SBS: sbs_append_string: after sbs init:\n\
\tsbs ptr:   [%p][%s]\n\
\tcur cap:   [%zu]\n\
",
		context->buffer,
		context->buffer,
		context->capacity
	);

	sbs_str_autofix(str);

	// ===============================
	// grow sentence buffer
	// ===============================
	required_capacity =
		strlen(context->buffer)    // existing data in buf
		+ strlen(str)     // length of new string
		+ 1               // trailing \0
		+ 1               // space control (will add one space , if required)
	;

	if (required_capacity >= context->capacity)
	{
		LOG_DEBUG("SBS: sbs_append_string: REALLOC BUF:\n\
\tsbs ptr:   [%p]\n\
\tcur cap:   [%zu]\n\
\treq cap:   [%d]\n\
",
			context->buffer,
			context->capacity,
			required_capacity
		);

		new_capacity = context->capacity;

		while (new_capacity < required_capacity)
		{
			// increase NEW_capacity, and check, that increment
			// is less than 8 Mb. Because 8Mb - it is a lot
			// for a TEXT buffer. It is weird...
			new_capacity += (new_capacity > 1048576 * 8)
				? 1048576 * 8
				: new_capacity;
		}

		context->buffer = (unsigned char *)realloc(
			context->buffer,
			new_capacity * sizeof(/*unsigned char*/ context->buffer[0] )
		);

		if (!context->buffer)
			fatal(EXIT_NOT_ENOUGH_MEMORY, "In sbs_append_string: Not enough memory to append buffer");

		context->capacity = new_capacity;

		LOG_DEBUG("SBS: sbs_append_string: REALLOC BUF DONE:\n\
\tsbs ptr:   {%p}\n\
\tcur cap:   [%zu]\n\
\treq cap:   [%d]\n\
\tbuf:       [%s]\n\
",
			context->buffer,
			context->capacity,
			required_capacity,
			context->buffer
		);
	}

	// ===============================
	// append to buffer
	//
	// will update buffer, handled_len
	// ===============================
	sbs_strcpy_without_dup(str, context);

	// ===============================
	// break to sentences
	// ===============================
	resub = NULL;
	tmpsub = NULL;

	alphanum_total = 0;
	alphanum_cur = 0;

	anychar_total = 0;
	anychar_cur = 0;

	sbs_undone_start = context->buffer + context->handled_len;
	bp_last_break = sbs_undone_start;

	LOG_DEBUG("SBS: BEFORE sentence break.\n\
\tLast break: [%s]\n\
\tsbs_undone_start: [%zu]\n\
\tsbs_undone: [%s]\n\
",
		bp_last_break,
		context->handled_len,
		sbs_undone_start
	);

	for (bp_current = sbs_undone_start; bp_current && *bp_current; bp_current++)
	{
		if (
			0 < anychar_cur	// skip empty!
			&& sbs_is_pointer_on_sentence_breaker(bp_last_break, bp_current) )
		{
			// it is new sentence!
			tmpsub = malloc(sizeof(struct cc_subtitle));

			tmpsub->type = CC_TEXT;
			// length of new string:
			tmpsub->nb_data =
				bp_current - bp_last_break
				+ 1	 // terminating '\0'
				+ 1  // skip '.'
			;
			tmpsub->data = strndup(bp_last_break, tmpsub->nb_data - 1);
			tmpsub->datatype = CC_DATATYPE_GENERIC;
			tmpsub->got_output = 1;

			tmpsub->start_time = alphanum_cur;
			alphanum_cur = 0;
			tmpsub->end_time = anychar_cur;
			anychar_cur = 0;

			bp_last_break = bp_current + 1;

			// tune last break:
			while (
				*bp_last_break
				&& isspace(*bp_last_break)
			)
			{
				bp_last_break++;
			}

			// ???
			// tmpsub->info = NULL;
			// tmpsub->mode = NULL;

			// link with prev sub:
			tmpsub->next = NULL;
			tmpsub->prev = resub;
			if (NULL != resub)
			{
				resub->next = tmpsub;
			}

			resub = tmpsub;
		}

		if (*bp_current && isalnum(*bp_current))
		{
			alphanum_total++;
			alphanum_cur++;
		}
		anychar_total++;
		anychar_cur++;
	}

	// ===============================
	// okay, we have extracted several sentences, now we should
	// save the position of the "remainder" - start of the last
	// incomplete sentence
	// ===============================
	if (bp_last_break != sbs_undone_start)
	{
		context->handled_len = bp_last_break - sbs_undone_start;
	}

	LOG_DEBUG("SBS: AFTER sentence break:\
\n\tHandled Len    [%4zu]\
\n\tAlphanum Total [%4ld]\
\n\tOverall chars  [%4ld]\
\n\tSTRING:[%s]\
\n\tBUFFER:[%s]\
\n",
		context->handled_len,
		alphanum_total,
		anychar_total,
		str,
		context->buffer
	);

	// ===============================
	// Calculate time spans
	// ===============================
	if ((0 > context->time_from) && (0 > context->time_trim))
	{
		context->time_from = time_from;
		context->time_trim = time_trim;
	}

	available_time = time_trim - context->time_from;
	use_alphanum_counters = alphanum_total > 0 ? 1 : 0;

	tmpsub = resub;
	while (tmpsub)
	{
		alphanum_cur = tmpsub->start_time;
		anychar_cur = tmpsub->end_time;

		if (use_alphanum_counters)
		{
			duration = available_time * alphanum_cur / alphanum_total;
		}
		else
		{
			duration = available_time * anychar_cur / anychar_total;
		}

		tmpsub->start_time = context->time_from;
		tmpsub->end_time = tmpsub->start_time + duration;

		context->time_from = tmpsub->end_time + 1;

		tmpsub = tmpsub->next;
	}
	return resub;
}

struct cc_subtitle * reformat_cc_bitmap_through_sentence_buffer(struct cc_subtitle *sub, struct encoder_ctx * context)
{
	struct cc_bitmap* rect;
	LLONG ms_start, ms_end;
	int used;
	int i = 0;
	char *str;

	// this is a sub with a full sentence (or chain of such subs)
	struct cc_subtitle * resub = NULL;

#ifdef ENABLE_OCR

	if (sub->flags & SUB_EOD_MARKER)
	{
		// the last sub from input

		if (context->prev_start == -1)
		{
			ms_start = 1;
			ms_end = sub->start_time;
		}
		else
		{
			ms_start = context->prev_start;
			ms_end = sub->start_time;
		}
	}
	else
	{
		// not the last sub from input
		ms_start = sub->start_time;
		ms_end = sub->end_time;
	}

	if (sub->nb_data == 0)
		return 0;

	if (sub->flags & SUB_EOD_MARKER)
		context->prev_start = sub->start_time;

	str = paraof_ocrtext(sub, context->encoded_crlf, context->encoded_crlf_length);

	if (str)
	{
		LOG_DEBUG("SBS: reformat_cc_bitmap: string is not empty\n");

		if (context->prev_start != -1 || !(sub->flags & SUB_EOD_MARKER))
		{
			resub = sbs_append_string(str, ms_start, ms_end, sbs_init_context());
		}
		freep(&str);
	}

	for(i = 0, rect = sub->data; i < sub->nb_data; i++, rect++)
	{
		freep(&rect->data0);
		freep(&rect->data1);
	}
#endif
	sub->nb_data = 0;
	freep(&sub->data);

	return resub;
}
