/*
 * File: graphics.c
 * Last modified on Mon Jul 25 11:53:41 1994 by eroberts
 * -----------------------------------------------------
 * This file implements the graphics.h and extgraph.h interfaces
 * for the Macintosh using THINK's Lightspeed C.
 */

/*
 * General implementation notes
 * ----------------------------
 * This implementation is relatively complex for two reasons.
 * The first source of complexity is that windows on the
 * Macintosh are dynamically repositioned, obscured, and
 * reexposed.  When these events occur, the drawing in the
 * window needs to be repainted.  Thus, it is not sufficient
 * to have the drawing package simply issue the appropriate
 * QuickDraw procedures directly to the screen.  This
 * implementation solves the problem by rendering the image
 * to an offscreen bitmap and then repainting the image from
 * that copy whenever update events occur.
 *
 * The second problem, which is indeed more severe, is that
 * that is much more easily understood by experts than by
 * novices.  In a traditional Macintosh application, the
 * main program is coded as an "event loop," which
 * continually polls the event manager to see if any activity
 * is necessary.  This strategy runs counter to the more
 * conventional view of a main program as the highest level
 * in the problem decomposition.  In the context of teaching
 * programming, we want the main program and its subprograms
 * to draw the figures -- a strategy which is not easy to
 * reconcile with the standard Macintosh approach.  This
 * implementation solves this problem by patching in a call
 * to an update procedure as part of the event loop embedded
 * in the console input mechanism.
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <console.h>
#include <math.h>
#include <time.h>
#include <Traps.h>

#include "genlib.h"
#include "gcalloc.h"
#include "strlib.h"
#include "extgraph.h"

/*
 * Parameters
 * ----------
 * MinConsoleLines -- Console height never drops below this
 * MaxConsoleLines -- Console height never drops exceeds this
 * Console Margin  -- Required margin between windows
 * DefaultFont     -- Font used to implement "Default" font
 * DefaultSize     -- Initial point size for text
 */

#define MinConsoleLines   8
#define MaxConsoleLines  20
#define ConsoleMargin     3

#define DefaultFont "Geneva"
#define DefaultSize 10

/*
 * Other constants
 * ---------------
 * LargeInt -- Used to signify out of range values
 * Epsilon  -- Small arithmetic offset to reduce aliasing/banding
 * Pi       -- Mathematical constant pi
 */

#define LargeInt 16000
#define Epsilon  0.00000000001
#define Pi       3.1415926535

/*
 * Type: graphicsStateT
 * --------------------
 * This structure holds the variables that make up the graphics state.
 */

typedef struct graphicsStateT {
    double cx, cy;
    string font;
    int size;
    bool erase;
    struct graphicsStateT *link;
} *graphicsStateT;

/*
 * Type: regionStateT
 * ------------------
 * The region assembly process has the character of a finite state
 * machine with the following four states:
 *
 *   NoRegion       Region has not yet been started
 *   RegionStarting Region is started but no line segments yet
 *   RegionActive   First line segment appears
 *   PenHasMoved    Pen has moved during definition
 *
 * The current state determines whether other operations are legal
 * at that point.
 */

typedef enum {
    NoRegion, RegionStarting, RegionActive, PenHasMoved
} regionStateT;

/*
 * Global variables
 * ----------------
 * initialized  -- TRUE if initialization has been done
 * windowTitle  -- Current window title (initialized statically)
 * gConsole     -- Pointer to console file for graphics window
 * osPort       -- Offscreen memory port for rendering
 * xResolution  -- Horizontal resolution of screen (dots per inch)
 * yResolution  -- Vertical resolution of screen (dots per inch)
 * windowWidth  -- Width of graphics window
 * windowHeight -- Height of graphics window
 * regionState  -- Current state of the region
 * regionGray   -- Gray scale to apply to region
 * stateStack   -- Stack of graphicStateT blocks
 * cx, cy       -- Current coordinates     | These four variable
 * eraseMode    -- Setting of erase flag   | consititute the
 * textFont     -- Current font            | current graphics
 * pointSize    -- Current point size      | state
 */

static bool initialized = FALSE;
static string windowTitle = "Graphics Window";

static FILE *gConsole;
static GrafPtr osPort;
static short xResolution;
static short yResolution;
static double windowWidth, windowHeight;

static regionStateT regionState;
static double regionGray;

static graphicsStateT stateStack;

static double cx, cy;
static bool eraseMode;
static string textFont;
static int pointSize;

/*
 * Static table: grayList
 * ----------------------
 * This table contains the bitmap patterns for the various gray
 * scale values.  Adding more patterns to this list increases
 * the precision at which the client can specify gray scales.
 */

static Pattern grayList[] = {
    { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 },
    { 0x88, 0x22, 0x88, 0x22, 0x88, 0x22, 0x88, 0x22 },
    { 0x55, 0xAA, 0x55, 0xAA, 0x55, 0xAA, 0x55, 0xAA },
    { 0x77, 0xDD, 0x77, 0xDD, 0x77, 0xDD, 0x77, 0xDD },
    { 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF },
};

#define NGrays (sizeof grayList / sizeof grayList[0])

/* Private function prototypes */

static void InitCheck(void);
static void InitGraphicsState(void);
static void EraseWindow(void);
static void CreateGraphicsWindow(void);
static int ComputeRowPixels(void);
static void DisplayLine(double x, double y, double dx, double dy);
static void DisplayArc(double x, double y, double rx, double ry,
                       double start, double sweep);
static void RenderArc(double x, double y, double rx, double ry,
                      double start, double sweep);
static void DisplayText(double x, double y, string text,
                        string font, int size);
static void SetArcBB(Rect *rp, double xc, double yc,
                      double rx, double ry, double start, double sweep);
static void SetTextBB(Rect *rp, double x, double y,
                       string text, string font, int size);
static void UpdateRect(Rect *rp);
static StringPtr PascalString(string cstr);
static int GetFontNumber(string fontName);
static void PatchEventHandler(void);
static long FindSystemTrap(short trap);
static void MyEventHandler(EventRecord *ep);
static void DoUpdateDisplay(void);
static int RectWidth(Rect r);
static int RectHeight(Rect r);
static double Radians(double degrees);
static double Degrees(double radians);
static double InchesX(int x);
static double InchesY(int y);
static int PixelsX(double x);
static int PixelsY(double y);
static int ScaleX(double x);
static int ScaleY(double y);
static int Round(double x);
static int Min(int x, int y);
static int Max(int x, int y);
static double MinF(double x, double y);
static double MaxF(double x, double y);

/* Exported entries */

/* Section 1 -- Basic functions from graphics.h */

void InitGraphics(void)
{
    InitGraphicsState();
    if (initialized) {
        EraseWindow();
    } else {
        ProtectVariable(stateStack);
        ProtectVariable(windowTitle);
        ProtectVariable(textFont);
        CreateGraphicsWindow();
        PatchEventHandler();
        initialized = TRUE;
    }
}

void MovePen(double x, double y)
{
    InitCheck();
    if (regionState == RegionActive) regionState = PenHasMoved;
    cx = x;
    cy = y;
}

void DrawLine(double dx, double dy)
{
    InitCheck();
    switch (regionState) {
      case NoRegion:
        DisplayLine(cx, cy, dx, dy);
        break;
      case RegionStarting: case RegionActive:
        DisplayLine(cx, cy, dx, dy);
        regionState = RegionActive;
        break;
      case PenHasMoved:
        Error("Region segments must be contiguous");
    }
    cx += dx;
    cy += dy;
}

void DrawArc(double r, double start, double sweep)
{
    DrawEllipticalArc(r, r, start, sweep);
}

double GetWindowWidth(void)
{
    InitCheck();
    return (windowWidth);
}

double GetWindowHeight(void)
{
    InitCheck();
    return (windowHeight);
}

double GetCurrentX(void)
{
    InitCheck();
    return (cx);
}

double GetCurrentY(void)
{
    InitCheck();
    return (cy);
}

/* Section 2 -- Elliptical arcs */

void DrawEllipticalArc(double rx, double ry,
                       double start, double sweep)
{
    double x, y;

    InitCheck();
    x = cx + rx * cos(Radians(start + 180));
    y = cy + ry * sin(Radians(start + 180));
    switch (regionState) {
      case NoRegion:
        DisplayArc(x, y, rx, ry, start, sweep);
        break;
      case RegionStarting: case RegionActive:
        RenderArc(x, y, rx, ry, start, sweep);
        regionState = RegionActive;
        break;
      case PenHasMoved:
        Error("Region segments must be contiguous");
    }
    cx = x + rx * cos(Radians(start + sweep));
    cy = y + ry * sin(Radians(start + sweep));
}

/* Section 3 -- Graphical structures */

void StartFilledRegion(double grayScale)
{
    GrafPtr saveWindow;

    InitCheck();
    if (regionState != NoRegion) {
        Error("Region is already in progress");
    }
    if (grayScale < 0 || grayScale > 1) {
        Error("Gray scale for regions must be between 0 and 1");
    }
    regionState = RegionStarting;
    regionGray = grayScale;
    GetPort(&saveWindow);
    SetPort((WindowPtr) osPort);
    OpenRgn();
    SetPort(saveWindow);
}

void EndFilledRegion(void)
{
    RgnHandle region;
    GrafPtr saveWindow;
    int px;

    InitCheck();
    switch (regionState) {
      case NoRegion:
        Error("EndFilledRegion without StartFilledRegion");
      case RegionStarting:
        regionState = NoRegion;
        return;
      case RegionActive: case PenHasMoved:
        regionState = NoRegion;
        break;
    }
    GetPort(&saveWindow);
    SetPort((WindowPtr) osPort);
    region = NewRgn();
    CloseRgn(region);
    UpdateRect(&(*region)->rgnBBox);
    px = regionGray * (NGrays - 1) + 0.5 - Epsilon;
    FillRgn(region, grayList[(eraseMode) ? 0 : px]);
    SetPort(saveWindow);
    DisposeRgn(region);
}

/* Section 4 -- String functions */

void DrawTextString(string text)
{
    InitCheck();
    if (regionState != NoRegion) {
        Error("Text strings are illegal inside a region");
    }
    DisplayText(cx, cy, text, textFont, pointSize);
    cx += TextStringWidth(text);
}

double TextStringWidth(string text)
{
    Rect rect;

    InitCheck();
    SetTextBB(&rect, cx, cy, text, textFont, pointSize);
    return (InchesX(rect.right - rect.left));
}

void SetFont(string font)
{
    InitCheck();
    if (GetFontNumber(font) != 0) {
        textFont = CopyString(font);
    }
}

string GetFont(void)
{
    InitCheck();
    return (CopyString(textFont));
}

void SetPointSize(int size)
{
    InitCheck();
    pointSize = size;
}

int GetPointSize(void)
{
    InitCheck();
    return (pointSize);
}

/* Section 5 -- Miscellaneous functions */

void SetEraseMode(bool mode)
{
    GrafPtr saveWindow;

    InitCheck();
    eraseMode = mode;
    GetPort(&saveWindow);
    SetPort((WindowPtr) osPort);
    PenMode((mode) ? patBic : patCopy);
    TextMode((mode) ? srcBic : srcOr);
    SetPort(saveWindow);
}

bool GetEraseMode(void)
{
    InitCheck();
    return (eraseMode);
}

void SetWindowTitle(string title)
{
    windowTitle = CopyString(title);
    if (initialized) {
        SetWTitle(gConsole->window, PascalString(title));
    }
}

string GetWindowTitle(void)
{
    return (CopyString(windowTitle));
}

void UpdateDisplay(void)
{
    int cnt;

    InitCheck();
    DoUpdateDisplay();
}

void Pause(double seconds)
{
    long dummy;

    UpdateDisplay();
    Delay(seconds * CLOCKS_PER_SEC, &dummy);
}

void SaveGraphicsState(void)
{
    graphicsStateT sb;

    InitCheck();
    sb = New(graphicsStateT);
    sb->cx = cx;
    sb->cy = cy;
    sb->font = textFont;
    sb->size = pointSize;
    sb->erase = eraseMode;
    sb->link = stateStack;
    stateStack = sb;
}

void RestoreGraphicsState(void)
{
    graphicsStateT sb;

    InitCheck();
    if (stateStack == NULL) {
        Error("RestoreGraphicsState called before SaveGraphicsState");
    }
    sb = stateStack;
    cx = sb->cx;
    cy = sb->cy;
    textFont = sb->font;
    pointSize = sb->size;
    eraseMode = sb->erase;
    stateStack = sb->link;
    FreeBlock(sb);
}

/* Private functions */

/*
 * Function: InitCheck
 * Usage: InitCheck();
 * -------------------
 * This function merely ensures that the package has been
 * initialized before the client functions are called.
 */

static void InitCheck(void)
{
    if (!initialized) Error("InitGraphics has not been called");
}

/*
 * Function: InitGraphicsState
 * Usage: InitGraphicsState();
 * ---------------------------
 * This function initializes the graphics state elements to
 * their default values.
 */

static void InitGraphicsState(void)
{
    cx = cy = 0;
    eraseMode = FALSE;
    textFont = "Default";
    pointSize = DefaultSize;
    stateStack = NULL;
    regionState = NoRegion;
}

/*
 * Function: EraseWindow
 * Usage: EraseWindow();
 * ---------------------
 * This function erases all of the offscreen memory bits, thereby
 * clearing the screen image.  Note that the osPort->portBits array
 * is typically larger than a Macintosh segment and therefore cannot
 * be cleared with array operations or memset.
 */

static void EraseWindow(void)
{
    GrafPtr saveWindow;
    WindowPeek window;
    Rect rect;
    int width, height;

    GetPort(&saveWindow);
    SetPort((WindowPtr) (window = gConsole->window));
    width = window->port.portRect.right - window->port.portRect.left;
    height = window->port.portRect.bottom - window->port.portRect.top;
    SetRect(&rect, 0, 0, width, height);
    InvalRect(&rect);
    SetPort(saveWindow);
    CopyBits(&osPort->portBits, &osPort->portBits,
             &osPort->portRect, &osPort->portRect,
             srcBic, NULL);
}

/*
 * Function: CreateGraphicsWindow
 * Usage: CreateGraphicsWindow();
 * ------------------------------
 * This function creates the graphics window on the screen and
 * the offscreen memory to back it up.  The graphics window is
 * created by using the THINK C console package, even though
 * the window is never used for text; the graphics package
 * instead uses the window as a drawing space.  Most of the
 * code here is used to compute the dimensions of the windows
 * based on the size of the screen.
 */

static void CreateGraphicsWindow(void)
{
    GrafPtr saveWindow, wPort;
    WindowPeek window;
    double consoleHeight, screenHeight, graphicsHeight;
    int width, height, rowPixels, consoleRows, saveTop;

    ScreenRes(&xResolution, &yResolution);
    rowPixels = ComputeRowPixels();
    GetWMgrPort(&wPort);
    screenHeight = InchesY(RectHeight(wPort->portRect));
    saveTop = console_options.top;
    graphicsHeight = InchesY(console_options.nrows * rowPixels);
    consoleHeight = screenHeight - graphicsHeight
                      - InchesY(saveTop + 2 * ConsoleMargin);
    consoleRows = consoleHeight / InchesY(rowPixels);
    if (consoleRows > MaxConsoleLines) {
         consoleRows = MaxConsoleLines;
     }
    if (consoleRows < MinConsoleLines) {
        graphicsHeight -= InchesY((MinConsoleLines - consoleRows)
                                  * rowPixels);
        consoleRows = MinConsoleLines;
    }
    console_options.top += PixelsY(graphicsHeight) + ConsoleMargin;
    console_options.nrows = consoleRows;
    console_options.title = "\pConsole Window";
    printf("\n");
    fflush(stdout);
    console_options.top = saveTop;
    console_options.nrows = PixelsY(graphicsHeight) / rowPixels;
    console_options.title = PascalString(windowTitle);
    gConsole = fopenc();
    SetPort((WindowPtr) (window = gConsole->window));
    width = RectWidth(window->port.portRect);
    height = RectHeight(window->port.portRect);
    windowWidth = InchesX(width);
    windowHeight = InchesY(height);
    osPort = malloc(sizeof(GrafPort));
    *osPort = window->port;
    osPort->portBits.baseAddr =
         calloc((long) height * osPort->portBits.rowBytes, 1);
    TextFont(console_options.txFont);
    TextSize(console_options.txSize);
    TextFace(console_options.txFace);
    fprintf(gConsole, "\n");
    fflush(gConsole);
}

/*
 * Function: ComputeRowPixels
 * Usage: rowPixels = ComputeRowPixels();
 * --------------------------------------
 * This function estimates the number of pixels in a text row on
 * the screen.  The complication in the implementation is that the
 * actual font information cannot be obtained using GetFontInfo
 * until the Toolbox has been initialized.  Unfortunately, C
 * programs that use the console package do not initialize the
 * Toolbox until the console window is created.  This code
 * therefore estimates the size from the parameters.  If
 * other fonts are used for the console in addition to Geneva
 * and Monaco, this function can be augmented to include them.
 */

static int ComputeRowPixels(void)
{
    short monaco;

    GetFNum("\pMonaco", &monaco);
    if (console_options.txFont == monaco) {
        return (Round(console_options.txSize * 1.5));
    } else {
        return (Round(console_options.txSize * 1.2));
    }
}

/*
 * Function: DisplayLine
 * Usage: DisplayLine(x, y, dx, dy);
 * ---------------------------------
 * This function displays a line segment from the point (x, y)
 * to the point (x + dx, y + dy).  Like all of the other graphical
 * operations, the line is rendered into offscreen memory.  The
 * actual screen is updated only in response to the update event.
 */

static void DisplayLine(double x, double y, double dx, double dy)
{
    GrafPtr saveWindow;
    WindowPeek window;
    Rect rect;
    int ix0, iy0, ix1, iy1;

    ix0 = ScaleX(x);
    iy0 = ScaleY(y);
    ix1 = ScaleX(x + dx);
    iy1 = ScaleY(y + dy);
    SetRect(&rect, Min(ix0,ix1), Min(iy0,iy1), Max(ix0,ix1), Max(iy0,iy1));
    UpdateRect(&rect);
    GetPort(&saveWindow);
    SetPort((WindowPtr) osPort);
    MoveTo(ix0, iy0);
    LineTo(ix1, iy1);
    SetPort(saveWindow);
}

/*
 * Function: DisplayArc
 * Usage: DisplayArc(x, y, rx, ry, start, sweep);
 * ---------------------------------------------
 * This function displays an elliptical arc segment centered at
 * the point (x, y).  (Note that the argument signature is different
 * from that of the client function DrawEllipticalArc.)
 */

static void DisplayArc(double x, double y, double rx, double ry,
                       double start, double sweep)
{
    GrafPtr saveWindow;
    WindowPeek window;
    Rect rect;
    int ix0, iy0, ix1, iy1, istart, isweep;

    istart = Round(start);
    isweep = Round(sweep);
    ix0 = ScaleX(x - rx);
    iy0 = ScaleY(y + ry);
    ix1 = ix0 + PixelsX(2 * rx);
    iy1 = iy0 + PixelsY(2 * ry);
    SetArcBB(&rect, x, y, rx, ry, start, sweep);
    UpdateRect(&rect);
    GetPort(&saveWindow);
    SetPort((WindowPtr) osPort);
    SetRect(&rect, ix0, iy0, ix1+1, iy1+1);
    FrameArc(&rect, 90 - istart, -isweep);
    SetPort(saveWindow);
}

/*
 * Function: RenderArc
 * Usage: RenderArc(x, y, rx, ry, start, sweep);
 * ---------------------------------------------
 * This function is identical to the DisplayArc function except
 * that the arc is rendered using line segments.  This complication
 * is necessary on the Macintosh for filled shapes because arcs are
 * not counted as region boundaries.  As an optimization, a
 * complete circle or oval is drawn using the DrawOval tool, which
 * does add a region boundary.  The arc unit is chosen to represent
 * a pixel width at the specified arc radius.
 */

static void RenderArc(double x, double y, double rx, double ry,
                      double start, double sweep)
{
    GrafPtr saveWindow;
    WindowPeek window;
    double t, mint, maxt, dt;
    int ix0, iy0, ix1, iy1;
    Rect rect;

    GetPort(&saveWindow);
    SetPort((WindowPtr) osPort);
    if (sweep < 0) {
        start += sweep;
        sweep = -sweep;
    }
    if (sweep > 360 - Epsilon) {
        ix0 = ScaleX(x - rx);
        iy0 = ScaleY(y + ry);
        ix1 = ix0 + PixelsX(2 * rx);
        iy1 = iy0 + PixelsY(2 * ry);
        SetRect(&rect, ix0, iy0, ix1+1, iy1+1);
        FrameOval(&rect);
    } else {
        dt = atan2(InchesY(1), MaxF(fabs(rx), fabs(ry)));
        mint = Radians(start);
        maxt = Radians(start + sweep);
        for (t = mint; t < maxt; t += dt) {
            if (t > maxt - dt / 2) t = maxt;
            ix0 = ScaleX(x + rx * cos(t));
            iy0 = ScaleY(y + ry * sin(t));
            if (t == mint) {
                MoveTo(ix0, iy0);
            } else {
                LineTo(ix0, iy0);
            }
        }
    }
    SetArcBB(&rect, x, y, rx, ry, start, sweep);
    UpdateRect(&rect);
    SetPort(saveWindow);
}

/*
 * Function: DisplayText
 * Usage: DisplayText(x, y, text, font, size);
 * -------------------------------------------
 * Displays the specified text starting at the point (x, y)
 * at the given font and size.
 */

static void DisplayText(double x, double y, string text,
                        string font, int size)
{
    GrafPtr saveWindow;
    WindowPeek window;
    Rect rect;
    int ix, iy;

    ix = ScaleX(x);
    iy = ScaleY(y);
    SetTextBB(&rect, x, y, text, font, size);
    UpdateRect(&rect);
    GetPort(&saveWindow);
    SetPort((WindowPtr) osPort);
    MoveTo(ix, iy);
    DrawText(text, 0, strlen(text));
    SetPort(saveWindow);
}

/*
 * Function: SetArcBB
 * Usage: SetArcBB(&rect, xc, yc, rx, ry, start, sweep);
 * -----------------------------------------------------
 * This function sets the rectangle dimensions to the bounding
 * box of the arc segment specified by the remaining arguments.
 */

static void SetArcBB(Rect *rp, double xc, double yc,
                      double rx, double ry, double start, double sweep)
{
    int xmax, xmin, ymax, ymin;
    int xl, xr, yt, yb;
    int ix0, iy0, ix1, iy1;

    xmin = ScaleX(xc - rx);
    ymin = ScaleY(yc + ry);
    xmax = xmin + PixelsX(2 * rx);
    ymax = ymin + PixelsX(2 * ry);
    if (sweep < 0) {
        start += sweep;
        sweep = -sweep;
    }
    if (sweep >= 360) {
        SetRect(rp, xmin, ymin, xmax, ymax);
        return;
    }
    if (start < 0) {
        start = 360 - fmod(-start, 360);
    } else {
        start = fmod(start, 360);
    }
    ix0 = ScaleX(xc + rx * cos(Radians(start)));
    iy0 = ScaleY(yc + ry * sin(Radians(start)));
    ix1 = ScaleX(xc + rx * cos(Radians(start + sweep)));
    iy1 = ScaleY(yc + ry * sin(Radians(start + sweep)));
    if (start + sweep > 360) {
        xr = xmax;
    } else {
        xr = Max(ix0, ix1);
    }
    start = fmod(start + 270, 360);
    if (start + sweep > 360) {
        yt = ymin;
    } else {
        yt = Min(iy0, iy1);
    }
    start = fmod(start + 270, 360);
    if (start + sweep > 360) {
        xl = xmin;
    } else {
        xl = Min(ix0, ix1);
    }
    start = fmod(start + 270, 360);
    if (start + sweep > 360) {
        yb = ymax;
    } else {
        yb = Max(iy0, iy1);
    }
    SetRect(rp, xl, yt, xr + 1, yb + 1);
}

/*
 * Function: SetTextBB
 * Usage: SetTextBB(&rect, x, y, text, font, size);
 * ------------------------------------------------
 * This function sets the rectangle dimensions to the bounding
 * box of the text string specified by the remaining arguments.
 */

static void SetTextBB(Rect *rp, double x, double y,
                       string text, string font, int size)
{
    GrafPtr saveWindow;
    WindowPeek window;
    FontInfo fInfo;
    int ix, iy, width, fnum;

    GetPort(&saveWindow);
    SetPort((WindowPtr) osPort);
    ix = ScaleX(x);
    iy = ScaleY(y);
    fnum = GetFontNumber(font);
    if (fnum == 0) Error("Illegal font");
    TextFont(fnum);
    TextSize(size);
    GetFontInfo(&fInfo);
    width = TextWidth(text, 0, strlen(text));
    SetRect(rp, ix, iy - fInfo.ascent, ix + width, iy + fInfo.descent);
    SetPort(saveWindow);
}

/*
 * Function: UpdateRect
 * Usage: UpdateRect(&rect);
 * -------------------------
 * This function sets the invalid area of the screen to include
 * the update rectangle.
 */

static void UpdateRect(Rect *rp)
{
    GrafPtr saveWindow;
    WindowPeek window;
    Rect rect;

    GetPort(&saveWindow);
    SetPort((WindowPtr) (window = gConsole->window));
    rect = *rp;
    InsetRect(&rect, -1, -1);
    InvalRect(&rect);
    SetPort(saveWindow);
}

/*
 * Function: PascalString
 * Usage: pstr = PascalString(cstr);
 * --------------------------------
 * This function generates a dynamically allocated Pascal string
 * from the corresponding C string.
 */

static StringPtr PascalString(string cstr)
{
    int len;
    StringPtr pstr;

    len = strlen(cstr);
    pstr = GetBlock(len + 2);
    strcpy((char *) &pstr[1], cstr);
    pstr[0] = len;
    return (pstr);
}

/*
 * Function: GetFontNumber
 * Usage: fontNumber = GetFontNumber(fontName);
 * --------------------------------------------
 * This function returns the font number corresponding to a
 * Macintosh font name.
 */

static int GetFontNumber(string fontName)
{
    StringPtr pstr;
    short fnum;

    if (StringEqual(fontName, "Default")) fontName = DefaultFont;
    GetFNum(pstr = PascalString(fontName), &fnum);
    FreeBlock(pstr);
    return (fnum);
}

/*
 * Event handler
 * -------------
 * The trickiest part of the graphics library code is the
 * following patch system, which adds a call to MyEventHandler
 * prior to the SystemEvent trap.  This makes it possible for
 * the same event loop that handles console events to update
 * the graphics window instead.
 */

static char patchArea[0x20];

/*
 *  00  PEA    $FFF0(A6)
 *  04  JSR    xxxx.L
 *  0A  CLR.L  -(A7)
 *  0C  PEA    $FFF0(A6)
 *  10  $A9B2
 *  12  JMP    yyyy.L
 */

/*
 * Function: PatchEventHandler
 * Usage: PatchEventHandler();
 * ---------------------------
 * This call inserts the patch into the event handling code to
 * call MyEventHandler.  The function first finds the address
 * of the SystemEvent trap and then replaces it with a jump
 * to the code fragment shown above, after xxxx is replaced
 * by the address of MyEventHandler and yyyy by the address
 * after the SystemEvent trap in the existing code.
 */

static void PatchEventHandler(void)
{
    long trapAddress;
    char *patch;

    trapAddress = FindSystemTrap(_SystemEvent);
    patch = patchArea;
    if ((int) patch & 1) patch++;
    memcpy(&patch[0x00], (void *) (trapAddress - 4), 4);
    *((short *) (&patch[0x04])) = 0x4EB9;
    *((long *) (&patch[0x06])) = (long) MyEventHandler;
    *((short *) (&patch[0x0A])) = 0x42A7;
    memcpy(&patch[0x0C], (void *) (trapAddress - 4), 6);
    *((short *) (&patch[0x12])) = 0x4EF9;
    *((long *) (&patch[0x14])) = trapAddress + 2;
    *((short *) (trapAddress - 4)) = 0x4EF9;
    *((long *) (trapAddress - 2)) = (long) patch;
}

/*
 * Function: FindSystemTrap
 * Usage: addr = FindSystemTrap(trap);
 * ----------------------------------
 * This function finds the first occurrence of the specified trap
 * in the code stream beginning at the input function.
 */

static long FindSystemTrap(short trap)
{
    long ptr, base;
    int i;

    base = (long) stdin->proc;
    if (*((short *) base) == 0x4EF9) {
        base = *((long *) (base + 2));
    }
    for (ptr = base; *((short *) ptr) != trap; ptr -= 2);
    return (ptr);
}

/*
 * Function: MyEventHandler
 * Usage: MyEventHandler();
 * ------------------------
 * This function catches update events for the console window.
 */

static void MyEventHandler(EventRecord *ep)
{
    if (ep->what != updateEvt) return;
    if ((WindowPeek) ep->message != gConsole->window) return;
    DoUpdateDisplay();
}

/*
 * Function: DoUpdateDisplay
 * Usage: DoUpdateDisplay();
 * -------------------------
 * This function copies the bits from the offscreen memory to
 * the invalid area of the display.
 */

static void DoUpdateDisplay(void)
{
    GrafPtr saveWindow;
    WindowPeek window;
    Rect rect;
    int width, height;

    window = (WindowPeek) gConsole->window;
    BeginUpdate((WindowPtr) window);
    GetPort(&saveWindow);
    SetPort((WindowPtr) window);
    CopyBits(&osPort->portBits, &window->port.portBits,
             &osPort->portRect, &window->port.portRect,
             srcCopy, NULL);
    SetPort(saveWindow);
    EndUpdate((WindowPtr) window);
}

/*
 * Utility functions
 * -----------------
 * This section contains several extremely short utility functions
 * that improve the readability of the code.
 */

/*
 * Functions: RectWidth, RectHeight
 * Usage: w = RectWidth(r);
 *        h = RectHeight(r);
 * --------------------------------
 * These functions return the width and height of a rectangle.
 */

static int RectWidth(Rect r)
{
    return (r.right - r.left);
}

static int RectHeight(Rect r)
{
    return (r.bottom - r.top);
}

/*
 * Functions: Radians, Degrees
 * Usage: radians = Radians(degrees);
 *        degrees = Degrees(radians);
 * ----------------------------------
 * These functions convert back and forth between degrees and radians.
 */

static double Radians(double degrees)
{
    return (degrees * Pi / 180);
}

static double Degrees(double radians)
{
    return (radians * 180 / Pi);
}

/*
 * Functions: InchesX, InchesY
 * Usage: inches = InchesX(pixels);
 *        inches = InchesY(pixels);
 * --------------------------------
 * These functions convert distances measured in pixels to inches.
 * Because the resolution may not be uniform in the horizontal and
 * vertical directions, the coordinates are treated separately.
 */

static double InchesX(int x)
{
    return ((double) x / xResolution);
}

static double InchesY(int y)
{
    return ((double) y / yResolution);
}

/*
 * Functions: PixelsX, PixelsY
 * Usage: pixels = PixelsX(inches);
 *        pixels = PixelsY(inches);
 * --------------------------------
 * These functions convert distances measured in inches to pixels.
 */

static int PixelsX(double x)
{
    return (Round(x * xResolution + Epsilon));
}

static int PixelsY(double y)
{
    return (Round(y * yResolution + Epsilon));
}

/*
 * Functions: ScaleX, ScaleY
 * Usage: pixels = ScaleX(inches);
 *        pixels = ScaleY(inches);
 * --------------------------------
 * These functions are like PixelsX and PixelsY but convert coordinates
 * rather than lengths.  The difference is that y-coordinate values must
 * be inverted top to bottom to support the cartesian coordinates of
 * the graphics.h model.
 */

static int ScaleX(double x)
{
    return (PixelsX(x));
}

static int ScaleY(double y)
{
    int height;
    WindowPeek window;

    window = gConsole->window;
    height = window->port.portRect.bottom - window->port.portRect.top;
    return (Round(height - PixelsY(y)));
}

static int Round(double x)
{
    return ((int) floor(x + 0.5));
}

/*
 * Functions: Min, Max
 * Usage: min = Min(x, y);
 *        max = Max(x, y);
 * -----------------------
 * These functions find the minimum and maximum of two integers.
 */

static int Min(int x, int y)
{
    return ((x < y) ? x : y);
}

static int Max(int x, int y)
{
    return ((x > y) ? x : y);
}

/*
 * Functions: MinF, MaxF
 * Usage: min = MinF(x, y);
 *        max = MaxF(x, y);
 * ------------------------
 * These functions find the minimum and maximum of two doubles.
 */

static double MinF(double x, double y)
{
    return ((x < y) ? x : y);
}

static double MaxF(double x, double y)
{
    return ((x > y) ? x : y);
}
