/*
 *  PLAYBACK.C  -  Plays back mouse and keyboard events that were recorded
 *                 by the JOURNAL program.
 *
 *             Copyright (c) 1987 by Davide P. Cervone
 *  You may use this code provided this copyright notice is kept intact.
 */

#include "journal.h"

/*
 *  Version number and author
 */
char *version = "Playback v1.0 (June 1987)";
char *author  = "Copyright (c) 1987 by Davide P. Cervone";

/*
 *  Usage string
 */
#define USAGE   "PLAYPACK [FROM] file [EVENTS n] [[NO]SMOOTH"

/*
 *  Macros to tell whether the user pressed CTRL-C
 */
#define CONTROL       IEQUALIFIER_CONTROL
#define KEY_C         0x33
#define CTRL_C(e)     (((e)->ie_Qualifier & CONTROL) && ((e)->ie_Code == KEY_C))

/*
 *  The packed code for a RAWMOUSE event with NOBUTTON pressed (i.e., one
 *  that probably contains more than one event compressed into a single 
 *  entry in the file).
 */
#define MOUSEMOVE     0xE2

/*
 *  Macro to check whether a command-line argument matches a given string
 */
#define ARGMATCH(s)   (stricmp(s,*argv) == 0)

/*
 *  The functions that PLAYBACK can perform
 */
#define SHOW_USAGE    0
#define READ_JOURNAL  1
#define JUST_EXIT     2


/*
 *  Global Variables
 */

struct MsgPort *InputPort = NULL;       /* Port for the Input.Device */
struct IOStdReq *InputBlock = NULL;     /* Request block for the Input.Device */
struct Task *theTask = NULL;            /* pointer to the main process */
int  HandlerActive = FALSE;             /* TRUE when handler has been added */
LONG InputDevice = FALSE;               /* TRUE when Input.Device is open */
LONG theSignal = 0;                     /* used when an event is freed */
LONG theMask;                           /* 1 << theSignal */

UWORD Ticks = 0;                        /* number of ticks between events */
LONG  TimerMics = 0;                    /* last timer event's micros field */
LONG  TimerSecs = 0;                    /* last timer event's seconds field */

struct InputEvent *Event = NULL;        /* pointer to array of input events */
struct SmallEvent TinyEvent;            /* a compressed event from the file */
long   MaxEvents  = 50;                 /* size of the Event array */
short  Smoothing  = TRUE;               /* TRUE if smoothing requested */
short  LastPosted = 0;                  /* Event index for last-posted event */
short  NextToPost = 0;                  /* Event index for next event to post */
short  NextFree   = 0;                  /* Event index for next event to use */

FILE *InFile = NULL;                    /* journal file pointer */
char *JournalFile = NULL;               /* name of journal file */


struct Interrupt HandlerData =          /* used to add an input handler */
{
   {NULL, NULL, 0, 51, NULL},             /* Node structure (nl_Pri = 51) */
   NULL,                                  /* data pointer */
   &myHandlerStub                         /* code pointer */
};


/*
 *  myHandler()
 *
 *  This is the input handler that posts the events read from the journal file.
 *
 *  First, free any events that were posted last time myHandler was
 *  called by the Input.Device.  Signal the main process when any are freed,
 *  in case it is waiting for an event to be freed.
 *
 *  Then, look through the list of events received from the Input.Device.
 *  Check whether a new event is ready to be posted (i.e., one is available
 *  and the proper number of ticks have been counted).  If so, then set its
 *  time fields to the proper time, add it into the event list, and look at
 *  the next event.  Set the tick count to zero again and check the next
 *  event in the array.
 *
 *  Once any new events have been added, check whether the current event
 *  from the Input.Device is a timer event.  If so, then increment the tick 
 *  count and record its time field.  If not, then check whether it is a
 *  raw mouse or raw key event.  If it is, then if it is a CTRL-C, signal the 
 *  main process that the user wants to abort the playback.  Remove the mouse
 *  or key event from the event list so that it will not interfere with the 
 *  playback events (i.e., the keyboard and mouse are disabled while PLAYBACK
 *  is running).
 *
 *  Finally, go on to the the next event in the chain and continue the loop.
 *  Once all the events have been processed, return the modified list
 *  (with new events added from the file and keyboard and mouse events removed)
 *  so that Intuition can act on them.
 */

struct InputEvent *myHandler(EventList,data)
struct InputEvent *EventList;
APTR data;
{
   struct InputEvent **EventPtr = &EventList;
   struct InputEvent *toPost = &Event[NextToPost];
   
   while (NextToPost != LastPosted)
   {
      Event[LastPosted].my_InUse = FALSE;
      Event[LastPosted].my_Ready = FALSE;
      LastPosted = (LastPosted + 1) % MaxEvents;
      Signal(theTask,theMask);
   }
   Forbid();
   while (*EventPtr)
   {
      while (toPost->my_Ready && Ticks >= toPost->my_Ticks)
      {
         toPost->ie_Secs = TimerSecs;
         toPost->ie_Mics += TimerMics;
         if (toPost->ie_Mics > MILLION)
         {
            toPost->ie_Secs++;
            toPost->ie_Mics -= MILLION;
         }
         toPost->ie_NextEvent = *EventPtr;
         *EventPtr = toPost;
         EventPtr = &(toPost->ie_NextEvent);
         NextToPost = (NextToPost + 1) % MaxEvents;
         toPost = &Event[NextToPost];
         Ticks = 0;
      }
      if ((*EventPtr)->ie_Class == IECLASS_TIMER)
      {
         Ticks++;
         TimerSecs = (*EventPtr)->ie_Secs;
         TimerMics = (*EventPtr)->ie_Mics;
      } else {
         if ((*EventPtr)->ie_Class == IECLASS_RAWMOUSE ||
             (*EventPtr)->ie_Class == IECLASS_RAWKEY)
         {
            if (CTRL_C(*EventPtr)) Signal(theTask,SIGBREAKF_CTRL_C);
            *EventPtr = (*EventPtr)->ie_NextEvent;
         }
      }
      EventPtr = &((*EventPtr)->ie_NextEvent);
   }
   Permit();
   return(EventList);
}


/*
 *  Ctrl_C()
 *
 *  Dummy routine to disable Lattice-C CTRL-C trapping.
 */

#ifndef MANX
int Ctrl_C()
{
   return(0);
}
#endif


/*
 *  DoExit()
 *
 *  General purpose exit routine.  If 's' is not NULL, then print an
 *  error message with up to three parameters.  Remove the handler (if
 *  it is active), free any memory, close any open files, delete any ports, 
 *  free any used signals, etc.
 */

void DoExit(s,x1,x2,x3)
char *s, *x1, *x2, *x3;
{
   long status = 0;
   
   if (s != NULL)
   {
      printf(s,x1,x2,x3);
      printf("\n");
      status = RETURN_ERROR;
   }
   if (HandlerActive) RemoveHandler();
   if (Event)         FreeMem(Event,IE_SIZE * MaxEvents);
   if (InFile)        fclose(InFile);
   if (InputDevice)   CloseDevice(InputBlock);
   if (InputBlock)    DeleteStdIO(InputBlock);
   if (InputPort)     DeletePort(InputPort);
   if (theSignal)     FreeSignal(theSignal);
   exit(status);
}

/*
 *  ParseArguements()
 *
 *  Check that all the command-line arguments are valid and set the 
 *  proper variables as requested by the user.  If no keyword is specified,
 *  assume "FROM".  If no file is specified, then show the usage.  EVENTS
 *  regulates the size of the Event array used for buffering event 
 *  communication between the main process and the handler.  SMOOTH and
 *  NOSMOOTH regulate the interpolation of mouse movements between recorded
 *  events.  The default is SMOOTH.
 */

int ParseArguments(argc,argv)
int argc;
char **argv;
{
   int function = READ_JOURNAL;

   while (--argc > 0)
   {
      argv++;
      if (argc > 1 && ARGMATCH("FROM"))
      {
         JournalFile = *(++argv);
         argc--;
      }
      else if (argc > 1 && ARGMATCH("EVENTS"))
      {
         argc--;
         if (sscanf(*(++argv),"%ld",&MaxEvents) != 1)
         {
            printf("Event count must be numeric:  '%s'\n",*argv);
            function = JUST_EXIT;
         }
         if (MaxEvents <= 1)
         {
            printf("Event count must be greater than 1:  '%d'\n",MaxEvents);
            function = JUST_EXIT;
         }
      }
      else if (ARGMATCH("NOSMOOTH")) Smoothing = FALSE;
      else if (ARGMATCH("SMOOTH"))   Smoothing = TRUE;
      else if (JournalFile == NULL) JournalFile = *argv;
      else function = SHOW_USAGE;
   }
   if (JournalFile == NULL && function == READ_JOURNAL) function = SHOW_USAGE;
   return(function);
}

/*
 *  OpenJournal()
 *
 *  Open the journal file and check for errors.  Read the version
 *  information to the file (someday we may need to check this).
 */

void OpenJournal()
{
   char fileversion[32];

   InFile = fopen(JournalFile,"r");
   if (InFile == NULL)
      DoExit("Can't Open Journal File '%s', error %ld",JournalFile,_OSERR);
   if (fread(fileversion,sizeof(fileversion),1,InFile) != 1)
      DoExit("Can't read version from '%s', error %ld",JournalFile,_OSERR);
}


/*
 *  GetEventMemory()
 *
 *  Allocate memory for the Event array (of size MaxEvents, specified by
 *  the EVENT option).
 */

void GetEventMemory()
{
   Event = AllocMem(IE_SIZE * MaxEvents, MEMF_CLEAR);
   if (Event == NULL) DoExit("Can't get memory for %d Events",MaxEvents);
}


/*
 *  GetSignal()
 *
 *  Allocate a signal (error if none available) and set the mask to
 *  the proper value.
 */

void GetSignal(theSignal,theMask)
LONG *theSignal, *theMask;
{
   LONG signal;

   if ((signal = AllocSignal(-ONE)) == -ONE) DoExit("Can't Allocate Signal");
   *theSignal = signal;
   *theMask = (ONE << signal);
}


/*
 *  SetupTask();
 *
 *  Find the task pointer for the main task (so the input handler can 
 *  signal it).  Clear the CTRL signal flags (so we don't get any left
 *  over from before PLAYBACK was run) and allocate a signal for 
 *  when the handler frees an event.
 */

void SetupTask()
{
   theTask = FindTask(NULL);
   SetSignal(0L,SIGBREAKF_ANY);
   GetSignal(&theSignal,&theMask);
   #ifndef MANX
      onbreak(&Ctrl_C);
   #endif
}


/*
 *  AddHandler()
 *
 *  Add the input handler to the Input.Device handler chain.  Since the
 *  priority is 51 it will appear BEFORE intuition, so when we insert
 *  new events into the chain, Intuition will process them just as though
 *  they came from the Input.Device.
 */
 
void AddHandler()
{
   long status;

   if ((InputPort = CreatePort(0,0)) == NULL)
      DoExit("Can't Create Port");
   if ((InputBlock = CreateStdIO(InputPort)) == NULL)
      DoExit("Can't Create Standard IO Block");
   InputDevice = (OpenDevice("input.device",0,InputBlock,0) == 0);
   if (InputDevice == 0) DoExit("Can't Open Input.Device");
   
   InputBlock->io_Command = IND_ADDHANDLER;
   InputBlock->io_Data    = (APTR) &HandlerData;
   if (status = DoIO(InputBlock)) DoExit("Error from DoIO:  %ld",status);
   printf("%s - Press CTRL-C to Cancel\n",version);
   HandlerActive = TRUE;
}

/*
 *  RemoveHandler()
 *
 *  Remove the input handler from the Input.Device handler chain.
 */
 
void RemoveHandler()
{
   long status;

   if (HandlerActive && InputDevice && InputBlock)
   {
      HandlerActive = FALSE;
      InputBlock->io_Command = IND_REMHANDLER;
      InputBlock->io_Data = (APTR) &HandlerData;
      if (status = DoIO(InputBlock)) DoExit("Error from DoIO:  %ld",status);
   }
   printf("Playback Complete\n");
}


/*
 *  Create an event that moves the pointer to the upper, left-hand corner
 *  of the screen so that the pointer is at a known position.  This is a
 *  large relative move (-1000,-1000).
 */

void PointerToHome()
{
   struct InputEvent *theEvent = &(Event[0]);
   
   theEvent->ie_Class     = IECLASS_RAWMOUSE;
   theEvent->ie_Code      = IECODE_NOBUTTON;
   theEvent->ie_Qualifier = IEQUALIFIER_RELATIVEMOUSE;
   theEvent->ie_X         = -1000;
   theEvent->ie_Y         = -1000;
   theEvent->my_Ticks     = 0;
   theEvent->my_Time      = 0;
   theEvent->my_Ready     = READY;
}


/*
 *  CheckForCTRLC()
 *
 *  Read the current task signals (without changing them) and check whether
 *  a CTRL-C has been signalled.  If so, abort the playback.
 */

void CheckForCTRLC()
{
   LONG signals = SetSignal(0,0);
   
   if (signals & SIGBREAKF_CTRL_C) DoExit("Playback Aborted");
}


/*
 *  GetNextFree()
 *
 *  Set NextFree to point to the next free event in the Event array.
 *  If there are no free events, Wait() for the handler to signal that it
 *  has freed one (or for CTRL-C to be pressed).
 */

void GetNextFree()
{
   LONG signals;

   NextFree = (NextFree + 1) % MaxEvents;
   while (Event[NextFree].my_InUse)
   {
      signals = Wait(theMask | SIGBREAKF_CTRL_C);
      if (signals & SIGBREAKF_CTRL_C) DoExit("Playback Aborted");
   }
}


#define ABS(x)  (((x)<0)?-(x):(x))


/*
 *  MovePointer()
 *
 *  Interpolate mouse move events that were compressed into one record in
 *  the journal file.  The se_Count field holds the number of events that
 *  were compressed into one.
 *
 *  First, unpack the X and Y movements.  Record their directions in dx and dy
 *  and their magnitudes in abs_x and abs_y.  Reduce 'count' if there would
 *  be events with offset of (0,0).  'x_move' specifies the x-offset for each
 *  event and 'x_add' specifies the fraction of a pixel correction that must
 *  be made (x_add/count is the fraction).  'x_count' counts the fraction of
 *  a pixel that has been added so far (when x_count/count >= 1 (i.e., when
 *  x_count >= count) we add another pixel to the x-offset).  Similarly for
 *  the y and t variables (t is for ticks).  Starting the counts at 'count/2' 
 *  makes for smoother movement.
 *
 *  Once these are set up, we create new mouse move events with the proper
 *  offsets, adding up the fractions of pixels and adding in addional 
 *  movements whenever the fractions add up to a whole pixel (or tick).
 *  When a new event is set up, we mark it as READY so that the handler will
 *  see it and post it.
 *
 *  Once we have sent all the events, the mouse should be in the proper
 *  position, so we set the tick count and XY-offset fields to 0.
 */

void MovePointer()
{
   WORD abs_x,abs_y, x_count,y_count, dx,dy, x_add,y_add, x_move,y_move;
   WORD t_count, t_add, t_move;
   WORD x = TinyEvent.se_XY & 0xFFF;
   WORD y = (TinyEvent.se_XY >> 12) & 0xFFF;
   WORD i, count = TinyEvent.se_Count & COUNTMASK;
   LONG Time = TinyEvent.se_Micros & 0xFFFFF;
   LONG Ticks = TinyEvent.se_Ticks >> 20;
   struct InputEvent *NewEvent;

   x_count = y_count = t_count = 0;
   if (x & 0x800) x |= 0xF000;
   if (x < 0) dx = -1; else dx = 1;
   if (y & 0x800) y |= 0xF000;
   if (y < 0) dy = -1; else dy = 1;
   abs_x = ABS(x); abs_y = ABS(y);
   if (abs_x > abs_y)
   {
      if (count > abs_x) count = abs_x;
   } else {
      if (count > abs_y) count = abs_y;
   }
   if (count)
   {
      x_move = x / count; y_move = y / count; t_move = Ticks / count;
      x_add = abs_x % count; y_add = abs_y % count; t_add = Ticks % count;
   } else {
      x_move = x; y_move = y; t_move = Ticks;
      x_add = y_add = t_add = -1; count = 1;
   }
   x_count = y_count = t_count = count / 2;
   for (i = count; i > 0; i--)
   {
      GetNextFree();
      NewEvent = &Event[NextFree];
      NewEvent->ie_Class     = IECLASS_RAWMOUSE;
      NewEvent->ie_Code      = IECODE_NOBUTTON;
      NewEvent->ie_Qualifier = TinyEvent.se_Qualifier;
      NewEvent->ie_X         = x_move;
      NewEvent->ie_Y         = y_move;
      NewEvent->my_Ticks     = t_move;
      NewEvent->my_Time      = Time;
      if ((x_count += x_add) >= count)
      {
         x_count -= count;
         NewEvent->ie_X += dx;
      }
      if ((y_count += y_add) >= count)
      {
         y_count -= count;
         NewEvent->ie_Y += dy;
      }
      if ((t_count += t_add) > count)
      {
         t_count -= count;
         NewEvent->my_Ticks++;
      }
      NewEvent->my_Ready = READY;
   }
   TinyEvent.se_XY    &= 0xFF000000;
   TinyEvent.se_Ticks &= 0xFFFFF;
}


/*
 *  PostNextEvent()
 *
 *  Read an event from the journal file.  If we are smoothing and the 
 *  event is a mouse movement, them interpolate the compressed mouse
 *  movements.  Get the next event in the Event array and unpack the
 *  proper values from the TinyEvent read from the file.  Mark the finished
 *  event as READY so the handler will see it and post it.
 */

void PostNextEvent()
{
   struct InputEvent *NewEvent = NULL;

   if (fread((char *)&TinyEvent,sizeof(TinyEvent),1,InFile) == 1)
   {
      if (Smoothing && TinyEvent.se_Type == MOUSEMOVE) MovePointer();

      GetNextFree();
      NewEvent = &Event[NextFree];
   
      NewEvent->ie_Class     = TinyEvent.se_Type & 0x1F;
      NewEvent->ie_Qualifier = TinyEvent.se_Qualifier;
      NewEvent->my_Ticks     = TinyEvent.se_Ticks >> 20;
      NewEvent->my_Time      = TinyEvent.se_Micros & 0xFFFFF;
      
      switch(NewEvent->ie_Class)
      {
         case IECLASS_RAWKEY:
            NewEvent->ie_Code = TinyEvent.se_Code;
            NewEvent->ie_X = NewEvent->ie_Y = TinyEvent.se_Prev;
            break;

         case IECLASS_RAWMOUSE:
            NewEvent->ie_Code = (TinyEvent.se_Type >> 5) & 0x03;
            if (NewEvent->ie_Code == 0x03)
               NewEvent->ie_Code = IECODE_NOBUTTON;
              else
               NewEvent->ie_Code |= (TinyEvent.se_Type & IECODE_UP_PREFIX) |
                  (IECODE_LBUTTON & ~(0x03 | IECODE_UP_PREFIX));
            NewEvent->ie_X = TinyEvent.se_XY & 0xFFF;
            NewEvent->ie_Y = (TinyEvent.se_XY >> 12) & 0xFFF;
            NewEvent->my_Ticks = TinyEvent.se_Ticks >> 20;
            if (NewEvent->ie_X & 0x800) NewEvent->ie_X |= 0xF000;
            if (NewEvent->ie_Y & 0x800) NewEvent->ie_Y |= 0xF000;
            break;

         default:
            printf("[ Unknown Event Class:  %02X]\n",NewEvent->ie_Class);
            break;
      }
      NewEvent->my_Ready = READY;
   }
}


/*
 *  WaitForEvents()
 *
 *  Wait for the handler to finish posting all the events in the Event
 *  array (so we don't remove the handler before it is done).
 */

void WaitForEvents()
{
   short LastFree = NextFree;
   
   do GetNextFree(); while (NextFree != LastFree);
}


/*
 *  PlayJournal()
 *
 *  Open the journal file, set up the task and signals, and allocate the
 *  Event array.  Add the input handler and send the pointer to the upper,
 *  left-hand corner of the screen.  While there are still events in the
 *  journal file, check whether the user wants to cancel the playback and
 *  if not, post the next event in the file.  When the end-of-file is reached
 *  wait for the handler to finish posting all the events in the array, and 
 *  then remove the handler.
 */

void PlayJournal()
{
   OpenJournal();
   SetupTask();
   GetEventMemory();

   AddHandler(&myHandler);
   PointerToHome();

   while (feof(InFile) == 0)
   {
      CheckForCTRLC();
      PostNextEvent();
   }
   
   WaitForEvents();
   RemoveHandler();
}


/*
 *  main()
 *
 *  Parse the command-line arguments, and perform the proper function
 *  (either show the usage, read a journal, or fall through and exit).
 */

void main(argc,argv)
int argc;
char **argv;
{
   switch(ParseArguments(argc,argv))
   {
      case SHOW_USAGE:
         printf("Usage:  %s\n",USAGE);
         break;

      case READ_JOURNAL:
         PlayJournal();
         break;
   }
   DoExit(NULL);
}
