#charset "us-ascii"

/* Copyright (c) 2000, 2002 Michael J. Roberts.  All Rights Reserved. */
/*
 *   TADS 3 Library: input
 *   
 *   This modules defines functions and objects related to reading input
 *   from the player.  
 */

#include "adv3.h"


/* ------------------------------------------------------------------------ */
/*
 *   Keyboard input parameter definition. 
 */
class InputDef: object
    /* 
     *   The prompt function.  This is a function pointer (which is
     *   frequently given as an anonymous function) or nil; if it's nil,
     *   we won't show any prompt at all, otherwise we'll call the
     *   function pointer to display a prompt as needed. 
     */
    promptFunc = nil

    /* 
     *   Allow real-time events.  If this is true, we'll allow real-time
     *   events to interrupt the input; if it's nil, we'll freeze the
     *   real-time clock while reading input.  
     */
    allowRealTime = nil

    /* 
     *   Begin the input style.  This should do anything required to set
     *   the font to the desired attributes for the input text.  By
     *   default, we'll simply display <.inputline> to set up the default
     *   input style.  
     */
    beginInputFont() { "<.inputline>"; }

    /* 
     *   End the input style.  By default, we'll close the <.inputline>
     *   that we opened in beginInputFont(). 
     */
    endInputFont() { "<./inputline>"; }
;

/*
 *   Basic keyboard input parameter definition.  This class defines
 *   keyboard input parameters with the real-time status and prompt
 *   function specified via the constructor.  
 */
class BasicInputDef: InputDef
    construct(allowRealTime, promptFunc)
    {
        self.allowRealTime = allowRealTime;
        self.promptFunc = promptFunc;
    }
;


/* ------------------------------------------------------------------------ */
/*
 *   Keyboard input manager. 
 */
inputManager: PostRestoreObject
    /*
     *   Read a line of input from the keyboard.
     *   
     *   If allowRealTime is true, we'll execute any real-time events that
     *   are already due to run, and then we'll allow the input to be
     *   interrupted by real-time events, if interrupted input is
     *   supported on the local platform.  Otherwise, we will not process
     *   any real-time events.
     *   
     *   promptFunc is a callback function to invoke to display the
     *   prompt.  This is provided as a callback so that we can re-display
     *   the prompt as necessary after real-time event interruptions.
     *   Note that if real-time interruption is not to be allowed, the
     *   caller can simply display the prompt before calling this routine
     *   rather than passing in a prompt callback, if desired.
     *   
     *   If we're in HTML mode, this will switch into the 'tads-input'
     *   font while reading the line, so this routine should be used
     *   wherever possible rather than calling inputLine() or
     *   inputLineTimeout() directly.  
     */
    getInputLine(allowRealTime, promptFunc)
    {
        /* read input using a basic InputDef for the given parameters */
        return getInputLineExt(new BasicInputDef(allowRealTime, promptFunc));
    }

    /*
     *   Read a line of input from the keyboard - extended interface,
     *   using the InputDef object to define the input parameters.
     *   'defObj' is an instance of class InputDef, defining how we're to
     *   handle the input.  
     */
    getInputLineExt(defObj)
    {
        /* make sure the command transcript is flushed */
        if (gTranscript != nil)
            gTranscript.flushForInput();
        
        /* 
         *   If a previous input was in progress, cancel it - this must be
         *   a recursive entry from a real-time event that's interrupting
         *   the enclosing input attempt. Simply cancel out the enclosing
         *   read attempt entirely in this case; if and when we return to
         *   the enclosing reader, that reader will start over with a
         *   fresh read attempt at that point.  
         */
        cancelInputInProgress(true);
        
        /* 
         *   Keep going until we finish reading the command.  We might
         *   have to try several times, because our attempts might be
         *   interrupted by real-time events. 
         */
        for (;;)
        {
            local result;
            local timeout;
            local t0;

            /* note the starting time, in case we want to freeze the clock */
            t0 = realTimeManager.getElapsedTime();

            /* process real-time events, if possible */
            timeout = processRealTimeEvents(defObj.allowRealTime);

            /* show the prompt and any pre-input codes */
            inputLineBegin(defObj);

        getInput:
            /* 
             *   Read the input.  (Note that if our timeout is nil, this
             *   will simply act like the ordinary untimed inputLine.)  
             */
            result = inputLineTimeout(timeout);

            /*
             *   If we're not allowing real-time event processing, freeze
             *   the clock during the read - set the elapsed game
             *   real-time clock back to the value it had on entry, so
             *   that the input effectively consumes no real time.  
             */
            if (!defObj.allowRealTime)
                realTimeManager.setElapsedTime(t0);

            /* check the event code from the result list */
            switch(result[1])
            {
            case InEvtNoTimeout:
                /* 
                 *   the platform doesn't support timeouts - note it for
                 *   future reference so that we don't ask for input with
                 *   timeout again, then go back to try the input again
                 *   without a timeout 
                 */
                noInputTimeout = true;
                timeout = nil;
                goto getInput;
            
            case InEvtLine:
                /* we've finished the current line - end input mode */
                inputLineEnd();

                /* return the line of text we got */
                return result[2];
            
            case InEvtTimeout:
                /* 
                 *   We got a timeout without finishing the input line.
                 *   This means that we have reached the time when the
                 *   next real-time event is ready to execute.  Simply
                 *   continue looping; we'll process all real-time events
                 *   that are ready to go, then we'll resume reading the
                 *   command.  
                 *   
                 *   Before we proceed, though, notify the command
                 *   sequencer (via the command-interrupt pseudo-tag) that
                 *   we're at the start of output text after an
                 *   interrupted command line input 
                 */
                "<.commandint>";
                break;
            
            case InEvtEof:
                /* 
                 *   End of file - this indicates that the user has closed
                 *   down the application, or that the keyboard has become
                 *   unreadable due to a hardware or OS error.
                 *   
                 *   Write a blank line to the display in an attempt to
                 *   flush any partially-entered command line text, then
                 *   throw an error to signal the EOF condition.  
                 */
                "\b";
                throw new EndOfFileException();

            case InEvtEndQuietScript:
                /* 
                 *   End of "quiet" script - this indicates that we've
                 *   been reading input from a script file, but we've now
                 *   reached the end of that file and are about to return
                 *   to reading from the keyboard.
                 *   
                 *   "Quiet script" mode causes all output to be hidden
                 *   while the script is being processed.  This means that
                 *   we won't have displayed a prompt for the current
                 *   line, or updated the status line.  We'll
                 *   automatically display a new prompt when we loop back
                 *   for another line of input, but we have to mark the
                 *   current input line as actually ended now for that to
                 *   happen.  
                 */
                inputLineInProgress = nil;
                inProgressDefObj = nil;

                /* 
                 *   update the status line, since the quiet script mode
                 *   will have suppressed all status line updates while we
                 *   were reading the script, and thus the last update
                 *   before this prompt won't have been shown 
                 */
                statusLine.showStatusLine();

                /* back for more */
                break;
            }
        }
    }

    /*
     *   Pause for a MORE prompt.  If freezeRealTime is true, we'll stop
     *   the real-time clock; otherwise we'll let it keep running.  Even
     *   if we don't freeze the clock, we won't actually process any
     *   real-time events while waiting: the point of the MORE prompt is
     *   to allow the player to read and acknowledge the on-screen display
     *   before showing anything new, so we don't want to allow any output
     *   to result from real-time events that occur while waiting for user
     *   input.  If any real-time events come due while we're waiting,
     *   we'll process them when we're done.  
     */
    pauseForMore(freezeRealTime)
    {
        local t0;

        /* 
         *   flush any command transcript and turn off transcript capture,
         *   so that we show any pent-up reports before pausing for the
         *   MORE prompt 
         */
        if (gTranscript != nil)
            gTranscript.flushForInput();
        
        /* 
         *   cancel any pending input - we must be interrupting the
         *   pending input with a real-time event 
         */
        cancelInputInProgress(true);

        /* note the starting time, in case we want to freeze the clock */
        t0 = realTimeManager.getElapsedTime();

        /* run the MORE prompt */
        morePrompt();

        /* 
         *   if the caller wanted us to freeze the clock, restore the
         *   elapsed game real time to what it was when we started, so
         *   that the time the player took to acknowledge the MORE prompt
         *   won't count against the elapsed game time; otherwise, process
         *   any real-time events that came due while we were waiting 
         */
        if (freezeRealTime)
        {
            /* time was frozen - restore the original elapsed time */
            realTimeManager.setElapsedTime(t0);
        }
        else
        {
            /* 
             *   time wasn't frozen - check for any events that have come
             *   due since we started waiting, and process them
             *   immediately 
             */
            processRealTimeEvents(true);
        }
    }

    /*
     *   Read a keystroke, processing real-time events while waiting, if
     *   desired.  'allowRealTime' and 'promptFunc' work the same way they
     *   do with getInputLine().  
     */
    getKey(allowRealTime, promptFunc)
    {
        local evt;
        
        /* get an event */
        evt = getEventOrKey(allowRealTime, promptFunc, true);

        /* 
         *   the only event that getEventOrKey will return is a keystroke,
         *   so return the keystroke from the event record 
         */
        return evt[2];
    }

    /*
     *   Read an event, processing real-time events while waiting, if
     *   desired.  'allowRealTime' and 'promptFunc' work the same way they
     *   do with getInputLine().  
     */
    getEvent(allowRealTime, promptFunc)
    {
        /* read and return an event */
        return getEventOrKey(allowRealTime, promptFunc, nil);
    }

    /*
     *   Read an event or keystroke.  'allowRealTime' and 'promptFunc' work
     *   the same way they do in getInputLine().  If 'keyOnly' is true,
     *   then we're only interested in keystroke events, and we'll ignore
     *   any other events entered.
     *   
     *   Note that this routine is not generally called directly; callers
     *   should usually call the convenience routines getKey() or
     *   getEvent(), as needed.  
     */
    getEventOrKey(allowRealTime, promptFunc, keyOnly)
    {
        /* make sure the command transcript is flushed */
        if (gTranscript != nil)
            gTranscript.flushForInput();
        
        /* 
         *   Cancel any in-progress input.  If there's an in-progress
         *   input, a real-time event must be interrupting the input,
         *   which is recursively invoking us to start a new input. 
         */
        cancelInputInProgress(true);
        
        /* keep going until we get a keystroke or other event */
        for (;;)
        {
            local result;
            local timeout;
            local t0;

            /* note the starting time, in case we want to freeze the clock */
            t0 = realTimeManager.getElapsedTime();

            /* process real-time events, if possible */
            timeout = processRealTimeEvents(allowRealTime);

            /* show the prompt and any pre-input codes */
            inputEventBegin(promptFunc);

        getInput:
            /* 
             *   Read the input.  (Note that if our timeout is nil, this
             *   will simply act like the ordinary untimed inputLine.)  
             */
            result = inputEvent(timeout);

            /*
             *   If we're not allowing real-time event processing, freeze
             *   the clock during the read - set the elapsed game
             *   real-time clock back to the value it had on entry, so
             *   that the input effectively consumes no real time.  
             */
            if (!allowRealTime)
                realTimeManager.setElapsedTime(t0);

            /* check the event code from the result list */
            switch(result[1])
            {
            case InEvtNoTimeout:
                /* 
                 *   the platform doesn't support timeouts - note it for
                 *   future reference so that we don't ask for input with
                 *   timeout again, then go back to try the input again
                 *   without a timeout 
                 */
                noInputTimeout = true;
                timeout = nil;
                goto getInput;
            
            case InEvtTimeout:
                /* 
                 *   We got a timeout without finishing the input line.
                 *   This means that we have reached the time when the
                 *   next real-time event is ready to execute.  Simply
                 *   continue looping; we'll process all real-time events
                 *   that are ready to go, then we'll resume reading the
                 *   command.  
                 */
                break;
            
            case InEvtEof:
                /* 
                 *   End of file - this indicates that the user has closed
                 *   down the application, or that the keyboard has become
                 *   unreadable due to a hardware or OS error.
                 *   
                 *   Write a blank line to the display in an attempt to
                 *   flush any partially-entered command line text, then
                 *   throw an error to signal the EOF condition.  
                 */
                "\b";
                throw new EndOfFileException();

            case InEvtKey:
                /* keystroke - finish the input and return the event */
                inputEventEnd();
                return result;

            case InEvtHref:
                /* 
                 *   Hyperlink activation - if we're allowed to return
                 *   events other than keystrokes, finish the input and
                 *   return the event; otherwise, ignore the event and keep
                 *   looping.  
                 */
                if (!keyOnly)
                {
                    inputEventEnd();
                    return result;
                }
                break;

            default:
                /* ignore other events */
                break;
            }
        }
    }

    /*
     *   Cancel input in progress.
     *   
     *   If 'reset' is true, we'll clear any input state saved from the
     *   interrupted in-progress editing session; otherwise, we'll retain
     *   the saved editing state for restoration on the next input.
     *   
     *   This MUST be called before calling tadsSay().  Games should
     *   generally never call tadsSay() directly (call the library
     *   function say() instead), so in most cases authors will not need
     *   to worry about calling this on output.
     *   
     *   This MUST ALSO be called before performing any keyboard input.
     *   Callers using inputManager methods for keyboard operations won't
     *   have to worry about this, because the inputManager methods call
     *   this routine when necessary.  
     */
    cancelInputInProgress(reset)
    {
        /* cancel the interpreter's internal input state */
        inputLineCancel(reset);

        /* if we were editing a command line, terminate the editing session */
        if (inputLineInProgress)
        {
            /* do our normal after-input work */
            inputLineEnd();
        }

        /* if we were waiting for event input, note that we are no longer */
        if (inputEventInProgress)
        {
            /* do our normal after-input work */
            inputEventEnd();
        }
    }

    /*
     *   Process any real-time events that are ready to run, and return the
     *   timeout until the next real-time event.
     *   
     *   If allowRealTime is nil, we won't process real-time events at all;
     *   we'll merely return nil for the timeout to indicate to the caller
     *   that any user input interaction about to be attempted should wait
     *   indefinitely.  
     */
    processRealTimeEvents(allowRealTime)
    {
        local timeout;
        
        /* presume we will not use a timeout */
        timeout = nil;

        /* process real-time events, if allowed */
        if (allowRealTime)
        {
            local tNext;
            
            /* 
             *   Process any real-time events that are currently ready to
             *   execute, and note the amount of time until the next
             *   real-time event is ready.  
             */
            tNext = realTimeManager.executeEvents();

            /* 
             *   If there's an event pending, note the interval between the
             *   current time and the event's scheduled time - this will
             *   give us the maximum amount of time we want to wait for the
             *   user to edit the command line before interrupting to
             *   execute the pending event.  Ignore this if the platform
             *   doesn't support timeouts to begin with.  
             */
            if (tNext != nil && !noInputTimeout)
                timeout = tNext - realTimeManager.getElapsedTime();
        }

        /* return the timeout until the next real-time event */
        return timeout;
    }

    /*
     *   Begin reading key/event input.  We'll cancel any report gatherer
     *   so that prompt text shows immediately, and show the prompt if
     *   desired.  
     */
    inputEventBegin(promptFunc)
    {
        /* if we're not continuing previous input, show the prompt */
        if (!inputEventInProgress)
        {
            inputBegin(promptFunc);

            /* note that we're in input mode */
            inputEventInProgress = true;
        }
    }

    /*
     *   End keystroke/event input.
     */
    inputEventEnd()
    {
        /* if input is in progress, terminate it */
        if (inputEventInProgress)
        {
            /* note that we're no longer reading an event */
            inputEventInProgress = nil;
        }
    }

    /*
     *   Begin command line editing.  If we're in HTML mode, we'll show
     *   the appropriate codes to establish the input font.  
     */
    inputLineBegin(defObj)
    {
        /* notify the command sequencer that we're reading a command */
        "<.commandbefore>";
        
        /* if we're not resuming a session, set up a new session */
        if (!inputLineInProgress)
        {
            /* begin input */
            inputBegin(defObj.promptFunc);
            
            /* switch to input font */
            defObj.beginInputFont();

            /* note that we're in input mode */
            inputLineInProgress = true;

            /* remember the parameter object for this input */
            inProgressDefObj = defObj;
        }
    }

    /*
     *   End command line editing.  If we're in HTML mode, we'll show the
     *   appropriate codes to close the input font.  
     */
    inputLineEnd()
    {
        /* if input is in progress, terminate it */
        if (inputLineInProgress)
        {
            /* note that we're no longer reading a line of input */
            inputLineInProgress = nil;

            /* end input font mode */
            inProgressDefObj.endInputFont();

            /* notify the command sequencer that we're done reading */
            "<.commandafter>";

            /* 
             *   tell the main text area's output stream that we just
             *   ended an input line 
             */
            mainOutputStream.inputLineEnd();

            /* forget the parameter object for the input */
            inProgressDefObj = nil;
        }
    }

    /*
     *   Begin generic input.  Cancels command report list capture, and
     *   shows the prompt if given.  
     */
    inputBegin(promptFunc)
    {
        /* 
         *   Turn off command transcript capture, if it's active.  Once
         *   we're soliciting input interactively, we can no longer
         *   usefully capture the text output of commands, but this is fine
         *   because we must be doing something for which capture isn't
         *   important anyway.  Reporting capture is used for things like
         *   selecting the kind of result to show, which clearly isn't a
         *   factor for actions involving interactive input.  
         */
        if (gTranscript != nil)
            gTranscript.flushForInput();

        /* if we have a prompt, display it */
        if (promptFunc != nil)
            (promptFunc)();
    }

    /* receive post-restore notification */
    execute()
    {
        /* 
         *   Reset the inputLine state.  If we had any previously
         *   interrupted input from the current interpreter session,
         *   forget it by cancelling and resetting the input line.  If we
         *   had an interrupted line in the session being restored, forget
         *   about that, too.  
         */
        inputLineCancel(true);
        inputLineInProgress = nil;
        inputEventInProgress = nil;

        /* 
         *   Clear the inputLineTimeout disabling flag - we might be
         *   restoring the game on a different platform from the one where
         *   the game started, so we might be able to use timed command
         *   line input even if we didn't when we started the game.  By
         *   clearing this flag, we'll check again to see if we can
         *   perform timed input; if we can't, we'll just set the flag
         *   again, so there will be no harm done.  
         */
        noInputTimeout = nil;
    }

    /* 
     *   Flag: command line input is in progress.  If this is set, it means
     *   that we interrupted command-line editing by a timeout, so we
     *   should not show a prompt the next time we go back to the keyboard
     *   for input.  
     */
    inputLineInProgress = nil

    /* the InputDef object for the input in progress */
    inProgressDefObj = nil

    /* flag: keystroke/event input is in progress */
    inputEventInProgress = nil

    /*
     *   Flag: inputLine does not support timeouts on the current platform.
     *   We set this when we get an InEvtNoTimeout return code from
     *   inputLineTimeout, so that we'll know not to try calling again with
     *   a timeout.  This applies to the current interpreter only, so we
     *   must ignore any value restored from a previously saved game, since
     *   the game might have been saved on a different platform.
     *   
     *   Note that if this value is nil, it means only that we've never
     *   seen an InEvtNoTimeout return code from inputLineEvent - it does
     *   NOT mean that timeouts are supported locally.
     *   
     *   We assume that the input functions are uniform in their treatment
     *   of timeouts; that is, we assume that if inputLineTimeout supports
     *   timeout, then so does inputEvent, and that if one doesn't support
     *   timeout, the other won't either.  
     */
    noInputTimeout = nil
;


/* ------------------------------------------------------------------------ */
/*
 *   Read a command line from the player.  Displays the main command
 *   prompt and returns a line of input.
 *   
 *   We process any pending real-time events before reading the command.
 *   If the local platform supports real-time command-line interruptions,
 *   we'll continue processing real-time events as they occur in the
 *   course of command editing.  
 */
readMainCommand(which)
{
    local str;
    
    /* execute any pre-command-prompt daemons */
    eventManager.executePrompt();
            
    /* 
     *   Read a line of input, allowing real-time event processing, and
     *   return the line of text we read.  Use the appropriate main
     *   command prompt for the given prompt mode.  
     */
    str = inputManager.getInputLine(
        true, {: libMessages.mainCommandPrompt(which) });

    /* return the string we read */
    return str;
}


/* ------------------------------------------------------------------------ */
/*
 *   End-of-file exception - this is thrown when readMainCommand()
 *   encounters end of file reading the console input. 
 */
class EndOfFileException: Exception
;


/* ------------------------------------------------------------------------ */
/*
 *   'Quitting' exception.  This isn't an error - it merely indicates that
 *   the user has explicitly asked to quit the game. 
 */
class QuittingException: Exception
;

/* ------------------------------------------------------------------------ */
/*
 *   Base class for command input string preparsers.
 *   
 *   Preparsers must be registered in order to run.  During
 *   preinitialization, we will automatically register any existing
 *   preparser objects; preparsers that are created dynamically during
 *   execution must be registered explicitly, which can be accomplished by
 *   inheriting the default constructor from this class.  
 */
class StringPreParser: PreinitObject
    /*
     *   My execution order number.  When multiple preparsers are
     *   registered, we'll run the preparsers in ascending order of this
     *   value (i.e., smallest runOrder goes first).  
     */
    runOrder = 100

    /*
     *   Do our parsing.  Each instance should override this method to
     *   define the parsing that it does.
     *   
     *   'str' is the string to parse, and 'which' is the rmcXxx enum
     *   giving the type of command we're working with.
     *   
     *   This method returns a string or nil.  If the method returns a
     *   string, the caller will forget the original string and work from
     *   here on out with the new version returned; this allows the method
     *   to rewrite the original input as desired.  If the method returns
     *   nil, it means that the string has been fully handled and that
     *   further parsing of the same string is not desired.  
     */
    doParsing(str, which)
    {
        /* return the original string unchanged */
        return str;
    }

    /* 
     *   construction - when we dynamically create a preparser, register
     *   it by default
     */
    construct()
    {
        /* register the preparser */
        StringPreParser.registerPreParser(self);
    }

    /* run pre-initialization */
    execute()
    {
        /* register the preparser if it's not already registered */
        StringPreParser.registerPreParser(self);
    }

    /* register a preparser */
    registerPreParser(pp)
    {
        /* if the preparser isn't already in our list, add it */
        if (regList.indexOf(pp) == nil)
        {
            /* append this new item to the list */
            regList.append(pp);

            /* the list is no longer sorted */
            regListSorted = nil;
        }
    }

    /*
     *   Class method - Run all preparsers.  Returns the result of
     *   successively calling each preparser on the given string.  
     */
    runAll(str, which)
    {
        /* 
         *   if the list of preparsers isn't sorted, sort it in ascending
         *   order of execution order number
         */
        if (!regListSorted)
        {
            /* sort the list */
            regList.sort(SortAsc, {x, y: x.runOrder - y.runOrder});
            
            /* the list is now sorted */
            regListSorted = true;
        }

        /* run each preparser */
        foreach (local cur in regList)
        {
            /* run this preparser */
            str = cur.doParsing(str, which);

            /* 
             *   if the result is nil, it means that the string has been
             *   fully handled, so we need not run any further preparsing 
             */
            if (str == nil)
                return nil;
        }

        /* return the result of the series of preparsing steps */
        return str;
    }

    /* class property containing the list of registered parsers */
    regList = static new Vector(10)

    /* class property - the registration list has been sorted */
    regListSorted = nil
;


/* ------------------------------------------------------------------------ */
/*
 *   Read a line of text and return the token list and the original text.
 *   We keep going until a non-empty line of text is read.
 *   
 *   'which' is one of the rmcXxx enum values specifying what kind of
 *   command line we're reading.
 *   
 *   The return value is a list of two elements.  The first element is the
 *   string entered, and the second element is the token list.  
 */
readMainCommandTokens(which)
{
    local str;
    local toks;

    /* keep going until we get a non-empty command line */
    for (;;)
    {
        /* read a command line */
        str = readMainCommand(which);

        /* run any preparsing desired on the string */
        str = StringPreParser.runAll(str, which);

        /* 
         *   if preparsing returned nil, it means that the preparser fully
         *   handled the string - simply return nil to tell the caller
         *   that its work is done 
         */
        if (str == nil)
            return nil;

        try
        {
            /* tokenize the command string */
            toks = cmdTokenizer.tokenize(str);
        }
        catch (TokErrorNoMatch tokExc)
        {
            /* 
             *   Invalid tokens in the response - complain about it.  Flag
             *   the error as being in the first character of the
             *   remaining string, since that's the character for which we
             *   could find no match. 
             */
            libMessages.invalidCommandToken(tokExc.curChar_.htmlify());

            /* go back for another input line */
            continue;
        }

        /* if we got a non-empty token list, return it */
        if (toks.length() != 0)
            return [str, toks];

        /* show the empty-command reply */
        libMessages.emptyCommandResponse();
    }
}


