#ifndef BUFFER_H
#define BUFFER_H

#include <stdbool.h>
#include <stdlib.h>

struct buffer_line {
	size_t start;
	size_t end;
};

DA_DEF_STRUCT(char, buffer_data);
DA_DEF_STRUCT(struct buffer_line, buffer_line_da);

struct buffer;

typedef void (*buffer_hook_func)(struct buffer *);
DA_DEF_STRUCT(buffer_hook_func, buffer_hook_da);

struct buffer {
	struct buffer_data data;
	struct buffer_line_da lines;
	size_t render_start;
	size_t cur;
	size_t high_col;
	const char *filepath;
	struct {
		struct vec2 pos;
		struct vec2 vel;
	} cam;
	struct {
		struct buffer_hook_da insert;
		struct buffer_hook_da remove;
	} hooks;
};

enum buffer_err {
	BUFFER_ERR_OK = 0,
	BUFFER_ERR_EMPTY,
	BUFFER_ERR_NOT_FOUND,
	BUFFER_ERR_INVALID_CUR_POS,
	BUFFER_ERR_FAIL_READ_FILE,
	BUFFER_ERR_START_GT_END,
	BUFFER_ERR_INVALID_END_SIZE,
	BUFFER_ERR_FAIL_GET_REALPATH,
	BUFFER_ERR_FAIL_SAVE,
	BUFFER_ERR_START_EQ_END,
};

struct ret_buffer_err {
	struct buffer f1;
	enum buffer_err f2;
};

struct ret_size_t_err {
	size_t f1;
	enum buffer_err f2;
};

struct ret_buffer_size_t_err {
	struct buffer f1;
	size_t f2;
	enum buffer_err f3;
};

struct ret_size_t_size_t {
	size_t f1;
	size_t f2;
};

struct buffer buffer_create(void);
void buffer_destroy(struct buffer *buf);

void buffer_parse_lines(struct buffer *buf);
size_t buffer_find_line_index(const struct buffer *buf, size_t index);
struct buffer_line buffer_find_line(const struct buffer *buf, size_t index);

void buffer_run_all_hooks(struct buffer *buf);
void buffer_run_hooks(struct buffer *buf, const struct buffer_hook_da *bh);

void buffer_load_from_file(struct buffer *buf, const char *filepath,
			   enum buffer_err *err);
void buffer_save(const struct buffer *buf, enum buffer_err *err);
void buffer_save_to_file(const struct buffer *buf, const char *filepath,
			 enum buffer_err *err);

void buffer_mv_cur_left(struct buffer *buf);
void buffer_mv_cur_right(struct buffer *buf);
void buffer_mv_cur_up(struct buffer *buf);
void buffer_mv_cur_down(struct buffer *buf);

bool buffer_check_and_fix_cur(struct buffer *buf);

void buffer_insert_char(struct buffer *buf, char c, size_t index);
void buffer_remove_char_mv_cur(struct buffer *buf, size_t index);
void buffer_remove_char(struct buffer *buf, size_t index);

size_t buffer_remove_between(struct buffer *buf, size_t start, size_t end,
			     enum buffer_err *err);

size_t buffer_index_bw_word(struct buffer *buf, enum buffer_err *err);
size_t buffer_index_fw_word(struct buffer *buf, enum buffer_err *err);

size_t buffer_index_bw_char(struct buffer *buf, char c, size_t start,
			    enum buffer_err *err);
size_t buffer_index_fw_char(struct buffer *buf, char c, size_t start,
			    enum buffer_err *err);

size_t buffer_count_char_between(const struct buffer *buf, char c,
				 size_t start, size_t end,
				 enum buffer_err *err);

size_t buffer_calc_cur_col(const struct buffer *buf);
size_t buffer_calc_cur_row(const struct buffer *buf);
struct ret_size_t_size_t buffer_calc_cur_pos(const struct buffer *buf);

void buffer_err_set(enum buffer_err *err, enum buffer_err err_);

#if defined(BUFFER_IMP) || defined(IMP)

#include <stdlib.h>
#include <stdbool.h>
#include <limits.h>
#include <ctype.h>

#include "vec2.h"
#include "file.h"
#include "da.h"
#include "unwrap.h"
#include "assert.h"


struct buffer
buffer_create(void)
{
	struct buffer buf = {0};
	DA_CREATE(buf.data);
	DA_CREATE(buf.lines);

	DA_CREATE(buf.hooks.insert);
	DA_CREATE(buf.hooks.remove);

	DA_APPEND(buf.hooks.insert, buffer_parse_lines);
	DA_APPEND(buf.hooks.remove, buffer_parse_lines);

	return buf;
}

void
buffer_destroy(struct buffer *buf)
{
	free(buf->data.items);
	if ( buf->filepath != NULL ) {
		free((char *) buf->filepath);
	}
}

void
buffer_parse_lines(struct buffer *buf)
{
	ASSERT_NN(buf);
	struct buffer_data data = buf->data;

	buf->lines.size = 1;
	struct buffer_line *tail = buf->lines.items;
	for ( size_t i = 0; i < data.size; ++i ) {
		if ( data.items[i] == '\n' ) {
			tail->end = i;
			DA_APPEND(buf->lines, (struct buffer_line){0});
			tail = &DA_TAIL(buf->lines);
			tail->start = i + 1;
		}
	}
	if ( tail->end == 0 ) {
		tail->end = data.size;
	}
}

#define MAX(a, b) ((a) > (b)) * (a) + ((a) < (b)) * (b)

size_t
buffer_find_line_index(const struct buffer *buf, size_t index)
{
	ASSERT_NN(buf);
	struct buffer_line_da lda = buf->lines;
	if ( (ssize_t)index <= 0 ) {
		return 0;
	}
	if ( index >= buf->data.size ) {
		return lda.size - 1;
	}
	size_t rest = lda.size / 2;
	size_t i = rest;
	while (1) {
		struct buffer_line l = lda.items[i];
		if ( index >= l.start && index <= l.end ) {
			break;
		}
		rest = rest/2;
		rest = MAX(rest, (size_t)1);
		if ( index > l.start ) {
			i += rest;
			if ( i > lda.size ) {
				i = lda.size - 1;
			}
		} else {
			i -= rest;
			if ( (ssize_t)(i) < 0 ) {
				i = 0;
			}
		}
	}
	return i;
}

struct buffer_line
buffer_find_line(const struct buffer *buf, size_t index)
{
	ASSERT_NN(buf);
	size_t i = buffer_find_line_index(buf, index);

	return buf->lines.items[i];
}

void
buffer_run_all_hooks(struct buffer *buf)
{
	buffer_run_hooks(buf, &buf->hooks.insert);
	buffer_run_hooks(buf, &buf->hooks.remove);
}

void
buffer_run_hooks(struct buffer *buf, const struct buffer_hook_da *bh)
{
	for ( size_t i = 0; i < bh->size; ++i ) {
		bh->items[i](buf);
	}
}


void
buffer_load_from_file(struct buffer *buf, const char *filepath,
		      enum buffer_err *err)
{
	void *ptr;
	size_t file_size;
	enum file_err file_err;
	char *fp = realpath(filepath, NULL);
	if ( fp == NULL ) {
		buffer_err_set(err, BUFFER_ERR_FAIL_GET_REALPATH);
		return;
	}

	ptr = file_read_all(fp, &file_size, &file_err);

	if ( file_err != FILE_ERR_OK ) {
		if ( file_err == FILE_ERR_EMPTY ) {
			buf->filepath = fp;
			goto exit;
		}
		free(fp);
		buffer_err_set(err, BUFFER_ERR_FAIL_READ_FILE);
		return;
	}

	buffer_destroy(buf);

	buf->filepath = fp;
	buf->data.items = ptr;
	buf->data.size = file_size - 1;
	buf->data.cap = file_size;

	buffer_run_hooks(buf, &buf->hooks.insert);
exit: ;
	buffer_err_set(err, BUFFER_ERR_OK);
}

void
buffer_save(const struct buffer *buf, enum buffer_err *err)
{
	buffer_save_to_file(buf, buf->filepath, err);
}

void
buffer_save_to_file(const struct buffer *buf, const char *filepath,
		    enum buffer_err *err)
{
	enum file_err file_err;
	file_save(filepath, buf->data.items, buf->data.size, &file_err);
	if ( file_err != FILE_ERR_OK ) {
		buffer_err_set(err, BUFFER_ERR_FAIL_SAVE);
		return;
	}

	buffer_err_set(err, BUFFER_ERR_OK);
}


void
buffer_mv_cur_left(struct buffer *buf)
{
	if ( buf->cur == 0 ) {
		return;
	}
	--buf->cur;
}

void
buffer_mv_cur_right(struct buffer *buf)
{
	if ( buffer_check_and_fix_cur(buf) ) {
		return;
	}
	++buf->cur;
}

void
buffer_mv_cur_up(struct buffer *buf)
{
	size_t index = 0;
	size_t col = buf->high_col;
	size_t l_index = buffer_find_line_index(buf, buf->cur);
	if ( l_index != 0 ) {
		--l_index;
	}
	struct buffer_line l = buf->lines.items[l_index];
	index = l.start;
	if ( l.start + col > l.end ) {
		index = l.end;
	} else {
		index += col;
	}
	buf->cur = index;
}

void
buffer_mv_cur_down(struct buffer *buf)
{
	size_t index;
	size_t col = buf->high_col;
	size_t l_index = buffer_find_line_index(buf, buf->cur);
	if ( ++l_index >= buf->lines.size ) {
		l_index = buf->lines.size - 1;
	}
	struct buffer_line l = buf->lines.items[l_index];
	index = l.start;
	if ( l.start + col > l.end ) {
		index = l.end;
	} else {
		index += col;
	}
	buf->cur = index;
}

bool buffer_check_and_fix_cur(struct buffer *buf) {
	if ( buf->cur >= buf->data.size ) {
		buf->cur = buf->data.size;
		return true;
	}
	return false;
}

void
buffer_insert_char(struct buffer *buf, char c, size_t index)
{
	if ( index > buf->data.size ) {
		DA_APPEND(buf->data, c);
		++buf->cur;
		return;
	}

	DA_INSERT(buf->data, c, index);
	++buf->cur;

	buffer_run_hooks(buf, &buf->hooks.insert);
}

void
buffer_remove_char_mv_cur(struct buffer *buf, size_t index)
{
	if ( buf->data.size == 0 ) {
		return;
	}

	if ( index > buf->data.size ) {
		return;
	}

	if ( buf->data.size == index ) {
		buf->data.items[--buf->data.size] = 0;
		--buf->cur;
		return;
	}

	DA_DELETE(buf->data, index);
	--buf->cur;

	buffer_run_hooks(buf, &buf->hooks.remove);
}

void
buffer_remove_char(struct buffer *buf, size_t index)
{
	if ( buf->data.size == 0 ) {
		return;
	}

	if ( index > buf->data.size ) {
		return;
	}

	if ( buf->data.size == index ) {
		buf->data.items[--buf->data.size] = 0;
		return;
	}

	DA_DELETE(buf->data, index);

	buffer_run_hooks(buf, &buf->hooks.remove);
}

size_t
buffer_remove_between(struct buffer *buf, size_t start, size_t end,
		      enum buffer_err *err)
{
	if ( buf->data.size == 0 ) {
		buffer_err_set(err, BUFFER_ERR_EMPTY);
		return 0;
	}

	if ( start == end ) {
		buffer_remove_char(buf, start);
		buffer_err_set(err, BUFFER_ERR_OK);
		return 1;
	}

	if ( start > end ) {
		buffer_err_set(err, BUFFER_ERR_START_GT_END);
		return 0;
	}

	struct buffer_data *da = &buf->data;

	/* There is no need to check start > buf->data.size, because */
	/* we know that start < end */
	if ( end > da->size ) {
		buffer_err_set(err, BUFFER_ERR_INVALID_END_SIZE);
		return 0;
	}

	size_t nrm = end - start;
	memmove(da->items+start, da->items+end, da->size - nrm);
	da->size -= nrm;
	memset(da->items+da->size, 0, da->cap - da->size);
	buffer_err_set(err, BUFFER_ERR_OK);
	return nrm;

	buffer_run_hooks(buf, &buf->hooks.remove);
}


size_t
buffer_index_bw_word(struct buffer *buf, enum buffer_err *err)
{
	if ( buf->data.size == 0 ) {
		buffer_err_set(err, BUFFER_ERR_EMPTY);
		return 0;
	}

	buffer_check_and_fix_cur(buf);

	bool found_letter = false;
	size_t i = buf->cur - 1;
	for ( ; i > 0; --i) {
		char c = buf->data.items[i];
		if ( isalnum(c) ) {
			found_letter = true;
		} else if ( found_letter ) {
			++i;
			break;
		}
	}
	if ( i == 0 ) {
		char c = buf->data.items[i];
		if ( isalnum(c) ) {
			found_letter = true;
		} else if ( found_letter ) {
			++i;
		}
	}

	buffer_err_set(err, BUFFER_ERR_OK);
	return i;
}

size_t
buffer_index_fw_word(struct buffer *buf, enum buffer_err *err)
{
	if ( buf->data.size == 0 ) {
		buffer_err_set(err, BUFFER_ERR_EMPTY);
		return 0;
	}

	buffer_check_and_fix_cur(buf);

	bool found_letter = false;
	size_t i = buf->cur;
	for ( ; i < buf->data.size; ++i) {
		char c = buf->data.items[i];
		if ( isalnum(c) ) {
			found_letter = true;
		} else if ( found_letter ) {
			break;
		}
	}
	/* if ( i == 0 ) { */
	/* 	char c = buf->data.items[i]; */
	/* 	if ( isalnum(c) ) { */
	/* 		found_letter = true; */
	/* 	} else if ( found_letter ) { */
	/* 	} */
	/* } */

	buffer_err_set(err, BUFFER_ERR_OK);
	return i;
}

size_t
buffer_index_bw_char(struct buffer *buf, char c, size_t start,
		     enum buffer_err *err)
{
	if ( buf->data.size == 0 ) {
		buffer_err_set(err, BUFFER_ERR_EMPTY);
		return 0;
	}

	buffer_check_and_fix_cur(buf);

	const char *str = buf->data.items;

	bool cond = false;
	/* TODO: Find a way to not use ssize_t buf size_t */
	ssize_t closest = -1;
	for ( ssize_t i = 0; i < (ssize_t) start; ++i ) {
		cond = (str[i] == c);
		closest = ( cond ) * i + ( ! cond ) * closest;
	}
	if ( closest == -1 ) {
		buffer_err_set(err, BUFFER_ERR_NOT_FOUND);
		return 0;
	}

	buffer_err_set(err, BUFFER_ERR_OK);
	return (size_t) closest;
}

size_t
buffer_index_fw_char(struct buffer *buf, char c, size_t start,
		     enum buffer_err *err)
{
	if ( buf->data.size == 0 ) {
		buffer_err_set(err, BUFFER_ERR_EMPTY);
		return 0;
	}

	if ( buf->cur > buf->data.size ) {
		buf->cur = buf->data.size - 1;
	}

	const char *str = buf->data.items;

	for ( size_t i = start; i < buf->data.size; ++i ) {
		if ( str[i] == c ) {
			buffer_err_set(err, BUFFER_ERR_OK);
			return i;
		}
	}

	buffer_err_set(err, BUFFER_ERR_NOT_FOUND);
	return 0;
}

size_t
buffer_count_char_between(const struct buffer *buf, char c,
			  size_t start, size_t end,
			  enum buffer_err *err)
{
	if ( buf->data.size == 0 ) {
		buffer_err_set(err, BUFFER_ERR_EMPTY);
		return 0;
	}

	if ( start == end ) {
		buffer_err_set(err, BUFFER_ERR_START_EQ_END);
		return 0;
	}

	if ( start > end ) {
		buffer_err_set(err, BUFFER_ERR_START_GT_END);
		return 0;
	}

	/* There is no need to check start > buf.data.size, because */
	/* we know that start < end */
	if ( end > buf->data.size ) {
		buffer_err_set(err, BUFFER_ERR_INVALID_END_SIZE);
		return 0;
	}

	size_t count = 0;
	for ( size_t i = start; i < end; ++i ) {
		count += ( buf->data.items[i] == c );
	}

	buffer_err_set(err, BUFFER_ERR_OK);
	return count;
}

size_t
buffer_calc_cur_col(const struct buffer *buf)
{
	struct buffer_line l = buffer_find_line(buf, buf->cur);
	return buf->cur - l.start;
}

size_t
buffer_calc_cur_row(const struct buffer *buf)
{
	return buffer_find_line_index(buf, buf->cur);
}

struct ret_size_t_size_t
buffer_calc_cur_pos(const struct buffer *buf)
{
	size_t col = buffer_calc_cur_col(buf);
	size_t row = buffer_calc_cur_row(buf);

	return (struct ret_size_t_size_t) {
		.f1 = col,
		.f2 = row
	};
}

void
buffer_err_set(enum buffer_err *err, enum buffer_err err_)
{
	if ( err != NULL ) {
		*err = err_;
	}
}


#endif /* defined(BUFFER_IMP) || defined(IMP) */

#endif
