/*
 * File: graphics.c
 * Last modified on Tue Jul 26 16:18:32 1994 by eroberts
 * -----------------------------------------------------
 * This file implements the graphics.h and extgraph.h interfaces
 * for the Borland/Windows platform.
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <math.h>
#include <conio.h>
#include <windows.h>
#include <time.h>

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

/*
 * Parameters
 * ----------
 * PixelsPerInch -- Device resolution (not obtainable directly)
 * DefaultSize   -- Default point size
 * MaxTitle      -- Maximum window title length
 * MaxFontName   -- Maximum font name length
 * MaxFonts      -- Maximum number of fonts
 * TopMargin     -- Margin from top of screen to graphics window
 * LeftMargin    -- Margin from left of screen to both windows
 * RightMargin   -- Margin from right of screen to both windows
 * BottomMargin  -- Margin at bottom of screen from console window
 * WindowSep     -- Separation between graphics and console windows
 * ConsoleHeight -- Height of the console window
 * PStartSize    -- Starting size for polygon (must be greater than 1)
 * GWClassName   -- Class name of the graphics window
 * DefaultFont   -- Font that serves as the "Default" font
 */

#define PixelsPerInch   72
#define DefaultSize     12
#define MaxTitle        75
#define MaxFontName     50
#define MaxFonts        25
#define TopMargin       10
#define LeftMargin      15
#define RightMargin     15
#define BottomMargin    10
#define WindowSep        8
#define ConsoleHeight  140
#define PStartSize      50

#define GWClassName "Graphics Window"
#define DefaultFont "Courier"

/*
 * Other constants
 * ---------------
 * LargeInt -- Integer too large for a coordinate value
 * 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: fontEntryT
 * ----------------
 * This structure holds the data for a font.
 */

typedef struct {
    string name;
    int size, ascent;
    HFONT font;
} fontEntryT;

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

/*
 * Static table: grayList
 * ----------------------
 * This table contains the stock object identifiers for the gray
 * scale values.
 */

static int grayList[] = {
    WHITE_BRUSH, LTGRAY_BRUSH, GRAY_BRUSH, DKGRAY_BRUSH, BLACK_BRUSH
};

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

/*
 * Global variables
 * ----------------
 * initialized    -- TRUE if initialization has been done
 * consoleWindow  -- Window handle for console window
 * graphicsWindow -- Window handoe for graphics window
 * osdc           -- Offscreen DC (memory backup)
 * osbits         -- Offscreen bitmap
 * drawPen        -- Pen used for drawing
 * erasePen       -- Pen used for erasing
 * nullPen        -- Pen used for filling
 * drawColor      -- Color used for drawing
 * eraseColor     -- Color used for erasing
 * grayBrush      -- Array of brush colors for filling
 * windowTitle    -- Current window title (initialized statically)
 * xResolution    -- Horizontal resolution of screen (dots per inch)
 * yResolution    -- Vertical resolution of screen (dots per inch)
 * windowWidth    -- Width of graphics window (inches)
 * windowHeight   -- Height of graphics window (inches)
 * pixelWidth     -- Width of graphics window (pixels)
 * pixelHeight    -- Height of graphics window (pixels)
 * fontTable      -- Table of stored fonts
 * nFonts         -- Number of fonts in fontTable
 * currentFont    -- Index of current font in fontTable
 * regionState    -- Current state of the region
 * regionGray     -- Gray scale to apply to region
 * polygonPoints  -- Array of points used in current region
 * nPolygonPoints -- Number of active points
 * polygonSize    -- Number of allocated points
 * polygonBounds  -- Bounding box of polygon
 * 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 HWND consoleWindow, graphicsWindow;
static HDC osdc;
static HBITMAP osBits;
static HPEN drawPen, erasePen, nullPen;
static COLORREF drawColor, eraseColor;
static HBRUSH grayBrush[NGrays];
static PAINTSTRUCT ps;
static string windowTitle = "Graphics Window";

static double xResolution, yResolution;
static double windowWidth, windowHeight;
static int pixelWidth, pixelHeight;

static fontEntryT fontTable[MaxFonts];
static int nFonts;
static int currentFont;

static regionStateT regionState;
static double regionGray;
static POINT *polygonPoints;
static int nPolygonPoints;
static int polygonSize;
static RECT polygonBounds;

static graphicsStateT stateStack;

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

/* Private function prototypes */

static void InitCheck(void);
static void InitGraphicsState(void);
static void InitDisplay(void);
static void InitDrawingTools(void);
static void DisplayExit(void);
static HWND FindConsoleWindow(void);
static BOOL CALLBACK EnumerateProc(HWND window, LPARAM clientData);
static void RegisterWindowClass(void);
static LONG FAR PASCAL GraphicsEventProc(HWND w, UINT msg,
                                         WPARAM p1, LPARAM p2);
static void DoUpdate(void);
static void DisplayClear(void);
static void DisplayLine(double x, double y, double dx, double dy);
static void DisplayArc(double xc, double yc, 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);
static void DisplayEraseMode(bool flag);
static void DisplayFontAndSize(string font, int size);
static int FindExistingFont(string name, int size);
static void SetLineBB(RECT *rp, double x, double y, double dx, double dy);
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);
static void StartPolygon(void);
static void AddSegment(int x0, int y0, int x1, int y1);
static void DisplayPolygon(void);
static void AddPolygonPoint(int x, int y);

static bool PrefixMatch(string prefix, string str);
static int RectWidth(RECT r);
static int RectHeight(RECT r);
static double Radians(double degrees);
static int Round(double x);
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 Min(int x, int y);
static int Max(int x, int y);

/* Exported entries */

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

void InitGraphics(void)
{
    if (!initialized) InitDisplay();
    DisplayClear();
    InitGraphicsState();
    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)
{
    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;
    StartPolygon();
}

void EndFilledRegion(void)
{
    InitCheck();
    if (regionState == NoRegion) {
        Error("EndFilledRegion without StartFilledRegion");
    }
    DisplayPolygon();
    regionState = NoRegion;
}

/* Section 4 -- String functions */

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

double TextStringWidth(string text)
{
    RECT r;

    InitCheck();
    SetTextBB(&r, cx, cy, text);
    return (InchesX(RectWidth(r)));
}

void SetFont(string font)
{
    InitCheck();
    DisplayFontAndSize(font, pointSize);
}

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

void SetPointSize(int size)
{
    InitCheck();
    DisplayFontAndSize(textFont, size);
}

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

/* Section 5 -- Miscellaneous functions */

void SetEraseMode(bool mode)
{
    InitCheck();
    eraseMode = mode;
    DisplayEraseMode(mode);
}

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

void SetWindowTitle(string title)
{
    windowTitle = CopyString(title);
    if (initialized) {
        SetWindowText(graphicsWindow, windowTitle);
    }
}

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

void UpdateDisplay(void)
{
    InitCheck();
    DoUpdate();
}

void Pause(double seconds)
{
    double finish;

    UpdateDisplay();
    finish = (double) clock() / CLK_TCK + seconds;
    while (((double) clock() / CLK_TCK) < finish);
}

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);
    DisplayEraseMode(eraseMode);
    DisplayFontAndSize(textFont, pointSize);
}

/* 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.  Because the erase mode and font
 * information are also maintained in the display state,
 * InitGraphicsState must call functions to ensure that these
 * values are initialized there as well.
 */

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

/*
 * Function: InitDisplay
 * Usage: InitDisplay();
 * ---------------------
 * This function does everything necessary to initialize the display.
 * In this implementation, the graphics window is created as an
 * overlapping child of the console window, which has been created
 * by EasyWin.  As a child of the console, the window automatically
 * gets events whenever the console window is waiting for events,
 * simplifying the logic of the implementation considerably.  Most
 * of the InitDisplay implementation is concerned with calculating
 * the new tiling geometry, after which the function creates the
 * new window and an offscreen bitmap to use as a target for any
 * rendering.  When the window gets a Paint event in the event
 * procedure GraphicsEventProc, all it has to do is copy the
 * bits from the offscreen bitmap onto the screen.
 */

static void InitDisplay(void)
{
    HWND desktop;
    RECT bounds, desktopRect, consoleRect, graphicsRect;
    DWORD style;

    clrscr();
    atexit(DisplayExit);
    RegisterWindowClass();
    InitDrawingTools();
    consoleWindow = FindConsoleWindow();
    desktop = GetDesktopWindow();
    GetWindowRect(desktop, &desktopRect);
    GetWindowRect(consoleWindow, &consoleRect);
    xResolution = GetDeviceCaps(GetDC(consoleWindow), LOGPIXELSX);
    yResolution = GetDeviceCaps(GetDC(consoleWindow), LOGPIXELSY);
    graphicsRect.left = consoleRect.left = desktopRect.left + LeftMargin;
    graphicsRect.right = consoleRect.right = desktopRect.right - RightMargin;
    consoleRect.bottom = desktopRect.bottom - BottomMargin;
    consoleRect.top = consoleRect.bottom - ConsoleHeight;
    graphicsRect.top = desktopRect.top + TopMargin;
    graphicsRect.bottom = consoleRect.top - WindowSep;
    style = WS_OVERLAPPEDWINDOW & ~(WS_MINIMIZEBOX | WS_MAXIMIZEBOX);
    graphicsWindow =
      CreateWindow(GWClassName, windowTitle, style,
                   graphicsRect.left, graphicsRect.top,
                   RectWidth(graphicsRect), RectHeight(graphicsRect),
                   consoleWindow, (HMENU) NULL, (HINSTANCE) NULL,
                   (LPSTR) NULL);
    if (graphicsWindow == NULL) {
        printf("Internal error: CreateGraphicsWindow failed.\n");
    }
    ShowWindow(graphicsWindow, SW_SHOW);
    UpdateWindow(graphicsWindow);
    SetWindowText(consoleWindow, "Console Window");
    SetWindowPos(consoleWindow, HWND_TOP, consoleRect.left, consoleRect.top,
                 RectWidth(consoleRect), RectHeight(consoleRect), 0);
    GetClientRect(graphicsWindow, &bounds);
    pixelWidth = RectWidth(bounds);
    pixelHeight = RectHeight(bounds);
    windowWidth = InchesX(pixelWidth);
    windowHeight = InchesY(pixelHeight);
    osdc = CreateCompatibleDC(GetDC(graphicsWindow));
    if (osdc == NULL) {
        Error("Internal error: Can't create offscreen device");
    }
    osBits = CreateCompatibleBitmap(osdc, pixelWidth, pixelHeight);
    if (osBits == NULL) {
        Error("Internal error: Can't create offscreen bitmap");
    }
    (void) SelectObject(osdc, osBits);
}

/*
 * Function: InitDrawingTools
 * Usage: InitDrawingTools();
 * --------------------------
 * This function initializes all of the standard objects used for
 * drawing except for fonts, which are initialized dynamically by
 * the DisplayFontAndSize procedure.  This function creates the
 * foreground/background colors, the drawing pens, and the brushes
 * for filled regions.
 */

static void InitDrawingTools(void)
{
    int i;

    nFonts = 0;
    drawColor = RGB(0, 0, 0);
    eraseColor = RGB(255, 255, 255);
    drawPen = (HPEN) GetStockObject(BLACK_PEN);
    erasePen = (HPEN) GetStockObject(WHITE_PEN);
    nullPen = (HPEN) GetStockObject(NULL_PEN);
    if (drawPen == NULL || erasePen == NULL || nullPen == NULL) {
        Error("Internal error: Can't initialize pens");
    }
    for (i = 0; i < NGrays; i++) {
        grayBrush[i] = (HBRUSH) GetStockObject(grayList[i]);
        if (grayBrush[i] == NULL) {
            Error("Internal error: Can't initialize gray scales");
        }
    }
}

/*
 * Function: DisplayExit
 * Usage: DisplayExit();
 * ---------------------
 * This function is called when the program exits and waits for the
 * user to type a carriage return.  After reading and ignoring the
 * return key, this function frees the window system handles and
 * destroys the console window, thereby exiting the program.
 */

static void DisplayExit(void)
{
    int i;

    (void) getchar();
    DeleteObject(drawPen);
    DeleteObject(erasePen);
    DeleteObject(nullPen);
    for (i = 0; i < NGrays; i++) {
        DeleteObject(grayBrush[i]);
    }
    for (i = 0; i < nFonts; i++) {
        DeleteObject(fontTable[i].font);
    }
    DeleteDC(osdc);
    DestroyWindow(consoleWindow);
}

/*
 * Function: FindConsoleWindow
 * Usage: window = FindConsoleWindow();
 * ------------------------------------
 * The EasyWin package makes almost everything about the graphics
 * package easy in the Borland world.  The only thing that is hard
 * is getting the handle of the window used for the console in the
 * first place.  This function finds the console window handle by
 * enumerating the windows and looking for the first one whose
 * title ends with .EXE, which the EasyWin package puts there.
 */

static HWND FindConsoleWindow(void)
{
    HWND result;

    EnumWindows(EnumerateProc, (LPARAM) &result);
    return (result);
}

/*
 * Function: EnumerateProc
 * Usage: Not called directly
 * --------------------------
 * This callback procedure is used by the FindConsoleWindow
 * call to find the window whose title ends with .EXE.
 */

static BOOL CALLBACK EnumerateProc(HWND window, LPARAM clientData)
{
    HWND *wptr;
    char title[MaxTitle];
    bool ok;

    wptr = (HWND *) clientData;
    ok = GetWindowText(window, title, MaxTitle-1);
    if (ok && strcmp(title + strlen(title) - 4, ".EXE")==0) {
        *wptr = window;
        return (0);
    }
    return (1);
}

/*
 * Function: RegisterWindowClass
 * Usage: RegisterWindowClass();
 * -----------------------------
 * This function registers the window class used for the graphics
 * window.
 */

static void RegisterWindowClass(void)
{
    WNDCLASS wcApp;

    wcApp.lpszClassName = GWClassName;
    wcApp.hInstance = NULL;
    wcApp.lpfnWndProc = GraphicsEventProc;
    wcApp.hCursor = NULL;
    wcApp.hIcon = NULL;
    wcApp.lpszMenuName = NULL;
    wcApp.hbrBackground = GetStockObject(WHITE_BRUSH);
    wcApp.style = CS_HREDRAW | CS_VREDRAW;
    wcApp.cbClsExtra = wcApp.cbWndExtra = 0;
    if (!RegisterClass(&wcApp)) {
        Error("Internal error: RegisterClass failed\n");
    }
}

/*
 * Function: GraphicsEventProc
 * Usage: Not called directly
 * --------------------------
 * This function is called when an event is received for the
 * graphics window.  The only event this package needs to handle
 * is the paint event, which forces a screen update.
 */

static LONG FAR PASCAL GraphicsEventProc(HWND w, UINT msg,
                                         WPARAM p1, LPARAM p2)
{
    if (msg == WM_PAINT) {
        DoUpdate();
        return (0L);
    } else {
        return (DefWindowProc(w, msg, p1, p2));
    }
}

/*
 * Function: DoUpdate
 * Usage: DoUpdate();
 * ------------------
 * This function redraws the graphics window by copying bits from
 * the offscreen bitmap behind the osdc device context into the
 * actual display context.
 */

static void DoUpdate(void)
{
    HDC dc;

    dc = BeginPaint(graphicsWindow, &ps);
    BitBlt(dc, 0, 0, pixelWidth, pixelHeight, osdc, 0, 0, SRCCOPY);
    EndPaint(graphicsWindow, &ps);
}

/*
 * Function: DisplayClear
 * Usage: DisplayClear();
 * ----------------------
 * This function clears all the bits in the offscreen bitmap.
 */

static void DisplayClear(void)
{
    RECT r;

    SetRect(&r, 0, 0, pixelWidth, pixelHeight);
    InvalidateRect(graphicsWindow, &r, TRUE);
    BitBlt(osdc, 0, 0, pixelWidth, pixelHeight, osdc, 0, 0, WHITENESS);
}

/*
 * Function: DisplayLine
 * Usage: DisplayLine(x, y, dx, dy);
 * ---------------------------------
 * This function renders a line into the offscreen bitmap.  If the
 * region is started, it adds the line to the developing polygonal
 * region instead.
 */

static void DisplayLine(double x, double y, double dx, double dy)
{
    int x0, y0, x1, y1;
    RECT r;

    x0 = ScaleX(x);
    y0 = ScaleY(y);
    x1 = ScaleX(x + dx);
    y1 = ScaleY(y + dy);
    if (regionState == NoRegion) {
        SetLineBB(&r, x, y, dx, dy);
        InvalidateRect(graphicsWindow, &r, TRUE);
        MoveTo(osdc, x0, y0);
        LineTo(osdc, x1, y1);
    } else {
        AddSegment(x0, y0, x1, y1);
    }
}

/*
 * Function: DisplayArc
 * Usage: DisplayArc(xc, yc, rx, ry, start, sweep);
 * ------------------------------------------------
 * This function is used to draw an arc.  The arguments are slightly
 * different from those in the client interface because xc and yc
 * designate the center.  This function is only called if a region
 * is not being assembled; if it is, the package calls RenderArc
 * instead.
 */

static void DisplayArc(double xc, double yc, double rx, double ry,
                       double start, double sweep)
{
    RECT r;
    int xmax, xmin, ymax, ymin;
    int ix0, iy0, ix1, iy1;

    SetArcBB(&r, xc, yc, rx, ry, start, sweep);
    InvalidateRect(graphicsWindow, &r, TRUE);
    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 (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)));
    Arc(osdc, xmin, ymin, xmax, ymax, ix0, iy0, ix1, iy1);
}

/*
 * Function: RenderArc
 * Usage: RenderArc(xc, yc, rx, ry, start, sweep);
 * -----------------------------------------------
 * This function is identical to DisplayArc except that, instead
 * of calling the Arc function, RenderArc simulates the arc by
 * constructing a path of consecutive segments, which are added
 * to the current polygonal region.
 */

static void RenderArc(double x, double y, double rx, double ry,
                      double start, double sweep)
{
    double t, mint, maxt, dt, maxd;
    int ix0, iy0, ix1, iy1;

    if (sweep < 0) {
        start += sweep;
        sweep = -sweep;
    }
    if (fabs(rx) > fabs(ry)) {
        maxd = fabs(rx);
    } else {
        maxd = fabs(rx);
    }
    dt = atan2(InchesY(1), maxd);
    mint = Radians(start);
    maxt = Radians(start + sweep);
    ix0 = ScaleX(x + rx * cos(mint));
    iy0 = ScaleY(y + ry * sin(mint));
    for (t = mint + dt; t < maxt; t += dt) {
        if (t > maxt - dt / 2) t = maxt;
        ix1 = ScaleX(x + rx * cos(t));
        iy1 = ScaleY(y + ry * sin(t));
        AddSegment(ix0, iy0, ix1, iy1);
        ix0 = ix1;
        iy0 = iy1;
    }
}

/*
 * Function: DisplayText
 * Usage: DisplayText(x, y, text);
 * -------------------------------
 * This function displays a text string at (x, y) in the current
 * font and size.  The hard work is done in DisplayFontAndSize.
 */

static void DisplayText(double x, double y, string text)
{
    RECT r;

    SetTextBB(&r, x, y, text);
    InvalidateRect(graphicsWindow, &r, TRUE);
    TextOut(osdc, ScaleX(x), ScaleY(y) - fontTable[currentFont].ascent,
            text, strlen(text));
}

/*
 * Function: DisplayEraseMode
 * Usage: DisplayEraseMode(flag);
 * ------------------------------
 * This function updates the pens and color state when the erase
 * mode changes.
 */

static void DisplayEraseMode(bool flag)
{
    SelectObject(osdc, (flag) ? erasePen : drawPen);
    SetTextColor(osdc, (flag) ? eraseColor : drawColor);
}

/*
 * Function: DisplayFontAndSize
 * Usage: DisplayFontAndSize(font, size);
 * --------------------------------------
 * This function updates the font and size information used for
 * drawing text.  The program first uses FindExistingFont to see
 * if the desired font/size pair has been entered in the table,
 * in which case the program uses the stored handle of the font.
 * If not, the program uses CreateFont to try to create an
 * appropriate font, accepting only those whose typeface
 * matches the desired font string.  If an acceptable font
 * is found, its data is entered into the font table.
 */

static void DisplayFontAndSize(string font, int size)
{
    char fontBuffer[MaxFontName + 1];
    char faceName[MaxFontName + 1];
    string fontName;
    HFONT newFont, oldFont;
    TEXTMETRIC metrics;
    int i, fontIndex;

    for (i = 0; (fontBuffer[i] = tolower(font[i])) != '\0'; i++);
    if (StringEqual("default", fontBuffer)) {
        fontName = DefaultFont;
    } else {
        fontName = fontBuffer;
    }
    fontIndex = FindExistingFont(fontName, size);
    if (fontIndex == -1) {
        newFont =
          CreateFont(-size, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, fontName);
        if (newFont != NULL) {
            oldFont = (HFONT) SelectObject(osdc, newFont);
            GetTextFace(osdc, MaxFontName, faceName);
            if (PrefixMatch(fontName, faceName)
                && GetTextMetrics(osdc, &metrics)) {
                if (nFonts == MaxFonts) Error("Too many fonts loaded");
                fontIndex = nFonts++;
                fontTable[fontIndex].name = CopyString(fontName);
                fontTable[fontIndex].size = size;
                fontTable[fontIndex].font = newFont;
                fontTable[fontIndex].ascent = metrics.tmAscent;
                currentFont = fontIndex;
                textFont = CopyString(font);
                pointSize = size;
            } else {
                (void) SelectObject(osdc, oldFont);
            }
        }
    } else {
        (void) SelectObject(osdc, fontTable[fontIndex].font);
        currentFont = fontIndex;
        textFont = CopyString(font);
        pointSize = size;
    }
}

/*
 * Function: FindExistingFont
 * Usage: fontIndex = FindExistingFont(name, size);
 * ------------------------------------------------
 * This function searches the font table for a matching name and
 * size.  The function returns the matching table index or -1 if
 * no match is found, The caller has already converted the name
 * to lower case to preserve the case-insensitivity requirement.
 */

static int FindExistingFont(string name, int size)
{
    int i;

    for (i = 0; i < nFonts; i++) {
        if (StringEqual(name, fontTable[i].name)
            && size == fontTable[i].size) return (i);
    }
    return (-1);
}

/*
 * Function: SetLineBB
 * Usage: SetLineBB(&rect, x, y, dx, dy);
 * --------------------------------------
 * This function sets the rectangle dimensions to the bounding
 * box of the line.
 */

static void SetLineBB(RECT *rp, double x, double y, double dx, double dy)
{
    int x0, y0, x1, y1;

    x0 = ScaleX(x);
    y0 = ScaleY(y);
    x1 = ScaleX(x + dx);
    y1 = ScaleY(y + dy);
    rp->top = Min(y0, y1);
    rp->bottom = Max(y0, y1) + 1;
    rp->left = Min(x0, x1);
    rp->right = Max(x0, x1) + 1;
}

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

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

static void SetTextBB(RECT *rp, double x, double y, string text)
{
    SIZE textSize;
    int ix, iy;

    if (!GetTextExtentPoint(osdc, text, strlen(text), &textSize)) {
        Error("Internal error: Text size calculation failed");
    }
    ix = ScaleX(x);
    iy = ScaleY(y);
    SetRect(rp, ix, iy - textSize.cy, ix + textSize.cx, iy);
}

/*
 * Functions: StartPolygon, AddSegment, EndPolygon
 * Usage: StartPolygon();
 *        AddSegment(x0, y0, x1, y1);
 *        AddSegment(x1, y1, x2, y2);
 *        . . .
 *        DisplayPolygon();
 * ----------------------------------
 * These functions implement the notion of a region in the PC
 * world, where the easiest shape to fill is a polygon.  Calling
 * StartPolygon initializes the array polygonPoints so that
 * subsequent calls to AddSegment will add points to it.
 * The points in the polygon are assumed to be contiguous,
 * because the client interface checks for this property.
 * Because polygons involving arcs can be quite large, the
 * AddSegment code extends the polygonPoints list if needed
 * by doubling the size of the array.  All storage is freed
 * after calling DisplayPolygon.
 */

static void StartPolygon(void)
{
    polygonPoints = NewArray(PStartSize, POINT);
    polygonSize = PStartSize;
    nPolygonPoints = 0;
    SetRect(&polygonBounds, LargeInt, LargeInt, 0, 0);
}

static void AddSegment(int x0, int y0, int x1, int y1)
{
    if (nPolygonPoints == 0) AddPolygonPoint(x0, y0);
    AddPolygonPoint(x1, y1);
}

static void DisplayPolygon(void)
{
    int px;
    HBRUSH fillBrush;
    HPEN oldPen, fillPen;

    InvalidateRect(graphicsWindow, &polygonBounds, TRUE);
    if (eraseMode) {
        px = 0;
        fillPen = erasePen;
    } else {
        px = regionGray * (NGrays - 1) + 0.5 - Epsilon;
        fillPen = nullPen;
    }
    oldPen = (HPEN) SelectObject(osdc, fillPen);
    fillBrush = grayBrush[px];
    if (fillBrush == NULL) {
        Error("Internal error: Can't load brush");
    }
    (void) SelectObject(osdc, fillBrush);
    Polygon(osdc, polygonPoints, nPolygonPoints);
    (void) SelectObject(osdc, oldPen);
    FreeBlock(polygonPoints);
}

/*
 * Function: AddPolygonPoint
 * Usage: AddPolygonPoint(x, y);
 * -----------------------------
 * AddPolygonPoint acts as a helper function for AddSegment.  This
 * function does the work, but AddSegment has a more easily understood
 * interface.
 */

static void AddPolygonPoint(int x, int y)
{
    POINT *newPolygon;
    int i;

    if (nPolygonPoints >= polygonSize) {
        polygonSize *= 2;
        newPolygon = NewArray(polygonSize, POINT);
        for (i = 0; i < nPolygonPoints; i++) {
            newPolygon[i] = polygonPoints[i];
        }
        FreeBlock(polygonPoints);
        polygonPoints = newPolygon;
    }
    polygonBounds.left = Min(polygonBounds.left, x);
    polygonBounds.right = Max(polygonBounds.right, x);
    polygonBounds.top = Min(polygonBounds.top, y);
    polygonBounds.bottom = Max(polygonBounds.bottom, y);
    polygonPoints[nPolygonPoints].x = x;
    polygonPoints[nPolygonPoints].y = y;
    nPolygonPoints++;
}

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

/*
 * Function: PrefixMatch
 * Usage: if (PrefixMatch(prefix, str)) . . .
 * -------------------------------------------------
 * This function returns TRUE if prefix is the initial substring
 * of str, ignoring differences in case.
 */

static bool PrefixMatch(char *prefix, char *str)
{
    while (*prefix != '\0') {
        if (tolower(*prefix++) != tolower(*str++)) return (FALSE);
    }
    return (TRUE);
}

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

/*
 * Function: Radians
 * Usage: radians = Radians(degrees);
 * ----------------------------------
 * This functions convert an angle in degrees to radians.
 */

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

/*
 * Function: Round
 * Usage: n = Round(x);
 * --------------------
 * This function rounds a double to the nearest integer.
 */

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

/*
 * 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)
{
    return (PixelsY(windowHeight - y));
}

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