/* 
 * 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.
 *
 * RCS: @(#) Id
 */

#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;
}
