/*
 * File: xdisplay.c
 * Last modified on Mon Jul 25 14:08:21 1994 by eroberts
 * -----------------------------------------------------
 * This file implements the xdisplay.h interface, which is
 * responsible for all of the X windows interaction.  Before
 * you attempt to understand this implementation, you need to
 * understand the basics of X11 library.
 */

/*
 * General implementation notes
 * ----------------------------
 * This implementation creates two X drawables: the graphics
 * window itself (mainWindow) and an offscreen window (osWindow).
 * All rendering is done into the offscreen window.  When update
 * events occur, the graphics window is updated by copying bits
 * from the offscreen window.
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>
#include <sys/time.h>
#include <sys/types.h>
#include <X11/Xlib.h>
#include <X11/Xutil.h>

#include "genlib.h"
#include "strlib.h"
#include "glibrary.h"
#include "xmanager.h"
#include "xdisplay.h"

/*
 * Parameters
 * ----------
 * DesiredWidth  -- Desired width of the graphics window in inches
 * DesiredHeight -- Desired height of the graphics window in inches
 * BorderPixels  -- Width of the window border in pixels
 * MaxFontList   -- Size of the font list we will accept
 * PStartSize    -- Starting size for polygon (must be greater than 1)
 * DefaultFont   -- Font that serves as the "Default" font
 */

#define DesiredWidth    7.0
#define DesiredHeight   5.0
#define BorderPixels    1
#define MaxFontList   500
#define PStartSize     50
#define DefaultFont    "courier-medium"

/*
 * Other constants
 * ---------------
 * Epsilon -- Small offset used to avoid banding/aliasing
 * GCFgBg  -- GC mask for both foreground and background
 */

#define Epsilon 0.0000000001
#define GCFgBg (GCForeground | GCBackground)

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

static char grayList[][8] = {
    { 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 variables
 * -----------------
 * redraw          -- TRUE if mainWindow needs redrawing
 * eraseMode       -- TRUE if erase mode has been set
 * xdpi, ydpi      -- Dots per inch in each coordinate
 *
 * disp            -- X display containing windows
 * mainWindow      -- Handle of graphics window
 * osWindow        -- Handle of offscreen window bitmap
 * drawColor       -- Color used for drawing (black)
 * eraseColor      -- Color used for erasing (white)
 * mainGC          -- The graphics context (GC) for mainWindow
 * drawGC          -- The GC used for drawing on osWindow
 * eraseGC         -- The GC used for erasing on osWindow
 * grayGC          -- Array of GCs representing gray scales
 * grayStipple     -- Array of stipples used for gray scales
 * currentFont     -- Name of current font
 * currentSize     -- Current point size
 * fontInfo        -- Font structure pointer for current font
 * windowWidth     -- Width of the window in inches
 * windowHeight    -- Height of the window in inches
 *
 * argV, argC      -- Used to set corresponding X window fields
 *
 * regionStarted   -- TRUE is a region is in progress
 * regionGrayScale -- Gray scale density [0,1]
 * polygonPoints   -- Array of points used in current region
 * nPolygonPoints  -- Number of active points
 * polygonSize     -- Number of allocated points
 */

static bool redraw = FALSE;
static bool eraseMode;
static double xdpi, ydpi;

static Display *disp;
static Window mainWindow;
static Pixmap osWindow;
static unsigned long drawColor, eraseColor;
static GC mainGC, drawGC, eraseGC;
static GC grayGC[NGrays];
static Pixmap grayStipple[NGrays];
static string currentFont;
static XFontStruct *fontInfo;
static int windowWidth = DesiredWidth;
static int windowHeight = DesiredHeight;

static string argV[] = { "xdisplay" };
static int argC = sizeof argV / sizeof argV[0];

static bool regionStarted;
static double regionGrayScale;
static XPoint *polygonPoints;
static int nPolygonPoints;
static int polygonSize;

/* Private function prototypes */

static void InitGC(void);
static void ForceRedraw(void);
static void RedrawWindow(void);
static void StartPolygon(void);
static void AddSegment(int x0, int y0, int x1, int y1);
static void DisplayPolygon(void);
static void RenderArc(double x, double y, double rx, double ry,
                      double start, double sweep);
static int SizeFromFontName(string fontName);
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);

/* Exported entries */

/*
 * Function: OpenDisplay
 * ---------------------
 * The OpenDisplay function is relatively long but consists of
 * little more than a series of X calls to establish the
 * parameters of the display.
 */

void OpenDisplay(string title)
{
    XWMHints xwmh;
    XSizeHints xsh;
    Colormap colormap;
    XFontStruct *font;
    Screen *screen;
    XSetWindowAttributes xswa;
    long width, height;
    int bitmask, scnum;
    char geometry[100];

    if ((disp = XOpenDisplay(NULL)) == NULL) {
        Error("Can't open display");
    }
    if ((font = XLoadQueryFont(disp, "fixed")) == NULL) {
        Error("Can't create font");
    }
    scnum = DefaultScreen(disp);
    screen = ScreenOfDisplay(disp, scnum);
    colormap = DefaultColormap(disp, scnum);
    drawColor = BlackPixel(disp, scnum);
    eraseColor = WhitePixel(disp, scnum);
    xdpi = 25.4 * WidthOfScreen(screen) / WidthMMOfScreen(screen);
    ydpi = 25.4 * HeightOfScreen(screen) / HeightMMOfScreen(screen);
    width = PixelsX(DesiredWidth);
    height = PixelsY(DesiredHeight);
    sprintf(geometry, "%dx%d+%d+%d", width, height,
            (DisplayWidth(disp, scnum) - width) / 2,
            (DisplayHeight(disp, scnum) - height) / 2);
    bitmask = XGeometry(disp, scnum, geometry, geometry,
                        BorderPixels,
                        font->max_bounds.width,
                        font->max_bounds.ascent + font->max_bounds.descent,
                        1, 1, &xsh.x, &xsh.y, &xsh.width, &xsh.height);
    if (bitmask & (XValue | YValue)) xsh.flags |= USPosition;
    if (bitmask & (WidthValue | HeightValue)) xsh.flags |= USSize;
    mainWindow = XCreateSimpleWindow(disp, DefaultRootWindow(disp),
                                     xsh.x, xsh.y, xsh.width, xsh.height,
                                     BorderPixels, drawColor, eraseColor);
    XSetStandardProperties(disp, mainWindow, title, title,
                           None, argV, argC, &xsh);
    xwmh.flags = (InputHint | StateHint);
    xwmh.input = False;
    xwmh.initial_state = NormalState;
    XSetWMHints(disp, mainWindow, &xwmh);
    xswa.colormap = colormap;
    xswa.bit_gravity = CenterGravity;
    XChangeWindowAttributes(disp, mainWindow,
                            CWColormap | CWBitGravity, &xswa);
    XSelectInput(disp, mainWindow, ExposureMask);
    XMapWindow(disp, mainWindow);
    osWindow = XCreatePixmap(disp, DefaultRootWindow(disp),
                             xsh.width, xsh.height, 1);
    fontInfo = NULL;
    InitGC();
    regionStarted = FALSE;
    ClearDisplay();
    XFlush(disp);
}

/*
 * Function: CloseDisplay
 * ----------------------
 * This function frees the X structures allocated by the package.
 */

void CloseDisplay(void)
{
    int i;

    XDestroyWindow(disp, mainWindow);
    XFreePixmap(disp, osWindow);
    XFreeGC(disp, mainGC);
    XFreeGC(disp, drawGC);
    XFreeGC(disp, eraseGC);
    for (i = 0; i < NGrays; i++) {
        XFreeGC(disp, grayGC[i]);
        XFreePixmap(disp, grayStipple[i]);
    }
    XCloseDisplay(disp);
}

/*
 * Function: DisplayFD
 * -------------------
 * This function simply returns the connection number and exists
 * only to isolate the client from any interaction with X, which
 * is necessary for the disp argument.
 */

int DisplayFD(void)
{
    return (XConnectionNumber(disp));
}

/*
 * Function: ProcessXEvent
 * -----------------------
 * This function is used to read and respond to a pending X event,
 * although the only event considered in this implementation is the
 * expose event.  The function returns TRUE if an event was processed
 * and FALSE otherwise.
 */

bool ProcessXEvent(void)
{
    XEvent event;

    if (!XPending(disp)) return (FALSE);
    XNextEvent(disp, &event);
    if (event.xany.window == mainWindow) {
        if (event.type == Expose && event.xexpose.count == 0) {
            RedrawWindow();
        }
    }
    return (TRUE);
}

/*
 * Function: CheckForRedraw
 * ------------------------
 * This function allows the client to specify that a quiescent point
 * has been achieved and that it would be a good time to redraw the
 * window.  A redraw occurs only if graphics updates have been made.
 */

void CheckForRedraw(void)
{
    if (redraw) ForceRedraw();
}

/*
 * Function: SetRedrawFlag
 * -----------------------
 * This function allows the client to indicate that the display has
 * changed and that a redraw operation should be performed when the
 * next call to CheckForRedraw occurs.  This mechanism currently
 * sets a single flag and should at some point be redesigned to
 * maintain the update region.
 */

void SetRedrawFlag(void)
{
    redraw = TRUE;
}

/*
 * Function: ClearDisplay
 * ----------------------
 * This function erases the entire display by filling with the
 * erase color.
 */

void ClearDisplay(void)
{
    int itemp;
    unsigned int width, height, utemp;
    Window wtemp;

    if (XGetGeometry(disp, osWindow, &wtemp, &itemp, &itemp,
                     &width, &height, &utemp, &utemp) == 0) return;
    XFillRectangle(disp, osWindow, eraseGC, 0, 0, width, height);
}

/*
 * Function: DisplayLine
 * ---------------------
 * This function draws the requested line unless a region is in
 * progress, in which case it adds the line segment to the polygon.
 */

void DisplayLine(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);
    if (regionStarted) {
        AddSegment(x0, y0, x1, y1);
    } else {
        XDrawLine(disp, osWindow, (eraseMode) ? eraseGC : drawGC,
                  x0, y0, x1, y1);
    }
}

/*
 * Function: DisplayArc
 * --------------------
 * This function ordinarily scales its arguments and uses them
 * to draw an arc using the standard XDrawArc call.  If, however,
 * a region has been started, that arc must be rendered using
 * line segments, which is handled by RenderArc.
 */

void DisplayArc(double x, double y, double rx, double ry,
                double start, double sweep)
{
    int ixc, iyc, irx, iry, istart, isweep;

    if (regionStarted) {
        RenderArc(x, y, rx, ry, start, sweep);
    } else {
        ixc = ScaleX(x);
        iyc = ScaleY(y);
        irx = PixelsX(rx);
        iry = PixelsY(ry);
        istart = Round(start);
        isweep = Round(sweep);
        if (isweep < 0) {
            isweep = -isweep;
            istart -= isweep;
        }
        if (istart < 0) {
            istart = 360 - (-istart % 360);
        }
        istart %= 360;
        XDrawArc(disp, osWindow, (eraseMode) ? eraseGC : drawGC,
                 ixc - irx, iyc - iry, 2 * irx, 2 * iry,
                 64 * istart, 64 * isweep);
    }
}

/*
 * Function: DisplayText
 * ---------------------
 * This function transforms the client arguments and makes the
 * appropriate X call to display text on the screen.
 */

void DisplayText(double x, double y, string text)
{
    int ix, iy;

    ix = ScaleX(x);
    iy = ScaleY(y);
    XDrawString(disp, osWindow, (eraseMode) ? eraseGC : drawGC,
                ix, iy, text, strlen(text));
}

/*
 * Function: DisplayTextWidth
 * --------------------------
 * This function simply calls the XTextWidth function to compute the
 * width of the displayed text and scales it to the client coordinate
 * system.
 */

double DisplayTextWidth(string text)
{
    return (InchesX(XTextWidth(fontInfo, text, strlen(text))));
}

/*
 * Function: DisplaySetFont
 * ------------------------
 * This function searches the font names table for a font that matches
 * the argument characteristics as closely as possible.  As required
 * by the extgraph.h client interface, the function does not change
 * the font if no such font exists and selects the closest existing
 * size.
 */

string DisplaySetFont(string font, int size)
{
    char fontbuf[MaxFontName + 30];
    string fontName, *fontList;
    int i, nFonts, bestIndex, bestSize, currentSize;
    bool ok, boldFlag, styleFlag;

    fontName = ConvertToLowerCase(font);
    if (StringEqual(fontName, "default")) {
        sprintf(fontbuf, "*-%s-*", DefaultFont);
    } else {
        sprintf(fontbuf, "*-%s-*", fontName);
    }
    boldFlag = styleFlag = FALSE;
    if (FindString("-bold", fontName, 0) != -1) boldFlag = TRUE;
    if (FindString("-i", fontName, 0) != -1) styleFlag = TRUE;
    if (FindString("-o", fontName, 0) != -1) styleFlag = TRUE;
    FreeBlock(fontName);
    fontList = XListFonts(disp, fontbuf, MaxFontList, &nFonts);
    if (nFonts != 0) {
        bestIndex = -1;
        for (i = 1; i < nFonts; i++) {
            fontName = fontList[i];
            ok = TRUE;
            ok &= boldFlag || (FindString("-medium-", fontName, 0) != -1);
            ok &= styleFlag || (FindString("-r-", fontName, 0) != -1);
            if (ok) {
                currentSize = SizeFromFontName(fontName);
                if (bestIndex == -1 || abs(size - currentSize)
                                        < abs(size - bestSize)) {
                    bestSize = currentSize;
                    bestIndex = i;
                }
            }
        }
        if (bestIndex != -1) {
            if (fontInfo != NULL) XFreeFont(disp, fontInfo);
            fontInfo = XLoadQueryFont(disp, fontList[bestIndex]);
            if (fontInfo == NULL) {
                Error("Internal error: Can't find font");
            }
            XSetFont(disp, drawGC, fontInfo->fid);
            XSetFont(disp, eraseGC, fontInfo->fid);
            XFreeFontNames(fontList);
            currentFont = CopyString(font);
            currentSize = bestSize;
        }
    }
    sprintf(fontbuf, "%d %s", currentSize, currentFont);
    return (CopyString(fontbuf));
}

/*
 * Function: DisplaySetTitle
 * -------------------------
 * This function copies the user-supplied string into the window and
 * icon name.
 */

void DisplaySetTitle(string title)
{
    XTextProperty tp;

    XStringListToTextProperty(&title, 1, &tp);
    XSetWMIconName(disp, mainWindow, &tp);
    XSetWMName(disp, mainWindow, &tp);
}

/*
 * Function: DisplaySetEraseMode
 * -----------------------------
 * This function sets the internal state of the display logic so
 * that it maintains the correct state of the eraseMode flag.  In
 * the rest of the code, the eraseMode flag is used to control
 * which colors are used.
 */

void DisplaySetEraseMode(bool flag)
{
    eraseMode = flag;
}

/*
 * Function: DisplayStartRegion
 * ----------------------------
 * This function changes the state of the package so that subsequent
 * calls to DisplayLine and DisplayArc are used to add segments to a
 * polygonal region instead of having them appear on the display.
 * See the code for StartPolygon, AddSegment, and DisplayPolygon for
 * details.
 */

void DisplayStartRegion(double grayScale)
{
    regionStarted = TRUE;
    regionGrayScale = grayScale;
    StartPolygon();
}

/*
 * Function: DisplayEndRegion
 * --------------------------
 * This function closes the region opened by DisplayStartRegion
 * and displays the assembled polygon.
 */

void DisplayEndRegion(void)
{
    DisplayPolygon();
    regionStarted = FALSE;
}

/*
 * Functions: DisplayGetWidth, DisplayGetHeight
 * --------------------------------------------
 * These functions allow the client to get the width and height
 * of the graphics window.
 */

double DisplayGetWidth(void)
{
    return (windowWidth);
}

double DisplayGetHeight(void)
{
    return (windowHeight);
}

/* Private functions */

/*
 * Function: InitGC
 * Usage: InitGC();
 * ----------------
 * This function is used as part of the initialization and creates
 * all of the GC structures used for drawing.
 */

static void InitGC(void)
{
    XGCValues gcv;
    Window root;
    int i;

    gcv.foreground = eraseColor;
    gcv.background = eraseColor;
    eraseGC = XCreateGC(disp, osWindow, GCFgBg, &gcv);
    gcv.foreground = drawColor;
    mainGC = XCreateGC(disp, mainWindow, GCFgBg, &gcv);
    drawGC = XCreateGC(disp, osWindow, GCFgBg, &gcv);
    gcv.fill_style = FillOpaqueStippled;
    root = DefaultRootWindow(disp);
    for (i = 0; i < NGrays; i++) {
        grayStipple[i] = gcv.stipple =
           XCreateBitmapFromData(disp, root, grayList[i], 8, 8);
        grayGC[i] = XCreateGC(disp, osWindow,
                              GCFgBg | GCStipple | GCFillStyle, &gcv);
    }
}

/*
 * Function: ForceRedraw
 * Usage: ForceRedraw();
 * ---------------------
 * This function forces the screen to update itself by sending an
 * Expose event for the window.
 */

static void ForceRedraw(void)
{
    XEvent event;

    event.type = Expose;
    event.xany.window = mainWindow;
    event.xexpose.count = 0;
    XSendEvent(disp, mainWindow, False, 0, &event);
    XFlush(disp);
}

/*
 * Function: RedrawWindow
 * Usage: RedrawWindow();
 * ----------------------
 * This function redraws the active display window by copying bits
 * from the offscreen bitmap.
 */

static void RedrawWindow(void)
{
    int itemp;
    unsigned int width, height, utemp;
    Window wtemp;

    if (XGetGeometry(disp, mainWindow, &wtemp, &itemp, &itemp,
                     &width, &height, &utemp, &utemp) == 0) return;
    XCopyPlane(disp, osWindow, mainWindow, mainGC, 0, 0, width, height,
               0, 0, 1);
    redraw = FALSE;
}

/*
 * 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 X
 * 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, which uses the XFillPolygon
 * call to generate the display.
 */

static void StartPolygon(void)
{
    polygonPoints = NewArray(PStartSize, XPoint);
    polygonSize = PStartSize;
    nPolygonPoints = 0;
}

static void AddSegment(int x0, int y0, int x1, int y1)
{
    XPoint *newPolygon;
    int i;

    if (nPolygonPoints >= polygonSize) {
        polygonSize *= 2;
        newPolygon = NewArray(polygonSize, XPoint);
        for (i = 0; i < nPolygonPoints; i++) {
            newPolygon[i] = polygonPoints[i];
        }
        FreeBlock(polygonPoints);
        polygonPoints = newPolygon;
    }
    if (nPolygonPoints == 0) {
        polygonPoints[nPolygonPoints].x = x0;
        polygonPoints[nPolygonPoints].y = y0;
        nPolygonPoints++;
    }
    polygonPoints[nPolygonPoints].x = x1;
    polygonPoints[nPolygonPoints].y = y1;
    nPolygonPoints++;
}

static void DisplayPolygon(void)
{
    GC fillGC;
    int px;

    if (eraseMode) {
        fillGC = eraseGC;
    } else {
        px = regionGrayScale * (NGrays - 1) + 0.5 - Epsilon;
        fillGC = grayGC[px];
    }
    XFillPolygon(disp, osWindow, fillGC,
                 polygonPoints, nPolygonPoints,
                 Complex, CoordModeOrigin);
    FreeBlock(polygonPoints);
}

/*
 * 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 as part of a
 * polygonal region.
 */

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

    if (sweep < 0) {
        start += sweep;
        sweep = -sweep;
    }
    dt = atan2(InchesY(1), MaxF(fabs(rx), fabs(ry)));
    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: SizeFromFontName
 * Usage: SizeFromFontName();
 * --------------------------
 * This function is a simple utility for DisplaySetFont that returns
 * the font size from an X font name.
 */

static int SizeFromFontName(string fontName)
{
    char *sizePtr;

    sizePtr = strstr(fontName, "--");
    if (sizePtr == NULL) return (-1);
    return (atoi(sizePtr + 2));
}

/* Low-level conversion functions */

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

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

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

static int PixelsY(double y)
{
    return (Round(y * ydpi + 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));
}
