/*
 * Copyright (c) 1999 by The XFree86 Project, Inc.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a
 * copy of this software and associated documentation files (the "Software"),
 * to deal in the Software without restriction, including without limitation
 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
 * and/or sell copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL
 * THE XFREE86 PROJECT BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
 * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF
 * OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 *
 * Except as contained in this notice, the name of the XFree86 Project shall
 * not be used in advertising or otherwise to promote the sale, use or other
 * dealings in this Software without prior written authorization from the
 * XFree86 Project.
 *
 * Author: Paulo César Pereira de Andrade
 */

#ifdef HAVE_CONFIG_H
#include <config.h>
#endif
#include <X11/IntrinsicP.h>
#include <X11/StringDefs.h>
#include <X11/Xos.h>
#include <X11/Xaw/TipP.h>
#include <X11/Xaw/XawInit.h>
#include <X11/Xmu/Converters.h>
#include "Private.h"

#define	TIP_EVENT_MASK (ButtonPressMask	  |	\
			ButtonReleaseMask |	\
			PointerMotionMask |	\
			ButtonMotionMask  |	\
			KeyPressMask	  |	\
			KeyReleaseMask	  |	\
			EnterWindowMask	  |	\
			LeaveWindowMask)

/*
 * Types
 */
typedef struct _XawTipInfo {
    Screen *screen;
    TipWidget tip;
    Widget widget;
    Bool mapped;
    struct _XawTipInfo *next;
} XawTipInfo;

/*
 * Class Methods
 */
static void XawTipClassInitialize(void);
static void XawTipInitialize(Widget, Widget, ArgList, Cardinal*);
static void XawTipDestroy(Widget);
static void XawTipExpose(Widget, XEvent*, Region);
static void XawTipRealize(Widget, Mask*, XSetWindowAttributes*);
static Boolean XawTipSetValues(Widget, Widget, Widget, ArgList, Cardinal*);

/*
 * Prototypes
 */
static void TipEventHandler(Widget, XtPointer, XEvent*, Boolean*);
static void TipShellEventHandler(Widget, XtPointer, XEvent*, Boolean*);
static XawTipInfo *CreateTipInfo(Widget);
static XawTipInfo *FindTipInfo(Widget);
static void ResetTip(XawTipInfo*, Bool);
static void TipTimeoutCallback(XtPointer, XtIntervalId*);
static void TipLayout(XawTipInfo*);
static void TipPosition(XawTipInfo*);

/*
 * Initialization
 */
#define offset(field) XtOffsetOf(TipRec, tip.field)
static XtResource resources[] = {
  {
    XtNforeground,
    XtCForeground,
    XtRPixel,
    sizeof(Pixel),
    offset(foreground),
    XtRString,
    XtDefaultForeground,
  },
  {
    XtNfont,
    XtCFont,
    XtRFontStruct,
    sizeof(XFontStruct*),
    offset(font),
    XtRString,
    XtDefaultFont
  },
  {
    XtNfontSet,
    XtCFontSet,
    XtRFontSet,
    sizeof(XFontSet),
    offset(fontset),
    XtRString,
    XtDefaultFontSet
  },
  {
    XtNtopMargin,
    XtCVerticalMargins,
    XtRDimension,
    sizeof(Dimension),
    offset(top_margin),
    XtRImmediate,
    (XtPointer)2
  },
  {
    XtNbottomMargin,
    XtCVerticalMargins,
    XtRDimension,
    sizeof(Dimension),
    offset(bottom_margin),
    XtRImmediate,
    (XtPointer)2
  },
  {
    XtNleftMargin,
    XtCHorizontalMargins,
    XtRDimension,
    sizeof(Dimension),
    offset(left_margin),
    XtRImmediate,
    (XtPointer)6
  },
  {
    XtNrightMargin,
    XtCHorizontalMargins,
    XtRDimension,
    sizeof(Dimension),
    offset(right_margin),
    XtRImmediate,
    (XtPointer)6
  },
  {
    XtNbackingStore,
    XtCBackingStore,
    XtRBackingStore,
    sizeof(int),
    offset(backing_store),
    XtRImmediate,
    (XtPointer)(Always + WhenMapped + NotUseful)
  },
  {
    XtNtimeout,
    XtCTimeout,
    XtRInt,
    sizeof(int),
    offset(timeout),
    XtRImmediate,
    (XtPointer)500
  },
  {
    XawNdisplayList,
    XawCDisplayList,
    XawRDisplayList,
    sizeof(XawDisplayList*),
    offset(display_list),
    XtRImmediate,
    NULL
  },
};
#undef offset

TipClassRec tipClassRec = {
  /* core */
  {
    (WidgetClass)&widgetClassRec,	/* superclass */
    "Tip",				/* class_name */
    sizeof(TipRec),			/* widget_size */
    XawTipClassInitialize,		/* class_initialize */
    NULL,				/* class_part_initialize */
    False,				/* class_inited */
    XawTipInitialize,			/* initialize */
    NULL,				/* initialize_hook */
    XawTipRealize,			/* realize */
    NULL,				/* actions */
    0,					/* num_actions */
    resources,				/* resources */
    XtNumber(resources),		/* num_resources */
    NULLQUARK,				/* xrm_class */
    True,				/* compress_motion */
    True,				/* compress_exposure */
    True,				/* compress_enterleave */
    False,				/* visible_interest */
    XawTipDestroy,			/* destroy */
    NULL,				/* resize */
    XawTipExpose,			/* expose */
    XawTipSetValues,			/* set_values */
    NULL,				/* set_values_hook */
    XtInheritSetValuesAlmost,		/* set_values_almost */
    NULL,				/* get_values_hook */
    NULL,				/* accept_focus */
    XtVersion,				/* version */
    NULL,				/* callback_private */
    NULL,				/* tm_table */
    XtInheritQueryGeometry,		/* query_geometry */
    XtInheritDisplayAccelerator,	/* display_accelerator */
    NULL,				/* extension */
  },
  /* tip */
  {
    NULL,				/* extension */
  },
};

WidgetClass tipWidgetClass = (WidgetClass)&tipClassRec;

static XawTipInfo *first_tip;

/*
 * Implementation
 */
static void
XawTipClassInitialize(void)
{
    XawInitializeWidgetSet();
    XtAddConverter(XtRString, XtRBackingStore, XmuCvtStringToBackingStore,
		   NULL, 0);
    XtSetTypeConverter(XtRBackingStore, XtRString, XmuCvtBackingStoreToString,
		       NULL, 0, XtCacheNone, NULL);
}

/*ARGSUSED*/
static void
XawTipInitialize(Widget req, Widget w, ArgList args, Cardinal *num_args)
{
    TipWidget tip = (TipWidget)w;
    XGCValues values;

    if (!tip->tip.font) XtError("Aborting: no font found\n");
    if (tip->tip.international && !tip->tip.fontset)
	XtError("Aborting: no fontset found\n");

    tip->tip.timer = 0;

    values.foreground = tip->tip.foreground;
    values.background = tip->core.background_pixel;
    values.font = tip->tip.font->fid;
    values.graphics_exposures = False;

    tip->tip.gc = XtAllocateGC(w, 0, GCForeground | GCBackground | GCFont |
			       GCGraphicsExposures, &values, GCFont, 0);
}

static void
XawTipDestroy(Widget w)
{
    XawTipInfo *info = FindTipInfo(w);
    TipWidget tip = (TipWidget)w;

    if (tip->tip.timer)
	XtRemoveTimeOut(tip->tip.timer);

    XtReleaseGC(w, tip->tip.gc);

    XtRemoveEventHandler(XtParent(w), KeyPressMask, False, TipShellEventHandler,
			 (XtPointer)NULL);
    if (info == first_tip)
	first_tip = first_tip->next;
    else {
	XawTipInfo *p = first_tip;

	while (p && p->next != info)
	    p = p->next;
	if (p)
	    p->next = info->next;
    }
    XtFree((char*)info);
}

static void
XawTipRealize(Widget w, Mask *mask, XSetWindowAttributes *attr)
{
    TipWidget tip = (TipWidget)w;

    if (tip->tip.backing_store == Always ||
	tip->tip.backing_store == NotUseful ||
	tip->tip.backing_store == WhenMapped) {
	*mask |= CWBackingStore;
	attr->backing_store = tip->tip.backing_store;
    }
    else
	*mask &= ~CWBackingStore;
    *mask |= CWOverrideRedirect;
    attr->override_redirect = True;

    XtWindow(w) = XCreateWindow(DisplayOfScreen(XtScreen(w)),
				RootWindowOfScreen(XtScreen(w)),
				XtX(w), XtY(w),
				XtWidth(w) ? XtWidth(w) : 1,
				XtHeight(w) ? XtHeight(w) : 1,
				XtBorderWidth(w),
				DefaultDepthOfScreen(XtScreen(w)),
				InputOutput,
				(Visual *)CopyFromParent,
				*mask, attr);
}

static void
XawTipExpose(Widget w, XEvent *event, Region region)
{
    TipWidget tip = (TipWidget)w;
    GC gc = tip->tip.gc;
    char *nl, *label = tip->tip.label;
    Position y = tip->tip.top_margin + tip->tip.font->max_bounds.ascent;
    int len;

    if (tip->tip.display_list)
	XawRunDisplayList(w, tip->tip.display_list, event, region);

    if (tip->tip.international == True) {
	Position ksy = tip->tip.top_margin;
	XFontSetExtents *ext = XExtentsOfFontSet(tip->tip.fontset);

	ksy += XawAbs(ext->max_ink_extent.y);

	while ((nl = index(label, '\n')) != NULL) {
	    XmbDrawString(XtDisplay(w), XtWindow(w), tip->tip.fontset,
			  gc, tip->tip.left_margin, ksy, label,
			  (int)(nl - label));
	    ksy += ext->max_ink_extent.height;
	    label = nl + 1;
	}
	len = strlen(label);
	if (len)
	    XmbDrawString(XtDisplay(w), XtWindow(w), tip->tip.fontset, gc,
			  tip->tip.left_margin, ksy, label, len);
    }
    else {
	while ((nl = index(label, '\n')) != NULL) {
	    if (tip->tip.encoding)
		XDrawString16(XtDisplay(w), XtWindow(w), gc,
			      tip->tip.left_margin, y,
			      (XChar2b*)label, (int)(nl - label) >> 1);
	    else
		XDrawString(XtDisplay(w), XtWindow(w), gc,
			    tip->tip.left_margin, y, label, (int)(nl - label));
	    y += tip->tip.font->max_bounds.ascent +
		 tip->tip.font->max_bounds.descent;
	    label = nl + 1;
	}
	len = strlen(label);
	if (len) {
	    if (tip->tip.encoding)
		XDrawString16(XtDisplay(w), XtWindow(w), gc,
			      tip->tip.left_margin, y, (XChar2b*)label, len >> 1);
	    else
		XDrawString(XtDisplay(w), XtWindow(w), gc,
			    tip->tip.left_margin, y, label, len);
	}
    }
}

/*ARGSUSED*/
static Boolean
XawTipSetValues(Widget current, Widget request, Widget cnew,
		ArgList args, Cardinal *num_args)
{
    TipWidget curtip = (TipWidget)current;
    TipWidget newtip = (TipWidget)cnew;
    Boolean redisplay = False;

    if (curtip->tip.font->fid != newtip->tip.font->fid ||
	curtip->tip.foreground != newtip->tip.foreground) {
	XGCValues values;

	values.foreground = newtip->tip.foreground;
	values.background = newtip->core.background_pixel;
	values.font = newtip->tip.font->fid;
	values.graphics_exposures = False;
	XtReleaseGC(cnew, curtip->tip.gc);
	newtip->tip.gc = XtAllocateGC(cnew, 0, GCForeground | GCBackground |
				      GCFont | GCGraphicsExposures, &values,
				      GCFont, 0);
	redisplay = True;
    }
    if (curtip->tip.display_list != newtip->tip.display_list)
	redisplay = True;

    return (redisplay);
}

static void
TipLayout(XawTipInfo *info)
{
    XFontStruct	*fs = info->tip->tip.font;
    int width = 0, height;
    char *nl, *label = info->tip->tip.label;

    if (info->tip->tip.international == True) {
	XFontSet fset = info->tip->tip.fontset;
	XFontSetExtents *ext = XExtentsOfFontSet(fset);

	height = ext->max_ink_extent.height;
	if ((nl = index(label, '\n')) != NULL) {
	    /*CONSTCOND*/
	    while (True) {
		int w = XmbTextEscapement(fset, label, (int)(nl - label));

		if (w > width)
		    width = w;
		if (*nl == '\0')
		    break;
		label = nl + 1;
		if (*label)
		    height += ext->max_ink_extent.height;
		if ((nl = index(label, '\n')) == NULL)
		    nl = index(label, '\0');
	    }
	}
	else
	    width = XmbTextEscapement(fset, label, strlen(label));
    }
    else {
	height = fs->max_bounds.ascent + fs->max_bounds.descent;
	if ((nl = index(label, '\n')) != NULL) {
	    /*CONSTCOND*/
	    while (True) {
		int w = info->tip->tip.encoding ?
		    XTextWidth16(fs, (XChar2b*)label, (int)(nl - label) >> 1) :
		    XTextWidth(fs, label, (int)(nl - label));
		if (w > width)
		    width = w;
		if (*nl == '\0')
		    break;
		label = nl + 1;
		if (*label)
		    height += fs->max_bounds.ascent + fs->max_bounds.descent;
		if ((nl = index(label, '\n')) == NULL)
		    nl = index(label, '\0');
	    }
	}
	else
	    width = info->tip->tip.encoding ?
		XTextWidth16(fs, (XChar2b*)label, strlen(label) >> 1) :
		XTextWidth(fs, label, strlen(label));
    }
    XtWidth(info->tip) = width + info->tip->tip.left_margin +
			 info->tip->tip.right_margin;
    XtHeight(info->tip) = height + info->tip->tip.top_margin +
			  info->tip->tip.bottom_margin;
}

#define	DEFAULT_TIP_Y_OFFSET	12
static void
TipPosition(XawTipInfo *info)
{
    Window r, c;
    int rx, ry, wx, wy;
    unsigned mask;
    Position x, y;

    XQueryPointer(XtDisplay((Widget)info->tip), XtWindow((Widget)info->tip),
		  &r, &c, &rx, &ry, &wx, &wy, &mask);
    x = rx - (XtWidth(info->tip) >> 1);
    y = ry + DEFAULT_TIP_Y_OFFSET;

    if (x >= 0) {
	int scr_width = WidthOfScreen(XtScreen(info->tip));

	if (x + XtWidth(info->tip) + XtBorderWidth(info->tip) > scr_width)
	    x = scr_width - XtWidth(info->tip) - XtBorderWidth(info->tip);
    }
    if (x < 0)
	x = 0;
    if (y >= 0) {
	int scr_height = HeightOfScreen(XtScreen(info->tip));

	if (y + XtHeight(info->tip) + XtBorderWidth(info->tip) > scr_height)
	    y -= XtHeight(info->tip) + XtBorderWidth(info->tip) +
		 (DEFAULT_TIP_Y_OFFSET << 1);
    }
    if (y < 0)
	y = 0;

    XMoveResizeWindow(XtDisplay(info->tip), XtWindow(info->tip),
		      (int)(XtX(info->tip) = x), (int)(XtY(info->tip) = y),
		      (unsigned)XtWidth(info->tip), (unsigned)XtHeight(info->tip));
}

static XawTipInfo *
CreateTipInfo(Widget w)
{
    XawTipInfo *info = XtNew(XawTipInfo);
    Widget shell = w;

    info->screen = XtScreen(w);

    while (XtParent(shell))
	shell = XtParent(shell);

    info->tip = (TipWidget)XtCreateWidget("tip", tipWidgetClass, shell, NULL, 0);
    XtRealizeWidget((Widget)info->tip);
    info->widget = NULL;
    info->mapped = False;
    info->next = NULL;
    XtAddEventHandler(shell, KeyPressMask, False, TipShellEventHandler,
		      (XtPointer)NULL);

    return (info);
}

static XawTipInfo *
FindTipInfo(Widget w)
{
    XawTipInfo *ptip, *tip = first_tip;
    Screen *screen = XtScreenOfObject(w);

    if (tip == NULL)
	return (first_tip = tip = CreateTipInfo(w));

    for (ptip = tip; tip; ptip = tip, tip = tip->next)
	if (tip->screen == screen)
	    return (tip);

    return (ptip->next = CreateTipInfo(w));
}

static void
ResetTip(XawTipInfo *info, Bool add_timeout)
{
    if (info->tip->tip.timer) {
	XtRemoveTimeOut(info->tip->tip.timer);
	info->tip->tip.timer = 0;
    }
    if (info->mapped) {
	XtRemoveGrab(XtParent((Widget)info->tip));
	XUnmapWindow(XtDisplay((Widget)info->tip), XtWindow((Widget)info->tip));
	info->mapped = False;
    }
    if (add_timeout) {
	info->tip->tip.timer =
	    XtAppAddTimeOut(XtWidgetToApplicationContext((Widget)info->tip),
			    info->tip->tip.timeout, TipTimeoutCallback,
			    (XtPointer)info);
    }
}

static void
TipTimeoutCallback(XtPointer closure, XtIntervalId *id)
{
    XawTipInfo *info = (XawTipInfo*)closure;
    Arg args[3];

    info->tip->tip.label = NULL;
    info->tip->tip.international = False;
    info->tip->tip.encoding = 0;
    info->tip->tip.timer = 0;
    XtSetArg(args[0], XtNtip, &info->tip->tip.label);
    XtSetArg(args[1], XtNinternational, &info->tip->tip.international);
    XtSetArg(args[2], XtNencoding, &info->tip->tip.encoding);
    XtGetValues(info->widget, args, 3);

    if (info->tip->tip.label) {
	TipLayout(info);
	TipPosition(info);
	XMapRaised(XtDisplay((Widget)info->tip), XtWindow((Widget)info->tip));
	XtAddGrab(XtParent((Widget)info->tip), True, True);
	info->mapped = True;
    }
}

/*ARGSUSED*/
static void
TipShellEventHandler(Widget w, XtPointer client_data, XEvent *event,
		     Boolean *continue_to_dispatch)
{
    ResetTip(FindTipInfo(w), False);
}

/*ARGSUSED*/
static void
TipEventHandler(Widget w, XtPointer client_data, XEvent *event,
		Boolean *continue_to_dispatch)
{
    XawTipInfo *info = FindTipInfo(w);
    Boolean add_timeout;

    if (info->widget != w) {
	ResetTip(info, False);
	info->widget = w;
    }

    switch (event->type) {
	case EnterNotify:
	    add_timeout = True;
	    break;
	case MotionNotify:
	    /* If any button is pressed, timer is 0 */
	    if (info->mapped)
		return;
	    add_timeout = info->tip->tip.timer != 0;
	    break;
	default:
	    add_timeout = False;
	    break;
    }
    ResetTip(info, add_timeout);
}

/*
 * Public routines
 */
void
XawTipEnable(Widget w)
{
    XtAddEventHandler(w, TIP_EVENT_MASK, False, TipEventHandler,
		      (XtPointer)NULL);
}

void
XawTipDisable(Widget w)
{
    XawTipInfo *info = FindTipInfo(w);

    XtRemoveEventHandler(w, TIP_EVENT_MASK, False, TipEventHandler,
			 (XtPointer)NULL);
    if (info->widget == w)
	ResetTip(info, False);
}
