#!/usr/pkg/bin/python3.11

"""a filter that uses pexpect to send rlwrap filter messages through an external command"""

import sys
import os
import signal
import argparse
import pexpect
import time
import shlex

sys.path.append(os.environ['RLWRAP_FILTERDIR'])
import rlwrapfilter

test_without_filter_command = False # only for debuggging: make filter_command a NOP


########## parse filter arguments #################
parser = argparse.ArgumentParser(description='filter arguments:', add_help=False,
                     formatter_class=argparse.ArgumentDefaultsHelpFormatter)
parser.add_argument('--message_type', nargs='?', choices=['input', 'output', 'history', 'prompt', 'echo'],  
          help="message type to filter", default="output")                
parser.add_argument('--timeout', nargs='?', type=float, default = 0.5,
          help="timeout for <command> response")
parser.add_argument('--stick-around',
          help = "don't die immediately after error, so that rlwrap can show the error message",  action='store_true')
parser.add_argument('command_line', metavar='<command> [args...]', nargs=argparse.REMAINDER,
          help='external filter_command to turn into filter')
args = parser.parse_args()
command = args.command_line[0] if args.command_line else None


######### instantiate a RlwrapFilter ############
filter = rlwrapfilter.RlwrapFilter()
filter.help_text = """\
Usage:  rlwrap [-options] -z 'makefilter <command> [-options]' ...
convert <command> into rlwrap filter (e.g. to colourise output or censor history)
important: <command> *must* output exactly *one*  line for each input line.
(except possibly a startup message)
filter """ + parser.format_help() 

filter.minimal_rlwrap_version = 0.45 


########## spawn filter_command #################
filter_command = None
command_has_spoken = False

def handle_child_death(signum, frame):
    advice =  (f"\nConsider using 'makefilter --stick-around ...'  to see more error messages"
                if not args.stick_around and not command_has_spoken else "")
    filter.error(f"{args.command_line[0]} has died." + advice)            
        
signal.signal(signal.SIGCHLD, handle_child_death)

def spawn_filter_command():
    global filter_command
    command_line = args.command_line
    if args.stick_around:
        # replace "<command> <line>" by "sh -c '<command> <line>' || sleep 1"
        # in order to see messages at <command> death
        quoted_command_line  = list(map(lambda s : shlex.quote(s), args.command_line))
        quoted_command_line += ['||', 'sleep', '1']
        command_as_string = " ".join(quoted_command_line) 
        command_line = ["sh", "-c", command_as_string]
    filter_command= pexpect.spawn(command_line[0], command_line[1:], echo=False, timeout=args.timeout, encoding='utf-8')
    filter_command.delaybeforesend = None
    filter_command.timeout = 0.25
    # read welcome banner (or, more importantly: error message)
    welcome_or_errormessage = ""
    while True:
        try:
            welcome_or_errormessage += filter_command.readline()
        except pexpect.TIMEOUT:
            break
    if welcome_or_errormessage:
       filter.send_output_oob(welcome_or_errormessage)
    filter_command.timeout = args.timeout     
    #print(filter_command.__dict__)
    return filter_command


######### handlers #####################
@rlwrapfilter.intercept_error_with_message(f"Error in communication with {command}")
def pass_single_line_through_filter_command(line, strip=True):
   """
   pass one single (output, history, prompt,...) line through filter_command. 
   use strip = True for messages that don't end in CRNL, such as prompts, history and echo '''
   """
   global filter_command, command_has_spoken
   if test_without_filter_command:
       result = line
   else:
       if not filter_command:
          spawn_filter_command()
       try:
          filter_command.sendline(line)
          result = filter_command.readline()
          command_has_spoken =True
       except pexpect.TIMEOUT:
          filter.error(f"timeout readling from {filter_command}") 
       except pexpect.EOF as e:
          filter.error(f"EOF readling from {filter_command}") 
   return result.rstrip("\r\n") if strip else result

assembled_chunks = ""

def pass_chunks_through_filter_command_line_by_line(chunk):
   """
   pass chunk through filter_command, assembling and spliting multiple chunks into lines if needed 
   use global variable <assembled_chunks> to retain assembled chunks between invocation
   this should probably be done in rlwrapfilter eventually: filter.present_output_as_lines = True/False 
   """
   global assembled_chunks
   assembled_chunks += chunk 
   # chunk may contain zero, one or several CRNLs:
   lines   = assembled_chunks.split('\r\n');
   # keep last (possibly empty) non CRNL-terminated chunk, which may turn out to have been 
   # a prompt, in which case erase_assembled_chunks() will be called to forget about it, cf. below:
   assembled_chunks  = lines[-1] 
   output  = ""
   for line in lines[0:-1] :
      output += pass_single_line_through_filter_command(line, False)
   return output
 

def erase_assembled_chunks(prompt):
    """
    prompt_handler:  if we recognise a prompt, we need to clear assembled_chunks
    since they don't need to be treated as output anymore
    """
    global assembled_chunks
    assembled_chunks = ""    
    return prompt



############ determine which handler to actitvate ####################
if  args.message_type == 'output':
    filter.output_handler = pass_chunks_through_filter_command_line_by_line
    filter.prompt_handler = erase_assembled_chunks 
elif args.message_type == 'input':
    filter.input_handler = pass_single_line_through_filter_command
elif args.message_type == 'prompt':
    filter.prompt_handler = pass_single_line_through_filter_command
elif args.message_type == 'echo':
    filter.echo_handler = pass_single_line_through_filter_command
elif args.message_type == 'history':
    filter.history_handler = pass_single_line_through_filter_command


########### main loop: ###################

filter.run()
