/*
 *  JOURNAL.C  -  Records all mouse and keyboard activity so that
 *                it can be played back for demonstration of products,
 *                reporting errors, etc.
 *
 *             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[32] = "Journal v1.0 (June 1987)";
char *author     = "Copyright (c) 1987 by Davide P. Cervone";


/*
 *  Macros used to check for end-of-journal
 */
#define CTRL_AMIGA      (IEQUALIFIER_CONTROL | IEQUALIFIER_LCOMMAND)
#define KEY_E           0x12
#define CTRL_AMIGA_E(e) ((((e)->ie_Qualifier & CTRL_AMIGA) == CTRL_AMIGA) &&\
                          ((e)->ie_Code == KEY_E))

/*
 *  Match a command-line argument against a string (case insensitive)
 */
#define ARGMATCH(s)   (stricmp(s,*Argv) == 0)

/*
 *  Functions that JOURNAL can perform
 */
#define SHOW_USAGE      0
#define WRITE_JOURNAL   1
#define JUST_EXIT       2

/*
 *  Largest mouse move we want to record
 */
#define MAXMOUSEMOVE   32

/*
 *  Macros to tell whether a mouse movement event can be compressed with
 *  other mouse movement events
 */
#define MOUSEMOVE(e)\
   ((e)->ie_Class == IECLASS_RAWMOUSE && (e)->ie_Code == IECODE_NOBUTTON &&\
   ((e)->ie_Qualifier & IEQUALIFIER_RELATIVEMOUSE))
#define BIGX(e)         ((e)->ie_X >= XMINMOUSE || (e)->ie_X <= -XMINMOUSE)
#define BIGY(e)         ((e)->ie_Y >= YMINMOUSE || (e)->ie_Y <= -YMINMOUSE)
#define BIGTICKS(e)     ((e)->my_Ticks > LONGTIME)
#define NOTSAVED(e)     ((e)->my_Saved == FALSE)
#define SETSAVED(e)     ((e)->my_Saved = TRUE)


/*
 *  Global Variables:
 */

struct MsgPort *InputPort = NULL;     /* Port used to talk to Input.Device */
struct IOStdReq *InputBlock = NULL;   /* request block used with Input.Device */
struct Task *theTask = NULL;          /* pointer to our task */
LONG InputDevice = 0;                 /* flag whether Input.Device is open */
LONG theSignal = 0;                   /* signal used when an event is ready */
LONG ErrSignal = 0;                   /* signal used when an error occured */
LONG theMask;                         /* 1 << theSignal */
LONG ErrMask;                         /* 1 << ErrSignal */

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

WORD xmove = XDEFMIN;                 /* distance to compress into one event */
WORD ymove = YDEFMIN;                 /* distance to compress into one event */
WORD smoothxmove = 1;                 /* distance for smoothed events */
WORD smoothymove = 1;                 /* distnace for smoothed events */
WORD xminmove, yminmove;              /* distance actually in use */
UWORD SmoothMask = IEQUALIFIER_LCOMMAND;
                                      /* what keys are required for smoothing */
UWORD SmoothTrigger = 0xFFFF;         /* any of these keys trigger smoothing */

int Action = WRITE_JOURNAL;           /* action to be perfomed by JOURNAL */
int ArgMatched = FALSE;               /* TRUE if a parameter matched OK */
int Argc;                             /* global version of argc */
char **Argv;                          /* global version of argv */

struct InputEvent **EventPtr = NULL;  /* pointer to (pointer to next event) */
struct InputEvent *OldEvent = NULL;   /* pointer to last event */
struct SmallEvent TinyEvent;          /* packed event (ready to record) */

FILE *OutFile = NULL;                 /* where the events will be written */
char *JournalFile = NULL;             /* name of the output file */

int NotDone = TRUE;                   /* continue looking for events? */
int NotFirstEvent = FALSE;            /* TRUE after an event was recorded */
int PointerNotHomed = TRUE;           /* TRUE until pointer is moved */

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 */
};

struct InputEvent PointerToHome =     /* event to put pointer in upper-left */
{
   NULL,                                /* pointer to next event */
   IECLASS_RAWMOUSE,                    /* ie_Class = RAWMOUSE */
   0,                                   /* ie_SubClass */
   IECODE_NOBUTTON,                     /* ie_Code = NOBUTTON (just a move) */
   IEQUALIFIER_RELATIVEMOUSE,           /* ie_Qualifier = relative move */
   {-1000,-1000},                       /* move far to left and top */
   {0L,0L}                              /* seconds and micros */
};

struct SmallEvent TimeEvent =         /* pause written at beginning of file */
{
   {0xE2, 0, 0},                        /* MOUSEMOVE, NOBUTTON, X=0, Y=0 */
   IEQUALIFIER_RELATIVEMOUSE,           /* Qualifier */
   0x00A00000                           /* 1 second pause */
};


/*
 *  myHandler()
 *
 *  This is the input handler that makes copies of the input events and sends 
 *  them the to main process to be written to the output file.
 *
 *  The first time around, we add the PointerToHome event into the stream
 *  so that the pointer is put into a known position.
 *
 *  We check the event type of each event in the list, and do the following:
 *  for Timer events, we increment the tick count (which tells how many ticks
 *  have occured since the last recorded event); for raw key events, we check
 *  whether a CTRL-AMIGA-E has been pressed (if so, we signal the main process
 *  with a CTRL-E which tells it to remove the handler and quit); for raw 
 *  mouse and raw key events, we allocate memory for a new copy of the event
 *  (and signal an error if we can't), and copy the pertinent information
 *  from the current event into the copy event and mark it as not-yet-saved.
 *  We link it into the copied-event list (via EventPtr), and signal the
 *  main task that a new event is ready, and then zero the tick count.
 *
 *  Any other type of event is ignored.
 *
 *  When we are through with the event list, we return it so that Intuition
 *  can use it to do its thing.
 */

struct InputEvent *myHandler(event,data)
struct InputEvent *event;
APTR data;
{
   struct InputEvent *theEvent = event;
   struct InputEvent *theCopy;

   Forbid();
   if (PointerNotHomed)
   {
      PointerToHome.ie_NextEvent = event;
      event = &PointerToHome;
      PointerNotHomed = FALSE;
   }
   while(theEvent)
   {
      switch(theEvent->ie_Class)
      {
         case IECLASS_TIMER:
            Ticks++;
            TimerMics = theEvent->ie_Mics;
            break;

         case IECLASS_RAWKEY:
            if (CTRL_AMIGA_E(theEvent)) Signal(theTask,SIGBREAKF_CTRL_E);

         case IECLASS_RAWMOUSE:
            theCopy = NEWEVENT;
            if (theCopy == NULL)
            {
               Signal(theTask,ErrMask);
            } else {
               theCopy->ie_NextEvent    = NULL;
               theCopy->ie_Class        = theEvent->ie_Class;
               theCopy->ie_Code         = theEvent->ie_Code;
               theCopy->ie_Qualifier    = theEvent->ie_Qualifier;
               theCopy->ie_EventAddress = theEvent->ie_EventAddress;
               theCopy->my_Time         = TIME;
               theCopy->my_Ticks        = Ticks;
               theCopy->my_Saved        = FALSE;
               *EventPtr = theCopy;
               EventPtr = &(theCopy->ie_NextEvent);
               Signal(theTask,theMask);
               Ticks = 0;
            }
            break;
      }
      theEvent = theEvent->ie_NextEvent;
   }
   Permit();
   return(event);
}


/*
 *  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.  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 (OldEvent)    FREEVENT(OldEvent);
   if (OutFile)     fclose(OutFile);
   if (InputDevice) CloseDevice(InputBlock);
   if (InputBlock)  DeleteStdIO(InputBlock);
   if (InputPort)   DeletePort(InputPort);
   if (theSignal)   FreeSignal(theSignal);
   if (ErrSignal)   FreeSignal(ErrSignal);
   exit(status);
}


/*
 *  CheckNumber()
 *
 *  Check a command-line argument for the given keyword, and if it matches,
 *  makes sure that the next parameter is a positive numeric value that
 *  is less than the maximum expected value.
 */

void CheckNumber(keyword,value)
char *keyword;
WORD *value;
{
   long lvalue = *value;

   if (Argc > 1 && ARGMATCH(keyword))
   {
      ArgMatched = TRUE;
      Argc--;
      if (sscanf(*(++Argv),"%ld",&lvalue) != 1)
      {
         printf("%s must be numeric:  '%s'\n",keyword,*Argv);
         Action = JUST_EXIT;
      }
      if (lvalue < 1 || lvalue > MAXMOUSEMOVE)
      {
         printf("%s must be positive and less than %d:  '%ld'\n",
            keyword,MAXMOUSEMOVE,lvalue);
         Action = JUST_EXIT;
      }
      *value = lvalue;
   }
}


/*
 *  CheckHexNum()
 *
 *  Check a command-line argument for the given keyword, and if it
 *  matches, make sure that the next parameter is a legal HEX value.
 */

void CheckHexNum(keyword,value)
char *keyword;
WORD *value;
{
   ULONG lvalue = *value;

   if (Argc > 1 && ARGMATCH(keyword))
   {
      ArgMatched = TRUE;
      Argc--;
      if (sscanf(*(++Argv),"%lx",&lvalue) != 1)
      {
         printf("%s must be a HEX number:  '%s'\n",keyword,*Argv);
         Action = JUST_EXIT;
      }
      *value = lvalue;
   }
}


/*
 *  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 "TO".  If no file is specified, then show the usage.  DX and DY
 *  set the "granularity" of the mouse moves recorded (moves are combined
 *  into a single event until it's movement excedes either DX or DY).
 *  Similarly, SMOOTHX and SMOOTHY set alternate DX and DY values
 *  for when extra precision is needed (i.e., when drawing curves in a paint
 *  program).  SMOOTH specifies what qualifier keys MUST be present to 
 *  activate the SMOOTHX and SMOOTHY values, and TRIGGER specifies a set of
 *  qualifiers any one of which (together with the SMOOTH qualifiers)
 *  will active the SMOOTHX and SMOOTHY values.  In other words, all the
 *  SMOOTH qualifiers plus at least one of the TRIGGER qualifiers must be
 *  pressed in order to activate the smooth values.  For example, if SMOOTH
 *  is 0 and TRIGGER is 0x6000, then holding down either the left or the
 *  right button will activate the smooth values.  If SMOOTH is 0x0040 rather
 *  than 0, then the left Amiga button must also be held down in order to 
 *  activate SMOOTHX and SMOOTHY.  The qualifier flags are listed in
 *  DEVICES/INPUTEVENT.H
 *
 *  The default values are DX = 8, DY = 8, SMOOTHX = 1, SMOOTHY = 1,
 *  SMOOTH = left Amiga, TRIGGER = 0xFFFF.
 */

int ParseArguments(argc,argv)
int argc;
char **argv;
{
   Argc = argc;
   Argv = argv;

   while (--Argc > 0)
   {
      ArgMatched = FALSE;
      Argv++;
      if (Argc > 1 && ARGMATCH("TO"))
      {
         JournalFile = *(++Argv);
         Argc--;
         ArgMatched = TRUE;
      }
      CheckNumber("DX",&xmove);
      CheckNumber("DY",&ymove);
      CheckNumber("SMOOTHX",&smoothxmove);
      CheckNumber("SMOOTHY",&smoothymove);
      CheckHexNum("SMOOTH",&SmoothMask);
      CheckHexNum("TRIGGER",&SmoothTrigger);
      if (ArgMatched == FALSE)
      {
         if (JournalFile == NULL)
            JournalFile = *Argv;
           else
            Action = SHOW_USAGE;
      }
   }
   if (JournalFile == NULL && Action == WRITE_JOURNAL) Action = SHOW_USAGE;
   return(Action);
}


/*
 *  OpenJournal()
 *
 *  Open the journal file and check for errors.  Write the version
 *  information to the file.
 */

void OpenJournal()
{
   OutFile = fopen(JournalFile,"w");
   if (OutFile == NULL)
      DoExit("Can't Open Journal File '%s', error %ld",JournalFile,_OSERR);
   if (fwrite(version,sizeof(version),1,OutFile) != 1)
      DoExit("Error writing to output file:  %ld",_OSERR);
}


/*
 *  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 Get 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 JOURNAL was run) and allocate some signals for 
 *  new events and errors (so the input handler can signal them).
 */

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


/*
 *  SetupEvents()
 *
 *  Get a fake old-event to start off with, and mark it as saved (so we don't
 *  really try to use it).  Make it the end of the list (set its next pointer
 *  to NULL.  Tell the input handler where to start allocating new events
 *  by setting EventPtr to point the the next-pointer.  When the input
 *  handler allocates a new copy of an event, it will link it to this one
 *  so the main process can find it by following the next-pointer from the
 *  old event.
 */

void SetupEvents()
{
   if ((OldEvent = NEWEVENT) == NULL) DoExit("No Memory for OldEvent");
   SETSAVED(OldEvent);
   OldEvent->ie_NextEvent = NULL;
   EventPtr = &(OldEvent->ie_NextEvent);
}


/*
 *  AddHandler()
 *
 *  Add the input handler to the input.device handler chain.  Since the
 *  priority is 51, it will appear BEFORE intuition, so all it should
 *  see are raw key, raw mouse, timer, and disk insert/remove events.
 */
 
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-AMIGA-E to End Journal\n",version);
}


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

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


/*
 *  SaveEvent()
 *
 *  Pack an InputEvent into a SmallEvent (by shifting bits around) so that
 *  it takes up less space in the output file.  Write the SmallEvent to the
 *  output file, and mark it as already-saved.
 */

void SaveEvent(theEvent)
struct InputEvent *theEvent;
{
   if (theEvent->my_Time > MILLION) theEvent->my_Time += MILLION;
   TinyEvent.se_XY        = 0;
   TinyEvent.se_Type      = theEvent->ie_Class;
   TinyEvent.se_Qualifier = theEvent->ie_Qualifier;
   TinyEvent.se_Long2     = (theEvent->my_Ticks << 20) |
                            (theEvent->my_Time & 0xFFFFF);

   if (theEvent->ie_Class == IECLASS_RAWKEY)
   {
      TinyEvent.se_Code  = theEvent->ie_Code;
      TinyEvent.se_Prev  = theEvent->my_Prev;
   } else {
      TinyEvent.se_Type |= (theEvent->ie_Code & IECODE_UP_PREFIX) |
                           ((theEvent->ie_Code & 0x03) << 5);
      TinyEvent.se_XY |= (theEvent->ie_X & 0xFFF) |
                         ((theEvent->ie_Y & 0xFFF) << 12);
   }

   if (fwrite((char *)&TinyEvent,sizeof(TinyEvent),1,OutFile) != 1)
      DoExit("Error writing to output file:  %ld",_OSERR);
   SETSAVED(theEvent);
   NotFirstEvent = TRUE;
}


/*
 *  SaveTime()
 *
 *  Save a fake mouse event that doesn't move anywhere but that includes a
 *  tick count.  That is, pause without moving the mouse.
 */

void SaveTime(theEvent)
struct InputEvent *theEvent;
{
   if (NotFirstEvent) TimeEvent.se_Ticks = (theEvent->my_Ticks << 20);
   if (fwrite((char *)&TimeEvent,sizeof(TimeEvent),1,OutFile) != 1)
      DoExit("Error writing to output file:  %ld",_OSERR);
   theEvent->my_Ticks = 0;
}


/*
 *  SaveEventList()
 *
 *  Write the events in the event list (built by the input handler) out
 *  to the output file, compressing multiple, small mouse moves into larger,
 *  single mouse moves (to save space in the output file) in the following 
 *  way:
 *
 *    if the current event is a mouse move (not a button press), then
 *      set its event count to 1 (the number of events compressed into it),
 *      if the user is requesting smooth movement, then use the smooth
 *        movement variables, otherwise use the course (normal) values. 
 *      if the event's x or y movement is big enough, or if there was a long
 *           pause before the movement occured, then
 *        if the old event was not saved, save it.
 *        if the pause was long enough, save a separate pause (so that the
 *           smoothing algorithm in PLAYBACK does not spread the pause over
 *           the entire mouse move).
 *        save the current event.
 *      otherwise, (we can compress the movement)
 *        if there was an old mouse event that was not saved,
 *          add it to the current event,
 *          if the new x or y movement is big enough to record, do so.
 *    otherwise, (this was not a mouse movement)
 *      if there was a previous mouse movement that was not saved, save it.
 *      finally, save the current event.
 *  At this point the OldEvent is either posted, or has been combined with the
 *  current event, so we can free the old event.  The current event then
 *  becomes the old event.
 */

void SaveEventList()
{
   struct InputEvent *theEvent;
   
   while ((theEvent = OldEvent->ie_NextEvent) != NULL)
   {
      if (MOUSEMOVE(theEvent))
      {
         theEvent->my_Count &= (~COUNTMASK);
         theEvent->my_Count++;
         if ((theEvent->ie_Qualifier & SmoothMask) == SmoothMask &&
             (theEvent->ie_Qualifier & SmoothTrigger))
         {
            xminmove = smoothxmove;
            yminmove = smoothymove;
         } else {
            xminmove = xmove;
            yminmove = ymove;
         }
         if (BIGX(theEvent) || BIGY(theEvent) || BIGTICKS(theEvent))
         {
            if (NOTSAVED(OldEvent)) SaveEvent(OldEvent);
            if (BIGTICKS(theEvent)) SaveTime(theEvent);
            SaveEvent(theEvent);
         } else {
            if (NOTSAVED(OldEvent))
            {
               theEvent->ie_X += OldEvent->ie_X;
               theEvent->ie_Y += OldEvent->ie_Y;
               theEvent->my_Ticks += OldEvent->my_Ticks;
               theEvent->my_Count += OldEvent->my_Count & COUNTMASK;
               if (BIGX(theEvent) || BIGY(theEvent)) SaveEvent(theEvent);
            }
         }
      } else {
         if (NOTSAVED(OldEvent)) SaveEvent(OldEvent);
         SaveEvent(theEvent);
      }
      FREEVENT(OldEvent);
      OldEvent = theEvent;
   }
}


/*
 *  RecordJournal()
 *
 *  Open the journal file, set up the task and signals, and set up the
 *  initial pointers for the event list.  Then add the input handler
 *  into the Input.Device handler chain.  
 *
 *  Wait for the input handler to signal us that an event is ready (or that
 *  an error occured), or that the user to press CTRL-AMIGA-E.  If it's the 
 *  latter, cancel the Wait loop, otherwise save the events that are in the
 *  list into the file.  If the error signal was sent, inform the user that
 *  some events were lost.
 *
 *  Once we are signaled to end the journal, remove the handler, and
 *  record any remaining, unsaved events to the file.
 */

void RecordJournal()
{
   LONG signals;
   LONG SigMask;
   
   OpenJournal();
   SetupTask();
   SetupEvents();
   SigMask = theMask | ErrMask | SIGBREAKF_CTRL_E;

   AddHandler(&myHandler);
   while (NotDone)
   {
      signals = Wait(SigMask);
      if (signals & SIGBREAKF_CTRL_E)
         NotDone = FALSE;
        else
         SaveEventList();
      if (signals & ErrMask)
         printf("[ Out of memory - some events not recorded ]\n");
   }
   RemoveHandler(&myHandler);
   SaveEventList();
}


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

void main(argc,argv)
int argc;
char **argv;
{
   switch(ParseArguments(argc,argv))
   {
      case SHOW_USAGE:
         printf("Usage:  JOURNAL [TO] file [DX x] [DY y]\n");
         printf("                [SMOOTHX x] [SMOOTHY y] [SMOOTH mask]");
         printf(               " [TRIGGER mask]\n");
         break;

      case WRITE_JOURNAL:
         RecordJournal();
         break;
   }
   DoExit(NULL);
}
