/*
 *  FRAME.C     A general-purpose bitmap creating library.  It allows you
 *              to select a portion of any screen by dragging a selection 
 *              rectangle, and returns a bitmap structure that contains
 *              a copy of the selected region.
 *
 *  Copyright 1990 by Davide P. Cervone, all rights reserved.
 *  You may use this code, provided this copyright notice is kept intact.
 */

#define INTUITIONPRIVATE  TRUE
#include <intuition/intuitionbase.h>
#include <exec/interrupts.h>
#include <exec/memory.h>
#include <devices/input.h>
#include <devices/inputevent.h>
#include <libraries/dos.h>

#include "Frame.h"

struct LayersBase *LayersBase;

struct Screen *theScreen;        /* the selected screen */
UBYTE PlaneMask = 0xFF;          /* which bitplanes to use */
UBYTE theDepth;                  /* the depth of the bitmap */
WORD theWidth, theHeight;        /* its width and height */
ULONG theModes;                  /* a copy of the screen's modes */
struct ColorMap *theColorMap;    /* a copy of the screen's color map */
struct RastPort *theRastPort;    /* the screen's RastPort */
struct BitMap theBitMap;         /* the copied bitmap */
WORD StartX, StartY;             /* position within the screen */
WORD CurrentX, CurrentY;         /* current mouse position */
WORD LeftX, TopY;                /* corner of selection rectangle */


extern struct MsgPort *CreatePort();
extern struct IOStdReq *CreateStdIO();
extern LONG AllocSignal(), Wait(), SetSignal();
extern APTR FindTask();
extern PLANEPTR AllocRaster();
extern struct ColorMap *GetColorMap();
extern APTR AllocMem();

extern void FrameHandlerStub();


#define INTUITION_REV   0L
#define LAYERS_REV      0L
#define GRAPHICS_REV    0L

#define EXIT_OK         0L
#define EXIT_ERROR      10L

#define STARTQUAL       (IEQUALIFIER_LSHIFT | IEQUALIFIER_RSHIFT)

#define NewX(i)         (Frame->XY[i*2])
#define NewY(i)         (Frame->XY[i*2+1])
#define OldX(i)         (Frame->NextBorder->XY[i*2])
#define OldY(i)         (Frame->NextBorder->XY[i*2+1])
#define SCREENX(x)      (((x) >> Xshift) - theScreen->LeftEdge)
#define SCREENY(y)      (((y) >> Yshift) - theScreen->TopEdge)
#define INTUIX(x)       ((x) << Xshift)
#define INTUIY(y)       ((y) << Yshift)
#define SCREENTOP\
   (theScreen->TopEdge << ((theScreen->ViewPort.Modes & LACE)? 0: 1))
#define CTRL_C(e)\
   ((e)->ie_Code == 0x33 && ((e)->ie_Qualifier & IEQUALIFIER_CONTROL))

#define SIGBREAKF_ANY\
   (SIGBREAKF_CTRL_C| SIGBREAKF_CTRL_D| SIGBREAKF_CTRL_E| SIGBREAKF_CTRL_F)

#define WIDTH           ((long)theWidth)
#define HEIGHT          ((long)theHeight)
#define DEPTH           ((long)theDepth)
#define BLT_COPY        0x000000C0
#define BLT_ALLPLANES   0x000000FF

struct MsgPort *InputPort = NULL;     /* Port used to talk to Input.Device */
struct IOStdReq *InputBlock = NULL;   /* request block used with Input.Device */
static short InputOpen = FALSE;       /* flag for when input.Device is open */
static APTR theTask;                  /* pointer to our task */
static LONG OkSignal;                 /* signal when movement detected */
static LONG EndSignal;                /* signal when frame is done */
static LONG OkMask;                   /* 1 << OkSignal */
static LONG EndMask;                  /* 1 << EndSignal */

static short OkToSignal = TRUE;       /* TRUE if not using shared memory */
static short MouseMoved = FALSE;      /* TRUE when mouse moves buffered */
static short RemoveEvent;             /* should an input event be removed? */
static short Xshift, Yshift;          /* HIRES and LACE adjustments */
static WORD Xs,Ys, Xc, Yc;            /* starting and current X,Y */
static WORD Hmin,Hmax, Wmin, Wmax;    /* min and max movement values */

static SHORT NewBox[5][2];            /* current select frame */
static SHORT OldBox[5][2];            /* previous select frame */

struct Border NewFrame = {0,0, 0x01,0, COMPLEMENT, 5, &NewBox[0][0], NULL};
struct Border OldFrame = {0,0, 0x01,0, COMPLEMENT, 5, &OldBox[0][0], &NewFrame};
struct Border *Frame = &OldFrame;

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


/*
 *  GetMinMax()
 *
 *  If a screen is selected,
 *    Get the min and max values from the screen
 *  Otherwise get them from Intuition itself
 */

static void GetMinMax()
{
   if (theScreen)
   {
      Hmin = INTUIY(theScreen->TopEdge);
      Hmax = INTUIY(theScreen->Height + theScreen->TopEdge - 1);
      if (Hmax > IntuitionBase->MaxYMouse) Hmax = IntuitionBase->MaxYMouse;
      Wmin = INTUIX(theScreen->LeftEdge);
      Wmax = INTUIX(theScreen->Width + theScreen->LeftEdge - 1);
      if (Wmax > IntuitionBase->MaxXMouse) Wmax = IntuitionBase->MaxXMouse;
   } else {
      Hmin = IntuitionBase->MinYMouse;
      Hmax = IntuitionBase->MaxYMouse;
      Wmin = IntuitionBase->MinXMouse;
      Wmax = IntuitionBase->MaxXMouse;
   }
}


/*
 *  DoSelectDown()
 *
 *  Remove the event.
 *  Starting at the first screen find the first one higher than the mouse
 *  If none, choose the active screen, otherwise bring the screen to the front.
 *  Get the shift values from the screen's modes, and save the current X,Y
 */

static void DoSelectDown()
{
   RemoveEvent = TRUE;
   theScreen = IntuitionBase->FirstScreen;
   while (theScreen && IntuitionBase->MouseY < SCREENTOP)
      theScreen = theScreen->NextScreen;
   if (theScreen == NULL)
      theScreen = IntuitionBase->ActiveScreen;
     else
      ScreenToFront(theScreen);
   Xshift = (theScreen->ViewPort.Modes & HIRES)? 0: 1;
   Yshift = (theScreen->ViewPort.Modes & LACE)? 0: 1;
   Xs = Xc;
   Ys = Yc;
}


/*
 *  DoMouseMove()
 *
 *  Get the mouse's movement and clip it to the movement rectangle.
 *  If right button is presseed and the select rectangle is started,
 *    Adjust the mouse movement, if necessary
 *    Move the starting position by this amount (ie, move the frame)
 *  If there was some movement,
 *    Don't remove the event,
 *    Pass on the modified mouse movements
 *    Adjust the mouse's current position
 *    If the select rectangle is already started, indicate that it has changed.
 */

static void DoMouseMove(theEvent)
struct InputEvent *theEvent;
{
   register WORD dx,dy;

   dx = theEvent->ie_X;
   dy = theEvent->ie_Y;
   if (dx < Wmin - Xc) dx = Wmin - Xc;
   if (dy < Hmin - Yc) dy = Hmin - Yc;
   if (dx > Wmax - Xc) dx = Wmax - Xc;
   if (dy > Hmax - Yc) dy = Hmax - Yc;
   if ((theEvent->ie_Qualifier & IEQUALIFIER_RBUTTON) && theScreen)
   {
      if (dx < Wmin - Xs) dx = Wmin - Xs;
      if (dy < Hmin - Ys) dy = Hmin - Ys;
      if (dx > Wmax - Xs) dx = Wmax - Xs;
      if (dy > Hmax - Ys) dy = Hmax - Ys;
      Xs += dx; Ys += dy;
   }
   if (dx != 0 || dy != 0)
   {
      RemoveEvent = FALSE;
      theEvent->ie_X = dx;
      theEvent->ie_Y = dy;
      Xc += dx;
      Yc += dy;
      if (theScreen) MouseMoved = TRUE;
   }
}


/*
 *  SignalMove()
 *
 *  Mark that the shared memory is in use
 *  Save the start and current X,Y in the shared memory
 *  Signal the main task to draw the select rectangle
 *
 *  (This stuff should all be replaced by message passing - I wrote this
 *   a long time ago before I had much experience)
 */

static void SignalMove()
{
   OkToSignal = FALSE;
   MouseMoved = FALSE;
   StartX   = SCREENX(Xs);
   StartY   = SCREENY(Ys);
   CurrentX = SCREENX(Xc); 
   CurrentY = SCREENY(Yc);
   Signal(theTask,OkMask);
}


/*
 *  FrameHandler()
 *
 *  Get the current mouse position, and get the mouse movement rectangle.
 *  While there are more events to look at,
 *    Keep the event unless we say otherwise,
 *    Check the type of event:
 *      RAWMOUSE:
 *        Remove the event if the select box is up
 *        Check the button type:
 *          SELECTDOWN:
 *            If the select box is not already going, and the right qualifiers
 *              are present, then start the box
 *          SELECTUP:
 *            If the select box is going, signal that it's done
 *          MENU:
 *            Ignore these
 *          Anything else (i.e., MOUSEMOVE):
 *            Do a mouse move
 *      RAWKEY:
 *        If it is a CTRL-C, signal the task to stop.
 *    If we are removing the event, do so, otherwise go on to the next one.
 *  If mouse moves took placem and the memory is not in use, signal the move.
 *  Return the (modified) event list.
 */

struct InputEvent *FrameHandler(EventList,data)
struct InputEvent *EventList;
APTR data;
{
   register struct InputEvent **EventPtr = &EventList;
   register struct InputEvent *theEvent;

   Xc = IntuitionBase->MouseX;
   Yc = IntuitionBase->MouseY;
   GetMinMax();

   Forbid();
   while ((theEvent = *EventPtr) != NULL)
   {
      RemoveEvent = FALSE;
      switch(theEvent->ie_Class)
      {
         case IECLASS_RAWMOUSE:
            RemoveEvent = (theScreen != NULL);
            switch(theEvent->ie_Code)
            {
               case SELECTDOWN:
                  if (theScreen == NULL &&
                     (theEvent->ie_Qualifier & STARTQUAL))
                        DoSelectDown();
                  break;

               case SELECTUP:
                  if (theScreen) Signal(theTask,EndMask);
                  break;

               case MENUDOWN:
               case MENUUP:
                  break;

               default:
                  DoMouseMove(theEvent);
                  break;
            }
            break;

         case IECLASS_RAWKEY:
            if (CTRL_C(theEvent)) Signal(theTask,SIGBREAKF_CTRL_C);
            break;
      }
      if (RemoveEvent)
         *EventPtr = theEvent->ie_NextEvent;
        else
         EventPtr = &(theEvent->ie_NextEvent);
   }
   Permit();
   if (OkToSignal && MouseMoved) SignalMove();
   return(EventList);
}


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

#ifndef MANX
static 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 = EXIT_OK;
   short i;
   extern void DoUseExit();
   
   if (theRastPort) UnlockLayers(&theScreen->LayerInfo);
   if (s != NULL)
   {
      printf(s,x1,x2,x3);
      printf("\n");
      status = EXIT_ERROR;
   }
   DoUseExit();
   for (i=0; i<theDepth; i++)
   {
      if (theBitMap.Planes[i])
         FreeRaster(theBitMap.Planes[i],theWidth,theHeight);
   }
   if (InputOpen)     CloseDevice(InputBlock);
   if (InputBlock)    DeleteStdIO(InputBlock);
   if (InputPort)     DeletePort(InputPort);
   if (theColorMap)   FreeColorMap(theColorMap);
   if (OkSignal)      FreeSignal(OkSignal);
   if (EndSignal)     FreeSignal(EndSignal);
   if (IntuitionBase) CloseLibrary(IntuitionBase);
   if (LayersBase)    CloseLibrary(LayersBase);
   if (GfxBase)       CloseLibrary(GfxBase);
   exit(status);
}


/*
 *  New()
 *
 *  Get a chunk of memory and check that it has been allocated OK.
 *  If not, exit with an error message.
 */

APTR New(name,size)
char *name;
int size;
{
   APTR ptr;
   
   if ((ptr = AllocMem(size,MEMF_CLEAR | MEMF_PUBLIC)) == NULL)
      DoExit("Can't Allocate Memory for %s",name);
   return(ptr);
}


/*
 *  CheckLibOpen()
 *
 *  General library open routine.  It opens a library and sets a pointer
 *  to it.  It checks that the library was openned successfully.
 */

void CheckLibOpen(lib,name,rev)
APTR *lib;
char *name;
int rev;
{
   extern APTR OpenLibrary();

   if ((*lib = OpenLibrary(name,(LONG)rev)) == NULL)
      DoExit("Can't open %s",name);
}


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

static 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).
 */

static void SetupTask()
{
   theTask = FindTask(NULL);
   SetSignal(0L,SIGBREAKF_ANY);
   GetSignal(&OkSignal,&OkMask);
   GetSignal(&EndSignal,&EndMask);
   #ifndef MANX
      onbreak(&Ctrl_C);
   #endif
}


/*
 *  TellInputDevice()
 *
 *  Create a port and a standard IO block for talking to the input device.
 *  Open the input device and tell it to add or remove the input handler.
 *  The handler is at priority 51, so it will be ahead of Intuition.
 *  Once the handler is added or removed, close the device and delete the
 *  request block and port.
 */
 
static void TellInputDevice(function)
long function;
{
   long status;

   if ((InputPort = CreatePort(0,0)) == NULL)
      DoExit("Can't Create Port for Input Device");
   if ((InputBlock = CreateStdIO(InputPort)) == NULL)
      DoExit("Can't Create IO Block for Input Device");
   if (OpenDevice("input.device",0,InputBlock,0) != 0)
      DoExit("Can't Open Input Device");
   InputOpen = TRUE;
   
   InputBlock->io_Command = function;
   InputBlock->io_Data    = (APTR) &HandlerData;
   if (status = DoIO(InputBlock)) DoExit("Error from DoIO:  %ld",status);
   
   CloseDevice(InputBlock); InputOpen  = FALSE;
   DeleteStdIO(InputBlock); InputBlock = NULL;
   DeletePort(InputPort);   InputPort  = NULL;
}


/*
 *  DrawFrame()
 *
 *  Set the corners of the border to their new positions.
 *  If the select frame has been moved,
 *    Get the old X,Y and calculate the new X,Y for the intermediate
 *      part of the border (the border is not square so as to avoid
 *      unnecessary flickering when it is sized)
 *    Update the intermediate border positions
 *  Otherwise
 *    Update the intermediate positions
 &  Draw the borders
 *  Switch which one is the current one, and link them properly
 */

static void DrawFrame()
{
   WORD Xo,Yo, Xn,Yn;

   NewX(3) = NewX(4) = StartX;  NewX(1) = NewX(2) = CurrentX;
   NewY(0) = NewY(1) = StartY;  NewY(2) = NewY(3) = CurrentY;
   if (StartX == OldX(4) && StartY == OldY(0))
   {
      Xo = OldX(2); Yo = OldY(2);
      if ((CurrentX > StartX && StartX >= Xo) ||
          (CurrentX < StartX && StartX <= Xo))
      {
         Xn = StartX;
      } else {
         if ((CurrentX > Xo && Xo > StartX) ||
             (CurrentX < Xo && Xo < StartX))
            Xn = Xo;
           else
            Xn = CurrentX;
      }
      if ((CurrentY > StartY && StartY >= Yo) ||
          (CurrentY < StartY && StartY <= Yo))
      {
         Yn = StartY;
      } else {
         if ((CurrentY > Yo && Yo > StartY) ||
             (CurrentY < Yo && Yo < StartY))
            Yn = Yo;
           else
            Yn = CurrentY;
      }
      OldX(0) = NewX(0) = Xn;
      OldY(4) = NewY(4) = Yn;
   } else {
      OldX(0) = OldX(4); OldY(4) = OldY(0);
      NewX(0) = StartX;  NewY(4) = StartY;
   }
   DrawBorder(theRastPort,Frame,0L,0L);
   Frame->NextBorder->NextBorder = Frame;
   Frame = Frame->NextBorder;
   Frame->NextBorder->NextBorder = NULL;
}


/*
 *  CopyFrame()
 *
 *  Get the mask for the planes actually needed
 *  Count the number of planes involved
 *  If none, give an error
 *  Get the widthm height, and upper-left-hand corner of the rectangle.
 *  Get a color map of the required depth and copy the screen's
 *    color map into it, skipping unneeded colors
 *  Get the screen's modes
 *  Initialize the BitMap, and a temporary one (to be used for blitting)
 *  For each plane selected
 *    Allocate a raster area for it (error if it not possible)
 *    Use it as the bitplane for both the TempBitMap and the BitMap
 *      (BitMap has them sequentially, TempBitMap has them in the positions
 *       corresponding to their positions in the screen bitmap)
 *    Clear the last two bytes of each bitplane (to make sure they are 0)
 *  Copy the screen data to the TempBitMap (and hence the to BitMap, since
 *    they share bitplanes)
 */

void CopyFrame()
{
   short i,j,b;
   UWORD *OldC,*NewC;
   PLANEPTR thePlane;
   UBYTE TempMask, NotPlaneMask;
   struct BitMap TempBitMap;
   long TempDepth = theScreen->BitMap.Depth;
 
   PlaneMask &= (1 << TempDepth) - 1;
   NotPlaneMask = ~PlaneMask;
   for (TempMask = PlaneMask, theDepth = 0; TempMask; TempMask >>= 1)
      theDepth += (TempMask & 1);
   if (theDepth == 0) DoExit("No Bit Planes Selected By Mask");

   if (CurrentX > StartX)
   {
      theWidth = CurrentX - StartX + 1;
      LeftX = StartX;
   } else {
      theWidth = StartX - CurrentX + 1;
      LeftX = CurrentX;
   }
   if (CurrentY > StartY)
   {
      theHeight = CurrentY - StartY + 1;
      TopY = StartY;
   } else {
      theHeight = StartY - CurrentY + 1;
      TopY = CurrentY;
   }

   theColorMap = GetColorMap((long)(1 << theDepth));
   if (theColorMap == 0) DoExit("Can't Get Color Map");
   OldC = (UWORD *)(theScreen->ViewPort.ColorMap->ColorTable);
   NewC = (UWORD *)(theColorMap->ColorTable);
   for (i=0; i<theScreen->ViewPort.ColorMap->Count; i++, OldC++)
      if ((i & NotPlaneMask) == 0) *NewC++ = *OldC;

   theModes = theScreen->ViewPort.Modes;

   InitBitMap(&theBitMap,DEPTH,WIDTH,HEIGHT);
   InitBitMap(&TempBitMap,TempDepth,WIDTH,HEIGHT);
   for (TempMask=PlaneMask, i=j=0; TempMask; TempMask >>= 1,j++)
   {
      if (TempMask & 1)
      {
         thePlane = AllocRaster(WIDTH,HEIGHT);
         if (thePlane == NULL) DoExit("Can't Get BitPlane %d",i);
         TempBitMap.Planes[j] = theBitMap.Planes[i++] = thePlane;
         for (b=RASSIZE(theWidth,theHeight)-1; b>0; b -= theBitMap.BytesPerRow)
            thePlane[b] = thePlane[b-1] = 0;
      }
   }
   BltBitMap(&theScreen->BitMap,(long)LeftX,(long)TopY,&TempBitMap,0L,0L,
      WIDTH,HEIGHT,BLT_COPY,(long)PlaneMask,NULL);
}


/*
 *  TrackFrame()
 *
 *  Setup the task, and get the signal mask
 *  Add the input handler and give the installation message
 *  Tell the user how to get the select rectangle
 *  While there is more to do,
 *    Wait for something to happen
 *    If we have gotten a new mouse position,
 *      If the screen is already chosen,
 *        Update and draw the new frame
 *      Otherwise,
 *        Get the rastport of the selected screen,
 *        Lock its layers so nothing happens while we're selecting
 *        Initialize the frames and draw them on the screen
 *      Indicate that we are done with the shared memory.
 *    If some other signal is present,
 *      End the loop
 *      Check if the loop was cancelled by the user
 *      If the select box was drawn,
 *        Finish the frame and remove it
 *        If not cancelled, get a copy of the frame
 *        Unlock the screen layers so things can happen again
 *        Forget the rastport
 *  Remove the handler
 *  return the status
 */

static int TrackFrame()
{
   LONG signals;
   LONG SigMask;
   short NotDone = TRUE;
   int NotCancelled = TRUE;
   
   SetupTask();
   SigMask = OkMask | EndMask | SIGBREAKF_CTRL_C;

   TellInputDevice(IND_ADDHANDLER);
   printf("%s %s Installed\n",program,version);
   printf("Drag a selection rectangle while holding the SHIFT key.  To change\n");
   printf("the position of the box, press the right mouse button while dragging.\n");
   printf("Press CTRL-C to cancel at any time.\n");
   while (NotDone)
   {
      signals = Wait(SigMask);
      if (signals & OkMask)
      {
         if (theRastPort)
         {
            DrawFrame();
         } else {
            theRastPort = &theScreen->RastPort; 
            LockLayers(&theScreen->LayerInfo);
            OldX(0) = OldX(3) = OldX(4) = StartX; OldX(1) = OldX(2) = CurrentX;
            OldY(0) = OldY(1) = OldY(4) = StartY; OldY(2) = OldY(3) = CurrentY;
            DrawBorder(theRastPort,Frame->NextBorder,0L,0L);
         }
         OkToSignal = TRUE;
      }
      if (signals & ~OkMask)
      {
         NotDone = FALSE;
         NotCancelled = ((signals & SIGBREAKF_CTRL_C) == 0);
         if (theRastPort)
         {
            OldX(0) = OldX(4);
            OldY(4) = OldY(0);
            DrawBorder(theRastPort,Frame->NextBorder,0L,0L);
            if (NotCancelled) CopyFrame();
            UnlockLayers(&theScreen->LayerInfo);
            theRastPort = NULL;
         }
      }
   }
   TellInputDevice(IND_REMHANDLER);
   return(NotCancelled);
}


/*
 *  main()
 *
 *  Parse the command-line arguments and perform the proper function
 *  (either show the usage, exit, or get a section of bitmap and use it).
 */

void main(argc,argv)
int argc;
char **argv;
{
   int function;
   extern int ParseArguments();
   extern void UseFrame();

   function = ParseArguments(argc,argv);
   switch(function)
   {
      case SHOW_USAGE:
         printf("Usage:  %s\n",usage);
         break;

      case JUST_EXIT:
         break;

      default:
         CheckLibOpen(&IntuitionBase,"intuition.library",INTUITION_REV);
         CheckLibOpen(&LayersBase,"layers.library",LAYERS_REV);
         CheckLibOpen(&GfxBase,"graphics.library",GRAPHICS_REV);
         if (TrackFrame()) UseFrame(function);
         break;
   }
   DoExit(NULL);
}
