/* 
 * tkUnixColor.c --
 *
 *	This file contains the platform specific color routines
 *	needed for X support.
 *
 * Copyright (c) 1996 by Sun Microsystems, Inc.
 *
 * See the file "license.terms" for information on usage and redistribution
 * of this file, and for a DISCLAIMER OF ALL WARRANTIES.
 *
 * SCCS: @(#) tkUnixColor.c 1.1 96/10/22 16:52:31
 */

#include <tkColor.h>

/*
 * If a colormap fills up, attempts to allocate new colors from that
 * colormap will fail.  When that happens, we'll just choose the
 * closest color from those that are available in the colormap.
 * One of the following structures will be created for each "stressed"
 * colormap to keep track of the colors that are available in the
 * colormap (otherwise we would have to re-query from the server on
 * each allocation, which would be very slow).  These entries are
 * flushed after a few seconds, since other clients may release or
 * reallocate colors over time.
 */

struct TkStressedCmap {
    Colormap colormap;			/* X's token for the colormap. */
    int numColors;			/* Number of entries currently active
					 * at *colorPtr. */
    XColor *colorPtr;			/* Pointer to malloc'ed array of all
					 * colors that seem to be available in
					 * the colormap.  Some may not actually
					 * be available, e.g. because they are
					 * read-write for another client;  when
					 * we find this out, we remove them
					 * from the array. */
    struct TkStressedCmap *nextPtr;	/* Next in list of all stressed
					 * colormaps for the display. */
};

/*
 * Forward declarations for procedures defined in this file:
 */

static void		DeleteStressedCmap _ANSI_ARGS_((Display *display,
			    Colormap colormap));
static void		FindClosestColor _ANSI_ARGS_((Tk_Window tkwin,
			    XColor *desiredColorPtr, XColor *actualColorPtr));

/*
 *----------------------------------------------------------------------
 *
 * TkpFreeColor --
 *
 *	Release the specified color back to the system.
 *
 * Results:
 *	None
 *
 * Side effects:
 *	Invalidates the colormap cache for the colormap associated with
 *	the given color.
 *
 *----------------------------------------------------------------------
 */

void
TkpFreeColor(tkColPtr)
    TkColor *tkColPtr;		/* Color to be released.  Must have been
				 * allocated by TkpGetColor or
				 * TkpGetColorByValue. */
{
    Visual *visual;
    Screen *screen = tkColPtr->screen;

    /*
     * Careful!  Don't free black or white, since this will
     * make some servers very unhappy.  Also, there is a bug in
     * some servers (such Sun's X11/NeWS server) where reference
     * counting is performed incorrectly, so that if a color is
     * allocated twice in different places and then freed twice,
     * the second free generates an error (this bug existed as of
     * 10/1/92).  To get around this problem, ignore errors that
     * occur during the free operation.
     */

    visual = tkColPtr->visual;
    if ((visual->class != StaticGray) && (visual->class != StaticColor)
	    && (tkColPtr->color.pixel != BlackPixelOfScreen(screen))
	    && (tkColPtr->color.pixel != WhitePixelOfScreen(screen))) {
	Tk_ErrorHandler handler;

	handler = Tk_CreateErrorHandler(DisplayOfScreen(screen),
		-1, -1, -1, (Tk_ErrorProc *) NULL, (ClientData) NULL);
	XFreeColors(DisplayOfScreen(screen), tkColPtr->colormap,
		&tkColPtr->color.pixel, 1, 0L);
	Tk_DeleteErrorHandler(handler);
    }
    DeleteStressedCmap(DisplayOfScreen(screen), tkColPtr->colormap);
}

/*
 *----------------------------------------------------------------------
 *
 * TkpGetColor --
 *
 *	Allocate a new TkColor for the color with the given name.
 *
 * Results:
 *	Returns a newly allocated TkColor, or NULL on failure.
 *
 * Side effects:
 *	May invalidate the colormap cache associated with tkwin upon
 *	allocating a new colormap entry.  Allocates a new TkColor
 *	structure.
 *
 *----------------------------------------------------------------------
 */

TkColor *
TkpGetColor(tkwin, name)
    Tk_Window tkwin;		/* Window in which color will be used. */
    Tk_Uid name;		/* Name of color to allocated (in form
				 * suitable for passing to XParseColor). */
{
    Display *display = Tk_Display(tkwin);
    Colormap colormap = Tk_Colormap(tkwin);
    XColor color;
    TkColor *tkColPtr;

    /*
     * Map from the name to a pixel value.  Call XAllocNamedColor rather than
     * XParseColor for non-# names: this saves a server round-trip for those
     * names.
     */

    if (*name != '#') {
	XColor screen;

	if (XAllocNamedColor(display, colormap, name, &screen,
		&color) != 0) {
	    DeleteStressedCmap(display, colormap);
	} else {
	    /*
	     * Couldn't allocate the color.  Try translating the name to
	     * a color value, to see whether the problem is a bad color
	     * name or a full colormap.  If the colormap is full, then
	     * pick an approximation to the desired color.
	     */

	    if (XLookupColor(display, colormap, name, &color,
		    &screen) == 0) {
		return (TkColor *) NULL;
	    }
	    FindClosestColor(tkwin, &screen, &color);
	}
    } else {
	if (XParseColor(display, colormap, name, &color) == 0) {
	    return (TkColor *) NULL;
	}
	if (XAllocColor(display, colormap, &color) != 0) {
	    DeleteStressedCmap(display, colormap);
	} else {
	    FindClosestColor(tkwin, &color, &color);
	}
    }

    tkColPtr = (TkColor *) ckalloc(sizeof(TkColor));
    tkColPtr->color = color;

    return tkColPtr;
}

/*
 *----------------------------------------------------------------------
 *
 * TkpGetColorByValue --
 *
 *	Given a desired set of red-green-blue intensities for a color,
 *	locate a pixel value to use to draw that color in a given
 *	window.
 *
 * Results:
 *	The return value is a pointer to an TkColor structure that
 *	indicates the closest red, blue, and green intensities available
 *	to those specified in colorPtr, and also specifies a pixel
 *	value to use to draw in that color.
 *
 * Side effects:
 *	May invalidate the colormap cache for the specified window.
 *	Allocates a new TkColor structure.
 *
 *----------------------------------------------------------------------
 */

TkColor *
TkpGetColorByValue(tkwin, colorPtr)
    Tk_Window tkwin;		/* Window in which color will be used. */
    XColor *colorPtr;		/* Red, green, and blue fields indicate
				 * desired color. */
{
    Display *display = Tk_Display(tkwin);
    Colormap colormap = Tk_Colormap(tkwin);
    TkColor *tkColPtr = (TkColor *) ckalloc(sizeof(TkColor));

    tkColPtr->color.red = colorPtr->red;
    tkColPtr->color.green = colorPtr->green;
    tkColPtr->color.blue = colorPtr->blue;
    if (XAllocColor(display, colormap, &tkColPtr->color) != 0) {
	DeleteStressedCmap(display, colormap);
    } else {
	FindClosestColor(tkwin, &tkColPtr->color, &tkColPtr->color);
    }

    return tkColPtr;
}

/*
 *----------------------------------------------------------------------
 *
 * FindClosestColor --
 *
 *	When Tk can't allocate a color because a colormap has filled
 *	up, this procedure is called to find and allocate the closest
 *	available color in the colormap.
 *
 * Results:
 *	There is no return value, but *actualColorPtr is filled in
 *	with information about the closest available color in tkwin's
 *	colormap.  This color has been allocated via X, so it must
 *	be released by the caller when the caller is done with it.
 *
 * Side effects:
 *	A color is allocated.
 *
 *----------------------------------------------------------------------
 */

static void
FindClosestColor(tkwin, desiredColorPtr, actualColorPtr)
    Tk_Window tkwin;			/* Window where color will be used. */
    XColor *desiredColorPtr;		/* RGB values of color that was
					 * wanted (but unavailable). */
    XColor *actualColorPtr;		/* Structure to fill in with RGB and
					 * pixel for closest available
					 * color. */
{
    TkStressedCmap *stressPtr;
    double tmp, distance, closestDistance;
    int i, closest, numFound;
    XColor *colorPtr;
    TkDisplay *dispPtr = ((TkWindow *) tkwin)->dispPtr;
    Colormap colormap = Tk_Colormap(tkwin);
    XVisualInfo template, *visInfoPtr;

    /*
     * Find the TkStressedCmap structure for this colormap, or create
     * a new one if needed.
     */

    for (stressPtr = dispPtr->stressPtr; ; stressPtr = stressPtr->nextPtr) {
	if (stressPtr == NULL) {
	    stressPtr = (TkStressedCmap *) ckalloc(sizeof(TkStressedCmap));
	    stressPtr->colormap = colormap;
	    template.visualid = XVisualIDFromVisual(Tk_Visual(tkwin));
	    visInfoPtr = XGetVisualInfo(Tk_Display(tkwin),
		    VisualIDMask, &template, &numFound);
	    if (numFound < 1) {
		panic("FindClosestColor couldn't lookup visual");
	    }
	    stressPtr->numColors = visInfoPtr->colormap_size;
	    XFree((char *) visInfoPtr);
	    stressPtr->colorPtr = (XColor *) ckalloc((unsigned)
		    (stressPtr->numColors * sizeof(XColor)));
	    for (i = 0; i  < stressPtr->numColors; i++) {
		stressPtr->colorPtr[i].pixel = (unsigned long) i;
	    }
	    XQueryColors(dispPtr->display, colormap, stressPtr->colorPtr,
		    stressPtr->numColors);
	    stressPtr->nextPtr = dispPtr->stressPtr;
	    dispPtr->stressPtr = stressPtr;
	    break;
	}
	if (stressPtr->colormap == colormap) {
	    break;
	}
    }

    /*
     * Find the color that best approximates the desired one, then
     * try to allocate that color.  If that fails, it must mean that
     * the color was read-write (so we can't use it, since it's owner
     * might change it) or else it was already freed.  Try again,
     * over and over again, until something succeeds.
     */

    while (1)  {
	if (stressPtr->numColors == 0) {
	    panic("FindClosestColor ran out of colors");
	}
	closestDistance = 1e30;
	closest = 0;
	for (colorPtr = stressPtr->colorPtr, i = 0; i < stressPtr->numColors;
		colorPtr++, i++) {
	    /*
	     * Use Euclidean distance in RGB space, weighted by Y (of YIQ)
	     * as the objective function;  this accounts for differences
	     * in the color sensitivity of the eye.
	     */
    
	    tmp = .30*(((int) desiredColorPtr->red) - (int) colorPtr->red);
	    distance = tmp*tmp;
	    tmp = .61*(((int) desiredColorPtr->green) - (int) colorPtr->green);
	    distance += tmp*tmp;
	    tmp = .11*(((int) desiredColorPtr->blue) - (int) colorPtr->blue);
	    distance += tmp*tmp;
	    if (distance < closestDistance) {
		closest = i;
		closestDistance = distance;
	    }
	}
	if (XAllocColor(dispPtr->display, colormap,
		&stressPtr->colorPtr[closest]) != 0) {
	    *actualColorPtr = stressPtr->colorPtr[closest];
	    return;
	}

	/*
	 * Couldn't allocate the color.  Remove it from the table and
	 * go back to look for the next best color.
	 */

	stressPtr->colorPtr[closest] =
		stressPtr->colorPtr[stressPtr->numColors-1];
	stressPtr->numColors -= 1;
    }
}

/*
 *----------------------------------------------------------------------
 *
 * DeleteStressedCmap --
 *
 *	This procedure releases the information cached for "colormap"
 *	so that it will be refetched from the X server the next time
 *	it is needed.
 *
 * Results:
 *	None.
 *
 * Side effects:
 *	The TkStressedCmap structure for colormap is deleted;  the
 *	colormap is no longer considered to be "stressed".
 *
 * Note:
 *	This procedure is invoked whenever a color in a colormap is
 *	freed, and whenever a color allocation in a colormap succeeds.
 *	This guarantees that TkStressedCmap structures are always
 *	deleted before the corresponding Colormap is freed.
 *
 *----------------------------------------------------------------------
 */

static void
DeleteStressedCmap(display, colormap)
    Display *display;		/* Xlib's handle for the display
				 * containing the colormap. */
    Colormap colormap;		/* Colormap to flush. */
{
    TkStressedCmap *prevPtr, *stressPtr;
    TkDisplay *dispPtr = TkGetDisplay(display);

    for (prevPtr = NULL, stressPtr = dispPtr->stressPtr; stressPtr != NULL;
	    prevPtr = stressPtr, stressPtr = stressPtr->nextPtr) {
	if (stressPtr->colormap == colormap) {
	    if (prevPtr == NULL) {
		dispPtr->stressPtr = stressPtr->nextPtr;
	    } else {
		prevPtr->nextPtr = stressPtr->nextPtr;
	    }
	    ckfree((char *) stressPtr->colorPtr);
	    ckfree((char *) stressPtr);
	    return;
	}
    }
}

/*
 *----------------------------------------------------------------------
 *
 * TkpCmapStressed --
 *
 *	Check to see whether a given colormap is known to be out
 *	of entries.
 *
 * Results:
 *	1 is returned if "colormap" is stressed (i.e. it has run out
 *	of entries recently), 0 otherwise.
 *
 * Side effects:
 *	None.
 *
 *----------------------------------------------------------------------
 */

int
TkpCmapStressed(tkwin, colormap)
    Tk_Window tkwin;		/* Window that identifies the display
				 * containing the colormap. */
    Colormap colormap;		/* Colormap to check for stress. */
{
    TkStressedCmap *stressPtr;

    for (stressPtr = ((TkWindow *) tkwin)->dispPtr->stressPtr;
	    stressPtr != NULL; stressPtr = stressPtr->nextPtr) {
	if (stressPtr->colormap == colormap) {
	    return 1;
	}
    }
    return 0;
}
