/* This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 */
/** terminal prompt interface (code)
 *  @note allows line editing and supports some ANSI escape codes
 *  @file terminal.c
 *  @author King Kévin <kingkevin@cuvoodoo.info>
 *  @date 2018
 */
/* standard libraries */
#include <stdint.h> // standard integer types
#include <stdbool.h> // boolean type
#include <stdlib.h> // standard utilities
#include <string.h> // string utilities

/* own libraries */
#include "global.h" // global definitions
#include "terminal.h" // own definitions
#include "print.h" // printing utilities

char* terminal_prefix = NULL;
void (*terminal_process)(char* line) = NULL;

/** buffer to store user input and keep history */
static char terminal_buffer[1024] = {0};
/** how much of the buffer is user */
static uint16_t terminal_end = 0;
/** current position in the buffer */
static uint16_t terminal_pos = 0;
/** start position or current line in the buffer */
static uint16_t terminal_line = 0;
/** is the current line the last one */
static bool terminal_last = true;
/** currently inserting or replacing characters */
static bool terminal_insert = true;
/** current escape code */
static char escape_code[8] = {0};
/** current position in the escape code */
static uint8_t escape_pos = 0;

/** remove one line from buffer start and shift rest to start
 *  @return if one line has been removed
 */
static bool terminal_remove_line(void)
{
	if (0==terminal_line) { // be sure we are currently not on the first line
		return false;
	}
	uint16_t line_end = strlen(&terminal_buffer[0]); // get end of line
	if (terminal_end<=line_end) { // be sure there is a line to delete
		return false;
	}
	for (uint16_t i=line_end+1; i<=terminal_end && i<LENGTH(terminal_buffer); i++) { // line buffer after the line to start
		terminal_buffer[i-line_end-1] = terminal_buffer[i];
	}
	if (terminal_end>line_end+1) { // update buffer end
		terminal_end -= line_end+1;
	} else {
		terminal_end = 0;
	}
	if (terminal_pos>line_end+1) { // update buffer position
		terminal_pos -= line_end+1;
	} else {
		terminal_pos = 0;
	}
	if (terminal_line>line_end+1) { // update line position
		terminal_line -= line_end+1;
	} else {
		terminal_line = 0;
	}
	if (0==terminal_line) { // update if we are on the last line
		terminal_last = true;
	}
	return true;
}

/** shift with rotate current characters to other position
 *  @note this uses a recursive function
 *  @param[in] to_copy position of character(s) to shift
 *  @param[in] nb_copy number of characters to shift
 *  @param[in] to_shift where to shift the characters to
 */
static void terminal_shift_line(uint16_t to_copy, uint16_t nb_copy, uint16_t to_shift)
{
	char c = terminal_buffer[to_copy];
	terminal_buffer[to_copy] = terminal_buffer[to_shift];
	if (to_shift<terminal_end) { // characters to shift remain
		terminal_shift_line(to_copy+1, nb_copy>0 ? nb_copy-1 : 0, to_shift+1); // shift next character
	}
	if (nb_copy>0) {
		terminal_buffer[terminal_end-nb_copy] = c; // place back 
	}
}

/** copy current line to last line */
static void terminal_copy_line(void)
{
	if (terminal_last) { // current line is already last line
		return; // do nothing
	}
	uint16_t line_len = strlen(&terminal_buffer[terminal_line]);
	while (terminal_end>=LENGTH(terminal_buffer)-line_len-1 && terminal_remove_line()); // delete line if not enough space
	if (terminal_end<LENGTH(terminal_buffer)-line_len-1) { // there is enough space to copy the line
		for (uint16_t i=0; i<line_len; i++) { // copy line to end of buffer
			terminal_buffer[terminal_end+i+1] = terminal_buffer[terminal_line+i]; // copy character
		}
		terminal_pos += (terminal_end+1-terminal_line); // update current position
		terminal_line = terminal_end+1; // update line position
		terminal_end += line_len+1; // update buffer end position
		terminal_buffer[terminal_end] = '\0'; // ensure end is terminated
	} else if (0==terminal_line) { // shift (first) line to end of buffer
		terminal_shift_line(0, line_len, line_len+1); // shift line
		terminal_line = terminal_end-line_len; // update line position
		terminal_pos += terminal_line; // update current position
		// terminal_end did not change
	}
	terminal_last = true; // now we are on the last line
}

/** process current escape code */
static void terminal_process_escape(void)
{
	if (escape_pos<2) { // the escape code must have at least 2 bytes (C1 and final)
		return;
	}
	switch (escape_code[0]) { // process escape code according to C1
		case '[': // CSI - Control Sequence Introducer
			switch (escape_code[escape_pos-1]) { // process CSI code
				case 'A': // CUU - cursor up
					{
						uint16_t n = 1; // number of cells to move
						if (escape_pos>2) { // number of cells provided
							escape_code[escape_pos-1] = '\0'; // terminate string
							n = atoi(&escape_code[1]); // get number of cells
						}
						while (n--) { // go up number of line
							if (0==terminal_line) { // stop if we are already at the top line
								break;
							}
							uint16_t terminal_line_new=0; // new line start
							for (uint16_t pos=0; pos<terminal_line-1 && pos<LENGTH(terminal_buffer); pos++) { // find for the last line before the current
								if ('\0'==terminal_buffer[pos]) { // new line found
									terminal_line_new = pos+1; // save new line
								}
							}
							if (terminal_pos==terminal_line+strlen(&terminal_buffer[terminal_line])) { // if the position is the end of the current line
								terminal_pos=terminal_line_new+strlen(&terminal_buffer[terminal_line_new]); // set position to end of new line
							} else {
								terminal_pos -= (terminal_line-terminal_line_new); // move position to new line
								if (terminal_pos>terminal_line_new+strlen(&terminal_buffer[terminal_line_new])) { // position is outside of line
									terminal_pos = terminal_line_new+strlen(&terminal_buffer[terminal_line_new]); // set new position to end of line
								}
							}
							terminal_line = terminal_line_new; // set new line start
							terminal_last = (terminal_end==terminal_line+strlen(&terminal_buffer[terminal_line])); // check if we are on the last line
						}
					}
					break;
				case 'B': // CUD - cursor down
					{
						uint16_t n = 1; // number of cells to move
						if (escape_pos>2) { // number of cells provided
							escape_code[escape_pos-1] = '\0'; // terminate string
							n = atoi(&escape_code[1]); // get number of cells
						}
						while (n--) { // go down number of line
							if (terminal_last) { // stop if we are already at the last line
								break;
							}
							uint16_t terminal_line_new = terminal_line+strlen(&terminal_buffer[terminal_line])+1; // line start for the next line
							if (terminal_pos==terminal_line+strlen(&terminal_buffer[terminal_line])) { // if the position is the end of the current line
								terminal_pos=terminal_line_new+strlen(&terminal_buffer[terminal_line_new]); // set position to end of new line
							} else {
								terminal_pos += (terminal_line_new-terminal_line); // move position to new line
								if (terminal_pos>terminal_line_new+strlen(&terminal_buffer[terminal_line_new])) { // position is outside of line
									terminal_pos = terminal_line_new+strlen(&terminal_buffer[terminal_line_new]); // set new position to end of line
								}
							}
							terminal_line = terminal_line_new; // set new line start
							terminal_last = (terminal_end==terminal_line+strlen(&terminal_buffer[terminal_line])); // check if we are on the last line
						}
					}
					break;
				case 'C': // CUF - cursor forward
					{
						uint16_t n = 1; // number of cells to move
						if (escape_pos>2) { // number of cells provided
							escape_code[escape_pos-1] = '\0'; // terminate string
							n = atoi(&escape_code[1]); // get number of cells
						}
						while (n--) { // go right number of moves
							if (terminal_pos>=terminal_line+strlen(&terminal_buffer[terminal_line])) { // stop if we are already at the end
								break;
							}
							terminal_pos++;
						}
					}
					break;
				case 'D': // CUB - cursor back
					{
						uint16_t n = 1; // number of cells to move
						if (escape_pos>2) { // number of cells provided
							escape_code[escape_pos-1] = '\0'; // terminate string
							n = atoi(&escape_code[1]); // get number of cells
						}
						while (n--) { // go left number of moves
							if (terminal_pos<=terminal_line) { // stop if we are already at the beginning
								break;
							}
							terminal_pos--;
						}
					}
					break;
				case '~': // special key
					if (3==escape_pos) { // we only expect one parameter bytes
						switch (escape_code[1]) {
							case '2': // insert
								terminal_insert = !terminal_insert; // toggle insert/replace mode
								break;
							case '3': // delete
								if (!terminal_last) { // we are not editing the last line
									terminal_copy_line(); // make current line the last line
								}
								if (terminal_pos<terminal_end) { // be sure we are not at the end of the line, where there is nothing to erase
									for (uint16_t i=terminal_pos; i<terminal_end && i<LENGTH(terminal_buffer)-1; i++) { // delete character by shifting end of line
										terminal_buffer[i] = terminal_buffer[i+1]; // shift character
									}
									terminal_end--; // shorten line
								}
								break;
							case '7': // home
								terminal_pos = terminal_line; // set position to beginning of line
								break;
							case '8': // end
								terminal_pos = terminal_line+strlen(&terminal_buffer[terminal_line]); // set position to end of line
								break;
							case '5': // page up
								if (terminal_line>0) {
									uint16_t terminal_line_new = 0; // set to first line
									if (terminal_pos==terminal_line+strlen(&terminal_buffer[terminal_line])) { // if the position is the end of the current line
										terminal_pos=terminal_line_new+strlen(&terminal_buffer[terminal_line_new]); // set position to end of new line
									} else {
										terminal_pos -= (terminal_line-terminal_line_new); // move position to new line
										if (terminal_pos>terminal_line_new+strlen(&terminal_buffer[terminal_line_new])) { // position is outside of line
											terminal_pos = terminal_line_new+strlen(&terminal_buffer[terminal_line_new]); // set new position to end of line
										}
									}
									terminal_line = terminal_line_new; // set new line start
									terminal_last = (terminal_end==terminal_line+strlen(&terminal_buffer[terminal_line])); // check if we are on the last line
								}
								break;
							case '6': // page down
								if (!terminal_last) { // stop if we are already at the last line
									uint16_t terminal_line_new = terminal_line; // start last line search
									for (uint16_t i=terminal_line_new; i<terminal_end-1; i++) { // search for last line
										if ('\0'==terminal_buffer[i]) { // end of line found
											terminal_line_new = i+1; // set new line start
										}
									}
									if (terminal_pos==terminal_line+strlen(&terminal_buffer[terminal_line])) { // if the position is the end of the current line
										terminal_pos=terminal_line_new+strlen(&terminal_buffer[terminal_line_new]); // set position to end of new line
									} else {
										terminal_pos += (terminal_line_new-terminal_line); // move position to new line
										if (terminal_pos>terminal_line_new+strlen(&terminal_buffer[terminal_line_new])) { // position is outside of line
											terminal_pos = terminal_line_new+strlen(&terminal_buffer[terminal_line_new]); // set new position to end of line
										}
									}
									terminal_line = terminal_line_new; // set new line start
									terminal_last = (terminal_end==terminal_line+strlen(&terminal_buffer[terminal_line])); // check if we are on the last line
								}
								break;
						}
					}
					break;
				default: // we don't handle other codes
					break; // do nothing
			}
			break;
		default: // we don't handle this code
			break; // do nothing
	}
}

/** print current line and set position */
static void terminal_print_line(void)
{
	printf("\r\x1b[K%s%s%s", terminal_prefix ? terminal_prefix : "", ": ", &terminal_buffer[terminal_line]); // erase line and display last one
	if (terminal_pos<terminal_line+strlen(&terminal_buffer[terminal_line])) { // cursor is not at the end of the line
		printf("\x1b[%uD", terminal_line+strlen(&terminal_buffer[terminal_line])-terminal_pos); // set position by going back
	}
}

void terminal_setup(void) {
	// reset all buffers
	terminal_buffer[0] = '\0';
	terminal_end = 0;
	terminal_pos = 0;
	terminal_line = 0;
	terminal_last = true;
	escape_pos = 0;
	// start prompt
	terminal_send(0);
}

void terminal_send(volatile char c)
{
	static char newline_last = 0; // last new linefeed or carrier return character received

	if (escape_pos) { // currently receiving an escape code
		if (1==escape_pos) { // we received C0 and expected C1
			if (c>=0x40 && c<=0x5f) { // data is in the right range
				escape_code[0] = c; // save C1
				escape_pos++; // got to next position
			} else { // this is not a C1 code
				escape_pos = 0; // stop saving the escape code
				terminal_send(c); // process data as non-escape code
			}
		} else { // after C1 we expect a parameters, intermediate, or final byte
			if (c>=0x20 && c<=0x3f) { // received parameter (0x30-0x3f) or intermediate (0x20-0x2f) byte
				if (escape_pos<=LENGTH(escape_code)) {
					escape_code[escape_pos-1] = c; // save parameter byte
					escape_pos++; // go to next position
				}
			} else if (c>=0x40 && c<=0x7f) { // received final byte
				if (escape_pos<=LENGTH(escape_code)) {
					escape_code[escape_pos-1] = c; // save final byte
				}
				terminal_process_escape(); // process escape code since we received the final byte
				escape_pos = 0; // stop saving the escape code
				terminal_print_line(); // print current line since if might have changed
			} else { // this is not a expected byte
				escape_pos = 0; // stop saving the escape code
				terminal_send(c); // process data as non-escape code
			}
		}
	} else {
		if (0==c) { // string end received
			terminal_print_line(); // only update line
		} else if (0x1b==c) { // receiving new escape code (ESC)
			escape_pos = 1; // start filling buffer
		} else if (0x03==c) { // received CRTL+C
			if (!terminal_last) { // we are not on the last line
				uint16_t terminal_line_new = terminal_line; // start last line search
				for (uint16_t i=terminal_line_new; i<terminal_end-1; i++) { // search for last line
					if ('\0'==terminal_buffer[i]) { // end of line found
						terminal_line_new = i+1; // set new line start
					}
				}
				terminal_line = terminal_line_new; // set new line start
				terminal_last = true; // remember we are on the last line
			}	
			if (terminal_line==terminal_end) { // line is empty
				// nothing to do
			} else {
				// do not process current line
				while (terminal_end>=LENGTH(terminal_buffer)-1 && terminal_remove_line()); // delete line if not enough space
				if (terminal_end<LENGTH(terminal_buffer)-1) { // now there is space
					terminal_end++; // shift end to new line
				} else { // the current last line takes all the space -> erase it
					terminal_end = 0; // update end
				}
				terminal_buffer[terminal_end] = '\0'; // ensure end is terminated
			}
			terminal_pos = terminal_end; // set position to new line
			terminal_line = terminal_end; // update line position
			printf("\n"); // go to new line
			terminal_print_line(); // print current empty line
		} else { // all other bytes do some line editing
			if (!terminal_last) { // we are not editing the last line
				terminal_copy_line(); // make current line the last line
			}
			if ('\r'== c || '\n'==c) { // line finished
				if ('\r'==newline_last && '\n'==c) { // windows newline received
					// this newline has already been handled before
				} else {
					printf("\n"); // print new line
					if (terminal_process) {
						(*terminal_process)(&terminal_buffer[terminal_line]); // process line
					}
					if (strlen(&terminal_buffer[terminal_line])>0) { // only store non-empty line
						while (terminal_end>=LENGTH(terminal_buffer)-1 && terminal_remove_line()); // delete line if not enough space
						if (terminal_end<LENGTH(terminal_buffer)-1) { // now there is space
							terminal_end++; // shift end to new line
						} else { // the current last line takes all the space -> erase it
							terminal_end = 0; // update end
						}
						terminal_buffer[terminal_end] = '\0'; // ensure end is terminated
						terminal_pos = terminal_end; // set position to new line
						terminal_line = terminal_pos; // update line position
					}
				}
				newline_last = c; // remember last character
			} else if (0x7f==c) { // backspace
				if (terminal_pos>terminal_line) { // we are not at the beginning of the line
					for (uint16_t i=terminal_pos-1; i<terminal_end && i<LENGTH(terminal_buffer)-1; i++) { // delete character by shifting end of line
						terminal_buffer[i] = terminal_buffer[i+1]; // shift character
					}
					terminal_end--; // shorten line
					terminal_pos--; // move position back
				}
			} else if (terminal_insert) {
				while (terminal_end>=LENGTH(terminal_buffer)-1 && terminal_remove_line()); // delete line if not enough space
				if (terminal_end<LENGTH(terminal_buffer)-1) { // there is space to move the end
					for (int16_t i=terminal_end; i>=terminal_pos; i--) { // shift buffer
						terminal_buffer[i+1] = terminal_buffer[i];
					}
					terminal_buffer[terminal_pos++] = c; // insert new character
					terminal_buffer[++terminal_end] = '\0'; // update end
				}
			} else { // replace current character
				while (terminal_pos==terminal_end && terminal_end>=LENGTH(terminal_buffer)-1 && terminal_remove_line()); // delete line if not enough space
				terminal_buffer[terminal_pos] = c; // replace current character
				if (terminal_pos<LENGTH(terminal_buffer)-1) { // go to next character (if possible)
					terminal_pos++;
				}
				if (terminal_pos>terminal_end) { // update end if it moved
					terminal_end = terminal_pos; // move end
					terminal_buffer[terminal_end] = '\0'; // ensure end is terminated
				}
			}
			terminal_print_line(); // print current line
		}
	}
}

