/* DBWfeedback.c -
 *
 *	This file provides a standard set of procedures for Magic
 *	commands to use to provide feedback to users.  Feedback
 *	consists of areas of the screen that are highlighted, along
 *	with text describing why those particular areas are important.
 *	Feedback is used for things like displaying CIF, and for errors
 *	in CIF-generation and routing.
 *
 *     *********************************************************************
 *     * Copyright (C) 1985, 1990 Regents of the University of California. *
 *     * Permission to use, copy, modify, and distribute this              *
 *     * software and its documentation for any purpose and without        *
 *     * fee is hereby granted, provided that the above copyright          *
 *     * notice appear in all copies.  The University of California        *
 *     * makes no representations about the suitability of this            *
 *     * software for any purpose.  It is provided "as is" without         *
 *     * express or implied warranty.  Export of this software outside     *
 *     * of the United States of America may require an export license.    *
 *     *********************************************************************
 */

#ifndef lint
static char rcsid[] __attribute__ ((unused)) = "$Header: /usr/cvsroot/magic-8.0/dbwind/DBWfdback.c,v 1.2 2010/06/24 12:37:16 tim Exp $";
#endif  /* not lint */

#include <stdio.h>
#include <string.h>

#include "utils/magic.h"
#include "utils/geometry.h"
#include "tiles/tile.h"
#include "utils/hash.h"
#include "database/database.h"
#include "windows/windows.h"
#include "graphics/graphics.h"
#include "dbwind/dbwind.h"
#include "utils/utils.h"
#include "utils/styles.h"
#include "utils/malloc.h"
#include "utils/signals.h"

/* C99 compat */
#include "textio/textio.h"

/* Use a reference-counted character structure for feedback info */

typedef struct rcstring
{
    int refcount;
    char *string;
} RCString;


/* Each feedback area is stored in a record that looks like this: */

typedef struct feedback
{
    Rect fb_area;		/* The area to be highlighted, in coords of
				 * fb_rootDef, but prior to scaling by
				 * fb_scale.
				 */
    Rect fb_rootArea;		/* The area of the feedback, in Magic coords.
				 * of fb_rootDef, scaled up to the next
				 * integral Magic unit.
				 */
    RCString *fb_text;		/* Text explanation for the feedback. */
    CellDef *fb_rootDef;	/* Root definition of windows in which to
				 * display this feedback.
				 */
    int fb_scale;		/* Scale factor to use in redisplaying
				 * (fb_scale units in fb_area correspond
				 * to one unit in fb_rootDef).
				 */
    int fb_style;		/* Display style to use for this feedback. */
} Feedback;

/* The following stuff describes all the feedback information we know
 * about.  The feedback is stored in a big array that grows whenever
 * necessary.
 */

static Feedback *dbwfbArray = NULL;	/* Array holding all feedback info. */
global int DBWFeedbackCount = 0;	/* Number of active entries in
					 * dbwfbArray.
					 */
static int dbwfbSize = 0;		/* Size of dbwfbArray, in entries. */
static int dbwfbNextToShow = 0;		/* Index of first feedback area that
					 * hasn't been displayed yet.  Used by
					 * DBWFBShow.
					 */
static CellDef *dbwfbRootDef;		/* To pass root cell definition from
					 * dbwfbGetTransform back up to
					 * DBWFeedbackAdd.
					 */


/*
 * ----------------------------------------------------------------------------
 *
 * DBWFeedbackRedraw --
 *
 * 	This procedure is called by the highlight manager to redisplay
 *	feedback highlights.  The window is locked before entry.
 *
 * Results:
 *	None.
 *
 * Side effects:
 *	Any feedback information that overlaps a non-space tile in
 *	plane is redrawn.
 *
 * Tricky stuff:
 *	Redisplay is numerically difficult, particularly when feedbacks
 *	have a large internal scale factor:  the tendency is to get
 *	integer overflow and get everything goofed up.  Be careful
 *	when making changes to the code below.
 *
 * ----------------------------------------------------------------------------
 */

void
DBWFeedbackRedraw(window, plane)
    MagWindow *window;		/* Window in which to redraw. */
    Plane *plane;		/* Non-space tiles on this plane mark what
				 * needs to be redrawn.
				 */
{
    dlong x, y;
    int i, halfScale, curStyle, newStyle, curScale;
    CellDef *windowRoot;
    Rect worldArea, screenArea, tmp;
    Feedback *fb;
    extern int dbwFeedbackAlways1();	/* Forward reference. */

    if (DBWFeedbackCount == 0) return;

    windowRoot = ((CellUse *) (window->w_surfaceID))->cu_def;
    curStyle = -1;
    curScale = -1;
    for (i = 0, fb = dbwfbArray; i < DBWFeedbackCount; i++, fb++)
    {
	/* We expect that most of the feedbacks will have the same
	 * style and scale, so compute information with the current
	 * values, and recompute only when the values change.
	 */
	if (fb->fb_scale != curScale)
	{
	    curScale = fb->fb_scale;
	    halfScale = curScale / 2;
	    worldArea.r_xbot = window->w_surfaceArea.r_xbot * curScale;
	    worldArea.r_xtop = window->w_surfaceArea.r_xtop * curScale;
	    worldArea.r_ybot = window->w_surfaceArea.r_ybot * curScale;
	    worldArea.r_ytop = window->w_surfaceArea.r_ytop * curScale;
	}

	/*
	 * Check to make sure this feedback area is relevant.
	 * Clip it to TiPlaneRect as a sanity check against
	 * callers who provide a feedback area that extends
	 * out somewhere in never-never land.
	 */
	if (fb->fb_rootDef != windowRoot) continue;
	tmp = fb->fb_rootArea;
	GeoClip(&tmp, &TiPlaneRect);
	if (!DBSrPaintArea((Tile *) NULL, plane, &tmp,
		&DBAllButSpaceBits, dbwFeedbackAlways1, (ClientData) NULL))
	    continue;

	/* Transform the feedback area to screen coords.  This is
	 * very similar to the code in WindSurfaceToScreen, except
	 * that there's additional scaling involved.
	 */

	tmp = fb->fb_area;

	/* Don't clip non-Manhattan tiles, or else the diagonals are	*/
	/* clipped to an incorrect position.				*/

	if (!(fb->fb_style & TT_DIAGONAL))
	GeoClip(&tmp, &worldArea);

	/*
	 * Ignore areas which have been clipped out of existence
	 */
	if (tmp.r_xtop < tmp.r_xbot || tmp.r_ytop < tmp.r_ybot)
	    continue;

	x = ((dlong)(tmp.r_xbot - worldArea.r_xbot) * window->w_scale) + halfScale;
	screenArea.r_xbot = (int)((x/curScale + window->w_origin.p_x) >> SUBPIXELBITS);
	x = ((dlong)(tmp.r_xtop - worldArea.r_xbot) * window->w_scale) + halfScale;
	screenArea.r_xtop = (int)((x/curScale + window->w_origin.p_x) >> SUBPIXELBITS);
	y = ((dlong)(tmp.r_ybot - worldArea.r_ybot) * window->w_scale) + halfScale;
	screenArea.r_ybot = (int)((y/curScale + window->w_origin.p_y) >> SUBPIXELBITS);
	y = ((dlong)(tmp.r_ytop - worldArea.r_ybot) * window->w_scale) + halfScale;
	screenArea.r_ytop = (int)((y/curScale + window->w_origin.p_y) >> SUBPIXELBITS);

	/*
	 * Sanity check on integer bounds
	 */
	if (screenArea.r_xtop < screenArea.r_xbot ||
	    screenArea.r_ytop < screenArea.r_ybot)
	{
	    TxError("Internal error: Feedback area exceeds integer bounds "
		"on screen rectangle!\n");
	    continue;
	}

	newStyle = fb->fb_style & (TT_LEFTMASK | TT_RIGHTMASK);

	/* Another little trick:  when the feedback area is very small ("very
	 * small" is a hand-tuned constant), change all stippled styles to
	 * solid.
	 * (The usefulness of this trick is questionable, as it generally
	 * creates very bothersome output for the "cif see" function and
	 * other places where the feedback is generated from tiles.)
	 */
/*
	if (((GEO_WIDTH(&screenArea) < 18) || (GEO_HEIGHT(&screenArea) < 18))
			&& ((newStyle == STYLE_MEDIUMHIGHLIGHTS)
			|| (newStyle == STYLE_PALEHIGHLIGHTS)))
	    newStyle = STYLE_SOLIDHIGHLIGHTS;
*/
	if (newStyle != curStyle)
	{
	    curStyle = newStyle;
	    GrSetStuff(curStyle);
	}

	if (fb->fb_style & TT_DIAGONAL)
	    GrDiagonal(&screenArea, fb->fb_style);

	/* Special case (i.e., "hack")---to get a diagonal line	*/
	/* instead of a triangle, set TT_SIDE but not		*/
	/* TT_DIAGONAL.	 TT_DIRECTION still indicates / or \	*/

	else if (fb->fb_style & TT_SIDE)
	{
	    if (fb->fb_style & TT_DIRECTION)
	        GrClipLine(screenArea.r_xbot, screenArea.r_ytop,
			screenArea.r_xtop, screenArea.r_ybot);
	    else
	        GrClipLine(screenArea.r_xbot, screenArea.r_ybot,
			screenArea.r_xtop, screenArea.r_ytop);
	}
	else
	    GrFastBox(&screenArea);
    }
}

int
dbwFeedbackAlways1(
    Tile *tile,			/* (unused) */
    TileType dinfo,		/* (unused) */
    ClientData clientdata)	/* (unused) */
{
    return 1;
}

/*
 * ----------------------------------------------------------------------------
 *
 * DBWFeedbackClear --
 *
 * 	This procedure clears all existing feedback information from
 *	the screen.
 *
 * Results:
 *	None.
 *
 * Side effects:
 *	Any existing feedback information is cleared from the screen
 *	and from our database.
 *
 * ----------------------------------------------------------------------------
 */

void
DBWFeedbackClear(text)
    char *text;
{
    int i, oldCount;
    Feedback *fb, *fl, *fe;
    Rect area;
    CellDef *currentRoot;
    RCString *quickptr = NULL;

    /* Clear out the feedback array and recycle string storage.  Whenever
     * the root cell changes, make a call to erase from the screen.
     */

    currentRoot = (CellDef *) NULL;
    oldCount = DBWFeedbackCount;
    DBWFeedbackCount = 0;
    for (fb = dbwfbArray; fb < dbwfbArray + oldCount; fb++)
    {
	if ((text == NULL) ||
		((quickptr != NULL) && (fb->fb_text == quickptr)) ||
		(strstr(fb->fb_text->string, text) != NULL))
	{
	    // Track the last refcount string that matched the
	    // text so we can run through lists of matching text
	    // without having to call strstr()

	    if (text != NULL) quickptr = fb->fb_text;

	    if (currentRoot != fb->fb_rootDef)
	    {
		if (currentRoot != (CellDef *) NULL)
		    DBWHLRedraw(currentRoot, &area, TRUE);
		area = GeoNullRect;
	    }
	    (void) GeoInclude(&fb->fb_rootArea, &area);
	    currentRoot = fb->fb_rootDef;

	    // Decrement refcount and free if zero.
	    fb->fb_text->refcount--;
	    if (fb->fb_text->refcount == 0)
	    {
		freeMagic(fb->fb_text->string);
		freeMagic(fb->fb_text);
	    }
	    fb->fb_text = NULL;
	}
    }
    if (currentRoot != NULL)
	DBWHLRedraw(currentRoot, &area, TRUE);
    dbwfbNextToShow = 0;

    if (text != NULL)
    {
	/* Close up feedback list around removed text entries */

	fl = dbwfbArray;
	fe = fl + oldCount;
	for (fb = dbwfbArray; fb < fe; fb++)
	{
	    while ((fb->fb_text == NULL) && (fb < fe)) fb++;
	    if (fb < fe) *fl++ = *fb;
	}
	DBWFeedbackCount = (int)(fl - dbwfbArray);

	for (fb = fl; fb < dbwfbArray + oldCount; fb++)
	    fb->fb_text = NULL;
    }

    /* Free up feedback memory whenever we have removed all feedback entries */
    if (DBWFeedbackCount == 0)
    {
	if (dbwfbArray != NULL) {
	    freeMagic((char *) dbwfbArray);
	    dbwfbArray = NULL;
	}
	dbwfbSize = 0;
	return;
    }
}

/*
 * ----------------------------------------------------------------------------
 *
 * DBWFeedbackAdd --
 *
 * 	Adds a new piece of feedback information to the list we have
 *	already.
 *
 * Results:
 *	None.
 *
 * Side effects:
 *	CellDef's ancestors are searched until its first root definition
 *	is found, and the coordinates of area are transformed into the
 *	root.  Then the feedback area is added to our current list, using
 *	the style and scalefactor given.  This stuff will be displayed on
 *	the screen at the end of the current command.
 * ----------------------------------------------------------------------------
 */

void
DBWFeedbackAdd(area, text, cellDef, scaleFactor, style)
    Rect *area;			/* The area to be highlighted. */
    char *text;			/* Text associated with the area. */
    CellDef *cellDef;		/* The cellDef in whose coordinates area
				 * is given.
				 */
    int scaleFactor;		/* The coordinates provided for feedback
				 * areas are divided by this to produce
				 * coordinates in Magic database units.
				 * This will probably be 1 most of the time.
				 * By making it bigger, say 10, and scaling
				 * other coordinates appropriately, it's
				 * possible to draw narrow lines on the
				 * screen, or to handle CIF, which isn't in
				 * exactly the same coordinates as other Magic
				 * stuff.
				 */
    int style;			/* A display style to use for the feedback.
				 * Use one of:
				 * STYLE_OUTLINEHIGHLIGHTS:	solid outlines
				 * STYLE_DOTTEDHIGHLIGHTS:	dotted outlines
				 * STYLE_SOLIDHIGHLIGHTS:	solid fill
				 * STYLE_MEDIUMHIGHLIGHTS:	medium stipple
				 * STYLE_PALEHIGHLIGHTS:	pald stipple
				 * At very coarse viewing scales, the last
				 * two styles are hard to see, so they are
				 * turned into STYLE_SOLIDHIGHLIGHTS.
				 */
{
    Rect tmp, tmp2, tmp3;
    Transform transform;
    int i;
    Feedback *fb, *fblast;
    extern int dbwfbGetTransform();	/* Forward declaration. */

    /* Find a transform from this cell to the root, and use it to
     * transform the area.  If the root isn't an ancestor, just
     * return.
     */

    if (!DBSrRoots(cellDef, &GeoIdentityTransform,
	dbwfbGetTransform, (ClientData) &transform)) return;

    /* SigInterruptPending screws up DBSrRoots */
    if (SigInterruptPending)
	return;

    /* Don't get fooled like I did.  The translations for
     * this transform are in Magic coordinates, not feedback
     * coordinates.  Scale them into feedback coordinates.
     */

    transform.t_c *= scaleFactor;
    transform.t_f *= scaleFactor;
    GeoTransRect(&transform, area, &tmp2);
    area = &tmp2;

    /* Make sure there's enough space in the current array.  If
     * not, make a new array, copy the old to the new, then delete
     * the old array.  Use memcpy() to make sure this happens very fast.
     */

    if (DBWFeedbackCount == dbwfbSize)
    {
	Feedback *new;

	if (dbwfbSize == 0) dbwfbSize = 32;
	else dbwfbSize <<= 1;

	new = (Feedback *) mallocMagic(dbwfbSize * sizeof(Feedback));
	memcpy((char *)new, (char *)dbwfbArray, DBWFeedbackCount * sizeof(Feedback));

	for (i = DBWFeedbackCount; i < dbwfbSize; i++) new[i].fb_text = NULL;

	if (dbwfbArray != NULL) freeMagic((char *) dbwfbArray);
	dbwfbArray = new;
    }
    fb = &dbwfbArray[DBWFeedbackCount];
    fb->fb_area = *area;
    fblast = (DBWFeedbackCount == 0) ? NULL : &dbwfbArray[DBWFeedbackCount - 1];

    if (fblast && (!strcmp(fblast->fb_text->string, text)))
    {
	fb->fb_text = fblast->fb_text;
	fb->fb_text->refcount++;
    }
    else
    {
	fb->fb_text = (RCString *)mallocMagic(sizeof(RCString));
	fb->fb_text->refcount = 1;
	fb->fb_text->string = StrDup(NULL, text);
    }

    fb->fb_rootDef = dbwfbRootDef;
    fb->fb_scale = scaleFactor;
    fb->fb_style = style;
    DBWFeedbackCount++;

    /* Round the area up into Magic coords, and save it too. */
    if (area->r_xtop > 0)
	tmp.r_xtop = (area->r_xtop + scaleFactor - 1)/scaleFactor;
    else tmp.r_xtop = area->r_xtop/scaleFactor;
    if (area->r_ytop > 0)
	tmp.r_ytop = (area->r_ytop + scaleFactor - 1)/scaleFactor;
    else tmp.r_ytop = area->r_ytop/scaleFactor;
    if (area->r_xbot > 0) tmp.r_xbot = area->r_xbot/scaleFactor;
    else tmp.r_xbot = (area->r_xbot - scaleFactor + 1)/scaleFactor;
    if (area->r_ybot > 0) tmp.r_ybot = area->r_ybot/scaleFactor;
    else tmp.r_ybot = (area->r_ybot - scaleFactor + 1)/scaleFactor;

    /* Clip to ensure well within TiPlaneRect */
    tmp3.r_xbot = TiPlaneRect.r_xbot + 10;
    tmp3.r_ybot = TiPlaneRect.r_ybot + 10;
    tmp3.r_xtop = TiPlaneRect.r_xtop - 10;
    tmp3.r_ytop = TiPlaneRect.r_ytop - 10;
    GeoClip(&tmp, &tmp3);

    fb->fb_rootArea = tmp;
}

/* This utility procedure is invoked by DBSrRoots.  Save the root definition
 * in dbwfbRootDef, save the transform in the argument, and abort the search.
 * Make sure that the root we pick is actually displayed in a window
 * someplace (there could be root cells that are no longer displayed
 * anywhere).
 */

int
dbwfbGetTransform(use, transform, cdarg)
    CellUse *use;			/* A root use that is an ancestor
					 * of cellDef in DBWFeedbackAdd.
					 */
    Transform *transform;		/* Transform up from cellDef to use. */
    Transform *cdarg;			/* Place to store transform from
					 * cellDef to its root def.
					 */
{
    extern int dbwfbWindFunc();
    if (use->cu_def->cd_flags & CDINTERNAL) return 0;
    if (!WindSearch((ClientData) DBWclientID, (ClientData) use,
	    (Rect *) NULL, dbwfbWindFunc, (ClientData) NULL)) return 0;
    if (SigInterruptPending)
	return 0;
    dbwfbRootDef = use->cu_def;
    *cdarg = *transform;
    return 1;
}

/* This procedure is called if a window is found for the cell in
 * dbwfbGetTransform above.  It returns 1 to abort the search and
 * notify dbwfbGetTransform that there was a window for that root
 * cell.
 */

/*ARGSUSED*/
int
dbwfbWindFunc(w, clientData)
    MagWindow *w;		/* UNUSED */
    ClientData clientData;	/* UNUSED */
{
    return 1;
}

/*
 * ----------------------------------------------------------------------------
 *
 * Client initialization
 *
 * ----------------------------------------------------------------------------
 */

void
dbwFeedbackInit()
{
    DBWHLAddClient(DBWFeedbackRedraw);
}


/*
 * ----------------------------------------------------------------------------
 *
 * DBWFeedbackShow --
 *
 * 	Causes new feedback information actually to be displayed on
 *	the screen.
 *
 * Results:
 *	None.
 *
 * Side effects:
 *	All new feedback information that has been created since the
 *	last call to this procedure is added to the display.
 *
 * ----------------------------------------------------------------------------
 */

void
DBWFeedbackShow()
{
    Rect area;
    CellDef *currentRoot;
    Feedback *fb;
    int i;

    /* Scan through all of the feedback areas starting with dbwfbNextToShow.
     * Save up the total bounding box until the root definition changes,
     * then redisplay what's been saved up so far.
     */

    currentRoot = NULL;
    for (i = dbwfbNextToShow, fb = &(dbwfbArray[dbwfbNextToShow]);
	i < DBWFeedbackCount; i++, fb++)
    {
	if (currentRoot != fb->fb_rootDef)
	{
	    if (currentRoot != NULL)
		DBWHLRedraw(currentRoot, &area, FALSE);
	    area = GeoNullRect;
	}
	(void) GeoInclude(&fb->fb_rootArea, &area);
	currentRoot = fb->fb_rootDef;
    }
    if (currentRoot != NULL)
	DBWHLRedraw(currentRoot, &area, FALSE);
    dbwfbNextToShow = DBWFeedbackCount;
}

/*
 * ----------------------------------------------------------------------------
 *
 * DBWFeedbackNth --
 *
 * 	Provides the area and text associated with a particular
 *	feedback area.
 *
 * Results:
 *	Returns a pointer to the text associated with the Nth feedback
 *	entry.
 *
 * Side effects:
 *	The parameter "area" is filled with the area of the nth
 *	feedback, and the text of that feedback is returned. *pRootDef
 *	is filled in with rootDef for window of feedback area.  *pStyle
 *	is filled in with the display style for the feedback area.  If
 *	the particular area doesn't exist (nth >= DBWFeedbackCount),
 *	area and *pRootDef  and *pStyle are untouched and NULL is
 *	returned.  NULL	may also be returned if there simply wasn't
 *	any text associated with the selected feedback.
 *
 * ----------------------------------------------------------------------------
 */

char *
DBWFeedbackNth(nth, area, pRootDef, pStyle)
    int nth;			/* Selects which feedback area to return
				 * stuff from.  (0 <= nth < DBWFeedbackCount)
				 */
    Rect *area;			/* To be filled in with area of feedback, in
				 * rounded-outward Magic coordinates.
				 */
    CellDef **pRootDef;		/* *pRootDef gets filled in with root def for
				 * this feedback area.  If pRootDef is NULL,
				 * nothing is touched.
				 */
    int *pStyle;		/* *pStyle gets filled in with the display
				 * style for this feedback area.  If NULL,
				 * nothing is touched.
				 */
{
    if (nth >= DBWFeedbackCount) return NULL;
    *area = dbwfbArray[nth].fb_rootArea;
    if (pRootDef != NULL) *pRootDef = dbwfbArray[nth].fb_rootDef;
    if (pStyle != NULL) *pStyle = dbwfbArray[nth].fb_style;
    return dbwfbArray[nth].fb_text->string;
}
