/* $Id: xbabylon.c,v 1.8 2004/06/26 15:21:49 satan Exp $ */
/*
 * xbabylon translator, the translator on X Window System.
 * Copyright (c) 2001 Shigeki Kaneko, all right reserved.
 */
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <sys/time.h>
#include <X11/Xlib.h>
#include <X11/Xutil.h>
#include <X11/Xlocale.h>
#include <X11/Xatom.h>
#include <X11/cursorfont.h>
#include <assert.h>
#include "xbabylon.h"

/*
 * global variables
 */
static int  version_major = 0, version_minor = 9, version_extension = 3;

#define PROGRAM_NAME		("XBabylon")
#define ERR			(-1)
#define MARGIN_RATIO		(20)
#define WIDTH 			(600)
#define HEIGHT 			(100)
#define WINDOWX			(0)
#define WINDOWY			(0)
#define WIDTH_RATIO		(2)
#define HEIGHT_RATIO		(10)
#define TEXTOFFSETX		(12)
#define TEXTOFFSETY		(2)
#define LINE_WIDTH_ADJUSTMENT	(12)
#define CONTINUELINE_OFFSET	(30)
#define BORDER			(2)
#define COLORBASE		(255)

#ifdef FONT8
# define FONTSET ("-*-*-medium-r-normal-*-*-70-75-75-*-*-*-*")  // 8 dots
# define LINE_HEIGHT		(10)
# define LINE_HEIGHT_ADJUSTMENT	(-4)
#elif FONT10
# define FONTSET ("-*-*-medium-r-normal-*-*-90-75-75-*-*-*-*")  // 10 dots
# define LINE_HEIGHT		(12)
# define LINE_HEIGHT_ADJUSTMENT	(-6)
#elif FONT12
# define FONTSET ("-*-*-medium-r-normal-*-*-110-75-75-*-*-*-*") // 12 dots
# define LINE_HEIGHT		(14)
# define LINE_HEIGHT_ADJUSTMENT	(-8)
#else
# define FONTSET ("-*-*-medium-r-normal-*-*-130-75-75-*-*-*-*") // 14 dots
# define LINE_HEIGHT		(16)
# define LINE_HEIGHT_ADJUSTMENT	(-10)
#endif
#define SEC_BEFORE_DISAPPEAR 	(3)
#define USEC_BEFORE_DISAPPEAR	(0)
#define CUTBUFFER_NAME		("cut_buffer")
#define CURSOR_SHAPE		(XC_watch)

/* This define executes to remove unprintable tail from the word got
   from cut buffer. This case is often occurred on Mozilla (bug?) */
#define REMOVE_UNPRINTABLE_GARBAGE

// current locale setting.
char  *current_locale = NULL;

#if defined(USE_XUTF8_LIBS) && defined(X_HAVE_UTF8_STRING)
int (*xBabTextEscapement)(XFontSet, _Xconst char*, int);
void (*xBabDrawString)(Display*, Drawable, XFontSet, GC, int, int, _Xconst char *, int);
#endif // USE_XUTF8_LIBS && X_HAVE_UTF8_STRING

/*
 * Get Searchword Buffer
 */
Searchword * GetSearchwordBuffer (int size)
{
    Searchword *s;

    s = (Searchword*)malloc(sizeof(Searchword));
    if (!s) {
	perror("malloc");
	Dprint(("malloc error in GetSearchwordBuffer\n"));
	return NULL;
    }
    s->length = size;
    s->exact_match = TRUE;
    s->word = (char*)malloc(size + 1);
    if (!s) {
	perror("malloc");
	Dprint(("malloc error in GetSearchwordBuffer\n"));
	return NULL;
    }
    s->next = NULL;
    return s;
}
void  FreeSearchwordBuffer (Searchword *srchword)
{
    if (srchword->word)  free(srchword->word);
    free(srchword);
}

/*
 * GetTranslationBuffer
 */
Translation * GetTranslationBuffer (int size)
{
    Translation *t;

    t = (Translation*)malloc(sizeof(Translation));
    if (!t) {
	perror("malloc");
	Dprint(("malloc error in GetTranslationBuffer\n"));
	return NULL;
    }
    t->buf = (char*)malloc(size);
    if (!t) {
	perror("malloc");
	Dprint(("malloc error in GetTranslationBuffer\n"));
	return NULL;
    }
    t->next = NULL;
    return t;
}

/*
 * Retrieving word to be searched from selection or cut buffer.
 */
static int  GetSearchWord (Display *dpy, Window win, Atom cut_buf, XEvent *event, char *buf, int blen)
{
    XSelectionEvent  *xsl = &event->xselection;
    Atom  rtyp;
    int  rfmt;
    unsigned char *rprop;
    unsigned long  rlen, nbremain;
    int len = 0;

    Dprint(("XSlectionEvent[%d, %ld, %ld, %ld]\n", xsl->send_event, xsl->selection, xsl->target, xsl->property));
	    
    /* retrieving property */
    if (xsl->property != None) {
	XGetWindowProperty(dpy, win, cut_buf, 0, XBUFSIZ, False, XA_STRING,
			   &rtyp, &rfmt, &rlen, &nbremain, &rprop);
	Dprint(("Property: [%s]\n", rprop));
	if (rlen > 0) {
	    if (rlen < blen) {
		memcpy(buf, rprop, rlen);
		len = rlen;
	    } else {
		Dprint(("Words in selection are too large (%ld)\n", rlen));
		len = 0;
	    }
	    XFree(rprop);
	} else
	    len = 0;

	XDeleteProperty(dpy, win, cut_buf);
    }

    /* If there isn't any properties, check the latest cut buffer */
    if (len <= 0) {
	char *rstr = XFetchBytes(dpy, &len);
	if (len > 0) {
	    if (len < blen)
		memcpy(buf, rstr, len);
	    else {
		Dprint(("Words in cut buffer are too large (%d)\n", len));
		len = 0;
	    }
	    XFree(rstr);
	} else {
	    Dprint(("No words in the cut buffer\n"));
	    len = 0;
	}
    }

    buf[len] = '\0';
    Dprint(("GetSearchWord: (%d) %s\n", len, buf));

#ifdef REMOVE_UNPRINTABLE_GARBAGE
    {
	int  i;

	for (i = len - 1; i > 0; i--) {
	    if (!IsPrintable(buf[i])) {
		buf[i] = '\0';
		len--;
	    }
	}
    }
#endif
    return len;
}


/*
 * Print strings on the window
 */
/* ARGSUSED */
static void  PrintOutString (Display *dpy, int scn, Window win, XFontSet fs, GC gc, int wwidth, int wheight, Translation *trans)
{
    int  i, prv = 0, width = 0;
    int  mgx = TEXTOFFSETX, mgy = LINE_HEIGHT + TEXTOFFSETY;
    XColor fgcol;
    Translation  *str;

    for (str = trans; str; str = str->next) {
	Dprint(("PrintOutString: [%d/%d] [%s]\n", str->length, str->offset, str->buf));

	// setting start offset
	mgx = TEXTOFFSETX;
	// mgy = LINE_HEIGHT + TEXTOFFSETY;
	prv = 0;
	
	/* set foreground color for the searching word */
	fgcol.red = 0xff * COLORBASE, fgcol.green = 0 * COLORBASE, fgcol.blue = 0 * COLORBASE; /* red */
	XAllocColor(dpy, DefaultColormap(dpy, scn), &fgcol);
	XSetForeground(dpy, gc, fgcol.pixel);

	/* print out the searching word */
#if defined(USE_XUTF8_LIBS) && defined(X_HAVE_UTF8_STRING)
        xBabDrawString(dpy, win, fs, gc, mgx, mgy, str->buf, str->offset);
        mgx += xBabTextEscapement(fs, str->buf, str->offset);
#else // USE_XUTF8_LIBS && X_HAVE_UTF8_STRING
        XmbDrawString(dpy, win, fs, gc, mgx, mgy, str->buf, str->offset);
        mgx += XmbTextEscapement(fs, str->buf, str->offset);
#endif // USE_XUTF8_LIBS && X_HAVE_UTF8_STRING

	prv += str->offset;

	/* set foreground color for the translation string */
	fgcol.red = 0 * COLORBASE, fgcol.green = 0 * COLORBASE, fgcol.blue = 0x80 * COLORBASE; /* navy */
	XAllocColor(dpy, DefaultColormap(dpy, scn), &fgcol);
	XSetForeground(dpy, gc, fgcol.pixel);

	/* turning up if char position is out of range */
	for (i = str->offset; i < str->length; i++) {
#if defined(USE_XUTF8_LIBS) && defined(X_HAVE_UTF8_STRING)
	    width = xBabTextEscapement(fs, &str->buf[prv], i - prv);
#else // USE_XUTF8_LIBS && X_HAVE_UTF8_STRING
	    width = XmbTextEscapement(fs, &str->buf[prv], i - prv);
#endif // USE_XUTF8_LIBS && X_HAVE_UTF8_STRING
	    // Dprint(("mgx[%d], TEXTOFFSETX[%d], width[%d] (%d,%d)\n", mgx, TEXTOFFSETX, width, prv, i));
	    if ((width + mgx + TEXTOFFSETX) >= wwidth) {
#if defined(USE_XUTF8_LIBS) && defined(X_HAVE_UTF8_STRING)
		Xbabdrawstring(dpy, win, fs, gc, mgx, mgy, &str->buf[prv], i - prv);
#else // USE_XUTF8_LIBS && X_HAVE_UTF8_STRING
		XmbDrawString(dpy, win, fs, gc, mgx, mgy, &str->buf[prv], i - prv);
#endif // USE_XUTF8_LIBS && X_HAVE_UTF8_STRING
		mgx = TEXTOFFSETX + CONTINUELINE_OFFSET;
		mgy += LINE_HEIGHT;
		prv = i;
	    }
	}

	/* writing the last line, if exist */
	if (prv < i) {
	    // Dprint(("REMAIN: mgx[%d], TEXTOFFSETX[%d], width[%d] (%d,%d)\n", mgx, TEXTOFFSETX, width, prv, i));
#if defined(USE_XUTF8_LIBS) && defined(X_HAVE_UTF8_STRING)	    
	    xBabDrawString(dpy, win, fs, gc, mgx, mgy, &str->buf[prv], i - prv);
#else // USE_XUTF8_LIBS && X_HAVE_UTF8_STRING
	    XmbDrawString(dpy, win, fs, gc, mgx, mgy, &str->buf[prv], i - prv);
#endif // USE_XUTF8_LIBS && X_HAVE_UTF8_STRING 
	    mgy += LINE_HEIGHT;
	}
	wheight = mgy + TEXTOFFSETY + LINE_HEIGHT_ADJUSTMENT;
    }

    /* fixing the geometry to fit the written string */
    {
	int  mx, my, dx, dy, d_x, d_y, mgnx, mgny, cordX, cordY;
	Window dr, dc;
	unsigned int dm;
	
	/* Adjustment for the case of only one line */
	if (prv < 1)
	    wwidth = width + TEXTOFFSETX + LINE_WIDTH_ADJUSTMENT;
    	if (wheight < 0) wheight = 0;

	d_x = DisplayWidth(dpy, scn);
	d_y = DisplayHeight(dpy, scn);
	mgnx = d_x / MARGIN_RATIO;
	mgny = d_y / MARGIN_RATIO;
	XQueryPointer(dpy, RootWindow(dpy, scn), &dr, &dc, &mx, &my, &dx, &dy, &dm);
	cordX = mx - mgnx;
	if (cordX < 0) cordX = 0;
	else if (cordX + wwidth > d_x) cordX = d_x - wwidth;
	cordY = my - wheight - mgny;
	if (cordY < 0) cordY = my + mgny;

	XMoveResizeWindow(dpy, win, cordX, cordY, wwidth, wheight);
	XMapWindow(dpy, win);
    }

    XFlush(dpy);
}


/*
 * XBabylon main: 
 * acceptable arguments
 *   -d dictionary	setting dictionary file path
 *   -h			print help message
 *   -v			print version number
 */
int  main (int argc, char *argv[])
{
    Display  *dpy;
    Window  win;
    XFontSet  fset;
    GC  gc;
    XColor  bgcol;
    XSetWindowAttributes attr;
    int  scn, length, wwidth, wheight;
    char  word[XBUFSIZ];
    Translation  *trans = NULL;
    Searchword  *srchw;
    long  evmask = NoEventMask;
    Atom  cbufp;
    Boolean  focused = FALSE;
    char  *dictionary = NULL;

    /* Interpret arguments */
    {
	int  i;
	enum { MODENULL, MODEDICT } amod = MODENULL;

	for (i = 1; i < argc; i++) {
	    switch (argv[i][0]) {
	      case '-':
		switch (argv[i][1]) {
		  case 'v':
		    fprintf(stderr, "%s version %d.%2d.%2d\n", 
			    argv[0], version_major, version_minor, version_extension);
		    exit(0);
		    break;

		  case 'd':
		    amod = MODEDICT;
		    break;

		  case 'h':
		  default:
		    PrintUsage(argv[0]);
		    exit(0);
		    break;
		}
		break;

	      default:
		switch (amod) {
		  case MODEDICT:
		    dictionary = argv[i];
		    break;
		   
		  default:
		    PrintUsage(argv[0]);
		    break;
		}
		break;
	    }
	}
    }

    /* Locale setting  */
    if (!(current_locale = setlocale(LC_ALL, ""))) {
	perror("setlocale");
	exit (1);
    }
    if (!XSupportsLocale()) {
	perror("XSupportslocale");
	exit (1);
    }

    /* create dictionary path, if not specified */
    if (!dictionary) {
	char *c;
	for (c = current_locale; *c; c++) {
	    if (*c == '.') {
		c++;
		break;
	    }
	}
	dictionary = (char*)malloc(strlen(DICT_FILE) + strlen(c) + 2);
	if (!dictionary) {
	    perror("malloc");
	    exit(1);
	}
	sprintf(dictionary, "%s.%s", DICT_FILE, c);
    }
    Dprint(("Dictionary: - %s -\n", dictionary));
#if defined(USE_XUTF8_LIBS) && defined(X_HAVE_UTF8_STRING)
    if (strncmp(current_locale, "ja_JP.UTF-8", strlen("ja_JP.UTF-8")) == 0 || strncmp(current_locale, "ja_JP.utf8", strlen("ja_JP.utf8")) == 0) {
        xBabTextEscapement = Xutf8TextEscapement;
        xBabDrawString     = Xutf8DrawString;
    } else {
        xBabTextEscapement = XmbTextEscapement;
        xBabDrawString     = XmbDrawString;
    }
#endif // USE_XUTF8_LIBS && X_HAVE_UTF8_STRING
    

    /* Connect to X Server */
    dpy = XOpenDisplay(NULL);
    if (!dpy) {
	perror("XOpenDisplay");
	exit (1);
    }

    /* Create a window in which translation is described */
    scn = DefaultScreen(dpy);
    bgcol.red = 240 * COLORBASE, bgcol.green = 230 * COLORBASE, bgcol.blue = 140 * COLORBASE; /* khaki */
    XAllocColor(dpy, DefaultColormap(dpy, scn), &bgcol);

    // Default window size is fixed ratio of display size
    wwidth  = DisplayWidth(dpy, scn) / WIDTH_RATIO;
    wheight = DisplayHeight(dpy, scn) / HEIGHT_RATIO;
	
    win = XCreateSimpleWindow(dpy, RootWindow(dpy, scn), 
			      WINDOWX, WINDOWY,
			      wwidth, wheight, BORDER,
			      BlackPixel(dpy, scn),
			      bgcol.pixel);

    /* reset Window Manager control */
    attr.override_redirect = True;
    XChangeWindowAttributes(dpy, win, CWOverrideRedirect, &attr);

    //XStoreName(dpy, win, PROGRAM_NAME);
    XSetStandardProperties(dpy, win, PROGRAM_NAME, PROGRAM_NAME, None,
			   argv, argc, NULL);
    // XMapRaised(dpy, win);
    // XMapWindow(dpy, win);
    
    gc = DefaultGC(dpy, scn);
    {
	// load Japanese font set
	char**  missing_charsets;
	int  num_missing_charsets = 0;
	char*  default_string;

	fset = XCreateFontSet(dpy, FONTSET,
			      &missing_charsets, &num_missing_charsets,
			      &default_string);
	if (num_missing_charsets > 0) {
	    perror("XCreateFontSet");
	    exit (1);
	}
    }
    // set the cursor if the pointer is on the window
    {
	Cursor  csr;
	csr = XCreateFontCursor(dpy, CURSOR_SHAPE);
	XDefineCursor(dpy, win, csr);
    }

    // XSetForeground(dpy, gc, BlackPixel(dpy, scn));

    /* to prepare to get selection, after that we wait for Selection
       Notify event */
    cbufp = XInternAtom(dpy, CUTBUFFER_NAME, False);
    XConvertSelection(dpy, XA_PRIMARY, XA_STRING, cbufp, win, CurrentTime);

    /* Handling events if needed.  The only event must be Expose to
       Repaint at this moment */
    evmask |= ExposureMask;
    evmask |= FocusChangeMask; // to know the win is focused.
    evmask |= EnterWindowMask | LeaveWindowMask;

    XSelectInput(dpy, win, evmask);
    XFlush(dpy);

    /* The window will be unmapped after a few seconds if no event
       comes */
    for (;;) {
	int  xsock = ConnectionNumber(dpy), fdwidth, selected;
	fd_set  readmask;
	struct timeval  *timeoutSet, tset;
	XEvent  ev;

	FD_ZERO(&readmask);
	FD_SET(xsock, &readmask);
	fdwidth = xsock + 1;

	/* If no event comes, the window will disappear after the
           timeout */
	if (! focused) {
	    tset.tv_sec  = SEC_BEFORE_DISAPPEAR;
	    tset.tv_usec = USEC_BEFORE_DISAPPEAR;
	    timeoutSet = &tset;
	} else {
	    timeoutSet = NULL;
	}
#ifdef DEBUG
	Dprint(("focused = %d\n", focused));
	if (timeoutSet) {
	    Dprint(("Timeout (%d, %d)\n", (int)timeoutSet->tv_sec, (int)timeoutSet->tv_usec));
	} else {
	    Dprint(("Timeout NULL\n"));
	}
#endif // DEBUG

	/* Checking event and timeout */
	selected = select(fdwidth, &readmask, NULL, NULL, timeoutSet);
        if (selected < 0) {
            perror("select");
            exit(1);
        } else if (selected == 0) {
            /* select has returned by time out */
            Dprint(("select timeout\n"));
	    break;
        }

	XNextEvent(dpy, &ev);

	Dprint(("Event comes: %d\n", ev.type));

	switch (ev.type) {
	case SelectionNotify:
	    length = GetSearchWord(dpy, win, cbufp, &ev, word, sizeof(word));

	    if (length > 0 && length < MAXWORDLENGTH) {
		Dprint(("GetSearchWord: (%d) %s\n", length, word));
                srchw = GetSearchwordBuffer(length);
                if (!srchw) exit(1);
                srchw->exact_match = TRUE;
                memcpy(srchw->word, word, length + 1);

		/* --- looking into the dictionary */
		trans = SearchWord(dictionary, srchw);
		if (! trans) {
		    if ((strlen(word) + 20 + strlen(dictionary)) < XBUFSIZ) {
			trans = GetTranslationBuffer(XBUFSIZ);
			if (!trans) {
			    Dprint(("GetTranslationBuffer errors\n"));
			    exit(1);
			}
			sprintf(trans->buf, "%s is not found in \"%s\".", word, dictionary);
			trans->length = strlen(trans->buf);
			trans->offset = length;
		    } else {
			fprintf(stderr, "%s is not found in %s, and too long\n", word, dictionary);
			exit(1);
		    }
		}
	    }
	    else {
		// Something is wrong with search word
		trans = GetTranslationBuffer(XBUFSIZ);
		if (!trans) {
		    Dprint(("GetTranslationBuffer errors\n"));
		    exit(1);
		}
		sprintf(trans->buf, "There is no word / a wrong word in the cut buffer");
		trans->length = strlen(trans->buf);
		trans->offset = 0;
	    }

	    PrintOutString(dpy, scn, win, fset, gc, wwidth, wheight, trans);
	    break;
	    
	case Expose:
	    if (! trans) {
		Dprint(("Expose Event comes, but buf is not set\n"));
		break;
	    }
	    PrintOutString(dpy, scn, win, fset, gc, wwidth, wheight, trans);
	    break;

	case FocusIn:
	case EnterNotify:
	    focused = TRUE;
	    break;

	case FocusOut:
	case LeaveNotify:
	    focused = FALSE;
	    break;
	    
	default:
	    Dprint(("Unknown event comes: %d\n", ev.type));
	    break;
	}
    }

    exit(0);
}
