/*
 * ========================================================================
 * Copyright 2006-2008 University of Washington
 * Copyright 2013-2022 Eduardo Chappa
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * ========================================================================
 */
#include <system.h>
#include <general.h>

#include "../../c-client/mail.h"	/* for MAILSTREAM and friends */
#include "../../c-client/osdep.h"
#include "../../c-client/rfc822.h"	/* for soutr_t and such */
#include "../../c-client/misc.h"	/* for cpystr proto */
#include "../../c-client/utf8.h"	/* for CHARSET and such*/
#include "../../c-client/imap4r1.h"

#include "../../pith/charconv/utf8.h"
#include "../../pith/charconv/filesys.h"

#include "../../pith/osdep/color.h"
#include "../../pith/osdep/collate.h"
#include "../../pith/osdep/err_desc.h"

#include "../../pith/debug.h"
#include "../../pith/state.h"
#include "../../pith/conf.h"
#include "../../pith/detach.h"
#include "../../pith/adrbklib.h"
#include "../../pith/remote.h"
#include "../../pith/imap.h"
#include "../../pith/status.h"

#include "../pico/estruct.h"

#include "../../pico/estruct.h"
#include "../../pico/pico.h"
#include "../../pico/osdep/raw.h"
#include "../../pico/osdep/signals.h"
#include "../../pico/osdep/mouse.h"
#include "../../pico/osdep/read.h"
#include "../../pico/osdep/getkey.h"
#include "../../pico/osdep/tty.h"
#include "../../pico/keydefs.h"

#include "../talk.h"
#include "../radio.h"
#include "../dispfilt.h"
#include "../signal.h"
#include "../mailcmd.h"
#include "../setup.h"

#include "termin.gen.h"
#include "termout.gen.h"
#include "termin.unx.h"



/*======================================================================
       Things having to do with reading from the tty driver and keyboard
          - initialize tty driver and reset tty driver
          - read a character from terminal with keyboard escape sequence mapping
          - initialize keyboard (keypad or such) and reset keyboard
          - prompt user for a line of input
          - read a command from keyboard with timeouts.

 ====*/


/*
 * Helpful definitions
 */
/*
 * Should really be using pico's TERM's t_getchar to read a character but
 * we're just calling ttgetc directly for now. Ttgetc is the same as
 * t_getchar whenever we use it so we're avoiding the trouble of initializing
 * the TERM struct and calling ttgetc directly.
 */
#define READ_A_CHAR()	ttgetc(NO_OP_COMMAND, key_rec, read_bail)


/*
 * Internal prototypes
 */
int           pine_simple_ttgetc(int (*recorder)(int), void (*bail_handler)(void));
UCS           check_for_timeout(int);
void	      read_bail(void);


/*----------------------------------------------------------------------
    Initialize the tty driver to do single char I/O and whatever else  (UNIX)

   Args:  struct pine

 Result: tty driver is put in raw mode so characters can be read one
         at a time. Returns -1 if unsuccessful, 0 if successful.

Some file descriptor voodoo to allow for pipes across vforks. See 
open_mailer for details.
  ----------------------------------------------------------------------*/
int
init_tty_driver(struct pine *ps)
{
   if(ps->send_immediately)
     return 0;
#ifdef	MOUSE
    if(F_ON(F_ENABLE_MOUSE, ps_global))
      init_mouse();
#endif	/* MOUSE */

    /* turn off talk permission by default */
    
    if(F_ON(F_ALLOW_TALK, ps))
      allow_talk(ps);
    else
      disallow_talk(ps);

    return(PineRaw(1));
}



/*----------------------------------------------------------------------
   Set or clear the specified tty mode

   Args: ps --  struct pine
	 mode -- mode bits to modify
	 clear -- whether or not to clear or set

 Result: tty driver mode change. 
  ----------------------------------------------------------------------*/
void
tty_chmod(struct pine *ps, int mode, int func)
{
    char	*tty_name;
    int		 new_mode;
    struct stat  sbuf;
    static int   saved_mode = -1;

    /* if no problem figuring out tty's name & mode? */
    if((((tty_name = (char *) ttyname(STDIN_FD)) != NULL
	 && fstat(STDIN_FD, &sbuf) == 0)
	|| ((tty_name = (char *) ttyname(STDOUT_FD)) != NULL
	    && fstat(STDOUT_FD, &sbuf) == 0))
       && !(func == TMD_RESET && saved_mode < 0)){
	new_mode = (func == TMD_RESET)
		     ? saved_mode
		     : (func == TMD_CLEAR)
			? (sbuf.st_mode & ~mode)
			: (sbuf.st_mode | mode);
	/* assign tty new mode */
	if(our_chmod(tty_name, new_mode) == 0){
	    if(func == TMD_RESET)		/* forget we knew */
	      saved_mode = -1;
	    else if(saved_mode < 0)
	      saved_mode = sbuf.st_mode;	/* remember original */
	}
    }
}



/*----------------------------------------------------------------------
       End use of the tty, put it back into it's normal mode     (UNIX)

   Args: ps --  struct pine

 Result: tty driver mode change. 
  ----------------------------------------------------------------------*/
void
end_tty_driver(struct pine *ps)
{
    ps = ps; /* get rid of unused parameter warning */

#ifdef	MOUSE
    end_mouse();
#endif	/* MOUSE */
    fflush(stdout);
    dprint((2, "about to end_tty_driver\n"));

    tty_chmod(ps, 0, TMD_RESET);
    PineRaw(0);
}



/*----------------------------------------------------------------------
    Actually set up the tty driver                             (UNIX)

   Args: state -- which state to put it in. 1 means go into raw, 0 out of

  Result: returns 0 if successful and < 0 if not.
  ----*/
int
PineRaw(int state)
{
    int result;

    result = Raw(state);
    
    if(result == 0 && state == 1){
	/*
	 * Only go into 8 bit mode if we are doing something other
	 * than plain ASCII. This will save the folks that have
	 * their parity on their serial lines wrong the trouble of
	 * getting it right
	 */
        if((ps_global->keyboard_charmap && ps_global->keyboard_charmap[0] &&
	   strucmp(ps_global->keyboard_charmap, "us-ascii"))
	   || (ps_global->display_charmap && ps_global->display_charmap[0] &&
	       strucmp(ps_global->display_charmap, "us-ascii")))
	  bit_strip_off();

#ifdef	DEBUG
	if(debug < 9)			/* only on if full debugging set */
#endif
	quit_char_off();
	ps_global->low_speed = ttisslow();
	crlf_proc(0);
	xonxoff_proc(F_ON(F_PRESERVE_START_STOP, ps_global));
    }

    return(result);
}


#if	defined(SIGWINCH) && defined(TIOCGWINSZ)
jmp_buf winch_state;
int     winch_occured = 0;
int     ready_for_winch = 0;
#endif

/*----------------------------------------------------------------------
     This checks whether or not a character			(UNIX)
     is ready to be read, or it times out.

    Args:  time_out --  number of seconds before it will timeout

  Result: Returns a NO_OP_IDLE or a NO_OP_COMMAND if the timeout expires
	  before input is available, or a KEY_RESIZE if a resize event
	  occurs, or READY_TO_READ if input is available before the timeout.
  ----*/
UCS
check_for_timeout(int time_out)
{
    UCS res = NO_OP_COMMAND;

    fflush(stdout);

#if	defined(SIGWINCH) && defined(TIOCGWINSZ)
    if(!winch_occured){
	if(setjmp(winch_state) != 0){
	    winch_occured = 1;
	    ready_for_winch = 0;

	    /*
	     * Need to unblock signal after longjmp from handler, because
	     * signal is normally unblocked upon routine exit from the handler.
	     */
	    our_sigunblock(SIGWINCH);
	}
	else
	  ready_for_winch = 1;
    }

    if(winch_occured){
	winch_occured = ready_for_winch = 0;
	fix_windsize(ps_global);
	return(KEY_RESIZE);
    }
#endif /* SIGWINCH */

    switch(res = input_ready(time_out)){
      case BAIL_OUT:
	read_bail();			/* non-tragic exit */
	/* NO RETURN */

      case PANIC_NOW:
	panic1("Select error: %s\n", error_description(errno));
	/* NO RETURN */

      case READ_INTR:
	res = NO_OP_COMMAND;
	/* fall through */

      case NO_OP_IDLE:
      case NO_OP_COMMAND:
      case READY_TO_READ:
#if	defined(SIGWINCH) && defined(TIOCGWINSZ)
	ready_for_winch = 0;
#endif
	return(res);
    }

    /* not reachable */
    return(res);
}



/*----------------------------------------------------------------------
  Read input characters with lots of processing for arrow keys and such  (UNIX)

 Args:  time_out -- The timeout to for the reads 

 Result: returns the character read. Possible special chars.

    This deals with function and arrow keys as well. 

  The idea is that this routine handles all escape codes so it done in
  only one place. Especially so the back arrow key can work when entering
  things on a line. Also so all function keys can be disabled and not
  cause weird things to happen.
  ---*/
UCS
read_char(int time_out)
{
    UCS status, cc, ch;
    int (*key_rec)(int);

    key_rec = key_recorder;
    if(ps_global->conceal_sensitive_debugging)
      key_rec = NULL;

    /* Get input from initial-keystrokes */
    if(process_config_input(&cc)){
	ch = cc;
	return(ch);
    }

    if((ch = check_for_timeout(time_out)) != READY_TO_READ)
      goto done;
    
    switch(status = kbseq(pine_simple_ttgetc, key_rec, read_bail,
			  ps_global->input_cs, &ch)){
      case KEY_DOUBLE_ESC:
	/*
	 * Special hack to get around comm devices eating control characters.
	 */
	if(check_for_timeout(5) != READY_TO_READ){
	    dprint((9, "Read char: incomplete double escape timed out...\n"));
	    ch = KEY_JUNK;		/* user typed ESC ESC, then stopped */
	    goto done;
	}
	else
	  ch = READ_A_CHAR();

	ch &= 0x7f;

	/* We allow a 3-digit number between 001 and 255 */
	if(isdigit((unsigned char) ch)){
	    int n = 0, i = ch - '0';

	    if(i < 0 || i > 2){
		dprint((9, "Read char: double escape followed by 1st digit not 0, 1, or 2... (%d)\n", i));
		ch = KEY_JUNK;
		goto done;		/* bogus literal char value */
	    }

	    while(n++ < 2){
		if(check_for_timeout(5) != READY_TO_READ
		   || (!isdigit((unsigned char) (ch = READ_A_CHAR()))
		       || (n == 1 && i == 2 && ch > '5')
		       || (n == 2 && i == 25 && ch > '5'))){
		    dprint((9, "Read char: bad double escape, either timed out or too large 3-digit num...\n"));
		    ch = KEY_JUNK;	/* user typed ESC ESC #, stopped */
		    goto done;
		}

		i = (i * 10) + (ch - '0');
	    }

	    ch = i;
	}
	else{	/* or, normal case, ESC ESC c  means ^c */
	    if(islower((unsigned char) ch))	/* canonicalize if alpha */
	      ch = toupper((unsigned char) ch);

	    ch = (isalpha((unsigned char)ch) || ch == '@'
		  || (ch >= '[' && ch <= '_'))
		   ? ctrl(ch) : ((ch == SPACE) ? ctrl('@'): ch);
	    dprint((9, "Read char: this is a successful double escape...\n"));
	}

	goto done;

#ifdef MOUSE
      case KEY_XTERM_MOUSE:
	if(mouseexist()){
	    /*
	     * Special hack to get mouse events from an xterm.
	     * Get the details, then pass it past the keymenu event
	     * handler, and then to the installed handler if there
	     * is one...
	     */
	    static int    down = 0;
	    int           x, y, button;
	    unsigned long cmd;

	    clear_cursor_pos();
	    button = READ_A_CHAR() & 0x03;

	    x = READ_A_CHAR() - '!';
	    y = READ_A_CHAR() - '!';

	    ch = NO_OP_COMMAND;
	    if(button == 0){		/* xterm button 1 down */
		down = 1;
		if(checkmouse(&cmd, 1, x, y))
		  ch = cmd;
	    }
	    else if (down && button == 3){
		down = 0;
		if(checkmouse(&cmd, 0, x, y))
		  ch = cmd;
	    }

	    goto done;
	}

	break;
#endif /* MOUSE */

      case  KEY_UP	:
      case  KEY_DOWN	:
      case  KEY_RIGHT	:
      case  KEY_LEFT	:
      case  KEY_PGUP	:
      case  KEY_PGDN	:
      case  KEY_HOME	:
      case  KEY_END	:
      case  KEY_DEL	:
      case  PF1		:
      case  PF2		:
      case  PF3		:
      case  PF4		:
      case  PF5		:
      case  PF6		:
      case  PF7		:
      case  PF8		:
      case  PF9		:
      case  PF10	:
      case  PF11	:
      case  PF12	:
        dprint((9, "Read char returning: 0x%x %s\n", status, pretty_command(status)));
	return(status);

      case  CTRL_KEY_UP	:
	status = KEY_UP;
        dprint((9, "Read char returning: 0x%x %s (for CTRL_KEY_UP)\n", status, pretty_command(status)));
	return(status);

      case  CTRL_KEY_DOWN	:
	status = KEY_DOWN;
        dprint((9, "Read char returning: 0x%x %s (for CTRL_KEY_DOWN)\n", status, pretty_command(status)));
	return(status);

      case  CTRL_KEY_RIGHT	:
	status = KEY_RIGHT;
        dprint((9, "Read char returning: 0x%x %s (for CTRL_KEY_RIGHT)\n", status, pretty_command(status)));
	return(status);

      case  CTRL_KEY_LEFT	:
	status = KEY_LEFT;
        dprint((9, "Read char returning: 0x%x %s (for CTRL_KEY_LEFT)\n", status, pretty_command(status)));
	return(status);

      case KEY_SWALLOW_Z:
	status = KEY_JUNK;
      case KEY_SWAL_UP:
      case KEY_SWAL_DOWN:
      case KEY_SWAL_LEFT:
      case KEY_SWAL_RIGHT:
	do
	  if(check_for_timeout(2) != READY_TO_READ){
	      status = KEY_JUNK;
	      break;
	  }
	while(!strchr("~qz", READ_A_CHAR()));
	ch = (status == KEY_JUNK) ? status : status - (KEY_SWAL_UP - KEY_UP);
	goto done;

      case KEY_KERMIT:
	do{
	    cc = ch;
	    if(check_for_timeout(2) != READY_TO_READ){
		status = KEY_JUNK;
		break;
	    }
	    else
	      ch = READ_A_CHAR();
	}while(cc != '\033' && ch != '\\');

	ch = KEY_JUNK;
	goto done;

      case BADESC:
	ch = KEY_JUNK;
	goto done;

      case 0: 	/* regular character */
      default:
	/*
	 * we used to strip (ch &= 0x7f;), but this seems much cleaner
	 * in the face of line noise and has the benefit of making it
	 * tougher to emit mistakenly labeled MIME...
	 */
	if((ch & ~0x7f)
	   && ((!ps_global->keyboard_charmap || !strucmp(ps_global->keyboard_charmap, "US-ASCII"))
	       && (!ps_global->display_charmap || !strucmp(ps_global->display_charmap, "US-ASCII")))){
	    dprint((9, "Read char sees ch = 0x%x status=0x%x, returns KEY_JUNK\n", ch, status));
	    return(KEY_JUNK);
	}
	else if(ch == ctrl('Z')){
	    dprint((9, "Read char got ^Z, calling do_suspend\n"));
	    ch = do_suspend();
	    dprint((9, "After do_suspend Read char returns 0x%x %s\n", ch, pretty_command(ch)));
	    return(ch);
	}
#ifdef MOUSE
	else if(ch == ctrl('\\')){
	    int e;

	    dprint((9, "Read char got ^\\, toggle xterm mouse\n"));
	    if(F_ON(F_ENABLE_MOUSE, ps_global)){
		(e=mouseexist()) ? end_mouse() : (void) init_mouse();
		if(e != mouseexist())
		  q_status_message1(SM_ASYNC, 0, 2, "Xterm mouse tracking %s!",
						     mouseexist() ? "on" : "off");
		else if(!e)
		  q_status_message1(SM_ASYNC, 0, 2, "See help for feature \"%s\" ($DISPLAY variable set?)", pretty_feature_name(feature_list_name(F_ENABLE_MOUSE), -1));
	    }
	    else
	      q_status_message1(SM_ASYNC, 0, 2, "Feature \"%s\" not enabled",
				pretty_feature_name(feature_list_name(F_ENABLE_MOUSE), -1));

	    return(NO_OP_COMMAND);
	}
#endif /* MOUSE */


      done:
#ifdef	DEBUG
	if(ps_global->conceal_sensitive_debugging && debug < 10){
	    dprint((9, "Read char returning: <hidden char>\n"));
	}
	else{
	    dprint((9, "Read char returning: 0x%x %s\n", ch, pretty_command(ch)));
	}
#endif

        return(ch);
    }

    /* not reachable */
    return(KEY_JUNK);
}


/*----------------------------------------------------------------------
  Reading input somehow failed and we need to shutdown now

 Args:  none

 Result: pine exits

  ---*/
void
read_bail(void)
{
    dprint((1, "read_bail: cleaning up\n"));

    /* Do not bail out on a tcp timeout, instead close the troublesome stream */
    if(ps_global->tcptimeout && some_stream_is_locked()){
      ps_global->read_bail = 1;
      return;
    }
    end_signals(1);

    /*
     * This gets rid of temporary cache files for remote addrbooks.
     */
    completely_done_with_adrbks();

    /*
     * This flushes out deferred changes and gets rid of temporary cache
     * files for remote config files.
     */
    if(ps_global->prc){
	if(ps_global->prc->outstanding_pinerc_changes)
	  write_pinerc(ps_global, Main, WRP_NOUSER);

	if(ps_global->prc->rd)
	  rd_close_remdata(&ps_global->prc->rd);
	
	free_pinerc_s(&ps_global->prc);
    }

    /* as does this */
    if(ps_global->post_prc){
	if(ps_global->post_prc->outstanding_pinerc_changes)
	  write_pinerc(ps_global, Post, WRP_NOUSER);

	if(ps_global->post_prc->rd)
	  rd_close_remdata(&ps_global->post_prc->rd);
	
	free_pinerc_s(&ps_global->post_prc);
    }

    sp_end();

    dprint((1, "done with read_bail clean up\n"));

    imap_flush_passwd_cache(TRUE);
    end_keyboard(F_ON(F_USE_FK,ps_global));
    end_tty_driver(ps_global);
    if(filter_data_file(0))
      our_unlink(filter_data_file(0));

    exit(0);
}


int
pine_simple_ttgetc(int (*fi)(int), void (*fv)(void))
{
    int ch;

#if	defined(SIGWINCH) && defined(TIOCGWINSZ)
    if(!winch_occured){
	if(setjmp(winch_state) != 0){
	    winch_occured = 1;
	    ready_for_winch = 0;

	    /*
	     * Need to unblock signal after longjmp from handler, because
	     * signal is normally unblocked upon routine exit from the handler.
	     */
	    our_sigunblock(SIGWINCH);
	}
	else
	  ready_for_winch = 1;
    }

    if(winch_occured){
	winch_occured = ready_for_winch = 0;
	fix_windsize(ps_global);
	return(KEY_RESIZE);
    }
#endif /* SIGWINCH */

    ch = simple_ttgetc(fi, fv);

#if	defined(SIGWINCH) && defined(TIOCGWINSZ)
    ready_for_winch = 0;
#endif

    return(ch);
}



extern char term_name[];
/* -------------------------------------------------------------------
     Set up the keyboard -- usually enable some function keys  (UNIX)

    Args: struct pine 

So far all we do here is turn on keypad mode for certain terminals

Hack for NCSA telnet on an IBM PC to put the keypad in the right mode.
This is the same for a vtXXX terminal or [zh][12]9's which we have 
a lot of at UW
  ----*/
void
init_keyboard(int use_fkeys)
{
    if (ps_global->send_immediately)
	return;

    if(use_fkeys && (!strucmp(term_name,"vt102")
		     || !strucmp(term_name,"vt100")))
      printf("\033\133\071\071\150");
}



/*----------------------------------------------------------------------
     Clear keyboard, usually disable some function keys           (UNIX)

   Args:  pine state (terminal type)

 Result: keyboard state reset
  ----*/
void
end_keyboard(int use_fkeys)
{
    if(ps_global->send_immediately)
	return;

    if(use_fkeys && (!strcmp(term_name, "vt102")
		     || !strcmp(term_name, "vt100"))){
	printf("\033\133\071\071\154");
	fflush(stdout);
    }
}


/*
 * This is a bare-bones implementation which is missing most of the
 * features of the real optionally_enter. The initial value of string is
 * isgnored. The escape_list is ignored. The help is not implemented. The
 * only flag implemented is OE_PASSWD. We don't go into raw mode so the
 * only input possible is a line (the EOL is stripped before returning).
 */
int
pre_screen_config_opt_enter(char *string, int string_size, char *prompt,
			    ESCKEY_S *escape_list, HelpType help, int *flags)
{
    char *pw;
    int   return_v = -10;

    while(return_v == -10) {

	if(flags && (*flags & (OE_PASSWD | OE_PASSWD_NOAST))){
	    if((pw = getpass(prompt)) != NULL){
		if(strlen(pw) < string_size){
		    strncpy(string, pw, string_size);
		    string[string_size-1] = '\0';
		    return_v = 0;
		}
		else{
		    if(fputs("Password too long\n", stderr) != EOF)
		       return_v = -1;
		    else
		       alpine_panic(_("error on fputs() call!"));
		}
	    }
	    else
	      return_v = 1;	/* cancel? */
	}
	else{
	    char *p;

	    if(fputs(prompt, stdout) != EOF
		&& fgets(string, string_size, stdin) != NULL){
		string[string_size-1] = '\0';
		if((p = strpbrk(string, "\r\n")) != NULL)
		  *p = '\0';
		return_v = 0;
	    }
	    else
		alpine_panic(_("error on fputs() or fgets() call!"));
	}
    }

    return(return_v);
}


