Linux shell written in Odin.
package ncure

import "core:strings"
import "core:strconv"
import "core:os"
import "core:fmt"
import "core:mem"

import "../linux"

ESC :: "\e";
SEQUENCE_START :: "\e[";
NEWLINE :: "\n";

CLEAR :: "\e[2J";
CLEAR_DOWN :: "\e[J";
CLEAR_UP :: "\e[1J";
CLEAR_LINE :: "\e[2K";
CLEAR_LINE_RIGHT :: "\e[K";
CLEAR_LINE_LEFT :: "\e[1K";

TOP_LEFT :: "\e[1;1H";

GET_CURSOR :: "\e[6n";
HIDE_CURSOR :: "\e[?25l";
SHOW_CURSOR :: "\e[?25h";
SAVE_CURSOR :: "\e7";
RESTORE_CURSOR :: "\e8";

MOVE_UP :: "\e[1A";
MOVE_DOWN :: "\e[1B";
MOVE_LEFT :: "\e[1D";
MOVE_RIGHT :: "\e[1C";

BatchInfo :: struct {
	builder: strings.Builder,
	cursor: CursorPos, // Current cursor position at latest ncure call. NOTE: Doesn't necessarily work properly atm.
	cursor_start: CursorPos, // Cursor position at the start of a batch
	savedCursor: bool,
	savedCursorPos: CursorPos, // NOTE: Doesn't necessarily work atm.
	termSize: TermSize,
}
@private
_batch := false;
@private
_batchInfo: ^BatchInfo = nil; // TODO: Switch to a stack thing so we can have nested Batches

@private
_createBatchInfo :: proc(batchInfo: ^BatchInfo) {
	batchInfo.builder = strings.make_builder();
	batchInfo.cursor = getCursor();
	batchInfo.cursor_start = batchInfo.cursor;
	batchInfo.termSize = getTermSize();
	batchInfo.savedCursor = false;
	_batchInfo = batchInfo;
}

@private
_destroyBatchInfo :: proc(batchInfo: ^BatchInfo) {
	strings.destroy_builder(&batchInfo.builder);
}

batch_start :: proc() -> ^BatchInfo { // TODO
	batchInfo: ^BatchInfo = cast(^BatchInfo) mem.alloc(size_of(BatchInfo));
	_createBatchInfo(batchInfo);
	_batch = true;
	return batchInfo;
}
batch_end :: proc() {
	if _batch != true do return;
	
	os.write_string(os.stdout, strings.to_string(_batchInfo.builder));
	_destroyBatchInfo(_batchInfo);
	_batch = false;
}

// TODO: This isn't thread-safe at all
batch :: proc(p: #type proc(batchInfo: ^BatchInfo, args: ..any), args: ..any) {
	// State for Batch Build: builder, cursor, and termsize
	batchInfo: BatchInfo;
	_createBatchInfo(&batchInfo);
	defer _destroyBatchInfo(&batchInfo);
	_batch = true;

	p(&batchInfo, ..args);
	os.write_string(os.stdout, strings.to_string(batchInfo.builder));

	_batch = false;
}

getTermSize :: proc() -> (termSize: TermSize) {
	w: linux.winsize;
	if _, err := linux.ioctl(os.stdout, linux.TIOCGWINSZ, &w); err != os.ERROR_NONE {
		// Error
	}

	termSize.width = int(w.ws_col);
	termSize.height = int(w.ws_row);
	return termSize;
}

getCursor :: proc() -> CursorPos {
	if _batch do return _batchInfo.cursor;

	cursor: CursorPos;

	// Disable Echo, send request, then switch terminal
	// back to previous settings
	prev, _ := disableEcho(false);
	os.write_string(os.stdout, GET_CURSOR);
	if set_error := linux.tcsetattr(os.stdin, linux.TCSANOW, &prev); set_error != os.ERROR_NONE {
		fmt.println("Error setting terminal info: %s\n", set_error);
	}

	// Get response
	response := strings.make_builder();
	defer strings.destroy_builder(&response);
	data: byte;
	for {
		data = getch();

		strings.write_byte(&response, data);
		if data == 'R' do break;
	}

	// Parse response
	response_str := strings.to_string(response);
	arg1_start: int;
	arg1_end: int;
	arg2_start: int;
	arg2_end: int;
	for c, i in response_str {
		if c == '[' do arg1_start = i + 1;
		if c == ';' {
			arg1_end = i;
			arg2_start = i + 1;
		}
		if c == 'R' {
			arg2_end = i;
		}
	}

	arg1 := response_str[arg1_start:arg1_end];
	arg2 := response_str[arg2_start:arg2_end];

	cursor.y = strconv.atoi(arg1);
	cursor.x = strconv.atoi(arg2);

	return cursor;
}

getCursor_topleft :: proc() -> CursorPos {
	return CursorPos {1, 1};
}

getCursor_topright :: proc(termSize: ^TermSize = nil) -> CursorPos {
	new_ts: TermSize;

	if _batch {
		new_ts = _batchInfo.termSize;
	} else {
		new_ts = getTermSize();
		if termSize != nil do termSize^ = new_ts;
	}

	return CursorPos {new_ts.width, 1};
}

getCursor_bottomleft :: proc(termSize: ^TermSize = nil) -> CursorPos {
	new_ts: TermSize;

	if _batch {
		new_ts = _batchInfo.termSize;
	} else {
		new_ts = getTermSize();
		if termSize != nil do termSize^ = new_ts;
	}

	return CursorPos {1, new_ts.height};
}

getCursor_bottomright :: proc(termSize: ^TermSize = nil) -> CursorPos {
	new_ts: TermSize;

	if _batch {
		new_ts = _batchInfo.termSize;
	} else {
		new_ts = getTermSize();
		if termSize != nil do termSize^ = new_ts;
	}

	return CursorPos {new_ts.width, new_ts.height};
}

hideCursor :: proc() {
	if _batch {
		strings.write_string(&_batchInfo.builder, HIDE_CURSOR);
	} else {
		os.write_string(os.stdout, HIDE_CURSOR);
	}
}

showCursor :: proc() {
	if _batch {
		strings.write_string(&_batchInfo.builder, SHOW_CURSOR);
	} else {
		os.write_string(os.stdout, SHOW_CURSOR);
	}
}

saveCursor :: proc(overwrite := false) {
	if !overwrite {
		assert(!_batchInfo.savedCursor, "A cursor has already been saved without being restored.");
	}

	if _batch {
		strings.write_string(&_batchInfo.builder, SAVE_CURSOR);
		// Set savedCursor so that subsequent commands know when a saved cursor will be overridden
		_batchInfo.savedCursor = true;
		_batchInfo.savedCursorPos = _batchInfo.cursor;
	} else {
		os.write_string(os.stdout, SAVE_CURSOR);
	}
}

restoreCursor :: proc() {
	if _batch {
		strings.write_string(&_batchInfo.builder, RESTORE_CURSOR);
		// Set savedCursor so that subsequent commands know when a saved cursor is being overridden
		_batchInfo.savedCursor = false;
		_batchInfo.cursor = _batchInfo.savedCursorPos;
	} else {
		os.write_string(os.stdout, RESTORE_CURSOR);
	}
}

// TODO: Add option to do something like this in the batching stuff??
save_restore :: proc(cursor: CursorPos, f: #type proc()) {
	saveCursor();
	setCursor(cursor);
	f();
	restoreCursor();
}

getSequence_set :: proc(x, y: int, b: ^strings.Builder = nil) -> string {
	if x == 1 && y == 1 {
		if b != nil {
			strings.write_string(b, TOP_LEFT);
			return strings.to_string(b^);
		}
		return strings.clone(TOP_LEFT);
	}

	buf: [129]byte;
	builder_new: strings.Builder;
	builder: ^strings.Builder = b;
	if b == nil {
		// Create new builder for this sequence only if not
		// being added to a pre-existing builder.
		builder_new = strings.make_builder();
		builder = &builder_new;
	}

	strings.write_string(builder, SEQUENCE_START);

	if y == 1 do strings.write_string(builder, "1;");
	else {
		strings.write_string(builder, strconv.itoa(buf[:], y));
		strings.write_rune(builder, ';');
	}

	if x == 1 do strings.write_string(builder, "1H");
	else {
		strings.write_string(builder, strconv.itoa(buf[:], x));
		strings.write_rune(builder, 'H');
	}

	return strings.to_string(builder^);
}

getSequence_moveup :: proc(amt: int, b: ^strings.Builder = nil) -> string {
	if amt == 1 {
		if b != nil {
			strings.write_string(b, MOVE_UP);
			return strings.to_string(b^);
		}
		return strings.clone(MOVE_UP);
	}

	builder_new: strings.Builder;
	builder: ^strings.Builder = b;
	if b == nil {
		// Create new builder for this sequence only if not
		// being added to a pre-existing builder.
		builder_new = strings.make_builder();
		builder = &builder_new;
	}

	strings.write_string(builder, SEQUENCE_START);

	buf: [129]byte;
	strings.write_string(builder, strconv.itoa(buf[:], amt));
	strings.write_rune(builder, 'A');

	return strings.to_string(builder^);
}

getSequence_movedown :: proc(amt: int, b: ^strings.Builder = nil) -> string {
	if amt == 1 {
		if b != nil {
			strings.write_string(b, MOVE_DOWN);
			return strings.to_string(b^);
		}
		return strings.clone(MOVE_DOWN);
	}

	builder_new: strings.Builder;
	builder: ^strings.Builder = b;
	if b == nil {
		// Create new builder for this sequence only if not
		// being added to a pre-existing builder.
		builder_new = strings.make_builder();
		builder = &builder_new;
	}

	strings.write_string(builder, SEQUENCE_START);

	buf: [129]byte;
	strings.write_string(builder, strconv.itoa(buf[:], amt));
	strings.write_rune(builder, 'B');

	return strings.to_string(builder^);
}

getSequence_moveleft :: proc(amt: int, b: ^strings.Builder = nil) -> string {
	if amt == 1 {
		if b != nil {
			strings.write_string(b, MOVE_LEFT);
			return strings.to_string(b^);
		}
		return strings.clone(MOVE_LEFT);
	}

	builder_new: strings.Builder;
	builder: ^strings.Builder = b;
	if b == nil {
		// Create new builder for this sequence only if not
		// being added to a pre-existing builder.
		builder_new = strings.make_builder();
		builder = &builder_new;
	}

	strings.write_string(builder, SEQUENCE_START);

	buf: [129]byte;
	strings.write_string(builder, strconv.itoa(buf[:], amt));
	strings.write_rune(builder, 'D');

	return strings.to_string(builder^);
}

getSequence_moveright :: proc(amt: int, b: ^strings.Builder = nil) -> string {
	if amt == 1 {
		if b != nil {
			strings.write_string(b, MOVE_RIGHT);
			return strings.to_string(b^);
		}
		return strings.clone(MOVE_RIGHT);
	}

	builder_new: strings.Builder;
	builder: ^strings.Builder = b;
	if b == nil {
		// Create new builder for this sequence only if not
		// being added to a pre-existing builder.
		builder_new = strings.make_builder();
		builder = &builder_new;
	}

	strings.write_string(builder, SEQUENCE_START);

	buf: [129]byte;
	strings.write_string(builder, strconv.itoa(buf[:], amt));
	strings.write_rune(builder, 'C');

	return strings.to_string(builder^);
}

setCursor_xy :: proc(x, y: int, cursor: ^CursorPos = nil, savePrev := false) {
	str: string;
	defer delete(str);

	if savePrev {
		saveCursor();
	}

	if _batch {
		str := getSequence_set(x, y, &_batchInfo.builder);
		_batchInfo.cursor.x = x;
		_batchInfo.cursor.y = y;
	} else {
		str := getSequence_set(x, y);
		defer delete(str);
		os.write_string(os.stdout, str);
	}

	if cursor != nil {
		cursor.x = x;
		cursor.y = y;
	}
}
setCursor_cursor :: proc(cursor: CursorPos, savePrev := false) {
	setCursor_xy(x = cursor.x, y = cursor.y, savePrev = savePrev);
}
setCursor :: proc{setCursor_xy, setCursor_cursor};

setCursor_topleft :: proc(cursor: ^CursorPos = nil, savePrev := false) {
	if savePrev {
		saveCursor();
	}

	if _batch {
		strings.write_string(&_batchInfo.builder, TOP_LEFT);
		_batchInfo.cursor.x = 1;
		_batchInfo.cursor.y = 1;
	} else {
		os.write_string(os.stdout, TOP_LEFT);
	}

	if cursor != nil {
		cursor.x = 1;
		cursor.y = 1;
	}
}

setCursor_topright :: proc(termSize: ^TermSize = nil, cursor: ^CursorPos = nil, savePrev := false) {
	if savePrev {
		saveCursor();
	}

	c := getCursor_topright(termSize);
	setCursor(c);
	if cursor != nil do cursor^ = c;
}

setCursor_bottomleft :: proc(termSize: ^TermSize = nil, cursor: ^CursorPos = nil, savePrev := false) {
	if savePrev {
		saveCursor();
	}

	c := getCursor_bottomleft(termSize);
	setCursor(c);
	if cursor != nil do cursor^ = c;
}

setCursor_bottomright :: proc(termSize: ^TermSize = nil, cursor: ^CursorPos = nil, savePrev := false) {
	if savePrev {
		saveCursor();
	}

	c := getCursor_bottomright(termSize);
	setCursor(c);
	if cursor != nil do cursor^ = c;
}

// TODO: Add optional cursor argument to be set
moveCursor_up :: proc(amt: int = 1) {
	if _batch {
		str := getSequence_moveup(amt, &_batchInfo.builder);
		_batchInfo.cursor.y -= amt;
	} else {
		str := getSequence_moveup(amt);
		defer delete(str);
		os.write_string(os.stdout, str);
	}
}

moveCursor_down :: proc(amt: int = 1) {
	if _batch {
		str := getSequence_movedown(amt, &_batchInfo.builder);
		_batchInfo.cursor.y += amt;
	} else {
		str := getSequence_movedown(amt);
		defer delete(str);
		os.write_string(os.stdout, str);
	}
}

moveCursor_left :: proc(amt: int = 1) {
	if _batch {
		str := getSequence_moveleft(amt, &_batchInfo.builder);
		_batchInfo.cursor.x -= amt;
	} else {
		str := getSequence_moveleft(amt);
		defer delete(str);
		os.write_string(os.stdout, str);
	}
}

moveCursor_right :: proc(amt: int = 1) {
	if _batch {
		str := getSequence_moveright(amt, &_batchInfo.builder);
		_batchInfo.cursor.x += amt;
	} else {
		str := getSequence_moveright(amt);
		defer delete(str);
		os.write_string(os.stdout, str);
	}
}

moveCursor_start :: proc() {
	if _batch {
		strings.write_byte(&_batchInfo.builder, '\r');
		_batchInfo.cursor.x = 1;
	} else {
		os.write_byte(os.stdout, '\r');
	}
}

moveCursor_end :: proc(termSize: ^TermSize = nil) {
	new_ts: TermSize;
	moveCursor_start();
	if _batch {
		new_ts = _batchInfo.termSize;
		getSequence_moveright(new_ts.width, &_batchInfo.builder);
		_batchInfo.cursor.x = new_ts.width;
	} else {
		new_ts = getTermSize();
		if termSize != nil do termSize^ = new_ts;
		str := getSequence_moveright(new_ts.width);
		os.write_string(os.stdout, str);
	}
}

// TODO: The write and print functions don't change the cursor position correctly
// due to needing to scan the string for escape sequences, new lines, \b,
// non-printable characters, and combinational utf-8 characters
write_string_nocolor :: proc(s: string) {
	if _batch {
		strings.write_string(&_batchInfo.builder, s);
		_batchInfo.cursor.x += len(s); // TODO: This would not work with \b, non-printable chars, and escape sequences within the string
	} else {
		os.write_string(os.stdout, s);
	}
}

write_string_at_nocolor :: proc(cursor: CursorPos, s: string) {
	saveCursor();
	setCursor(cursor);
	write_string_nocolor(s);
	restoreCursor();
}

write_string_color :: proc(fg: ForegroundColor, s: string) {
	setColor(fg);
	if _batch {
		strings.write_string(&_batchInfo.builder, s);
		_batchInfo.cursor.x += len(s); // TODO: This would not work with \b, non-printable chars, and escape sequences within the string
	} else {
		os.write_string(os.stdout, s);
	}
	resetColors();
}

write_string_at_color :: proc(cursor: CursorPos, fg: ForegroundColor, s: string) {
	saveCursor();
	setCursor(cursor);
	write_string_color(fg, s);
	restoreCursor();
}

write_string :: proc{write_string_nocolor, write_string_color, write_string_at_nocolor, write_string_at_color};
// TODO: write_strings functions with ..string arg, but doesn't use print/printf/println

write_strings_nocolor :: proc(args: ..string) {
	for s in args {
		write_string(s);
	}
}

write_strings_at_nocolor :: proc(cursor: CursorPos, args: ..string) {
	saveCursor();
	setCursor(cursor);
	write_strings_nocolor(..args);
	restoreCursor();
}

write_strings_color :: proc(fg: ForegroundColor, args: ..string) {
	for s in args {
		write_string(fg, s);
	}
}

write_strings_at_color :: proc(cursor: CursorPos, fg: ForegroundColor, args: ..string) {
	saveCursor();
	setCursor(cursor);
	write_strings_color(fg, ..args);
	restoreCursor();
}

write_strings :: proc{write_strings_nocolor, write_strings_color, write_strings_at_nocolor, write_strings_at_color};

write_line_nocolor :: proc(s: string) {
	if _batch {
		strings.write_string(&_batchInfo.builder, s);
	} else {
		os.write_string(os.stdout, s);
	}
	newLine();
}

write_line_at_nocolor :: proc(cursor: CursorPos, s: string) {
	saveCursor();
	setCursor(cursor);
	write_line_nocolor(s);
	restoreCursor();
}

write_line_color :: proc(fg: ForegroundColor, s: string) {
	setColor(fg);
	if _batch {
		strings.write_string(&_batchInfo.builder, s);
	} else {
		os.write_string(os.stdout, s);
	}
	resetColors();
	newLine();
}

write_line_at_color :: proc(cursor: CursorPos, fg: ForegroundColor, s: string) {
	saveCursor();
	setCursor(cursor);
	write_line_color(fg, s);
	restoreCursor();
}

write_line :: proc{write_line_nocolor, write_line_color, write_line_at_nocolor, write_line_at_color};

write_byte_current :: proc(b: byte) {
	if _batch {
		strings.write_byte(&_batchInfo.builder, b);
		_batchInfo.cursor.x += 1;
	} else {
		os.write_byte(os.stdout, b);
	}
}

write_byte_at :: proc(cursor: CursorPos, b: byte) {
	saveCursor();
	setCursor(cursor);
	write_byte_current(b);
	restoreCursor();
}

write_byte :: proc{write_byte_current, write_byte_at};

write_rune_current :: proc(r: rune) {
	if _batch {
		strings.write_rune(&_batchInfo.builder, r);
		_batchInfo.cursor.x += 1; // TODO: non-printable/combinational rune
	} else {
		os.write_rune(os.stdout, r);
	}
}

write_rune_at :: proc(cursor: CursorPos, r: rune) {
	saveCursor();
	setCursor(cursor);
	write_rune_current(r);
	restoreCursor();
}

write_rune :: proc{write_rune_current, write_rune_at};

// TODO: Not sure how to handle separator
print_nocolor :: proc(args: ..any, sep := " ") {
	if _batch {
		fmt.sbprint(&_batchInfo.builder, ..args);
	} else {
		fmt.print(..args);
	}
}

print_at_nocolor :: proc(cursor: CursorPos, args: ..any, sep := " ") {
	saveCursor();
	setCursor(cursor);
	print_nocolor(..args);
	restoreCursor();
}

print_color :: proc(fg: ForegroundColor, args: ..any, sep := " ") {
	setColor(fg);
	if _batch {
		fmt.sbprint(&_batchInfo.builder, ..args);
	} else {
		fmt.print(..args);
	}
	resetColors();
}

print_at_color :: proc(cursor: CursorPos, fg: ForegroundColor, args: ..any, sep := " ") {
	saveCursor();
	setCursor(cursor);
	print_color(fg, ..args);
	restoreCursor();
}

print :: proc{print_nocolor, print_color, print_at_nocolor, print_at_color};

println_nocolor :: proc(args: ..any, sep := " ") {
	if _batch {
		fmt.sbprintln(&_batchInfo.builder, ..args);
		_batchInfo.cursor.y += 1; // For the last newline
	} else {
		fmt.println(..args);
	}
}

println_at_nocolor :: proc(cursor: CursorPos, args: ..any, sep := " ") {
	saveCursor();
	setCursor(cursor);
	println_nocolor(..args);
	restoreCursor();
}

println_color :: proc(fg: ForegroundColor, args: ..any, sep := " ") {
	setColor(fg);
	if _batch {
		fmt.sbprintln(&_batchInfo.builder, ..args);
		_batchInfo.cursor.y += 1; // For the last newline
	} else {
		fmt.println(..args);
	}
	resetColors();
}

println_at_color :: proc(cursor: CursorPos, fg: ForegroundColor, args: ..any, sep := " ") {
	saveCursor();
	setCursor(cursor);
	println_color(fg, ..args);
	restoreCursor();
}

println :: proc{println_nocolor, println_color, println_at_nocolor, println_at_color};

printf_nocolor :: proc(format: string, args: ..any) {
	if _batch {
		fmt.sbprintf(&_batchInfo.builder, format, ..args);
	} else {
		fmt.printf(format, ..args);
	}
}

printf_at_nocolor :: proc(cursor: CursorPos, format: string, args: ..any) {
	saveCursor();
	setCursor(cursor);
	printf_nocolor(format, ..args);
	restoreCursor();
}

printf_color :: proc(fg: ForegroundColor, format: string, args: ..any) {
	setColor(fg);
	if _batch {
		fmt.sbprintf(&_batchInfo.builder, format, ..args);
	} else {
		fmt.printf(format, ..args);
	}
	resetColors();
}

printf_at_color :: proc(cursor: CursorPos, fg: ForegroundColor, format: string, args: ..any) {
	saveCursor();
	setCursor(cursor);
	printf_color(fg, format, ..args);
	restoreCursor();
}

printf :: proc{printf_nocolor, printf_color, printf_at_nocolor, printf_at_color};

newLine :: proc(amt: int = 1) {
	if _batch {
		for i in 0..<amt {
			strings.write_string(&_batchInfo.builder, NEWLINE);
		}
		_batchInfo.cursor.x = 1;
		_batchInfo.cursor.y += amt;
	} else {
		for i in 0..<amt {
			os.write_string(os.stdout, NEWLINE);
		}
	}
}

clearScreen :: proc() {
	if _batch {
		// Clearing the screen with erase everything before it.
		// Therefore, we can reset everything that was already in
		// the string builder
		strings.reset_builder(&_batchInfo.builder);
		strings.write_string(&_batchInfo.builder, CLEAR);
	} else {
		os.write_string(os.stdout, CLEAR);
	}
}

clearLine :: proc() {
	if _batch {
		strings.write_string(&_batchInfo.builder, CLEAR_LINE);
	} else {
		os.write_string(os.stdout, CLEAR_LINE);
	}
}

clearLine_right :: proc() {
	if _batch {
		strings.write_string(&_batchInfo.builder, CLEAR_LINE_RIGHT);
	} else {
		os.write_string(os.stdout, CLEAR_LINE_RIGHT);
	}
}

clearLine_left :: proc() {
	if _batch {
		strings.write_string(&_batchInfo.builder, CLEAR_LINE_LEFT);
	} else {
		os.write_string(os.stdout, CLEAR_LINE_LEFT);
	}
}

backspace :: proc(amt := 1, clear := true) {
	if _batch {
		// TODO: This doesn't handle escape sequences, non-printable characters, or combinational characters
		// TODO: Problem - doing a backspace after a backspace that has added escape sequences will result
		// in the deletion of some of the previous backspace, potentially.
		/*for i in 0..<min(amt, strings.builder_len(_batchInfo.builder)) {
			strings.pop_rune(&_batchInfo.builder);
		}*/

		// If trying to backspace more than what was buffered, then
		// just add new escape sequences to the buffer to do this.
// 		diff := amt - strings.builder_len(_batchInfo.builder);
		diff := amt;
		if (diff > 0) {
			moveCursor_left(diff);
			if clear do clearLine_right();
			else {
				for i in 0..<diff {
					os.write_string(os.stdout, " ");
				}
				moveCursor_left(diff);
			}
		}
	} else {
		moveCursor_left(amt);
		if clear do clearLine_right();
		else {
			for i in 0..<amt {
				os.write_string(os.stdout, " ");
			}
			moveCursor_left(amt);
		}
	}
}