// :build command
// also handles :shell.

#include "ted.h"

void build_stop(Ted *ted) {
	if (ted->building)
		process_kill(&ted->build_process);
	ted->building = false;
	ted->build_shown = false;
	arr_foreach_ptr(ted->build_errors, BuildError, err) {
		free(err->path);
	}
	arr_clear(ted->build_errors);
	arr_foreach_ptr(ted->build_queue, char *, cmd) {
		free(*cmd);
	}
	arr_clear(ted->build_queue);
	if (ted->active_buffer == &ted->build_buffer) {
		ted_switch_to_buffer(ted, NULL);
		ted_reset_active_buffer(ted);
	}
}

void build_queue_start(Ted *ted) {
	build_stop(ted);
}

void build_queue_command(Ted *ted, const char *command) {
	char *copy = str_dup(command);
	if (copy)
		arr_add(ted->build_queue, copy);
}

// returns true if there are still commands left in the queue
static bool build_run_next_command_in_queue(Ted *ted) {
	if (!ted->build_queue)
		return false;
	assert(!ted->build_process);
	assert(*ted->build_dir);
	change_directory(ted->build_dir);
	char *command = ted->build_queue[0];
	arr_remove(ted->build_queue, 0);
	if (ted_save_all(ted)) {
		ted->build_process = process_run(command);
		const char *error = process_geterr(ted->build_process);
		if (!error) {
			ted->building = true;
			ted->build_shown = true;
			TextBuffer *build_buffer = &ted->build_buffer;
			char32_t text[] = {'$', ' '};
			buffer_insert_text_at_cursor(build_buffer, str32(text, 2));
			buffer_insert_utf8_at_cursor(build_buffer, command);
			buffer_insert_char_at_cursor(build_buffer, '\n');
			build_buffer->view_only = true;
			free(command);
			return true;
		} else {
			ted_error(ted, "Couldn't start build: %s", error);
			build_stop(ted);
			return false;
		}
	} else {
		build_stop(ted);
		return false;
	}
}

void build_setup_buffer(Ted *ted) {
	// new empty build output buffer
	TextBuffer *build_buffer = &ted->build_buffer;
	buffer_new_file(build_buffer, NULL);
	build_buffer->store_undo_events = false; // don't need undo events for build output buffer
}

void build_queue_finish(Ted *ted) {
	build_setup_buffer(ted);
	build_run_next_command_in_queue(ted); // run the first command
}

void build_start_with_command(Ted *ted, const char *command) {
	build_queue_start(ted);
	build_queue_command(ted, command);
	build_queue_finish(ted);
}

void build_start(Ted *ted) {
	Settings *settings = ted_active_settings(ted);
	char *command = settings->build_command;
	char *root = ted_get_root_dir(ted);
	strbuf_cpy(ted->build_dir, root);
	if (*command == 0) {
		command = settings->build_default_command;
		change_directory(root);
	
	#if _WIN32
		if (fs_file_exists("make.bat")) {
			command = "make.bat";
		} else
	#endif
		if (fs_file_exists("Cargo.toml")) {
			command = "cargo build";
		} else if (fs_file_exists("Makefile")) {
			command = "make -j16";
		} else if (fs_file_exists("go.mod")) {
			command = "go build";
		}
	}
	free(root);
	if (*command)
		build_start_with_command(ted, command);
}

static void build_go_to_error(Ted *ted) {
	if (ted->build_error < arr_len(ted->build_errors)) {
		BuildError error = ted->build_errors[ted->build_error];
		// open the file where the error happened
		if (ted_open_file(ted, error.path)) {
			TextBuffer *buffer = ted->active_buffer;
			assert(buffer);
			
			u32 index = error.column;
			
			if (error.columns_per_tab > 1) {
				// get correct index
				String32 line = buffer_get_line(buffer, error.line);
				u32 column = 0;
				index = 0;
				while (column < error.column) {
					if (index >= line.len)
						break;
					if (line.str[index] == '\t') {
						column += error.columns_per_tab;
					} else {
						column += 1;
					}
					++index;
				}
			}
			
			
			BufferPos pos = {
				.line = error.line,
				.index = index,
			};
			
			
			// move cursor to error
			buffer_cursor_move_to_pos(buffer, pos);
			buffer->center_cursor_next_frame = true;

			// move cursor to error in build output
			TextBuffer *build_buffer = &ted->build_buffer;
			BufferPos error_pos = {.line = error.build_output_line, .index = 0};
			buffer_cursor_move_to_pos(build_buffer, error_pos);
			buffer_center_cursor(build_buffer);
		}
	}
}

void build_next_error(Ted *ted) {
	if (ted->build_errors) {
		ted->build_error += 1;
		ted->build_error %= arr_len(ted->build_errors);
		build_go_to_error(ted);
	}
}

void build_prev_error(Ted *ted) {
	if (ted->build_errors) {
		ted->build_error += arr_len(ted->build_errors) - 1;
		ted->build_error %= arr_len(ted->build_errors);
		build_go_to_error(ted);
	}
}

// returns -1 if *str..end is not an integer or a negative integer.
// Otherwise, advances *str past the number and returns it.
static int parse_nonnegative_integer(char32_t **str, char32_t *end) {
	char32_t *s = *str;
	int number_len;
	int n = 0;
	for (number_len = 0; s != end && s[number_len] >= '0' && s[number_len] <= '9'; ++number_len) {
		n *= 10;
		n += (int)s[number_len] - '0';
	}
	if (number_len == 0)
		return -1;
	*str += number_len;
	return n;
}

// could this character (reasonably) appear in a source file path?
static bool is_source_path(char32_t c) {
	const char *allowed_ascii_symbols_in_path = "./\\-_:";
	return c > CHAR_MAX || is32_alnum((wint_t)c)
		|| strchr(allowed_ascii_symbols_in_path, (char)c);
}

// make sure you set ted->build_dir before running this!
void build_check_for_errors(Ted *ted) {
	TextBuffer *buffer = &ted->build_buffer;
	arr_clear(ted->build_errors);
	for (u32 line_idx = 0; line_idx < buffer->nlines; ++line_idx) {
		Line *line = &buffer->lines[line_idx];
		if (line->len < 3) {
			continue;
		}
		bool is_error = true;
		// well, for a bit of time i thought rust was weird
		// and treated tabs as 4 columns
		// apparently its just a bug, which ive filed here
		// https://github.com/rust-lang/rust/issues/109537
		// we could treat ::: references as 4-columns-per-tab,
		// but then that would be wrong if the bug gets fixed.
		// all this is to say that columns_per_tab is currently always 1,
		// but might be useful at some point.
		u8 columns_per_tab = 1;
		char32_t *p = line->str, *end = p + line->len;
		
		{
			// rust errors look like:
			// "     --> file:line:column"
			// and can also include stuff like
			// "     ::: file:line:column"
			while (p != end && *p == ' ') {
				++p;
			}
			if (end - p >= 4) {
				String32 first4 = str32(p, 4);
				if (str32_cmp_ascii(first4, "::: ") == 0 || str32_cmp_ascii(first4, "--> ") == 0) {
					p += 4;
				}
			}
		}
	
		// check if we have something like main.c:5 or main.c(5)
		
		// get file name
		char32_t *filename_start = p;
		while (p != end) {
			if ((*p == ':' || *p == '(')
				&& p != line->str + 1) // don't catch "C:\thing\whatever.c" as "filename: C, line number: \thing\whatever.c"
				break;
			if (!is_source_path(*p)) {
				is_error = false;
				break;
			}
			++p;
		}
		if (p == end) is_error = false;
		u32 filename_len = (u32)(p - filename_start);
		if (filename_len == 0) is_error = false;
	
		if (is_error) {
			++p; // move past : or (
			int line_number = parse_nonnegative_integer(&p, end);
			if (p != end && line_number > 0) {
				// it's an error
				line_number -= 1; // line numbers in output start from 1.
				int column_number = 0;
				// check if there's a column number
				if (*p == ':') {
					++p; // move past :
					int num = parse_nonnegative_integer(&p, end);
					if (num > 0) {
						column_number = num - 1; // column numbers in output start from 1
					}
				}
				char *filename = str32_to_utf8_cstr(str32(filename_start, filename_len));
				if (filename) {
					char full_path[TED_PATH_MAX];
					const char *pfilename = filename;
					path_full(ted->build_dir, pfilename, full_path, sizeof full_path);
					// if the file does not exist, try stripping ../
					// this can solve "file not found" problems if your build command involves
					// cd'ing to a directory inside build_dir
					while (fs_path_type(full_path) == FS_NON_EXISTENT
						&& (str_has_prefix(pfilename, "../")
						#if _WIN32
						|| str_has_prefix(pfilename, "..\\")
						#endif
						)) {
						pfilename += 3;
						path_full(ted->build_dir, pfilename, full_path, sizeof full_path);
					}
										
					BuildError error = {
						.path = str_dup(full_path),
						.line = (u32)line_number,
						.column = (u32)column_number,
						.columns_per_tab = columns_per_tab,
						.build_output_line = line_idx
					};
					arr_add(ted->build_errors, error);
					free(filename);
				}
			}
		}
	}
	// go to the first error (if there is one)
	ted->build_error = 0;
	build_go_to_error(ted);
}

void build_frame(Ted *ted, float x1, float y1, float x2, float y2) {
	TextBuffer *buffer = &ted->build_buffer;
	assert(ted->build_shown);
	char buf[256];
	if (ted->building) {
		buffer->view_only = false; // disable view only temporarily so we can edit it
		bool any_text_inserted = false;
		while (1) {
			char incomplete[4];
			memcpy(incomplete, ted->build_incomplete_codepoint, sizeof incomplete);
			*ted->build_incomplete_codepoint = 0;

			i64 bytes_read = (i64)process_read(ted->build_process, buf + 3, sizeof buf - 3);
			if (bytes_read == -2) {
				ted_error(ted, "Error reading command output: %s.", process_geterr(ted->build_process));
				build_stop(ted);
				break;
			} else if (bytes_read == -1) {
				// no data right now.
				break;
			} else if (bytes_read == 0) {
				// end of file
				break;
			} else {
				any_text_inserted = true;
				// got some data.
				char *p = buf + 3 - strlen(incomplete);
				char *end = buf + 3 + bytes_read;
				// start off data with incomplete code point from last time
				memcpy(p, incomplete, strlen(incomplete));
				while (p != end) {
					char32_t c = 0;
					size_t ret = unicode_utf8_to_utf32(&c, p, (size_t)(end - p));
					if (ret == (size_t)-1) {
						// invalid UTF-8. skip this byte.
						++p;
					} else if (ret == (size_t)-2) {
						// incomplete UTF-8
						size_t leftovers = (size_t)(end - p);
						assert(leftovers < 4);
						memcpy(ted->build_incomplete_codepoint, p, leftovers);
						ted->build_incomplete_codepoint[leftovers] = '\0';
						p = end;
					} else {
						if (ret == 0) ret = 1;
						// got a code point
						buffer_insert_char_at_pos(buffer, buffer_pos_end_of_file(buffer), c);
						p += ret;
					}
				}
			}
		}

		if (any_text_inserted) {
			// show bottom of output (only relevant if there are no build errors)
			buffer->cursor_pos = buffer_pos_end_of_file(buffer);
			buffer_scroll_to_cursor(buffer);
		}

		ProcessExitInfo info = {0};
		int status = process_check_status(&ted->build_process, &info);
		if (status == 0) {
			// hasn't exited yet
		} else {
			buffer_insert_utf8_at_cursor(buffer, info.message);
			buffer_insert_utf8_at_cursor(buffer, "\n");
			if (!build_run_next_command_in_queue(ted)) {
				ted->building = false;
				// done command queue; check for errors
				build_check_for_errors(ted);
			}
		}
		buffer->view_only = true;
	}
	buffer_render(buffer, rect4(x1, y1, x2, y2));
}