/* OpenDoors Online Software Programming Toolkit
 * (C) Copyright 1991 - 1999 by Brian Pirie.
 *
 * Oct-2001 door32.sys/socket modifications by Rob Swindell (www.synchro.net)
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 *
 *
 *        File: ODCore.c
 *
 * Description: Implements the core of OpenDoors, including chat mode
 *              and standard input/output functions that are
 *              used throughout OpenDoors.
 *
 *   Revisions: Date          Ver   Who  Change
 *              ---------------------------------------------------------------
 *              Oct 13, 1994  6.00  BP   New file header format.
 *              Oct 19, 1994  6.00  BP   Changed paging hours logic.
 *              Oct 21, 1994  6.00  BP   Further isolated com routines.
 *              Oct 22, 1994  6.00  BP   Name case conversion /w punct.
 *              Dec 08, 1994  6.00  BP   Allow custom chat mode deactivation.
 *              Dec 09, 1994  6.00  BP   Remove global dir entry structure.
 *              Dec 13, 1994  6.00  BP   Remove include of dir.h.
 *              Dec 31, 1994  6.00  BP   Remove #ifndef USEINLINE DOS code.
 *              Dec 31, 1994  6.00  BP   Remove old multitasker definitions.
 *              Jan 01, 1995  6.00  BP   Don't use ODComInbound().
 *              Jan 01, 1995  6.00  BP   _waitdrain() -> ODWaitDrain().
 *              Jan 01, 1995  6.00  BP   Use new millisecond timer functions.
 *              Jan 01, 1995  6.00  BP   Remove od_init() from _remotechar()
 *              Jan 01, 1995  6.00  BP   Split off odkrnl.c from odcore.c
 *              Aug 19, 1995  6.00  BP   32-bit portability.
 *              Nov 11, 1995  6.00  BP   Moved first_word() to odlist.c
 *              Nov 11, 1995  6.00  BP   Removed register keyword.
 *              Nov 14, 1995  6.00  BP   Added include of odscrn.h.
 *              Nov 16, 1995  6.00  BP   Create odcore.h.
 *              Nov 17, 1995  6.00  BP   Use new input queue mechanism.
 *              Dec 12, 1995  6.00  BP   Added od_set_color().
 *              Dec 12, 1995  6.00  BP   Added entry, exit and kernel macros.
 *              Dec 13, 1995  6.00  BP   Moved chat mode code to ODKrnl.h.
 *              Dec 19, 1995  6.00  BP   Request reason for chat outside hours.
 *              Dec 23, 1995  6.00  BP   Allow space to continue at page pause.
 *              Dec 24, 1995  6.00  BP   Added abtGreyBlock.
 *              Dec 30, 1995  6.00  BP   Added ODCALL for calling convention.
 *              Jan 03, 1996  6.00  BP   Use OD_API_VAR_DEFN for od_control.
 *              Jan 04, 1996  6.00  BP   tODInQueueEvent -> tODInputEvent.
 *              Jan 23, 1996  6.00  BP   No od_set_statusline() under Win32.
 *              Jan 30, 1996  6.00  BP   Replaced od_yield() with od_sleep().
 *              Jan 30, 1996  6.00  BP   Add ODInQueueGetNextEvent() timeout.
 *              Jan 09, 1996  6.00  BP   ODComOutbound() returns actual size.
 *              Jan 09, 1996  6.00  BP   Reduce kernel calls from od_disp...().
 *              Feb 19, 1996  6.00  BP   Changed version number to 6.00.
 *              Mar 03, 1996  6.10  BP   Begin version 6.10.
 *              Mar 19, 1996  6.10  BP   MSVC15 source-level compatibility.
 *              Mar 21, 1996  6.10  BP   Added od_control_get().
 *              Sep 01, 1996  6.10  BP   Update output area on od_set_per...().
 *              Oct 19, 2001  6.20  RS   od_get_key now ignores linefeeds.
 *              Mar 14, 2002  6.22  RS   Fixed od_get_key(bWait=FALSE)
 *              Aug 10, 2003  6.23  SH   *nix support
 */

#define BUILDING_OPENDOORS

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <time.h>
#include <errno.h>

#include "OpenDoor.h"
#include "ODStr.h"
#include "ODGen.h"
#include "ODPlat.h"
#include "ODCom.h"
#include "ODKrnl.h"
#include "ODScrn.h"
#include "ODCore.h"
#include "ODInQue.h"
#ifdef ODPLAT_WIN32
#include "ODFrame.h"
#endif /* ODPLAT_WIN32 */


/* GLOBAL VARIABLES SHARED THROUGHOUT OPENDOORS. */

/* Global declaration of the OpenDoors control structure. */
OD_API_VAR_DEFN tODControl 
#ifndef _WIN32	/* warning C4229: anachronism used : modifiers on data are ignored */
OD_GLOBAL_CONV 
#endif
od_control;

/* OpenDoors global initialized flag. */
BOOL bODInitialized = FALSE;

/* Global serial port object handle. */
tPortHandle hSerialPort;

/* Global input queue object handle. */
tODInQueueHandle hODInputQueue;

/* Reentrancy control. */
BOOL bIsCallbackActive = FALSE;
BOOL bShellChatActive = FALSE;

/* Global working space. */
char szODWorkString[OD_GLOBAL_WORK_STRING_SIZE];

/* Global instance of the text information structure for general use. */
tODScrnTextInfo ODTextInfo;

/* Logfile function hooks. */
BOOL (*pfLogWrite)(INT) = NULL;
void (*pfLogClose)(INT) = NULL;

/* od_color_config() support for od_printf(). */
char chColorCheck = 0;
char *pchColorEndPos;

/* Status line information. */
BYTE btCurrentStatusLine = STATUS_NONE;
OD_PERSONALITY_CALLBACK *pfCurrentPersonality = NULL;
char szDesiredPersonality[33] = "";
SET_PERSONALITY_FUNC *pfSetPersonality = NULL;

/* Commonly used character sequences. */
char abtBlackBlock[2] = {' ', 0x07};
char abtGreyBlock[2] = {' ', 0x70};
char szBackspaceWithDelete[4] = {8, ' ', 8, 0};

/* Current output area on screen. */
BYTE btOutputTop = 1;
BYTE btOutputBottom = 23;


/* PRIVATE VARIABLES. */

/* Display color varaibles. */
char bAnyColorChangeYet;

/* Static character sequences. */
static char szClearScreen[2] = {12, 0};

/* Lookup table to map colors from PC values to ANSI color values. */
static BYTE abtPCToANSIColorTable[8] = {30, 34, 32, 36, 31, 35, 33, 37};


/* LOCAL HELPER FUNCTIONS. */
static void ODAddANSIParameter(char *szControlSequence, int nParameterValue);


/* ----------------------------------------------------------------------------
 * ODWaitDrain()
 *
 * Waits for up to the specified number of milliseconds for the output serial
 * buffer to drain.
 *
 * Parameters: MaxWait - Specifies the maximum number of milliseconds to wait
 *                       before timing out.
 *
 *     Return: void
 */
void ODWaitDrain(tODMilliSec MaxWait)
{
   int nOutboundSize;
   tODTimer Timer;

   /* If we are operating in local mode, then don't do anything. */
   if(od_control.baud == 0) return;

   /* Otherwise, start a timer that is set to elapse after the maximum */
   /* wait period.                                                     */
   ODTimerStart(&Timer, MaxWait);

   /* Loop until either the outbound buffer is empty, or the */
   /* timer has elapsed.                                     */
   for(;;)
   {
      /* Check whether any data is in the outbound serial queue. */
      ODComOutbound(hSerialPort, &nOutboundSize);

      /* If the queue is empty or the timer has elapsed, then stop */
      /* waiting.                                                  */
      if(nOutboundSize == 0 || ODTimerElapsed(&Timer)) break;

      /* Otherwise, give other tasks a chance to run. */
      od_sleep(0);

      /* Give od_kernel() activities a chance to run. */
      CALL_KERNEL_IF_NEEDED();
   } 
}


/* ----------------------------------------------------------------------------
 * od_clr_scr()
 *
 * Clears the contents of the local and remote screens, if screen clearing is
 * enabled.
 *
 * Parameters: none
 *
 *     Return: void
 */
ODAPIDEF void ODCALL od_clr_scr(void)
{
   INT16 nOriginalAttrib;

   /* Log function entry if running in trace mode */
   TRACE(TRACE_API, "od_clr_scr()");

   if(!bODInitialized) od_init();

   OD_API_ENTRY();

   /* Don't clear screen if disabled. */
   if(!od_control.od_always_clear && !(od_control.user_attribute & 2)
      && (od_control.od_extended_info || od_control.od_info_type == CUSTOM))
   {
      OD_API_EXIT();
      return;
   }

   if(od_control.user_rip)
   {
      od_disp("!|*", 3, FALSE);
      if(!od_control.od_default_rip_win)
      {
         od_disp("!|w0000270M12", 13, FALSE);
      }
   }
   
   if(od_control.user_ansi)
   {
      od_disp("\x1b[2J\x1b[1;1H", 10, FALSE);
   }
   else {
	   /* Send ascii 12 to modem, no local echo. */
	   od_disp(szClearScreen, 1, FALSE);
   }

   /* Clear local window. */
   ODScrnClear();

   /* Get color set prior to screen clear. */
   nOriginalAttrib = od_control.od_cur_attrib;

   /* Current color state is unknown. */
   od_control.od_cur_attrib = -1;

   /* Set color to original value. This gurantees that local and */
   /* remote systems both have the same current color set.       */
   od_set_attrib(nOriginalAttrib);

   OD_API_EXIT();
}



/* ----------------------------------------------------------------------------
 * od_input_str()
 *
 * Allows the user to input a string up to the specified length, using
 * characters in the specified range. This string input function is designed
 * to be compatible with all terminal types.
 *
 * Parameters: pszInput   - Pointer to string to store input in.
 *
 *             nMaxLength - Maximum number of characters to permit the user
 *                          to input.
 *
 *             chMin      - The minimum character value to permit. This must
 *                          be at least ASCII 32.
 *
 *             chMax      - The maximum character value to permit.
 *
 *     Return: void
 */
ODAPIDEF void ODCALL od_input_str(char *pszInput, 
   INT nMaxLength,
   unsigned char chMin,
   unsigned char chMax)
{
   char chKeyPressed;
   INT nPosition;

   /* Log function entry if running in trace mode. */
   TRACE(TRACE_API, "od_input_str()");

   /* Initialize OpenDoors if it hasn't already been done. */
   if(!bODInitialized) od_init();

   OD_API_ENTRY();

   /* Start at the beginning of the string. */
   nPosition = 0;

   /* Check that input parameters are valid. */
   if(pszInput == NULL || nMaxLength < 1 || chMin > chMax)
   {
      od_control.od_error = ERR_PARAMETER;
      OD_API_EXIT();
      return;
   }

   for(;;)
   {
      chKeyPressed = od_get_key(TRUE);

      /* If user pressed enter. */
      if(chKeyPressed == '\r' || chKeyPressed == '\n')
      {
         /* Terminate the string. */
         pszInput[nPosition] = '\0';

         /* Display CR-LF sequence. */
         od_disp_str("\n\r");

         /* Exit the function. */
         OD_API_EXIT();
         return;
      }

      /* If the user pressed backspace. */
      else if(chKeyPressed == 8)
      {
         /* If we are not currently at the beginning of the string. */
         if(nPosition > 0)
         {
            /* Send backspace sequence. */
            od_disp_str(szBackspaceWithDelete);

            /* Move current position back by one position in the string. */
            --nPosition;
         }
      }

      /* If this is a valid character to place in the string and we have */
      /* not reached the maximum size of the string yet.                 */
      else if(chKeyPressed >= chMin && chKeyPressed <= chMax
         && nPosition < nMaxLength)
      {
         /* Display key that was pressed. */
         od_putch(chKeyPressed);

         /* Add the entered character to the string and increment our */
         /* current position in the string.                           */
         pszInput[nPosition++] = chKeyPressed;
      }
   }
}


/* ----------------------------------------------------------------------------
 * od_clear_keybuffer()
 *
 * Clears any keystrokes from the inbound buffers. Both input from local and
 * remote systems is discarded, by clearing both OpenDoors' common input
 * event queue, and the serial port inbound buffer. This function is called
 * to cause any input by the user prior to the time the function was called
 * to be ignored.
 *
 * Parameters: none
 *
 *     Return: void
 */
ODAPIDEF void ODCALL od_clear_keybuffer(void)
{
   /* Log function entry if running in trace mode. */
   TRACE(TRACE_API, "od_clear_keybuffer()");

   /* Initialize OpenDoors if it hasn't already been done. */
   if(!bODInitialized) od_init();

   OD_API_ENTRY();

   /* Empty any events in the common input event queue. */
   ODInQueueEmpty(hODInputQueue);

   /* If we are not operating in local mode ... */
   if(od_control.baud != 0)
   {
      /* ... then remove any items in the serial port inbound buffer. */
      ODComClearInbound(hSerialPort);
   }

   /* Call the OpenDoors kernel function. */
   CALL_KERNEL_IF_NEEDED();

   OD_API_EXIT();
}

/* ----------------------------------------------------------------------------
 * od_key_pending()
 *
 * Returns TRUE if there's a key pending, FALSE otherwise.
 *
 * Parameters: none
 *
 *     Return: TRUE if character is waiting, FALSE if no character is waiting.
 */
ODAPIDEF BOOL ODCALL od_key_pending(void)
{
   /* Initialize OpenDoors if it hasn't already been done. */
   if(!bODInitialized) od_init();

   /* Log function entry if running in trace mode. */
   TRACE(TRACE_API, "od_get_key()");

   OD_API_ENTRY();

   /* Call the OpenDoors kernel. */
   CALL_KERNEL_IF_NEEDED();

   if(!ODInQueueWaiting(hODInputQueue))
   {
         OD_API_EXIT();
         return(FALSE);
   }

   OD_API_EXIT();
   return(TRUE);
}

/* ----------------------------------------------------------------------------
 * od_get_key()
 *
 * Inputs a single character, optionally waiting for the next character if no
 * character has been received yet. This function returns data received from
 * either the local or remote system, in the order in which it was received.
 *
 * Parameters: bWait - FALSE if od_get_key() should return right away with
 *                     a value of 0 if no characters have been received, or
 *                     TRUE if od_get_key() should wait for the next received
 *                     character.
 *
 *     Return: Character that was received, or 0 if no character is waiting.
 */
ODAPIDEF char ODCALL od_get_key(BOOL bWait)
{
   tODInputEvent InputEvent;

   /* Initialize OpenDoors if it hasn't already been done. */
   if(!bODInitialized) od_init();

   /* Log function entry if running in trace mode. */
   TRACE(TRACE_API, "od_get_key()");

   OD_API_ENTRY();

   /* Call the OpenDoors kernel. */
   CALL_KERNEL_IF_NEEDED();

	do {

	   /* If we aren't supposed to wait for input, then check whether any   */
	   /* input is waiting in the input queue, and if not return right away */
	   /* without any data.                                                 */
	   if(!bWait)
	   {
		  if(!ODInQueueWaiting(hODInputQueue))
		  {
			 OD_API_EXIT();
			 return(0);
		  }
	   }

		/* Obtain the next character from the input queue. If we get to this */
		/* point and there is no data waiting in the input queue, then the   */
		/* ODInQueueGetNextEvent() function will block until a character     */
		/* is available in the input queue.                                  */
		ODInQueueGetNextEvent(hODInputQueue, &InputEvent, OD_NO_TIMEOUT);

		/* Only keyboard input events are currently supported by od_get_key(). */
		ASSERT(InputEvent.EventType == EVENT_CHARACTER);

		/* Update OpenDoors control structure member that records whether the */
		/* last input came from the local or remote user.                     */
		od_control.od_last_input = InputEvent.bFromRemote ? 0 : 1;

	} while(InputEvent.chKeyPress == '\n');	/* Ignore line-feed char */

   /* Return the character that was pressed by the user. */
   OD_API_EXIT();
   return(InputEvent.chKeyPress);
}



/* ----------------------------------------------------------------------------
 * od_carrier()
 *
 * Allows programs to determine the current state of the carrier detect
 * signal when OpenDoors' automatic carrier detection has been disabled.
 *
 * Parameters: none
 *
 *     Return: TRUE if the carrier detct signal is present, FALSE if it
 *             isn't. When operating in local mode, this function always
 *             returns FALSE.
 */
ODAPIDEF BOOL ODCALL od_carrier(void)
{
   BOOL bIsCarrier;

   /* Initialize OpenDoors if it hasn't already been done. */
   if(!bODInitialized) od_init();

   OD_API_ENTRY();

   /* Log function entry if running in trace mode */
   TRACE(TRACE_API, "od_carrier()");

   /* If we are operating in local mode, then return FALSE. */
   if(od_control.baud == 0)
   {
      od_control.od_error = ERR_NOREMOTE;
      OD_API_EXIT();
      return(FALSE);
   }

   /* In remote mode, obtain the current state of the carrier detect signal. */
   ODComCarrier(hSerialPort, &bIsCarrier);

   /* Return the current state of the carrier detect signal. */
   OD_API_EXIT();
   return(bIsCarrier);
}


/* ----------------------------------------------------------------------------
 * od_repeat()
 *
 * This function displays the same character the specified number of times on
 * the local and remote screens, using any available optimal control sequences
 * under the current display mode.
 *
 * Parameters: chValue - Character to repeat.
 *
 *             btTimes - Number of times to repeat the character.
 *
 *     Return: void
 */
ODAPIDEF void ODCALL od_repeat(char chValue, BYTE btTimes)
{
   char *pchCurStringPos;
   BYTE btLeft;
   char szBuffer[3];

   /* Log function entry if running in trace mode. */
   TRACE(TRACE_API, "od_repeat()");

   /* Ensure that OpenDoors has been initialized. */
   if(!bODInitialized) od_init();

   OD_API_ENTRY();

   /* If the caller asked to repeat the character 0 times, then we can */
   /* safely return right away without doing anything.                 */
   if(btTimes == 0)
   {
      OD_API_EXIT();
      return;
   }

   /* Generate string of repeat characters. */
   pchCurStringPos = szODWorkString; 
   for(btLeft = btTimes; btLeft--;)
   {
      *pchCurStringPos++ = chValue;
   }
   *pchCurStringPos = '\0';

   /* Display repeated string on local screen. */
   ODScrnDisplayString(szODWorkString);

   /* If we are operating in AVATAR mode. */
   if(od_control.user_avatar)
   {
      /* Generate the AVATAR control sequence to repeat this character */
      /* the specified number of times.                                */
      szBuffer[0] = 25;
      szBuffer[1] = chValue;
      szBuffer[2] = btTimes;
      od_disp(szBuffer, 3, FALSE);
   }

   /* If AVATAR mode is not available. */
   else
   {
      /* Send the entire repeated string to the remote system. */
      od_disp(szODWorkString, btTimes, FALSE);
   }

   OD_API_EXIT();
}


/* ----------------------------------------------------------------------------
 * od_page()
 *
 * This function is called when the user wished to page the system operator.
 *
 * Parameters: none
 *
 *     Return: void
 */
ODAPIDEF void ODCALL od_page(void)
{
   INT16 nCount;
   tODTimer Timer;
   time_t nUnixTime;
   struct tm *TimeBlock;
   INT16 nMinute;
   BOOL bFailed = FALSE;
   INT16 nOriginalAttrib;

   /* Log function entry if running in trace mode. */
   TRACE(TRACE_API, "od_page()");

   /* Initialize OpenDoors if it hasn't already been done. */
   if(!bODInitialized) od_init();

   OD_API_ENTRY();

   /* Save current display color attribute. */
   nOriginalAttrib = od_control.od_cur_attrib;

   /* Clear the screen. */
   od_clr_scr();
   od_set_attrib(od_control.od_chat_color1);

   /* Ask reason for chat. */
   od_disp_str(od_control.od_chat_reason);
   od_set_attrib(od_control.od_chat_color2);
   od_putch('[');

   /* Use extended ASCII characters if operating in ANSI or AVATAR mode. */
   if(od_control.user_ansi || od_control.user_avatar)
   {
      od_repeat('�',77);
   }
   else
   {
      od_repeat('-',77);
   }
   od_disp_str("]\n\r ");
   od_input_str(od_control.user_reasonforchat,77,32,255);

   /* If the user did not abort sysop paging by entering a blank reason */
   /* for chat.                                                         */
   if(strlen(od_control.user_reasonforchat) != 0)
   {
      /* Indicate that the user wants to chat. */
      od_control.user_wantchat = TRUE;
#ifdef ODPLAT_WIN32
      ODFrameUpdateWantChat();
#endif /* ODPLAT_WIN32 */

      /* Determine whether or not sysop paging should be permitted at */
      /* the current time.                                            */
      nUnixTime = time(NULL);
      TimeBlock = localtime(&nUnixTime);
      nMinute = (60 * TimeBlock->tm_hour) + TimeBlock->tm_min;
      if(od_control.od_pagestartmin < od_control.od_pageendmin)
      {
         if(nMinute < od_control.od_pagestartmin
            || nMinute >= od_control.od_pageendmin)
         {
            bFailed = TRUE;
         }
      }
      else if(od_control.od_pagestartmin > od_control.od_pageendmin)
      {
         if(nMinute < od_control.od_pagestartmin
            && nMinute >= od_control.od_pageendmin)
         {
            bFailed = TRUE;
         }
      }
      else
      {
         bFailed = FALSE;
      }

      /* If paging is set to PAGE_ENABLE, meaning that sysop paging should */
      /* be permitted regardless of the time of day, then allow paging.    */
      if(od_control.od_okaytopage == PAGE_ENABLE)
      {
         bFailed = FALSE;
      }

      /* If paging is explicitly disable by PAGE_DISABLE, or the current */
      /* time of the day is not normally permitted for paging.           */
      if(od_control.od_okaytopage == PAGE_DISABLE || bFailed)
      {
         /* Indicate this to user. */
         od_disp_str("\n\r");
         od_disp_str(od_control.od_no_sysop);
         od_disp_str(od_control.od_press_key);
         od_get_answer("\x0d\x0a");

         /* Return from this function. */
         goto cleanup;
      }

      /* Update status line right away. */
      bForceStatusUpdate = TRUE;
      CALL_KERNEL_IF_NEEDED();

      /* Write sysop page information to the logfile, if the log file */
      /* system is hooked up.                                         */
      if(pfLogWrite != NULL)
      {
         (*pfLogWrite)(8);
      }

      /* Tell the user that we are now paging the system operator. */
      od_set_attrib(od_control.od_chat_color1);
      od_disp_str(od_control.od_paging);

#ifdef OD_TEXTMODE
      /* Display sysop page status line if it exists and the sysop status */
      /* line is currently active.                                        */
      if(od_control.od_page_statusline != -1 && btCurrentStatusLine != 8)
      {
         od_set_statusline(od_control.od_page_statusline);
      }
#endif /* OD_TEXTMODE */

      /* Increment the total number of times that the user has paged */
      /* the sysop.                                                  */
      ++od_control.user_numpages;

      /* Sysop hasn't responded yet. */
      bChatted=FALSE;

      /* Loop for length of sysop page. */
      for(nCount = 0; nCount < od_control.od_page_len; ++nCount)
      {
         /* Start a timer that is set to elapse in exactly one second. */
         ODTimerStart(&Timer, 1000);

         /* Display another period character. */
         od_putch('.');

         /* Abort page if system operator answered */
         if(bChatted) goto cleanup;

         /* Send beep to local and remote systems. */
         od_putch('\a');

         /* Check whether system operator has answered after playing beep. */
         if (bChatted) goto cleanup;

         /* Wait for the timer to elapse, calling od_kernel() so that */
         /* chat mode will start as soon as the sysop presses the     */
         /* chat key.                                                 */
         while(!ODTimerElapsed(&Timer))
         {
            CALL_KERNEL_IF_NEEDED();
         }
      }

      /* If sysop page time has elapsed without a response from the */
      /* sysop, then notify the user.                               */
      od_disp_str(od_control.od_no_response);
      od_disp_str(od_control.od_press_key);
      od_get_answer("\x0d\x0a");
      od_disp_str("\n\r\n\r");
   }

cleanup:
   /* Restore original display color attribute. */
   od_set_attrib(nOriginalAttrib);

   OD_API_EXIT();
}


/* ----------------------------------------------------------------------------
 * od_disp()
 *
 * Function to send one or more character to the remote system, optionally
 * also echoing the same characters to the local screen.
 *
 * Parameters: pachBuffer - Pointer to buffer of characters to send.
 *
 *             nSize      - Number of characters to send from the buffer.
 *
 *             bLocalEcho - TRUE to also echo the characters to the local
 *                          screen, FALSE to just send the characters to the
 *                          remote system.
 *
 *     Return: void
 */
ODAPIDEF void ODCALL od_disp(const char *pachBuffer, INT nSize, BOOL bLocalEcho)
{
   /* Log function entry if running in trace mode. */
   TRACE(TRACE_API, "od_disp()");

   /* Initialize OpenDoors if it hasn't already been done. */
   if(!bODInitialized) od_init();

   OD_API_ENTRY();

   /* Call the OpenDoors kernel, if needed. */
#ifndef OD_MULTITHREADED
   if(ODTimerElapsed(&RunKernelTimer))
   {
      CALL_KERNEL_IF_NEEDED();
   }
#endif /* !OD_MULTITHREADED */

   /* If we are operating in remote mode, then send the buffer to the */
   /* remote system.                                                  */
   if(od_control.baud != 0)
   {
      ODComSendBuffer(hSerialPort, (BYTE *)pachBuffer, nSize);
   }

   /* If we are also to display the character on the local screen, then */
   /* display the buffer on the local screen.                           */
   if(bLocalEcho)
   {
      ODScrnDisplayBuffer(pachBuffer, nSize);
   }

   OD_API_EXIT();
}


/* ----------------------------------------------------------------------------
 * od_disp_str()
 *
 * Displays a string on both the local and remote systems.
 *
 * Parameters: pszToDisplay - Pointer to the string to be displayed.
 *
 *     Return: void
 */
ODAPIDEF void ODCALL od_disp_str(const char *pszToDisplay)
{
   /* Log function entry if running in trace mode */
   TRACE(TRACE_API, "od_disp_str()");

   /* Initialize OpenDoors if it hasn't already been done. */
   if(!bODInitialized) od_init();

   OD_API_ENTRY();

   /* Call the OpenDoors kernel, if needed. */
#ifndef OD_MULTITHREADED
   if(ODTimerElapsed(&RunKernelTimer))
   {
      CALL_KERNEL_IF_NEEDED();
   }
#endif /* !OD_MULTITHREADED */

   /* Send the string to the remote system, if we are running in remote mode. */
   if(od_control.baud != 0)
   {
      ODComSendBuffer(hSerialPort, (BYTE *)pszToDisplay, strlen(pszToDisplay));
   }

   /* Display the screen on the local screen. */
   ODScrnDisplayString(pszToDisplay);

   OD_API_EXIT();
}


/* ----------------------------------------------------------------------------
 * od_set_statusline()
 *
 * Switches to one of the available status lines provided by the current
 * personality, or turns off the status line altogether.
 *
 * Parameters: nSetting - Indicates which status line (if any) should be
 *                        activated.
 *
 *     Return: void
 */
ODAPIDEF void ODCALL od_set_statusline(INT nSetting)
{
#ifdef OD_TEXTMODE
   INT nDistance;
   BYTE btCount
#endif /* OD_TEXTMODE */

   /* Log function entry if running in trace mode. */
   TRACE(TRACE_API, "od_set_statusline()");

   /* Initialize OpenDoors if it hasn't already been done. */
   if(!bODInitialized) od_init();

   OD_API_ENTRY()

#ifdef OD_TEXTMODE

   /* If status line is disabled, then don't do anything. */
   if(!od_control.od_status_on)
   {
      OD_API_EXIT();
      return;
   }

   /* Ensure that the parameter is within the valid range. */
   if(nSetting < 0 || nSetting > 8)
   {
      nSetting = 0;
   }

   /* If the specified status line is already active, and status line */
   /* update isn't being forced, then return without doing anything.  */
   if(!od_control.od_update_status_now && nSetting == btCurrentStatusLine)
   {
      OD_API_EXIT();
      return;
   }

   /* Save the current cursor settings. */
   ODStoreTextInfo();

   /* Reset screen boundary to allow access to the entire screen. */
   ODScrnSetBoundary(1,1,80,25);

   /* If status line is being turned off. */
   if(btCurrentStatusLine == STATUS_NONE)
   {
      if((nDistance = (INT)ODTextInfo.cury - ( 1 + (INT)btOutputBottom
         - (INT)btOutputTop)) > 0)
      {
         ODScrnCopyText(1, (BYTE)((INT)btOutputTop + nDistance), 80,
            (BYTE)((INT)btOutputBottom + nDistance), (BYTE)btOutputTop, 1);
         ODTextInfo.cury = 1 + btOutputBottom - btOutputTop;
      }
      else if(ODTextInfo.cury < btOutputTop)
      {
         ODTextInfo.cury = btOutputTop;
         ODScrnCopyText(1, (BYTE)(btOutputTop + 24 - btOutputBottom), 80, 25,
            btOutputTop, 1);
      }
   }

   od_control.od_current_statusline = btCurrentStatusLine = nSetting;

   if(nSetting == 8)
   {
      ODScrnSetAttribute(0x07);

      for(btCount = 1; btCount <= 25; ++btCount)
      {
         if(btCount < btOutputTop || btCount > btOutputBottom)
         {
            if(btCount == 25)
            {
               ODScrnPutText(80, 25, 80, 25, abtBlackBlock);
               ODScrnSetCursorPos(1, 25);
               ODScrnDisplayString("                                                                               ");
            }
            else
            {
               ODScrnSetCursorPos(1, 24);
               ODScrnDisplayString("                                                                                ");
            }
         }
      }

      ODScrnSetAttribute(ODTextInfo.attribute);
      ODScrnSetCursorPos(ODTextInfo.curx, ODTextInfo.cury);
   }

   else
   {
      ODScrnEnableCaret(FALSE);
      ODScrnEnableScrolling(FALSE);

      (*pfCurrentPersonality)((BYTE)nSetting);

      ODScrnEnableCaret(TRUE);
      ODScrnEnableScrolling(TRUE);

      ODScrnSetBoundary(1, btOutputTop, 80, btOutputBottom);
      ODScrnSetAttribute(ODTextInfo.attribute);
      ODScrnSetCursorPos(ODTextInfo.curx, ODTextInfo.cury);
   }

#else /* !OD_TEXTMODE */

   od_control.od_error = ERR_UNSUPPORTED;

#endif /* !OD_TEXTMODE */

   OD_API_EXIT();
}


/* ----------------------------------------------------------------------------
 * ODStoreTextInfo()
 *
 * Stores the current text settings into the OpenDoors global text information
 * structure.
 *
 * Parameters: none
 *
 *     Return: void
 */
void ODStoreTextInfo(void)
{
   ODScrnGetTextInfo(&ODTextInfo);
}


/* ----------------------------------------------------------------------------
 * ODRestoreTextInfo()
 *
 * Restores display settings previously stored by ODStoreTextInfo()
 *
 * Parameters: none
 *
 *     Return: void
 */
void ODRestoreTextInfo(void)
{
   ODScrnSetBoundary(ODTextInfo.winleft, ODTextInfo.wintop,
      ODTextInfo.winright, ODTextInfo.winbottom);
   ODScrnSetAttribute(ODTextInfo.attribute);
   ODScrnSetCursorPos(ODTextInfo.curx, ODTextInfo.cury);
}


/* ----------------------------------------------------------------------------
 * ODStringToName()
 *
 * Reformats a string so that it has the correct capitalization for a name,
 * and removes any trailing line break character.
 *
 * Parameters: pszToConvert - Pointer to the string to reformat.
 *
 *     Return: void
 */
void ODStringToName(char *pszToConvert)
{
   /* Begin by changing the entire string to lower case. */
   strlwr(pszToConvert);

   /* Trim any newline character that may be at the end of the string. */
   if(pszToConvert[strlen(pszToConvert) - 1] == '\n')
   {
      pszToConvert[strlen(pszToConvert) - 1] = '\0';
   }
   /* Trim any CR character that may be at the end of the string. */
   if(pszToConvert[strlen(pszToConvert) - 1] == '\r')
   {
      pszToConvert[strlen(pszToConvert) - 1] = '\0';
   }

   /* Change the first character to lower case. */
   *pszToConvert = toupper(*pszToConvert);

   /* Loop through the rest of the string, capitalizing any other words */
   /* in the string.                                                    */
   while(*pszToConvert)
   {
      switch(*pszToConvert++)
      {
         case ' ':
         case '\t':
         case ',':
         case '.':
         case '-':
            *pszToConvert = toupper(*pszToConvert);
            break;
      }
   }
}


/* ----------------------------------------------------------------------------
 * od_set_color()
 *
 * Sets the current display color for both local and remote output.
 *
 * Parameters: nForeground - New foreground (text) color.
 *
 *             nBackground - New background color.
 *
 *     Return: void
 */
ODAPIDEF void ODCALL od_set_color(INT nForeground, INT nBackground)
{
   /* Use od_set_attrib() to perform the actual color setting.          */
   /* Here, we rely on od_set_attrib() to look after initialization,    */
   /* API_ENTRY() and API_EXIT() calls, etc. This allows od_set_color() */
   /* (which was previously just a macro) to be implemented with as     */
   /* little overhead as possible.                                      */
   od_set_attrib(nForeground | (nBackground << 4));
}


/* ----------------------------------------------------------------------------
 * od_set_attrib()
 *
 * Sets the current display color for both local and remote output.
 *
 * Parameters: nColor - New Display color to set, or -1 for no change.
 *
 *     Return: void
 */
ODAPIDEF void ODCALL od_set_attrib(INT nColor)
{
   char szControlSequence[40];

   /* Log function entry if running in trace mode */
   TRACE(TRACE_API, "od_set_attrib()");

   /* Ensure that OpenDoors has been initialized. */
   if(!bODInitialized) od_init();

   OD_API_ENTRY();

   /* If color value is -1, then make no change. */
   if(nColor == -1)
   {
      OD_API_EXIT();
      return;
   }

   /* If we are operating in AVATAR mode. */
   if(od_control.user_avatar)
   {
      if(od_control.od_cur_attrib != nColor || od_control.od_full_color)
      {
         /* Change local text color. */
         ODScrnSetAttribute((BYTE)(od_control.od_cur_attrib = nColor));

         /* Generate AVATAR control sequence. */
         szControlSequence[0] = 22;
         szControlSequence[1] = 1;
         szControlSequence[2] = nColor;

         /* Send AVATAR control sequence. */
         od_disp(szControlSequence, 3, FALSE);
      }
   }

   /* If we are operating in ANSI mode. */
   else if(od_control.user_ansi)
   {
      bAnyColorChangeYet = FALSE;

      if(od_control.od_cur_attrib == -1 || od_control.od_full_color)
      {
ansi_reset:
         /* Reset ANSI terminal status. */
         ODAddANSIParameter(szControlSequence, 0);

         /* If blink attribute is set. */
         if(nColor & 0x80)
         {
            /* Add it to the ANSI color sequence. */
            ODAddANSIParameter(szControlSequence, 5);
         }

         /* If high intensity attribute is set. */
         if(nColor & 0x08)
         {
            /* Add it to the ANSI color sequence. */
            ODAddANSIParameter(szControlSequence, 1);
         }
      }

      /* If current color is known. */
      else                             
      {
         /* If have to reset flashing or bright. */
         if(((od_control.od_cur_attrib&0x80) &&
            !(nColor & 0x80)) || ((od_control.od_cur_attrib & 0x08)
            && !(nColor & 0x08)))
         {
            /* Must reset entire colour settings. */
            od_control.od_cur_attrib = -1;
            goto ansi_reset;
         }
                                       
         /* If flashing has to be turned on. */
         if((nColor & 0x80) != (od_control.od_cur_attrib & 0x80))
         {
            /* Add it to the ANSI color sequence. */
            ODAddANSIParameter(szControlSequence, 5);
         }

         /* If bright has to be turned on. */
         if((nColor & 0x08) != (od_control.od_cur_attrib & 0x08)
            || od_control.od_cur_attrib == -1)
         {
            /* Add it to the ANSI color sequence. */
            ODAddANSIParameter(szControlSequence, 1);
         }
      }


      /* If foreground color has changed. */
      if((nColor & 0x07) != (od_control.od_cur_attrib & 0x07)
         || od_control.od_cur_attrib == -1 || od_control.od_full_color)
      {
         /* Add translated color to sequence. */
         ODAddANSIParameter(szControlSequence,
            abtPCToANSIColorTable[nColor&0x07]);
      }

      /* If background color has changed. */
      if((nColor & 0x70) != (od_control.od_cur_attrib & 0x70)
         || od_control.od_cur_attrib == -1 || od_control.od_full_color)
      {
         /* Add translated color to sequence. */
         ODAddANSIParameter(szControlSequence,
            abtPCToANSIColorTable[(nColor & 0x70) >> 4] + 10);
      }

      /* If any change in color. */
      if(bAnyColorChangeYet)
      {
         /* Append change-attribute command. */
         strcat(szControlSequence, "m");

         /* Send ANSI sequence to the modem. */
         od_disp(szControlSequence, strlen(szControlSequence), FALSE);
      }

      /* Change local text color. */
      ODScrnSetAttribute((BYTE)(od_control.od_cur_attrib = nColor));
   }
   else
   {
      od_control.od_error = ERR_NOGRAPHICS;
   }

   OD_API_EXIT();
}


/* ----------------------------------------------------------------------------
 * ODAddANSIParameter()                                *** PRIVATE FUNCTION ***
 *
 * Adds a parameter to an ANSI color sequence.
 *
 * Parameters: szControlSequence - The contents of the control sequence string
 *                                 generated so far.
 *
 *             nParameterValue   - Value of the parameter to add.
 *
 *     Return: void
 */
static void ODAddANSIParameter(char *szControlSequence, int nParameterValue)
{
   char szTemp[5];

   if(bAnyColorChangeYet)
   {
      sprintf(szTemp, ";%d", nParameterValue);
      strcat(szControlSequence, szTemp);
   }
   else
   {
      bAnyColorChangeYet = TRUE;
      sprintf(szControlSequence, "x[%d", nParameterValue);
      szControlSequence[0] = 27;
   }
}


/* ----------------------------------------------------------------------------
 * od_putch()
 *
 * Displays a character on the local and remote screens.
 *
 * Parameters: chToDisplay - The character to display.
 *
 *     Return: void
 */
ODAPIDEF void ODCALL od_putch(char chToDisplay)
{
   /* Log function entry if running in trace mode. */
   TRACE(TRACE_API, "od_putch()");

   /* Initialize OpenDoors if it hasn't been done already. */
   if(!bODInitialized) od_init();

   OD_API_ENTRY();

   /* Display the character on the local screen. */
   ODScrnDisplayChar(chToDisplay);

   /* If not operating in local mode, then send the character to the */
   /* serial port.                                                   */
   if(od_control.baud)
   {
      ODComSendByte(hSerialPort, chToDisplay);
   }

   /* If it is time to call the kernel, then do so. */
   if(ODTimerElapsed(&RunKernelTimer))
   {
      CALL_KERNEL_IF_NEEDED();
   }

   OD_API_EXIT();
}


/* ----------------------------------------------------------------------------
 * od_set_dtr()
 *
 * Changes the state of the DTR line to the modem, if not running in local
 * mode.
 *
 * Parameters: bHigh - TRUE to raise DTR, FALSE to lower it.
 *
 *     Return: void
 */
ODAPIDEF void ODCALL od_set_dtr(BOOL bHigh)
{
   /* Log function entry if running in trace mode. */
   TRACE(TRACE_API, "od_set_dtr()");

   /* Initialize OpenDoors if it hasn't already been done. */
   if(!bODInitialized) od_init();

   OD_API_ENTRY();

   /* If we are running in local mode, then return with an error. */
   if(!od_control.baud)
   {
      od_control.od_error = ERR_NOREMOTE;
      OD_API_EXIT();
      return;
   }

   /* Otherwise, change the state of the DTR line. */
   ODComSetDTR(hSerialPort, bHigh);

   OD_API_EXIT();
}


/* ----------------------------------------------------------------------------
 * od_get_answer()
 *
 * Waits for the user to press one of the keys listed in pszOptions. Case is
 * not sensitive, although the pressed key is returned in the same case as it
 * is specified in pszOptions.
 *
 * Parameters: pszOptions - String listing characters to accept.
 *
 *     Return: void
 */
ODAPIDEF char ODCALL od_get_answer(const char *pszOptions)
{
   char *pchPossibleOption;
   char chPressed;

   /* Log function entry if running in trace mode. */
   TRACE(TRACE_API, "od_get_answer()");

   /* Initialize OpenDoors if it hasn't already been done. */
   if(!bODInitialized) od_init();

   OD_API_ENTRY();

   for(;;)
   {
      /* Wait for the next key press by the user. */
      chPressed = od_get_key(TRUE);
      chPressed = tolower(chPressed);

      /* Loop through list of possible options. */
      pchPossibleOption = (char *)pszOptions;
      while(*pchPossibleOption)
      {
         /* If the key pressed matches this possible option. */
         if(tolower(*pchPossibleOption) == chPressed)
         {
            /* Then return the character in the case originally specified */
            /* by the caller.                                             */
            OD_API_EXIT();
            return(*pchPossibleOption);
         }

         /* Move on to the next possible option. */
         ++pchPossibleOption;
      }

      /* If the key pressed did not match a possible option, then we */
      /* just loop again, getting the next key.                      */
   }
}


/* ----------------------------------------------------------------------------
 * od_color_config()
 *
 * Determines the color attribute that is described by the provided string.
 * This string is in the same format that is used for specifying colors in the
 * OpenDoors configuration file.
 *
 * Parameters: pszColorDesc - Color description string.
 *
 *     Return: The PC-style color attribute corresponding to the color
 *             description string.
 */
ODAPIDEF BYTE ODCALL od_color_config(char *pszColorDesc)
{
   BYTE btColor = 0x07;
   char szToken[40];
   char *pszStart=(char *)pszColorDesc;
   char *pszEnd;
   BYTE btLength;
   BYTE btIdentifier;
   BOOL bForeground = TRUE;

   /* Log function entry if running in trace mode. */
   TRACE(TRACE_API, "od_color_config()");

   /* Initialize OpenDoros if it hasn't already been done. */
   if(!bODInitialized) od_init();

   OD_API_ENTRY();

   while(*pszStart && *pszStart!=chColorCheck)
   {
      if(*pszStart == ' ' || *pszStart== '\t')
      {
         ++pszStart;
      }
      else
      {
         btLength = 0;
         pszEnd = (char *)pszStart;
         while(*pszEnd && *pszEnd != chColorCheck && *pszEnd != ' '
            && *pszEnd != '\t')
         {
            ++btLength;
            ++pszEnd;
         }

         if(btLength > 39) btLength = 39;
         strncpy(szToken, pszStart, btLength);
         szToken[btLength] = '\0';
         strupr(szToken);

         for(btIdentifier = 0; btIdentifier < 12; ++btIdentifier)
            if(strcmp(od_config_colours[btIdentifier], szToken) == 0)
            {
               if(btIdentifier <= 9)
               {
                  if(btIdentifier >= 8) btIdentifier -= 2;

                  if(bForeground)
                  {
                     bForeground=FALSE;
                     btColor &=~ 0x07;
                     btColor |= btIdentifier;
                  }
                  else
                  {
                     btColor &=~ 0x70;
                     btColor |= (btIdentifier << 4);
                  }
               }

               else if(btIdentifier == 10)
               {
                  btColor |= 0x08;
               }

               else if(btIdentifier == 11)
               {
                  btColor |= 0x80;
               }

               break;
            }

         pszStart = (char *)pszEnd;
      }
   }

   pchColorEndPos = (char *)pszStart;

   OD_API_EXIT();

   return(btColor);
}


/* ----------------------------------------------------------------------------
 * ODPagePrompt()
 *
 * Called to display the page prompt at the end of a screen of text. This page
 * prompt allows the user to stop further display, to display the next page,
 * or to display in continuous (non-stop) mode with page pausing disabled.
 *
 * Parameters: pbPausing   - Pointer to current page pausing enabled flag.
 *
 *     Return: FALSE if display should be continued, or TRUE to abort display.
 */
BOOL ODPagePrompt(BOOL *pbPausing)
{
   INT nPromptLength = strlen(od_control.od_continue);
   tODScrnTextInfo TextInfo;
   BOOL bToReturn = FALSE;
   char chKeyPressed;
   BYTE btCount;

   /* Return right away if page pausing is disabled. */
   if(!*pbPausing) return(FALSE);

   /* Get current text color. */
   ODScrnGetTextInfo(&TextInfo);

   /* Set to prompt color. */
   od_set_attrib(od_control.od_continue_col);

   /* Display page prompt string. */
   od_disp_str(od_control.od_continue);

   /* Restore original text color. */
   od_set_attrib(TextInfo.attribute);

   /* Loop until the user makes a valid choice. */
   for(;;)
   {
      /* Obtain the next key from the user. */
      chKeyPressed = od_get_key(TRUE);

      /* If user chooses to continue. */
      if(chKeyPressed == tolower(od_control.od_continue_yes) ||
         chKeyPressed == toupper(od_control.od_continue_yes) ||
         chKeyPressed == 13 ||
         chKeyPressed == ' ')
      {
         /* Remove the prompt and return. */
         goto finished_pausing;
      }

      /* If user requested nonstop display. */
      else if(chKeyPressed == tolower(od_control.od_continue_nonstop) ||
              chKeyPressed == toupper(od_control.od_continue_nonstop))
      {
         /* Disable page pausing. */
         *pbPausing = FALSE;

         /* Remove the prompt and return. */
         goto finished_pausing;
      }

      /* If user chooses to stop display. */
      else if(chKeyPressed == tolower(od_control.od_continue_no) ||
              chKeyPressed == toupper(od_control.od_continue_no) ||
              chKeyPressed == 's' || chKeyPressed == 'S' || chKeyPressed == 3
              || chKeyPressed == 11 || chKeyPressed == 0x18)
      {
         /* If we are operating in remote mode. */
         if(od_control.baud)
         {
            /* Clear the output buffer. */
            ODComClearOutbound(hSerialPort);
         }

         /* Tell the caller to stop displaying more text. */
         bToReturn = TRUE;

         /* Remove the prompt and return. */
         goto finished_pausing;
      }
   }

finished_pausing:
   /* Remove the pause prompt. */
   for(btCount = 0; btCount < nPromptLength; ++btCount)
   {
      od_disp_str(szBackspaceWithDelete);
   }

   return(bToReturn);
}


/* ----------------------------------------------------------------------------
 * od_control_get()
 *
 * Returns a pointer to the od_control structure containing information
 * and settings associated with the current session.
 *
 * Parameters: None.
 *
 *     Return: A pointer to the od_control structure associated with this
 *             session.
 */
ODAPIDEF tODControl * ODCALL od_control_get(void)
{
   /* Log function entry if running in trace mode */
   TRACE(TRACE_API, "od_disp_str()");

   return(&od_control);
}