/* Synchronet single-key console functions */

/****************************************************************************
 * @format.tab-size 4		(Plain Text/Source Code File Header)			*
 * @format.use-tabs true	(see http://www.synchro.net/ptsc_hdr.html)		*
 *																			*
 * Copyright Rob Swindell - http://www.synchro.net/copyright.html			*
 *																			*
 * 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 2			*
 * of the License, or (at your option) any later version.					*
 * See the GNU General Public License for more details: gpl.txt or			*
 * http://www.fsf.org/copyleft/gpl.html										*
 *																			*
 * For Synchronet coding style and modification guidelines, see				*
 * http://www.synchro.net/source.html										*
 *																			*
 * Note: If this box doesn't appear square, then you need to fix your tabs.	*
 ****************************************************************************/

#include "sbbs.h"
#include "telnet.h"	// TELNET_GA

/****************************************************************************/
/* Waits for remote or local user to hit a key. Inactivity timer is checked */
/* and hangs up if inactive for 4 minutes. Returns key hit, or uppercase of */
/* key hit if mode&K_UPPER or key out of KEY BUFFER. Does not print key.    */
/* Called from functions all over the place.                                */
/****************************************************************************/
char sbbs_t::getkey(int mode)
{
	uchar	ch, coldkey;
	uint	c = sbbs_random(5);
	time_t	last_telnet_cmd=0;
	char* cursor = text[SpinningCursor0  + sbbs_random(10)];
	size_t cursors = strlen(cursor);

	if(online==ON_REMOTE && !input_thread_running)
		online=FALSE;
	if(!online) {
		YIELD();	// just in case someone is looping on getkey() when they shouldn't
		return(0);
	}
	sys_status&=~SS_ABORT;
	if((sys_status&SS_USERON || action==NODE_DFLT) && !(mode&(K_GETSTR|K_NOSPIN)))
		mode|=(useron.misc&SPIN);
	lncntr=0;
	getkey_last_activity=time(NULL);
#if !defined SPINNING_CURSOR_OVER_HARDWARE_CURSOR
	if(mode&K_SPIN)
		outchar(' ');
#endif
	do {
		if(sys_status&SS_ABORT) {
			if(mode&K_SPIN) {
#if defined SPINNING_CURSOR_OVER_HARDWARE_CURSOR
				bputs(" \b");
#else
				backspace();
#endif
			}
			return(0); 
		}

		if(mode&K_SPIN) {
#if !defined SPINNING_CURSOR_OVER_HARDWARE_CURSOR
			outchar('\b');
#endif
			outchar(cursor[(c++) % cursors]);
#if defined SPINNING_CURSOR_OVER_HARDWARE_CURSOR
			outchar('\b');
#endif
		}
		ch=inkey(mode,mode&K_SPIN ? 200:1000);
		if(sys_status&SS_ABORT)
			return(0);
		now=time(NULL);
		if(ch) {
			if(mode&K_NUMBER && IS_PRINTABLE(ch) && !IS_DIGIT(ch))
				continue;
			if(mode&K_ALPHA && IS_PRINTABLE(ch) && !IS_ALPHA(ch))
				continue;
			if(mode&K_NOEXASC && ch&0x80)
				continue;
			if(mode&K_SPIN) {
#if defined SPINNING_CURSOR_OVER_HARDWARE_CURSOR
				bputs(" \b");
#else
				backspace();
#endif
			}
			if(mode&K_COLD && ch>' ' && useron.misc&COLDKEYS) {
				if(mode&K_UPPER)
					outchar(toupper(ch));
				else
					outchar(ch);
				while((coldkey=inkey(mode,1000))==0 && online && !(sys_status&SS_ABORT))
					;
				backspace();
				if(coldkey==BS || coldkey==DEL)
					continue;
				if(coldkey>' ')
					ungetkey(coldkey); 
			}
			if(mode&K_UPPER)
				return(toupper(ch));
			return(ch); 
		}
		if(sys_status&SS_USERON && !(sys_status&SS_LCHAT)) gettimeleft();
		else if(online && now-answertime>SEC_LOGON && !(sys_status&SS_LCHAT)) {
			console&=~(CON_R_ECHOX|CON_L_ECHOX);
			console|=(CON_R_ECHO|CON_L_ECHO);
			bputs(text[TakenTooLongToLogon]);
			hangup(); 
		}
		if(sys_status&SS_USERON && online && (timeleft/60)<(5-timeleft_warn)
			&& !SYSOP && !(sys_status&SS_LCHAT)) {
			timeleft_warn=5-(timeleft/60);
			saveline();
			attr(LIGHTGRAY);
			bprintf(text[OnlyXminutesLeft]
				,((ushort)timeleft/60)+1,(timeleft/60) ? "s" : nulstr);
			restoreline();
		}

		if(!(startup->options&BBS_OPT_NO_TELNET_GA)
			&& now!=last_telnet_cmd && now-getkey_last_activity>=60 && !((now-getkey_last_activity)%60)) {
			// Let's make sure the socket is up
			// Sending will trigger a socket d/c detection
			send_telnet_cmd(TELNET_GA,0);
			last_telnet_cmd=now;
		}

		time_t inactive = now - getkey_last_activity;
		if(online==ON_REMOTE && !(console&CON_NO_INACT)
			&& ((cfg.inactivity_warn && inactive >= cfg.max_getkey_inactivity * (cfg.inactivity_warn / 100.0))
				|| inactive >= cfg.max_getkey_inactivity)) {
			if(sys_status&SS_USERON && inactive < cfg.max_getkey_inactivity) {
				saveline();
				bputs(text[AreYouThere]); 
			}
			else
				bputs(text[InactivityAlert]);
			while(!inkey(K_NONE,100) && online && now-getkey_last_activity < cfg.max_getkey_inactivity) {
				now=time(NULL);
			}
			if(now-getkey_last_activity >= cfg.max_getkey_inactivity) {
				if(online==ON_REMOTE) {
					console|=CON_R_ECHO;
					console&=~CON_R_ECHOX; 
				}
				bputs(text[CallBackWhenYoureThere]);
				logline(LOG_NOTICE,nulstr,"Maximum user input inactivity exceeded");
				hangup();
				return(0); 
			}
			if(sys_status&SS_USERON) {
				attr(LIGHTGRAY);
				carriage_return();
				cleartoeol();
				restoreline(); 
			}
			getkey_last_activity=now; 
		}

	} while(online);

	return(0);
}


/****************************************************************************/
/* Outputs a string highlighting characters preceded by a tilde             */
/****************************************************************************/
void sbbs_t::mnemonics(const char *instr)
{
    const char *ctrl_a_codes;
    size_t l;

	if(!strchr(instr,'~')) {
		mnestr= instr;
		bputs(instr);
		return; 
	}
	ctrl_a_codes=strchr(instr,1);
	if(!ctrl_a_codes) {
		const char* last = lastchar(instr);
		if(instr[0] == '@' && *last == '@' && strchr(instr + 1, '@') == last && strchr(instr, ' ') == NULL) {
			mnestr= instr;
			bputs(instr);
			return; 
		}
		attr(cfg.color[clr_mnelow]); 
	}
	char str[256];
	expand_atcodes(instr, str, sizeof str);
	l=0L;
	int term = term_supports();

	while(str[l]) {
		if(str[l]=='~' && str[l+1] < ' ') {
			add_hotspot('\r', /* hungry: */true);
			l+=2;
		}
		else if(str[l]=='~') {
			if(!(term&(ANSI|PETSCII)))
				outchar('(');
			l++;
			if(!ctrl_a_codes)
				attr(cfg.color[clr_mnehigh]);
			add_hotspot(str[l], /* hungry: */true);
			outchar(str[l]);
			l++;
			if(!(term&(ANSI|PETSCII)))
				outchar(')');
			if(!ctrl_a_codes)
				attr(cfg.color[clr_mnelow]); 
		}
		else if(str[l]=='`' && str[l+1]!=0) {
			if(!(term&(ANSI|PETSCII)))
				outchar('[');
			l++;
			if(!ctrl_a_codes)
				attr(cfg.color[clr_mnehigh]);
			add_hotspot(str[l], /* hungry: */false);
			outchar(str[l]);
			l++;
			if(!(term&(ANSI|PETSCII)))
				outchar(']');
			if(!ctrl_a_codes)
				attr(cfg.color[clr_mnelow]); 
		}
		else {
			if(str[l]==CTRL_A && str[l+1]!=0) {
				l++;
				if(str[l] == 'Z')	/* EOF (uppercase 'Z') */
					break;
				ctrl_a(str[l++]);
			} else {
				outchar(str[l++]);
			}
		} 
	}
	if(!ctrl_a_codes)
		attr(cfg.color[clr_mnecmd]);
}

/****************************************************************************/
/* Prompts user for Y or N (yes or no) and CR is interpreted as a Y         */
/* Returns true for Yes or false for No                                     */
/* Called from quite a few places                                           */
/****************************************************************************/
bool sbbs_t::yesno(const char *str, int mode)
{
    char ch;

	if(*str == 0)
		return true;
	SAFECOPY(question,str);
	sync();
	bprintf(mode, text[YesNoQuestion], str);
	while(online) {
		if(sys_status&SS_ABORT)
			ch=no_key();
		else
			ch=getkey(K_UPPER|K_COLD);
		if(ch==yes_key() || ch==CR) {
			if(bputs(text[Yes], mode) && !(mode&P_NOCRLF))
				CRLF;
			if(!(mode&P_SAVEATR))
				attr(LIGHTGRAY);
			lncntr=0;
			return(true); 
		}
		if(ch==no_key()) {
			if(bputs(text[No], mode) && !(mode&P_NOCRLF))
				CRLF;
			if(!(mode&P_SAVEATR))
				attr(LIGHTGRAY);
			lncntr=0;
			return(false); 
		} 
	}
	return(true);
}

/****************************************************************************/
/* Prompts user for N or Y (no or yes) and CR is interpreted as a N         */
/* Returns true for No or false for Yes                                     */
/****************************************************************************/
bool sbbs_t::noyes(const char *str, int mode)
{
    char ch;

	if(*str == 0)
		return true;
	SAFECOPY(question,str);
	sync();
	bprintf(mode, text[NoYesQuestion], str);
	while(online) {
		if(sys_status&SS_ABORT)
			ch=no_key();
		else
			ch=getkey(K_UPPER|K_COLD);
		if(ch==no_key() || ch==CR) {
			if(bputs(text[No], mode) && !(mode&P_NOCRLF))
				CRLF;
			if(!(mode&P_SAVEATR))
				attr(LIGHTGRAY);
			lncntr=0;
			return(true); 
		}
		if(ch==yes_key()) {
			if(bputs(text[Yes], mode) && !(mode&P_NOCRLF))
				CRLF;
			if(!(mode&P_SAVEATR))
				attr(LIGHTGRAY);
			lncntr=0;
			return(false); 
		} 
	}
	return(true);
}

/****************************************************************************/
/* Waits for remote or local user to hit a key among 'keys'.				*/
/* If 'keys' is NULL, *any* non-numeric key is valid input.					*/
/* 'max' is non-zero, allow that a decimal number input up to that size		*/
/* and return the value OR'd with 0x80000000.								*/
/* default mode value is K_UPPER											*/
/****************************************************************************/
int sbbs_t::getkeys(const char *keys, uint max, int mode)
{
	char	str[81]{};
	uchar	ch,n=0,c=0;
	uint	i=0;

	if(keys != NULL) {
		SAFECOPY(str,keys);
	}
	while(online) {
		ch=getkey(mode);
		if(max && ch>0x7f)  /* extended ascii chars are digits to isdigit() */
			continue;
		if(sys_status&SS_ABORT) {   /* return -1 if Ctrl-C hit */
			if(!(mode&(K_NOECHO|K_NOCRLF))) {
				attr(LIGHTGRAY);
				CRLF;
			}
			lncntr=0;
			return(-1); 
		}
		if(ch && !n && ((keys == NULL && !IS_DIGIT(ch)) || (strchr(str,ch)))) {  /* return character if in string */
			if(ch > ' ') {
				if(!(mode&K_NOECHO))
					outchar(ch);
				if(useron.misc&COLDKEYS) {
					while(online && !(sys_status&SS_ABORT)) {
						c=getkey(0);
						if(c==CR || c==BS || c==DEL)
							break; 
					}
					if(sys_status&SS_ABORT) {
						if(!(mode&(K_NOECHO|K_NOCRLF))) {
							CRLF;
						}
						return(-1); 
					}
					if(c==BS || c==DEL) {
						if(!(mode&K_NOECHO))
							backspace();
						continue; 
					} 
				}
				if(!(mode&(K_NOECHO|K_NOCRLF))) {
					attr(LIGHTGRAY);
					CRLF;
				}
				lncntr=0;
			}
			return(ch); 
		}
		if(ch==CR && max) {             /* return 0 if no number */
			if(!(mode&(K_NOECHO|K_NOCRLF))) {
				attr(LIGHTGRAY);
				CRLF;
			}
			lncntr=0;
			if(n)
				return(i|0x80000000L);		 /* return number plus high bit */
			return(0); 
		}
		if((ch==BS || ch==DEL) && n) {
			if(!(mode&K_NOECHO))
				backspace();
			i/=10;
			n--; 
		}
		else if(max && IS_DIGIT(ch) && (i*10)+(ch&0xf)<=max && (ch!='0' || n)) {
			i*=10;
			n++;
			i+=ch&0xf;
			if(!(mode&K_NOECHO))	
				outchar(ch);
			if(i*10>max && !(useron.misc&COLDKEYS) && keybuf_level() < 1) {
				if(!(mode&(K_NOECHO|K_NOCRLF))) {
					attr(LIGHTGRAY);
					CRLF;
				}
				lncntr=0;
				return(i|0x80000000L); 
			} 
		} 
	}
	return(-1);
}

/****************************************************************************/
/* Prints PAUSE message and waits for a key stoke                           */
/* Returns false if aborted by user											*/
/****************************************************************************/
bool sbbs_t::pause(bool set_abort)
{
	char	ch;
	uint	tempattrs=curatr; /* was lclatr(-1) */
	int		l=K_UPPER;
	size_t	len;

 	if((sys_status&SS_ABORT) || pause_inside)
		return false;
	pause_inside = true;
	lncntr=0;
	if(online==ON_REMOTE)
		rioctl(IOFI);
	if(mouse_hotspots.first == NULL)
		pause_hotspot = add_hotspot('\r');
	bputs(text[Pause]);
	len = bstrlen(text[Pause]);
	if(sys_status&SS_USERON && !(useron.misc&(HTML|WIP|NOPAUSESPIN))
		&& cfg.spinning_pause_prompt)
		l|=K_SPIN;
	ch=getkey(l);
	if(pause_hotspot) {
		clear_hotspots();
		pause_hotspot = NULL;
	}
	bool aborted = (ch==no_key() || ch==quit_key() || (sys_status & SS_ABORT));
	if(set_abort && aborted)
		sys_status|=SS_ABORT;
	else if(ch==LF)	// down arrow == display one more line
		lncntr=rows-2;
	if(text[Pause][0]!='@')
		backspace(len);
	getnodedat(cfg.node_num,&thisnode,0);
	nodesync();
	attr(tempattrs);
	pause_inside = false;
	return !aborted;
}

/****************************************************************************/
/* Puts a character into the input buffer                                   */
/****************************************************************************/
void sbbs_t::ungetkey(char ch, bool insert)
{
	char dbg[2] = {};
#if 0	/* this way breaks ansi_getxy() */
	RingBufWrite(&inbuf,(uchar*)&ch,sizeof(uchar));
#else
	if(keybuf_space()) {
		char* p = c_escape_char(ch);
		if(p == NULL) {
			dbg[0] = ch;
			p = dbg;
		}
		lprintf(LOG_DEBUG, "%s key into keybuf: %02X (%s)", insert ? "insert" : "append", ch, p);
		if(insert) {
			if(keybufbot == 0)
				keybufbot = KEY_BUFSIZE - 1;
			else
				keybufbot--;
			keybuf[keybufbot] = ch;
		} else {
			keybuf[keybuftop++]=ch;
			if(keybuftop==KEY_BUFSIZE)
				keybuftop=0;
		}
	} else
		lprintf(LOG_WARNING, "No space in keyboard input buffer");
#endif
}

/****************************************************************************/
/* Puts a string into the input buffer										*/
/****************************************************************************/
void sbbs_t::ungetstr(const char* str, bool insert)
{
	size_t i;

	for(i = 0; str[i] != '\0'; i++)
		ungetkey(str[i], insert);
}

size_t sbbs_t::keybuf_space(void)
{
	return sizeof(keybuf) - (keybuf_level() + 1);
}

size_t sbbs_t::keybuf_level(void)
{
	if(keybufbot > keybuftop)
		return (sizeof(keybuf) - keybufbot) + keybuftop;
	return keybuftop - keybufbot;
}