/* Input line and requesters handling.

   Copyright (C) 1993 Sebastiano Vigna

    This file is part of ne, the nice editor.

    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 1, 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, write to the Free Software
    Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.

In other words, you are welcome to use, share and improve this program.
You are forbidden to forbid anyone else to use, share and improve
what you give them.   Help stamp out software-hoarding!  */


#include "ne.h"
#include "termchar.h"
#include <dirent.h>

/* This is the maximum number of characters which can be typed on the input line. */

#define MAX_INPUT_LINE_LEN		1024

/* This is the expected max length of the current directory name. */

#define CUR_DIR_MAX_SIZE		4096

/* This is the name taken by unnamed documents in the document selector. */

#define UNNAMED_NAME				"<unnamed>"

/* These are the default allocation sizes for the entry array and for the
name array when reading a directory. The allocation sizes start with these
values, and they are doubled each time more space is needed. This ensures a
reasonable number of retries. */

#define DEF_ENTRIES_ALLOC_SIZE	256
#define DEF_NAMES_ALLOC_SIZE		(4*1024)



/* This function prints an input prompt in the input line. A colon is
postpended to the prompt. The position of the first character to use for
input is returned. Moreover, the status bar is reset, so that it will be
updated. */


static int print_prompt(const unsigned char *prompt) {

	assert(prompt != NULL);

	move_cursor(lines-1, 0);

	clear_to_eol();

	standout_on();

	output_string(prompt);
	output_string(":");

	standout_off();

	output_chars(NULL, 1);

	reset_status_bar();

	return(strlen(prompt)+2);
}



/* This function prompts the user for a yes/no answer. default_value has to
be TRUE or FALSE. TRUE is returned if 'y' was typed, FALSE in any other
case. Escaping is not allowed. RETURN returns the default value. */


int request_response(buffer *b, const unsigned char *prompt, int default_value) {

	unsigned char response = request_char(b, prompt, default_value ? 'y' : 'n');

	if (response == 'Y') return(TRUE);
	return(FALSE);
}



/* This function prompts the user for a single charancter answer.
default_value has to be a character which is used for the default answer.
The character typed by the user (upper cased) is returned. The default
character is used if the user types RETURN. Note that the cursor is moved
back to its current position. This offers a clear distinction between
immediate and long inputs, and allows for interactive search and replace. */


unsigned char request_char(buffer *b, const unsigned char *prompt, const unsigned char default_value) {

	int c;
	input_class ic;

	print_prompt(prompt);

	if (default_value) output_chars(&default_value, 1);
	move_cursor(b->cur_y, b->cur_x);

	while(TRUE) {
		while((ic = class[c = get_key_code()]) == IGNORE);

		switch(ic) {
			case ALPHA:
				return(up_case[(unsigned char)c]);

			case RETURN:
				return(up_case[default_value]);

			default:
				break;
		}
	}
}



/* This function requests a number, with a given prompt and default value.
Only nonnegative integers can be entered. The effects of a negative
default_value are mysterious, and not worth an investigation. The returned
value is nonnegative if something was entered, negative on escaping or on
entering the empty string. */


int request_number(const unsigned char *prompt, int default_value) {

	unsigned char t[MAX_INT_LEN], *result;

	if (default_value >= 0) sprintf(t, "%d", default_value);

	if (!(result = request(prompt, default_value >= 0 ? t : NULL, FALSE)) || !*result) return(ERROR);

	return(atoi(result));
}



/* This function requests a string. The returned string is never longer than
MAX_INPUT_LINE_LEN, and has been malloc()ed, so you can use it and then
free() it. NULL is returned on escaping, or if entering an empty string
(unless accept_null_string is TRUE, in which case the empty string is
duplicated and returned). */

unsigned char *request_string(const unsigned char *prompt, const unsigned char *default_string, int accept_null_string) {

	unsigned char *result = request(prompt, default_string, TRUE);

	if (result && (*result || accept_null_string)) return(strdup(result));

	return(NULL);
}



/* This is the main function which serves request_number() and
request_string(). Given a prompt, a default string and a boolean flag which
establish the possibility of any alphabetical input (as opposed to digits
only), the user can edit a string of at most MAX_INPUT_LINE_LEN characters.
Many useful commands can be used here. The string edited by the user is
returned, or NULL if the input was escaped, or the empty string was entered.
Note that presently this function always returns a pointer to a static
buffer, but this could change in the future.

The function relies on a number of auxiliary functions and static data. As
always, we would really need nested functions, but, alas, C can't cope with
them. */

static unsigned char input_buffer[MAX_INPUT_LINE_LEN+1];

/* start_x os the first usable x position for editing; len is the current
length of the input buffer; x is the x position of the cursor; pos the
position of the cursor inside the input buffer; offset is always pos-(x-start_x). */

static int start_x, len, pos, x, offset;


/* The following functions are rather intuitive. The perform basic editing
actions on the input line. */


static void input_refresh(void) {
	int i;

	move_cursor(lines-1, start_x);
	for(i=0; i<columns-1-start_x && i+offset<len; i++) output_chars(&input_buffer[i+offset], 1);
	clear_to_eol();
}


static void input_move_left(void) {
	if (pos>0) {

		pos--;
		x--;

		if (x < start_x) {
			x++;
			offset--;
			if (char_ins_del_ok) {
				move_cursor(lines-1, columns-2);
				delete_chars(1);
				move_cursor(lines-1, start_x);
				insert_chars(&input_buffer[pos], 1);
			}
			else input_refresh();
		}
	}
}


static void input_move_right(void) {
	if (pos<len) {
		pos++;
		x++;

		if (x == columns-1) {
			x--;
			offset++;

			if (char_ins_del_ok) {
				move_cursor(lines-1, start_x);
				delete_chars(1);
				move_cursor(lines-1, columns-2);
				if (pos < len) output_chars(&input_buffer[pos], 1);
			}
			else input_refresh();
		}
	}
}


static void input_move_to_sol(void) {

	if (offset == 0) {
		x = start_x;
		pos = 0;
		return;
	}

	x = start_x;
	offset = pos = 0;
	input_refresh();
}


static void input_move_to_eol(void) {

	if (x+(len-pos) < columns-1) {
		x += len-pos;
		pos = len;
		return;
	}

	x = columns-2;
	pos = len;
	offset = len-(columns-2-start_x);
	input_refresh();
}


static void input_next_word(void) {

	int space_skipped = FALSE;

	while(pos<len) {
		if (isspace(input_buffer[pos]) || ispunct(input_buffer[pos])) space_skipped = TRUE;
		else if (space_skipped) break;
		input_move_right();
	}
}


static void input_prev_word(void) {

	int word_skipped = FALSE;

	while(pos > 0) {
		if (!(isspace(input_buffer[pos-1]) || ispunct(input_buffer[pos-1]))) word_skipped = TRUE;
		else if (word_skipped) break;
		input_move_left();
	}
}


static void input_paste(void) {

	clip_desc *cd = get_nth_clip(cur_buffer->cur_clip);

	if (cd) {
		strncpy(input_buffer, cd->cs->stream, MAX_INPUT_LINE_LEN);

		len = strlen(input_buffer);
		pos = offset = 0;
		x = start_x;
		input_refresh();
	}
}


unsigned char *request(const unsigned char *prompt, const unsigned char *default_string, int alpha_allowed) {

	action a;
	input_class ic;
	int c, first_char_typed = TRUE;

	pos = len = offset = 0;
	x = start_x = print_prompt(prompt);

	if (default_string) {
		strncpy(input_buffer, default_string, MAX_INPUT_LINE_LEN);
		len = strlen(input_buffer);
		input_refresh();
	}

	while(TRUE) {

		move_cursor(lines-1, x);

		while((ic = class[c = get_key_code()]) == IGNORE);

		switch(ic) {
			case ALPHA:

				if (first_char_typed) {
					len = 0;
					clear_to_eol();
				}

				if (len<MAX_INPUT_LINE_LEN && (alpha_allowed || c>='0' && c<='9')) {

					memmove(&input_buffer[pos+1], &input_buffer[pos], len-pos);
					input_buffer[pos] = c;
					len++;

					move_cursor(lines-1, x);

					if (x < columns-2) {
						if (pos == len-1) output_chars(input_buffer+pos, 1);
						else if (char_ins_del_ok) insert_chars(input_buffer+pos, 1);
						else input_refresh();
					}
					else output_chars(input_buffer+pos, 1);

					input_move_right();
				}
				break;

			case RETURN:
				input_buffer[len] = 0;
				return(input_buffer);

			case COMMAND:
				if ((a = parse_command_line(key_binding[c], NULL, NULL))>=0) {
					switch(a) {

						case MOVE_LEFT:
							input_move_left();
							break;

						case MOVE_RIGHT:
							input_move_right();
							break;

						case BACKSPACE:
							if (pos == 0) break;
							input_move_left();

						case DELETE_CHAR:
							if (len>0 && pos<len) {

								memmove(&input_buffer[pos], &input_buffer[pos+1], len-pos-1);
								len--;

								if (char_ins_del_ok) {
									move_cursor(lines-1, x);
									delete_chars(1);

									if (pos+(columns-2-x) < len) {
										move_cursor(lines-1, columns-2);
										output_chars(&input_buffer[pos+(columns-2-x)], 1);
									}
								}
								else input_refresh();
							}
							break;

						case DELETE_LINE:
							move_cursor(lines-1, start_x);
							clear_to_eol();
							len = pos = offset = 0;
							x = start_x;
							break;

						case DELETE_TO_EOL:
							len = pos;
							clear_to_eol();
							break;

						case MOVE_TO_SOL:
						case MOVE_TO_SOF:
							input_move_to_sol();
							break;

						case MOVE_TO_EOL:
						case MOVE_TO_EOF:
							input_move_to_eol();
							break;

						case TOGGLE_SOL_EOL:
						case TOGGLE_SOF_EOF:
							if (pos != 0) input_move_to_sol();
							else input_move_to_eol();
							break;

						case PREV_WORD:
							input_prev_word();
							break;

						case NEXT_WORD:
							input_next_word();
							break;

						case REFRESH:
							input_refresh();
							break;

						case PASTE:
							input_paste();
							break;

						case ESCAPE:
							return(NULL);
					}
				}
				break;

			default:
				break;
		}

		first_char_typed = FALSE;
	}
}




/* This is the printing function used by the requester. It prints the
strings from the entries array existing in a certain page (a page contains
(lines-1)*names_per_line items) with max_name_len maximum width. */

static void print_strings(const unsigned char * const *entries, int num_entries, int page, int names_per_line, int max_name_len) {

	int i,j;

	for(i=0; i<lines-1; i++) {
		move_cursor(i, 0);
		clear_to_eol();

		for(j=0; j<names_per_line; j++) {
			if ((i+page*(lines-1))*names_per_line+j<num_entries) {
				move_cursor(i, j*max_name_len);
				output_string(entries[(i+page*(lines-1))*names_per_line+j]);
			}
		}
	}
}



/* This function prompts the user to choose one between several
(num_entries) strings, contained in the entries array. The maximum string
width is given as max_name_len. The strings are displayed as an array. More
than one page will be available if there are many strings. The number of the
selected string is returned, or -1 on escaping. */


int request_strings(const unsigned char * const *entries, int num_entries, int max_name_len) {

	action a;
	input_class ic;
	int c, i, n, x = 0, y = 0, page = 0;
	int names_per_line, names_per_page;

	assert(num_entries > 0);

	if (!(names_per_line = columns / (++max_name_len))) names_per_line = 1;
	names_per_page = names_per_line*(lines-1);

	print_strings(entries, num_entries, page, names_per_line, max_name_len);

	while(TRUE) {

		move_cursor(y, x*max_name_len);

		while((ic = class[c = get_key_code()]) == IGNORE);

		switch(ic) {
			case ALPHA:
				if ((n = page*names_per_page+y*names_per_line+x) >= num_entries) n = num_entries-1;

				c = up_case[c];

				for(i=1; i<num_entries; i++)
					if (up_case[entries[(n+i) % num_entries][0]] == c) {

						n = (n+i) % num_entries;

						if (n/names_per_page != page) {
							page = n/names_per_page;
							print_strings(entries, num_entries, page, names_per_line, max_name_len);
						}

						y = (n % names_per_page) / names_per_line;
						x = n % names_per_line;
						break;
					}
				break;

			case RETURN:
				n = page*names_per_page+y*names_per_line+x;
				if (n >= num_entries) return(-1);
				else return(n);

			case COMMAND:
				if ((a = parse_command_line(key_binding[c], NULL, NULL))>=0) {
					switch(a) {
					case MOVE_RIGHT:
						if (names_per_line>1)
							x = ++x%names_per_line;
						break;

					case MOVE_LEFT:
						if (names_per_line>1)
							if (--x<0) x = names_per_line-1;
						break;

					case MOVE_TO_SOL:
						x = 0;
						break;

					case MOVE_TO_EOL:
						x = names_per_line-1;
						break;

					case TOGGLE_SOL_EOL:
						if (x != 0) x = 0;
						else x = names_per_line-1;
						break;

					case LINE_UP:
						if (y>0) y--;
						else if (page) {
							page--;
							y = lines-2;
							print_strings(entries, num_entries, page, names_per_line, max_name_len);
						}
						break;

					case LINE_DOWN:
						if (y<lines-2 && y<(num_entries+names_per_line-1)/names_per_line-1-page*(lines-1)) y++;
						else if ((page+1)*names_per_page < num_entries) {
							page++;
							y = 0;
							print_strings(entries, num_entries, page, names_per_line, max_name_len);
						}
						break;

					case PREV_PAGE:
						if (y>0) y = 0;
						else if (page) {
							page--;
							print_strings(entries, num_entries, page, names_per_line, max_name_len);
						}
						break;

					case NEXT_PAGE:
						if (y<lines-2) y = lines-2;
						else if ((page+1)*names_per_page < num_entries) {
							page++;
							print_strings(entries, num_entries, page, names_per_line, max_name_len);
						}
						if (y>=(num_entries+names_per_line-1)/names_per_line-1-page*(lines-1))
							y = (num_entries+names_per_line-1)/names_per_line-1-page*(lines-1);
						break;

					case MOVE_TO_SOF:
						i = page;
						x = y = page = 0;
						if (i != page) print_strings(entries, num_entries, page, names_per_line, max_name_len);
						break;

					case TOGGLE_SOF_EOF:
					case MOVE_TO_EOF:
						i = page;
						if (a == MOVE_TO_EOF || x == 0 && y == 0 && page == 0) {
							page = (num_entries-1)/names_per_page;
							y = ((num_entries-1) % names_per_page) / names_per_line;
							x = (num_entries-1) % names_per_line;
						}
						else x = y = page = 0;
						if (i != page) print_strings(entries, num_entries, page, names_per_line, max_name_len);
						break;

					case ESCAPE:
						return(-1);
					}
				}
				break;

			default:
				break;
		}
	}
}



/* This is the file requester. It reads the directory in which the filename
lives, builds an array of string and calls request_strings(). If a directory name
is returned, it enters the directory. Returns NULL on error or escaping. */


unsigned char *request_files(const unsigned char *filename) {

	int i, num_entries, name_len, max_name_len, total_len, next_dir, is_directory,
		entries_alloc_size = DEF_ENTRIES_ALLOC_SIZE,
		names_alloc_size = DEF_NAMES_ALLOC_SIZE;

	unsigned char *dir_name, **entries = NULL, *names = NULL, *cur_dir_name, *result = NULL, *p;

	DIR *d;
	struct dirent *de;
	struct stat s;

	if (!(cur_dir_name = getcwd(NULL, CUR_DIR_MAX_SIZE))) return(NULL);

	if (dir_name = strdup(filename)) {
		if ((p = (unsigned char *)file_part(dir_name)) != dir_name) {
			*p = 0;
			chdir(tilde_expand(dir_name));
		}
		free(dir_name);
	}

	if (entries = malloc(sizeof(unsigned char *)*entries_alloc_size)) {
		if (names = malloc(sizeof(unsigned char)*names_alloc_size)) {
			do {
				next_dir = FALSE;

				if (d = opendir(CURDIR)) {

					num_entries = max_name_len = total_len = 0;

#ifdef _AMIGA
					total_len = 2;
					num_entries++;
					strcpy(names, "/");
					entries[0] = names;
#endif

					stop = FALSE;

					while(!stop && (de = readdir(d))) {
						is_directory = !stat(de->d_name, &s) && S_ISDIR(s.st_mode);
						name_len = strlen(de->d_name) + is_directory + 1;

						if (name_len > max_name_len) max_name_len = name_len;

						if (total_len+name_len > names_alloc_size) {
							char *t;
							t = realloc(names, sizeof(unsigned char)*(names_alloc_size = names_alloc_size*2 + name_len));
							if (!t) break;
							names = t;
						}

						if (num_entries >= entries_alloc_size) {
							unsigned char **t;
							t = realloc(entries, sizeof(unsigned char *)*(entries_alloc_size *= 2));
							if (!t) break;
							entries = t;
						}

						strcpy(entries[num_entries] = names+total_len, de->d_name);
						if (is_directory) strcpy(names+total_len+name_len-2, "/");
						total_len += name_len;
						num_entries++;
					}

					if (num_entries) {

#ifdef _AMIGA
						if (num_entries-1) qsort(entries+1, num_entries-1, sizeof(unsigned char *), strcmpp);
#else
						if (num_entries-2 >= 0) {
							if (!strcmp(entries[0], "./") && !strcmp(entries[1], "../")) {
								p = entries[0];
								entries[0] = entries[1];
								entries[1] = p;
							}
							if (num_entries-2 > 0 )
								qsort(entries+2, num_entries-2, sizeof(unsigned char *), strcmpp);
						}
#endif

						if ((i = request_strings(entries, num_entries, max_name_len)) >= 0) {
							p = entries[i];

							if (p[strlen(p)-1] == '/') {
#ifndef _AMIGA
								p[strlen(p)-1] = 0;
#endif
									chdir(p);
									next_dir = TRUE;
								}
							else {
								result = getcwd(NULL, CUR_DIR_MAX_SIZE+strlen(p));
								strcat(result, "/");
								strcat(result, p);
							}
						}
					}

					closedir(d);
				}

			} while(next_dir);

			free(names);
		}
		free(entries);
	}

	chdir(cur_dir_name);
	free(cur_dir_name);

	return(result);
}



/* This function requests a file name. If no_file_req is FALSE, the file requester
is firstly presented. If no_file_req is TRUE, or the file requester is escaped,
a long input is performed with the given prompt and default_name. */

unsigned char *request_file(buffer *b, const unsigned char *prompt, const unsigned char *default_name) {

	unsigned char *p;

	if (!b->no_file_req) {
		p = request_files(default_name);
		reset_window();
		if (p) return(p);
	}

	if (p = request_string(prompt, default_name, FALSE)) return(p);

	return(NULL);
}



/* This function presents to the user a list of the documents currently available.
It returns the number of the document selected, or -1 on escape or error. */

int request_document(void) {

	int i = -1, num_entries, max_name_len, total_len;
	unsigned char **entries, *names, *p, unnamed_name[] = UNNAMED_NAME;
	buffer *b = (buffer *)buffers.head;

	num_entries = max_name_len = total_len = 0;

	while(b->b_node.next) {
		p = b->filename ? b->filename : unnamed_name;

		if (strlen(p)>max_name_len) max_name_len = strlen(p);

		total_len += strlen(p)+1;
		num_entries++;

		b = (buffer *)b->b_node.next;
	}

	max_name_len += 8;

	if (num_entries) {

		if (entries = malloc(sizeof(unsigned char *)*num_entries)) {
			if (names = malloc(sizeof(unsigned char)*total_len)) {

				p = names;

				b = (buffer *)buffers.head;

				for(i=0; i<num_entries; i++) {
					entries[i] = p;
					strcpy(p, b->filename ? b->filename : unnamed_name);
					p += strlen(p)+1;

					b = (buffer *)b->b_node.next;
				}

				i = request_strings(entries, num_entries, max_name_len);

				reset_window();

				free(names);
			}
			free(entries);
		}
	}

	return(i);
}
