/*
**		BRIEF -- Basic Reconfigurable Interactive Editing Facility
**
**		Written by Dave Nanian and Michael Strickman.
*/

/*
**		prompt.cb:
**
**		This file contains routines that deal with prompt responses.  This
**	includes the prompt history function and file name completion.
**
**		The history macro keeps track of all the >1 character responses
**	made to prompts throughout the system.  They can then be retrieved
**	using the up and down arrow keys at the prompt.
**
**		The complete macro expands file names at the edit_file, read_file,
**	load_macro, and delete_macro prompts (as well as prompts that a
**	user has added to the list).	If a file name is ambiguous, it
**	pops up a menu of choices.
*/

#include "dialog.h"
#include "win_ctrl.h"

#define	DIRECTORY				0x10

/*
**		When doing file completion, BRIEF sorts the directory until
**	MAX_SORT_SECONDS has elapsed.  You can change this value to force BRIEF
**	to sort the entire directory (a high value), or to not sort at all (zero).
*/

#define	MAX_SORT_SECONDS		3

/*
**		_init:
**
**		When history is loaded, this macro creates the buffer that retains the
**	history information.  Note that this is a non-file system buffer.
*/

string	_last_response;		//	The last thing a user typed at a prompt.

int		_history_buf;			//	The buffer that contains the history info.

string 	pre_func; 				// The previous mouse menu function

void _init ()
{
	_history_buf = create_buffer ("History", NULL, 1);
	register_macro (5, "history_exit");
}

/*
**		history_exit:
**
**		This macro zeros the history buffer setting so, once exit has been
**	chosen (and confirmed), no further history processing is done.
*/

void history_exit ()
{
	_history_buf = 0;
}

/*
**		_prompt_begin:
**
**		This macro is called just before a prompt is displayed to the user.
**	It inserts the current prompt information to keep things straight, and
**	then attempts to locate previous history information in the buffer.	If
**	this information is found, the cursor is left there.	If not, the cursor
**	is left at the top of the file.
*/

string _prompt_begin (string)
{
	if (_history_buf)
		{
		int	old_buf = set_buffer (_history_buf),
				old_msg_level = set_msg_level (3);

		string	curr_prompt;

		save_position ();
		top_of_buffer ();
		get_parm (0, curr_prompt);
		insert (curr_prompt);
		beginning_of_line ();

		/*
		**		This pattern is used throughout the history processing code
		**	as it shouldn't occur to often in a real prompt.  In the first
		**	set of brackets is the name of the command, and in the second,
		**	the text of the prompt.  Each prompt of each command has a
		**	distinct 10 response history.
		*/

		search_fwd ("[" + inq_command () + "]::[" + curr_prompt + "]", 0);
		set_buffer (old_buf);
		set_msg_level (old_msg_level);
		}
	returns (inq_cmd_line ());
}

/*
**		_insert_sorted:
**
**		This macro inserts a filename into a list using a binary insertion.
**	It needs the upper and lower bounds of the area to search in.
**
**		Since directories are often in sorted order, we do an initial
**	probe at the bottom of the list to see if we can skip the insertion.
**	Although this adds an additional probe in the non-sorted case, it
**	significantly speeds up the sorted case.
*/

void _insert_sorted (string to_insert, int top, int bottom)
{
	if (top <= bottom)
		{
		string	line_text;

		move_abs (bottom, 1);
		line_text = read ();

		if (line_text != "\n" && to_insert < line_text)
			{
			int	middle_line;

			while (top < bottom)
				{
				middle_line = top + ((bottom - top) / 2);

				move_abs (middle_line, 1);

				if (to_insert < read ())
					bottom = middle_line - 1;
				else
					top = middle_line + 1;
				}
			move_abs (top, 1);
			line_text = read ();

			if (line_text != "\n" && to_insert > line_text)
				down ();
			}
		else if (line_text != "\n")
			down ();
		}
	else
		move_abs (top, 1);

	insert (to_insert + "\n");
}

/*
**		_insert_unsorted:
**
**		This routine inserts the passed item at the bottom of the list.  We use
**	this routine after a certain number of seconds has elapsed to ensure that
**	things don't take too long.
*/

void _insert_unsorted (string to_insert, int top, int bottom)
{
	if (top > bottom)
		move_abs (top, 1);
	else
		move_abs (bottom + 1, 1);

	insert (to_insert + "\n");
}


/*
**		_complete_edit_file, _complete_read_file,
**		_complete_load_macro, _complete_delete_macro:
**
**		These four macros provide "restricted" file name completion for
**	four common BRIEF functions.  Each restricts the displayed choices
**	appropriately for the type of function.
**
**		Note that these functions assume that you'll never edit an extensionless
**	file called "exe", "com", "cm" or "obj".
*/

_complete_edit_file (string file)
{
	switch (substr (file, rindex (file, ".") + 1))
		{
		case "exe":
		case "com":
		case "cm":
		case "obj":
			returns ("");

		default:
			returns (file);
		}
}

_complete_read_file (string file)
{
	returns (_complete_edit_file (file));
}

_complete_load_macro (string file)
{
	if (substr (file, rindex (file, ".") + 1) == "cm")
		returns (file);
	else
		returns ("");
}

_complete_delete_macro (string file)
{
	returns (_complete_load_macro (file));
}

/*
**		_bad_key:
**
**		This macro keeps tabs on the "illegal" keys pressed at the prompt.  If
**	the up or down arrows are pressed, we jump into action, moving around in
**	the history buffer.	If the bad key is <Tab>, we try to expand the filename.
*/

_bad_key ()
{
	int	key_pressed = read_char (),
			old_buf = inq_buffer (),
			old_msg_level = inq_msg_level ();

	string	command = inq_command ();

	/*
	**		If the bad key is a Tab, we check to see if we can complete
	**	the file name on the command line (if, indeed, the command
	**	is one of the ones we care about).
	*/

	if (key_pressed == key_to_int ("<Tab>"))
		{
		if (!inq_macro ("_complete_" + command))
			{
			push_back (key_pressed);
			returns (0);
			}
		else
			{
			int	loc,
					file_list_buf,
					attribute,
					fwd_slash,
					start_time,
					curr_time,
					hours,
					minutes,
					num_directories,
					num_files;

			string	file_spec,
						found_file,
						path;

			/*
			**		If BRIEF is using forward slashes in the path name rather
			**	than backslashes, or the user used forward slashes in his
			**	current prompt response, we use forward slashes too.
			*/

			inq_names (file_spec);
			fwd_slash = index (file_spec, "/") || index (inq_cmd_line (), "/");

			/*
			**		First, we separate the file_spec to be expanded into
			**	path and file parts.
			*/

			file_spec = trim (inq_cmd_line ());

			if (loc = search_string ("[:/\\\\][~:/\\\\]@>", file_spec, NULL, TRUE))
				{
				path = substr (file_spec, 1, loc);
				file_spec = substr (file_spec, loc + 1);
				}

			/*
			**		If there is no file name on the command line, or if
			**	there's just a base part with no extension, we look
			**	for files matching the base plus "*.*".  Otherwise, we
			**	add a single "*".  (If the user put wildcard
			**	characters into the file name, we just expand his
			**	specification.)
			**
			**		If the base part is eight characters, we just add
			**	".*":  OS/2 doesn't truncate match requests, so xxxxxxxx*.*
			**	won't match names beginning with xxxxxxxx (it's looking for
			**	more characters).
			*/

			if (!search_string ("[?*]", file_spec, NULL, TRUE))
				if (rindex (file_spec, "."))
					file_spec += "*";
				else
					file_spec += strlen (file_spec) >= 8 ? ".*" : "*.*";

			/*
			**		Get a list of matching files into a buffer.	Don't count
			**	directories.  When we're done, found_file contains the
			**	last name matched, and the cursor is one line past the last
			**	file name.
			*/

			set_buffer (file_list_buf = create_buffer ("Files", NULL, TRUE));
			delete_line ();
			file_pattern (file_spec = (path + file_spec));

			time (hours, minutes, start_time);
			start_time += (hours * 60) + (minutes * 60);

			while (find_file (found_file, NULL, NULL, NULL, attribute))
				if (found_file != ".")
					{
					found_file = lower (found_file);
					time (hours, minutes, curr_time);
					curr_time += (hours * 60) + (minutes * 60);

					if (attribute & DIRECTORY)
						{
						if (curr_time - start_time > MAX_SORT_SECONDS)
							_insert_unsorted (found_file + (fwd_slash ? "/" : "\\"), 1, num_directories);
						else
							_insert_sorted (found_file + (fwd_slash ? "/" : "\\"), 1, num_directories);

						num_directories++;
						}
					else if ((found_file = execute_macro ("_complete_" + command, found_file, file_spec)) != "")
						{
						if (curr_time - start_time > MAX_SORT_SECONDS)
							_insert_unsorted (found_file, num_directories + 1, num_directories + num_files);
						else
							_insert_sorted (found_file, num_directories + 1, num_directories + num_files);

						num_files++;
						}
					}
			num_files += num_directories;

			/*
			**		If we found exactly one match, update the command line.
			**
			**		If we found none, beep, but don't change the display.
			**
			**		If we found multiple matches, let the user choose some
			**	to return.	If he declines to choose, don't change the
			**	command line.	Note that we sometimes push back an <End>
			**	to prevent the command line being highlighted, sometimes
			**	an <Enter> to force the command to take the argument.
			**
			**		If the end result is a directory specifier, we push back
			**	a <Tab> to force expansion on the directory.
			*/

			if (num_files)
				{
				if (num_files == 1)
					{
					top_of_buffer ();
					path += trim (read ());
					push_back (key_to_int ("<End>"));
					}
				else
					{
					int	ty,
							by,
							multifile_keyboard,
							multifile = inq_macro ("_multifile_" + command);

					/*
					**		Compute the screen size of the menu.
					*/

					inq_screen_size (by);
					by -= 4;

					if ((ty = by - (num_files + 1)) <= 0)
						ty = 1;

					/*
					**		Turn messages off so we don't overwrite the
					**	current prompt.  Then pop up the file list.
					**	_process_menu will return the selected menu
					**	item in found_file.
					**
					**		Note that if there's a multifile macro for this
					**	command, we set up a local keyboard that allows
					**	multiple file selection.
					*/

					if (multifile)
						{
						keyboard_push ();
						assign_to_key ("<Space>", "_complete_select");
						assign_to_key ("*", "_complete_select_all");
						multifile_keyboard = inq_keyboard ();
						keyboard_pop (TRUE);
						use_local_keyboard (multifile_keyboard);
						}
					set_msg_level (3);
					set_buffer (old_buf);
					_process_menu (15, by, 30, ty, NULL, "", NULL, file_list_buf, "_complete_action", TRUE, NULL, found_file);
					set_msg_level (old_msg_level);
					set_buffer (file_list_buf);

					if (multifile)
						{
						set_buffer (old_buf);
						keyboard_push (multifile_keyboard);
						keyboard_pop ();
						set_buffer (file_list_buf);
						}
					found_file = trim (found_file);

					if (found_file != "")
						{
						if (search_string ("[/\\\\]", found_file, NULL, TRUE))
							{
							push_back (key_to_int ("<End>"));
							push_back (-1);
							push_back (key_to_int ("<Tab>"));
							}
						else
							{
							if (substr (found_file, 1, 1) == ">")
								found_file = substr (found_file, 2);

							num_files = 0;

							/*
							**		If there's a multifile macro for this command,
							**	then we do some more processing.
							*/

							if (multifile)
								{
								delete_line ();
								top_of_buffer ();

								while (!inq_position ())
									{
									if (read (1) != ">")
										delete_line ();
									else
										{
										num_files++;
										delete_char ();
										down ();
										}
									}
								if (num_files > 0)
									execute_macro ("_multifile_" + command, file_list_buf, path);
								}
							push_back (key_to_int ("<Enter>"));
							}
						path += found_file;
						}
					else
						{
						path = inq_cmd_line ();
						push_back (key_to_int ("<End>"));
						}
					}
				}
			else
				{
				beep ();
				path = inq_cmd_line ();
				push_back (key_to_int ("<End>"));
				}
			set_buffer (old_buf);

			/*
			**		We assume that if someone wants to keep the file_list_buf
			**	around, they'll set it to 0 using put_parm.  If it's non-zero,
			**	we assume it's OK to delete the buffer.
			*/

			if (file_list_buf)
				delete_buffer (file_list_buf);

			returns (path);
			}
		}

	/*
	**		If the bad key wasn't a Tab, we check to see if we have to
	**	to do history stuff.
	*/

	else if (!_history_buf)
		{
		push_back (key_pressed);
		returns (0);
		}
	else
		{
		int	line,
				pass;

		string	prompt;

		set_msg_level (3);
		set_buffer (_history_buf);
		inq_position (line);

		switch (key_pressed)
			{
			case key_to_int ("<Alt-l>"):
				prompt = _last_response;

			case key_to_int ("<Up>"):
			case key_to_int ("<Keypad-Up>"):
				{
				if (line != 1)
					while (pass < 2 && (!strlen (prompt) || prompt == inq_cmd_line ()))
						{
						down ();

						if (inq_position () || index (read (), "]::["))
							{
							pass++;
							search_back ("]::[", 0);
							down ();
							beginning_of_line ();
							}
						prompt = read ();
						prompt = substr (prompt, 1, strlen (prompt) - 1);
						}
				}
			case key_to_int ("<Down>"):
			case key_to_int ("<Keypad-Down>"):
				{
				if (line != 1)
					while (pass < 2 && (!strlen (prompt) || prompt == inq_cmd_line ()))
						{
						if (index (read (), "]::["))
							{
							down ();

							if (search_fwd ("]::[", 0))
								up ();
							else
								end_of_buffer ();

							beginning_of_line ();
							}
						else
							up ();

						prompt = read ();

						if (index (prompt, "]::["))
							{
							pass++;
							down ();

							if (search_fwd ("]::[", 0))
								up ();
							else
								end_of_buffer ();

							beginning_of_line ();
							prompt = read ();
							}
						prompt = substr (prompt, 1, strlen (prompt) - 1);
						}
				}
			}
		set_buffer (old_buf);
		set_msg_level (old_msg_level);

		if (strlen (prompt))
			returns (prompt);
		else
			{
			push_back (key_pressed);
			returns (0);
			}
		}
}

/*
**		_complete_action:
**
**		When the user presses <Enter> at the file list menu, mark the
**	selected file with a greater-than symbol and force an exit from
**	the menu.  The macro relies on the dialog manager's guarantee that
**	the cursor position when a button is picked will be the beginning
**	of the button line in the menu buffer in order to mark the button
**	in the least amount of time.
*/

int _complete_action (int event_type)
{
	switch (event_type)
		{
		case DIALOG_PICK_MENU:
			_dialog_esc ();

		case DIALOG_INIT:
			{
			/*
			**	We save the command assigned to <Tab> by assigning
			**	it to a key that's not used within menus.  This
			**	uses less memory than a global string.
			*/

			assign_to_key ("#255", inq_assignment ("<Tab>"));
			assign_to_key ("<Tab>", "_complete_dir");
			/* mouse_prompt will super class the previous menu handler */
			pre_func = inq_mouse_action();
			set_mouse_action( "mouse_prompt" );			// mouse event function
			}
		case DIALOG_TERM:
			{
			assign_to_key ("<Tab>", inq_assignment ("#255"));
			assign_to_key ("#255", "self_insert");
			}
		}
	returns (TRUE);
}

/*
**		_complete_dir:
**
**		If <Tab> is pressed while a directory is highlighted in the file
**	list, we immediately expand the list of files in that directory by
**	pretending <Enter> was pressed.
*/

void _complete_dir ()
{
	extern _dialog_menu_pick;

	if (search_string ("[/\\\\]", substr (read (), 2), NULL, TRUE))
		_dialog_menu_pick ();
	else
		beep ();
}

/*
**		_complete_select:
**
**		This function allows the user to select a group of files from the
**	list.  For each one selected, we toggle a selection character (currently
**	'>') on and off.  Note that you can't select directories with <Space>,
**	and the selected files are ignored if you have a group of selected files
**	and press <Enter> or <Tab> on a directory.
*/

void _complete_select ()
{
	if (search_string ("[/\\\\]", read (), NULL, TRUE))
		beep ();
	else if (read (1) == ">")
		delete_char ();
	else
		{
		insert (">");
		prev_char ();
		}
}

/*
**		_complete_select_all:
**
**		This function toggles selection of all the files in the list.
*/

void _complete_select_all ()
{
	save_position ();
	top_of_buffer ();

	while (!inq_position ())
		{
		if (!search_string ("[/\\\\]", read (), NULL, TRUE))
			_complete_select ();
 
		down ();
		}
	if (read (1) != ">")
		{
		top_of_buffer ();

		if (search_fwd ("<\\>"))
			{
			raise_anchor ();
			drop_anchor (3);
			restore_position (FALSE);
			}
		else
			restore_position ();
		}
	else
		restore_position ();
}

/*
**		_prompt_end:
**
**		After the prompting operation has been completed, this macro inserts the
**	response into the buffer if the prompt hasn't been cancelled and it's more
**	than one character long (we don't want to bother retaining [ynw] answers).
*/

int _prompt_end ()
{
	if (_history_buf)
		{
		int	old_buf,
				old_msg_level;

		string	response,
					prompt;

		old_msg_level = inq_msg_level ();
		set_msg_level (3);
		old_buf = inq_buffer ();
		set_buffer (_history_buf);
		top_of_buffer ();

		response = inq_cmd_line ();
		prompt = read ();
		prompt = substr (prompt, 1, strlen (prompt) - 1);
		delete_to_eol ();

		if (strlen (response) > 1)
			{
			_last_response = response;

			if ("Command cancelled." != inq_message ())
				{
				int	start_line,
						end_line;

				if (!search_fwd ("[" + inq_command () + "]::[" + prompt + "]", 0))
					{
					end_of_buffer ();
					down ();
					beginning_of_line ();
					insert ("[" + inq_command () + "]::[" + prompt + "]");
					}
				down ();
				inq_position (start_line);
				beginning_of_line ();
				insert (response + (inq_position () ? "" : "\n"));
				move_abs (start_line, 1);
				response = read ();

				if (search_fwd ("]::[", 0))
					up ();
				else
					end_of_buffer ();

				inq_position (end_line);

				if (end_line - start_line >= 10)
					delete_line ();

				move_abs (start_line, 1);
				down ();

				while (!inq_position () && !index (read (), "]::["))
					{
					if (response == read ())
						{
						delete_line ();
						break;
						}
					down ();
					}
				}
			}
		restore_position ();
		set_msg_level (old_msg_level);
		set_buffer (old_buf);
		}
	returns (0);
}

/*
**	mouse_prompt
**
**	super-class the default menu handler when a list of files is displayed
**
** This super classing makes the menu act more like a list box
**	
**	Mouse action assignments:
**   default menu actions plus
**	Dbl Click 1		- select and edit the file(s)
** Click 1			- select / unselect a file for editing same as space
** <Shift> Click 1  - toggle all selections     same as *
**																	
*/
void mouse_prompt	  (int event, 			// the event code
							int modifier,	  	// modifier keys
							int parm2,			// line or scroll bar code
							int parm3 )			// column or thumb position
{

	/* handle the events of interest */
	switch ( event ){
		case BTN1_DBLCLK:					// edit this file and all
			_dialog_menu_pick();		  	// previously selected files
		case BTN1_CLICK:					
			if ( modifier == KB_SHIFT ) {
				_complete_select_all();	// toggle all selections
			} else if ( modifier == 0 ) {
				_complete_select();		// select / unselect this file
			} else {
				execute_macro( pre_func, event, modifier, parm2, parm3 );
			}

		case BTN2_CLICK:			// exit the prompt window 
			exit();	
		default:
			/* let the previous event handler for menus process the event */
			execute_macro( pre_func, event, modifier, parm2, parm3 );
	}
}
