/*******************************************************************************
*									       *
* calltips.c -- Calltip UI functions  (calltip *file* functions are in tags.c) *
*									       *
* Copyright (C) 2002 Nathaniel Gray					       *
*									       *
* This is free software; you can redistribute it and/or modify it under the    *
* terms of the GNU General Public License as published by the Free Software    *
* Foundation; either version 2 of the License, or (at your option) any later   *
* version. In addition, you may distribute version of this program linked to   *
* Motif or Open Motif. See README for details.                                 *
* 									       *
* This software is distributed in the hope that it will be useful, but WITHOUT *
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or        *
* FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License        *
* for more details.							       *
* 									       *
* You should have received a copy of the GNU General Public License along with *
* software; if not, write to the Free Software Foundation, Inc., 59 Temple     *
* Place, Suite 330, Boston, MA  02111-1307 USA		                       *
*									       *
* Nirvana Text Editor	    						       *
* April, 1997								       *
*									       *
* Written by Mark Edel  						       *
*									       *
*******************************************************************************/

#ifdef HAVE_CONFIG_H
#include "../config.h"
#endif

#include "text.h"
#include "textP.h"
#include "calltips.h"
#include "../util/misc.h"
#include "../util/nedit_malloc.h"

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

#include <Xm/Xm.h>
#include <Xm/Label.h>
#include <X11/Shell.h>

#ifdef HAVE_DEBUG_H
#include "../debug.h"
#endif

static char *expandAllTabs( char *text, int tab_width );

/*
** Pop-down a calltip if one exists, else do nothing
*/
void KillCalltip(WindowInfo *window, int calltipID) {
    textDisp *textD = ((TextWidget)window->lastFocus)->text.textD;
    TextDKillCalltip( textD, calltipID );
}
    
void TextDKillCalltip(textDisp *textD, int calltipID) {
    if( textD->calltip.ID == 0 ) 
        return;
    if( calltipID == 0 || calltipID == textD->calltip.ID ) {
        XtPopdown( textD->calltipShell );
        textD->calltip.ID = 0;
    }
}

/*
** Is a calltip displayed?  Returns the calltip ID of the currently displayed 
** calltip, or 0 if there is no calltip displayed.  If called with 
** calltipID != 0, returns 0 unless there is a calltip being 
** displayed with that calltipID.
*/
int GetCalltipID(WindowInfo *window, int calltipID) {
    textDisp *textD = ((TextWidget)window->lastFocus)->text.textD;
    if( calltipID == 0 )
        return textD->calltip.ID;
    else {
        if( calltipID == textD->calltip.ID)
            return calltipID;
        else
            return 0;
    }
} 

#define CALLTIP_EDGE_GUARD 5
static Boolean offscreenV(XWindowAttributes *screenAttr, int top, int height) {
    return (top < CALLTIP_EDGE_GUARD || 
            top + height >= screenAttr->height - CALLTIP_EDGE_GUARD);
}

/*
** Update the position of the current calltip if one exists, else do nothing
*/
void TextDRedrawCalltip(textDisp *textD, int calltipID) {
    int lineHeight = textD->ascent + textD->descent;
    Position txtX, txtY, borderWidth, abs_x, abs_y, tipWidth, tipHeight;
    XWindowAttributes screenAttr;
    int rel_x, rel_y, flip_delta;
    
    if( textD->calltip.ID == 0 ) 
        return;
    if( calltipID != 0 && calltipID != textD->calltip.ID )
        return;
    
    /* Get the location/dimensions of the text area */
    XtVaGetValues(textD->w, XmNx, &txtX, XmNy, &txtY, NULL);
    
    if( textD->calltip.anchored ) {
        /* Put it at the anchor position */
        if (!TextDPositionToXY(textD, textD->calltip.pos, &rel_x, &rel_y)) {
            if (textD->calltip.alignMode == TIP_STRICT)
                TextDKillCalltip(textD, textD->calltip.ID);
            return;
        }
    } else {
        if (textD->calltip.pos < 0) {
            /* First display of tip with cursor offscreen (detected in 
                ShowCalltip) */
            textD->calltip.pos = textD->width/2;
            textD->calltip.hAlign = TIP_CENTER;
            rel_y = textD->height/3;
        } else if (!TextDPositionToXY(textD, textD->cursorPos, &rel_x, &rel_y)){
            /* Window has scrolled and tip is now offscreen */
            if (textD->calltip.alignMode == TIP_STRICT)
                TextDKillCalltip(textD, textD->calltip.ID);
            return;
        }
        rel_x = textD->calltip.pos;
    }

    XtVaGetValues(textD->calltipShell, XmNwidth, &tipWidth, XmNheight, 
            &tipHeight, XmNborderWidth, &borderWidth, NULL);
    rel_x += borderWidth;
    rel_y += lineHeight/2 + borderWidth;
    
    /* Adjust rel_x for horizontal alignment modes */
    if (textD->calltip.hAlign == TIP_CENTER)
        rel_x -= tipWidth/2;
    else if (textD->calltip.hAlign == TIP_RIGHT)
        rel_x -= tipWidth;
    
    /* Adjust rel_y for vertical alignment modes */
    if (textD->calltip.vAlign == TIP_ABOVE) {
        flip_delta = tipHeight + lineHeight + 2*borderWidth;
        rel_y -= flip_delta;
    } else
        flip_delta = -(tipHeight + lineHeight + 2*borderWidth);
    
    XtTranslateCoords(textD->w, rel_x, rel_y, &abs_x, &abs_y);
    
    /* If we're not in strict mode try to keep the tip on-screen */
    if (textD->calltip.alignMode == TIP_SLOPPY) {
        XGetWindowAttributes(XtDisplay(textD->w), 
                RootWindowOfScreen(XtScreen(textD->w)), &screenAttr);

        /* make sure tip doesn't run off right or left side of screen */
        if (abs_x + tipWidth >= screenAttr.width - CALLTIP_EDGE_GUARD)
    	    abs_x = screenAttr.width - tipWidth - CALLTIP_EDGE_GUARD;
        if (abs_x < CALLTIP_EDGE_GUARD)
            abs_x = CALLTIP_EDGE_GUARD;

        /* Try to keep the tip onscreen vertically if possible */
        if (screenAttr.height > tipHeight && 
                offscreenV(&screenAttr, abs_y, tipHeight)) {
            /* Maybe flipping from below to above (or vice-versa) will help */
            if (!offscreenV(&screenAttr, abs_y + flip_delta, tipHeight))
    	        abs_y += flip_delta;
            /* Make sure the tip doesn't end up *totally* offscreen */
            else if (abs_y + tipHeight < 0)
                abs_y = CALLTIP_EDGE_GUARD;
            else if (abs_y >= screenAttr.height)
                abs_y = screenAttr.height - tipHeight - CALLTIP_EDGE_GUARD;
            /* If no case applied, just go with the default placement. */
        }
    }
    
    XtVaSetValues( textD->calltipShell, XmNx, abs_x, XmNy, abs_y, NULL );
}

/* 
** Returns a new string with each \t replaced with tab_width spaces or
** a pointer to text if there were no tabs.  Returns NULL on malloc failure.
** Note that this is dumb replacement, not smart tab-like behavior!  The goal
** is to prevent tabs from turning into squares in calltips, not to get the
** formatting just right.
*/
static char *expandAllTabs( char *text, int tab_width ) {
    int i, nTabs=0;
    size_t len;
    char *c, *cCpy, *textCpy;
    
    /* First count 'em */
    for( c = text; *c; ++c )
        if( *c == '\t' )
            ++nTabs;
    if( nTabs == 0 )
        return text;

    /* Allocate the new string */
    len = strlen( text ) + ( tab_width - 1 )*nTabs;
    textCpy = (char*)NEditMalloc( len + 1 );
    if( !textCpy ) {
        fprintf(stderr, 
                "xnedit: Out of heap memory in expandAllTabs!\n");
        return NULL;
    }
    
    /* Now replace 'em */
    for( c = text, cCpy = textCpy;  *c;  ++c, ++cCpy) {
        if( *c == '\t' ) {
            for( i = 0; i < tab_width; ++i, ++cCpy )
                *cCpy = ' ';
            --cCpy;  /* Will be incremented in outer for loop */
        } else
            *cCpy = *c;
    }
    *cCpy = '\0';
    return textCpy;
}

/*
** Pop-up a calltip.  
** If a calltip is already being displayed it is destroyed and replaced with
** the new calltip.  Returns the ID of the calltip or 0 on failure.
*/
int ShowCalltip(WindowInfo *window, char *text, Boolean anchored, 
        int pos, int hAlign, int vAlign, int alignMode) {
    static int StaticCalltipID = 1;
    textDisp *textD = ((TextWidget)window->lastFocus)->text.textD;
    int rel_x, rel_y;
    Position txtX, txtY;
    char *textCpy;
    XmString str;
    
    /* Destroy any previous calltip */
    TextDKillCalltip( textD, 0 );

    /* Make sure the text isn't NULL */
    if (text == NULL) return 0;
    
    /* Expand any tabs in the calltip and make it an XmString */
    textCpy = expandAllTabs( text, BufGetTabDistance(textD->buffer) );
    if( textCpy == NULL )
        return 0;       /* Out of memory */
    str = XmStringCreateLtoR(textCpy, XmFONTLIST_DEFAULT_TAG);
    if( textCpy != text )
        NEditFree( textCpy );
    
    /* Get the location/dimensions of the text area */
    XtVaGetValues(textD->w,
        XmNx, &txtX,
        XmNy, &txtY,
        NULL);
    
    /* Create the calltip widget on first request */
    if (textD->calltipW == NULL) {
        Arg args[10];
        int argcnt = 0;
        XtSetArg(args[argcnt], XmNsaveUnder, True); argcnt++;
        XtSetArg(args[argcnt], XmNallowShellResize, True); argcnt++;
        
        textD->calltipShell = CreatePopupShellWithBestVis("calltipshell", 
             overrideShellWidgetClass, textD->w, args, argcnt);
                
        /* Might want to make this a read-only XmText eventually so that 
            users can copy from it */
        textD->calltipW = XtVaCreateManagedWidget( 
                "calltip", xmLabelWidgetClass, textD->calltipShell,
                XmNborderWidth, 1,              /* Thin borders */
                XmNhighlightThickness, 0,
                XmNalignment, XmALIGNMENT_BEGINNING,
                XmNforeground, textD->calltipFGPixel,
                XmNbackground, textD->calltipBGPixel,
                NULL );
    }
    
    /* Set the text on the label */
    XtVaSetValues( textD->calltipW, XmNlabelString, str, NULL );
    XmStringFree( str );
    
    /* Figure out where to put the tip */
    if (anchored) {
        /* Put it at the specified position */
        /* If position is not displayed, return 0 */
        if (pos < textD->firstChar || pos > textD->lastChar ) {
            XBell(TheDisplay, 0);
            return 0;
        }
        textD->calltip.pos = pos;
    } else {
        /* Put it next to the cursor, or in the center of the window if the
            cursor is offscreen and mode != strict */
        if (!TextDPositionToXY(textD, textD->cursorPos, &rel_x, &rel_y)) {
            if (alignMode == TIP_STRICT) {
                XBell(TheDisplay, 0);
                return 0;
            }
            textD->calltip.pos = -1;
        } else 
            /* Store the x-offset for use when redrawing */
            textD->calltip.pos = rel_x;
    }

    /* Should really bounds-check these enumerations... */
    textD->calltip.ID = StaticCalltipID;
    textD->calltip.anchored = anchored;
    textD->calltip.hAlign = hAlign;
    textD->calltip.vAlign = vAlign;
    textD->calltip.alignMode = alignMode;
    
    /* Increment the static calltip ID.  Macro variables can only be int, 
        not unsigned, so have to work to keep it > 0 on overflow */
    if(++StaticCalltipID <= 0)
        StaticCalltipID = 1;
    
    /* Realize the calltip's shell so that its width & height are known */
    XtRealizeWidget( textD->calltipShell );
    /* Move the calltip and pop it up */
    TextDRedrawCalltip(textD, 0);
    XtPopup( textD->calltipShell, XtGrabNone );
    return textD->calltip.ID;
}
