/* $Id: ntext.cc,v 1.5 2003/02/18 02:49:48 bergo Exp $ */

/*

    eboard - chess client
    http://eboard.sourceforge.net
    Copyright (C) 2000-2003 Felipe Paulo Guazzi Bergo
    bergo@seul.org

    This program 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.

    This program 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 this program; if not, write to the Free Software
    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA

*/

#include <iostream>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <time.h>
#include <gtk/gtk.h>
#include <gtk/gtkselection.h>
#include "ntext.h"

TPoint::TPoint() {
  SrcI = Offset = X = Y = 0;
  rs = ro = 0;
  atEOL = false;
}

TPoint & TPoint::operator=(TPoint &src) {
  SrcI   = src.SrcI;
  Offset = src.Offset;
  X      = src.X;
  Y      = src.Y;
  atEOL  = src.atEOL;
  rs     = src.rs;
  ro     = src.ro;
  return(*this);
}

int     TPoint::operator<(TPoint &src) {
  if (SrcI < src.SrcI) return 1;
  if (SrcI > src.SrcI) return 0;
  if (Offset < src.Offset) return 1;
  return 0;
}

int     TPoint::operator==(TPoint &src) {
  return(SrcI == src.SrcI && Offset == src.Offset);
}

int TPoint::operator<(int i)  { return(SrcI<i);  }
int TPoint::operator<=(int i) { return(SrcI<=i); }
int TPoint::operator>(int i)  { return(SrcI>i);  }
int TPoint::operator>=(int i) { return(SrcI>=i); }
int TPoint::operator==(int i) { return(SrcI==i); }

NText::NText() {
  textbuffer = 0;
  buffersz = 0;
  tlen = 0;
  font = 0;
  fmtw = 1;
  lw = lh = 0;
  cw = ch = 0;  
  leftb = rightb = 1;
  IgnoreChg = 0;
  WasAtBottom = true;
  MaxLines = 0;
  bgcolor = 0;
  havesel = false;
  dropmup = 0;
  toid = -1;
  lfl  = -1;
  createGui();
}

NText::~NText() {
  lines.clear();
  xlines.clear();
  if (buffersz && textbuffer) {    
    free(textbuffer);
    buffersz = 0;
    textbuffer = 0;
  }
  if (cgc)    gdk_gc_destroy(cgc);
  if (canvas) gdk_pixmap_unref(canvas);
}

void NText::createGui() {
  widget = gtk_hbox_new(FALSE,0);
  body   = gtk_drawing_area_new();

  gtk_widget_set_events(body,GDK_EXPOSURE_MASK|GDK_BUTTON_PRESS_MASK|
			GDK_BUTTON_RELEASE_MASK|GDK_BUTTON1_MOTION_MASK);

  canvas = 0;
  cgc    = 0;
  vsa    = (GtkAdjustment *) gtk_adjustment_new(0.0,
						0.0,
						0.0,1.0,10.0,10.0);
  sb     = gtk_vscrollbar_new(vsa);

  gtk_box_pack_start(GTK_BOX(widget), body, TRUE, TRUE, 0);
  gtk_box_pack_start(GTK_BOX(widget), sb, FALSE, TRUE, 0);

  gtk_signal_connect(GTK_OBJECT(body),"configure_event",
		     GTK_SIGNAL_FUNC(ntext_configure), (gpointer) this);
  gtk_signal_connect(GTK_OBJECT(body),"expose_event",
		     GTK_SIGNAL_FUNC(ntext_expose), (gpointer) this);
  gtk_signal_connect(GTK_OBJECT(vsa),"value_changed",
		     GTK_SIGNAL_FUNC(ntext_sbchange), (gpointer) this);

  gtk_signal_connect(GTK_OBJECT(body),"button_press_event",
		     GTK_SIGNAL_FUNC(ntext_mdown), (gpointer) this);
  gtk_signal_connect(GTK_OBJECT(body),"button_release_event",
		     GTK_SIGNAL_FUNC(ntext_mup), (gpointer) this);
  gtk_signal_connect(GTK_OBJECT(body),"motion_notify_event",
		     GTK_SIGNAL_FUNC(ntext_mdrag), (gpointer) this);

  gtk_signal_connect (GTK_OBJECT(body), "selection_clear_event",
                      GTK_SIGNAL_FUNC (ntext_ksel), (gpointer) this);

  gtk_selection_add_target(body,
			   GDK_SELECTION_PRIMARY,
			   GDK_SELECTION_TYPE_STRING, 1);

  gtk_signal_connect (GTK_OBJECT(body), "selection_get",
                      GTK_SIGNAL_FUNC (ntext_getsel), (gpointer) this);

  Gtk::show(body,sb,0);
}

void NText::repaint() {
  gtk_widget_queue_resize(body);
}

void NText::softRepaint() {
  GdkEventExpose ee;
  ntext_configure(body,0,(gpointer) this);
  ee.area.x = 0;
  ee.area.y = 0;
  ee.area.width  = cw;
  ee.area.height = ch;
  ntext_expose(body,&ee,(gpointer) this);
}

void NText::safeSoftRepaint() {
  if (canvas)
    softRepaint();
}

void NText::setFont(GdkFont *f) {
  font = f;
  fh = 2 + gdk_string_height(f,"HXqpbdg[]");
  fmtw = -1; /* invalidate current wrapping */
}

void NText::setBG(int c) {
  bgcolor = c;
  if (canvas)
    softRepaint();
}

void NText::grow(int newsz) {
  tlen = newsz;

  if (newsz <= buffersz)
    return;

  if (newsz - buffersz < 8192)
    newsz = buffersz + 8192;

  if (!buffersz || !textbuffer) {
    textbuffer = (char *) malloc(newsz);
    buffersz = newsz;
  } else if (newsz > buffersz) {
    textbuffer = (char *) realloc(textbuffer, newsz);
    buffersz = newsz;
  }
}

void NText::append(char *text, int len, int color) {
  int i,s0;
  NLine x;

  if (len < 0) {
    discardExcess();
    return;
  }

  s0 = tlen;
  x.Color = color;

  for(i=0;i<len;i++)
    if (text[i] == '\n') {
      grow(tlen+i);
      memcpy(&textbuffer[s0], text, i);
      x.Offset = s0;
      x.Length = i;
      lines.push_back(x);
      append(&text[i+1], len-(i+1), color);
      return;
    }

  // if search for \n failed, this is a single line
  grow(tlen+len);
  memcpy(&textbuffer[s0], text, len);
  x.Offset = s0;
  x.Length = len;
  lines.push_back(x);

  discardExcess();
  fmtw = -1;
}

void NText::formatBuffer() {
  int i,j;
  j = lines.size();
  xlines.clear();

  for(i=0;i<j;i++)
    formatLine(i);
  fmtw = lw;
}

void NText::formatLine(unsigned int i) {
  int j,k,l,w;
  bool fit = false;

  if (lines.size() <= i || !canvas || !font)
    return;

  j = 0;
  k = lines[i].Length;

  // empty line
  if (k==0) {
    FLine none;
    none.Offset = lines[i].Offset;
    none.Length = 0;
    none.Color  = lines[i].Color;
    none.SrcI   = i;
    xlines.push_back(none);
    return;
  }

  while(k-j > 0) {
    fit = false;
      
    // try full-fit for for unwrapped of last chunk of wrapping
    w = gdk_text_width(font,textbuffer+lines[i].Offset+j,k-j);
    if (w <= lw) {
      FLine last;

      last.Offset = lines[i].Offset + j;
      last.Length = k - j;
      last.Color  = lines[i]. Color;
      last.SrcI   = i;
      xlines.push_back(last);
      return;
    }

    for(l=k-1;l>=j;l--)
      if (textbuffer[lines[i].Offset+l] == ' ' || 
	  textbuffer[lines[i].Offset+l] == '\t') {
	w = gdk_text_width(font,textbuffer+lines[i].Offset+j,l-j);

	if (w <= lw) {
	  FLine y;
	  y.Offset = lines[i].Offset + j;
	  y.Length = l-j;
	  y.Color  = lines[i].Color;
	  y.SrcI   = i;
	  xlines.push_back(y);
	  j=l+1;
	  fit = true;

	  break;
	}
      }
    // no blanks to wrap ?
    if (l<j && !fit) {
      FLine z;
      z.Offset = lines[i].Offset + j;
      z.Length = k-j;
      z.Color  = lines[i].Color;
      z.SrcI   = i;
      xlines.push_back(z);
      j=k;
    }
  }

}

// topline:   index of line rendered at the top of the widget
// linecount: number of lines in the buffer
// nlines:    how many lines can be fit in the current widget
void NText::setScroll(int topline, int linecount, int nlines) {
  GtkAdjustment prev;
  memcpy(&prev,vsa,sizeof(GtkAdjustment));

  //  printf("this=%x setScroll topline=%d linecount=%d nlines=%d\n",
  //	 (int)this,topline,linecount,nlines);

  // all text fits in the widget
  if (nlines >= linecount) {
    vsa->value = 0.0;
    vsa->lower = 0.0;
    vsa->upper = nlines;
    vsa->step_increment = 1.0;
    vsa->page_increment = nlines;
    vsa->page_size      = nlines;
  } else {
    vsa->value = topline;
    vsa->lower = 0.0;
    vsa->upper = linecount;
    vsa->step_increment = 1.0;
    vsa->page_increment = nlines;
    vsa->page_size      = nlines;
  }

  if (memcmp(&prev,vsa,sizeof(GtkAdjustment))!=0) {
    ++IgnoreChg;
    gtk_adjustment_changed(vsa);
    gtk_adjustment_value_changed(vsa);
  }
}

void NText::pageUp(float pages) {
  int nval, j, disp;
  j = xlines.size() - (int)(vsa->page_size);
  disp = (int) (pages*vsa->page_size);
  nval = (int) (vsa->value - disp);

  //  printf("this=%x pageUp disp=%d nval=%d j=%d\n",
  //          (int)this, disp, nval, j);

  if (nval < 0) nval = 0;
  if (nval > j) nval = j;
  WasAtBottom = false;
  setScroll(nval, xlines.size(), (int) (vsa->page_size));
  softRepaint();
}

void NText::pageDown(float pages) {
  int nval, j, disp;
  j = xlines.size() - (int)(vsa->page_size);
  disp = (int) (pages*vsa->page_size);
  nval = (int) (vsa->value + disp);

  // printf("this=%x pageDown disp=%d nval=%d j=%d\n",
  //  (int)this, disp, nval, j);

  if (nval < 0) nval = 0;
  if (nval > j) nval = j;
  WasAtBottom = false;
  setScroll(nval, xlines.size(), (int) (vsa->page_size));
  softRepaint();
}

void NText::lineUp(int n) {
  int nval, j;
  j = xlines.size() - (int)(vsa->page_size);
  nval = (int) (vsa->value - n);
  if (nval < 0) nval = 0;
  if (nval > j) nval = j;
  WasAtBottom = false;
  setScroll(nval, xlines.size(), (int) (vsa->page_size));
  softRepaint();
}

void NText::lineDown(int n) {
  int nval, j;
  j = xlines.size() - (int)(vsa->page_size);
  nval = (int) (vsa->value + n);
  if (nval < 0) nval = 0;
  if (nval > j) nval = j;
  WasAtBottom = false;
  setScroll(nval, xlines.size(), (int) (vsa->page_size));
  softRepaint();
}

void NText::setScrollBack(int n) {
  if (n != (signed)MaxLines ) {
    if (n < 0) n=0;
    MaxLines = (unsigned) n;
    discardExcess(true);
  }
}

void NText::discardExcess(bool _repaint) {
  if (MaxLines != 0)
    if (lines.size() > MaxLines) {
      discardLines(lines.size() - MaxLines);
      if (_repaint) softRepaint();
    }
}

void NText::discardLines(int n) {
  int i, j, oldtop, bufferdisp, ntop;
  int hc;
  unsigned int ai=0, bi=0;

  oldtop = (int) vsa->value;
  oldtop = xlines[oldtop].SrcI;

  if (n > (signed) lines.size()) n = lines.size();
  if (!n) return;

  if (havesel) {
    ai = xlines[A.SrcI].Offset + A.Offset;
    bi = xlines[B.SrcI].Offset + B.Offset;
  }

  // pop entries from main line index
  for(i=0;i<n;i++)
    lines.pop_front();

  // fix line offsets and tlen, oldtop vars
  bufferdisp = lines[0].Offset;
  memmove(textbuffer, textbuffer+bufferdisp, buffersz-bufferdisp);
  j = lines.size();
  for(i=0;i<j;i++)
    lines[i].Offset -= bufferdisp;
  tlen   -= bufferdisp;
  oldtop -= n;

  // deallocate excess memory if we're wasting more than 16 KB
  if (buffersz - tlen >= 16384) {
    buffersz = tlen;
    textbuffer = (char *) realloc(textbuffer, buffersz);
  }

  // reformat the rendering index
  formatBuffer();

  // fix scroll values
  ntop = -1;
  if (oldtop < 0) oldtop = 0;
  for(i=oldtop;i<j;i++)
    if (xlines[i].SrcI == oldtop) {
      ntop = i;
      break;
    }

  if (ntop < 0) ntop = 0;
  setScroll(ntop, xlines.size(), (int) (vsa->page_size));

  // fix selection
  if (havesel) {
    ai -= bufferdisp;
    bi -= bufferdisp;
    if (ai<0 && bi<0) {
      havesel = false;
    } else {
      if (ai<0) ai=0;
      if (bi<0) bi=0;
      hc = 0;
      j = xlines.size();
      for(i=0;i<j;i++) {
	if ((hc&0x01) == 0)
	  if (ai >= xlines[i].Offset && 
	      ai <  xlines[i].Offset+xlines[i].Length) {
	    hc |= 0x01;
	    A.SrcI   = A.rs = i;
	    A.Offset = A.ro = ai - xlines[i].Offset;
	  }
	if ((hc&0x02) == 0)
	  if (bi >= xlines[i].Offset && 
	      bi <  xlines[i].Offset+xlines[i].Length) {
	    hc |= 0x02;
	    B.SrcI   = B.rs = i;
	    B.Offset = B.ro = bi - xlines[i].Offset;
	  }
	if (hc == 0x03)
	  break;
      }
    } 
  }

  // does NOT repaint the widget, do it yourself
}

void NText::gotoLine(int n) {
  int i,j,xn;
  
  j = xlines.size();
  xn = -1;
  for(i=0;i<j;i++)
    if (xlines[i].SrcI == n) {
      xn = i;
      break;
    }
  if (xn < 0)
    return;

  if (xn < ((int)(vsa->value))  || 
      xn >= ((int)(vsa->value+vsa->page_size))) {

    WasAtBottom = false;
    setScroll(xn, xlines.size(), (int) (vsa->page_size));
    scheduleRepaint(50);

  }
}


bool NText::findTextUpward(int top, int bottom, const char *needle, 
			   bool select) 
{
  int a,b,i;

  a=0; b=lines.size()-1;
  if (top     >= 0 && top     <= b) a=top;
  if (bottom  >= 0 && bottom  <= b) b=bottom;
  for(i=b;i>=a;i--)
    if (matchTextInLine(i,needle,select)) {
      lfl = i;
      return true;
    }
  return false;
}

int NText::getLastFoundLine() { return lfl; }

bool NText::matchTextInLine(int i, const char *needle, bool select) {
  int j;
  char b[256], *bp, *p, *q;
  int off;

  j = lines.size();
  if (i>=j || i<0) return false;

  p = 0;

  if (lines[i].Length < 256) {
    memset(b,0,256);
    memcpy(b, textbuffer + lines[i].Offset, lines[i].Length);
    bp = b;
  } else {
    p = (char *) malloc(lines[i].Length + 1);
    memset(p,0,256);
    memcpy(p, textbuffer + lines[i].Offset, lines[i].Length);
    bp = p;
  }

  q = strstr(bp, needle);
  if (q!=0) {
    off = q-bp;
    if (select)
      selectByOffset(lines[i].Offset + off, 
		     lines[i].Offset + off + strlen(needle) - 1);
  }

  if (p!=0)
    free(p);

  return(q!=0);
}

void NText::selectByOffset(int first, int last) {
  int i,j;
  bool ff = false;
  if (last<first || last<0 || first<0) return;

  j = xlines.size();
  for(i=0;i<j;i++) {
    if (!ff) {
      if (first >= (signed) xlines[i].Offset && 
	  first <  (signed) (xlines[i].Offset + xlines[i].Length) ) 
	{
	  A.SrcI   = A.rs = i;
	  A.Offset = A.ro = first - xlines[i].Offset;
	  A.X = A.Y = 0;
	  A.atEOL = false;
	  ff = true;
	}
    }
    if (last >= (signed) xlines[i].Offset && 
	last <  (signed) xlines[i].Offset + xlines[i].Length)
      {
	B.SrcI   = B.rs = i;
	B.Offset = B.ro = last - xlines[i].Offset;
	B.X = B.Y = 0;
	B.atEOL = false;	
	havesel = true;
	gtk_selection_owner_set(body, GDK_SELECTION_PRIMARY,time(0));
	scheduleRepaint();
	return;
      }
  }
}

bool NText::saveTextBuffer(const char *path) {
  int i,j;
  char *p;

  ofstream txt(path);
  
  if (!txt)
    return false;

  j = lines.size();
  for(i=0;i<j;i++) {
    p = textbuffer + lines[i].Offset;
    txt.write(p, lines[i].Length);
    txt << endl;

    if (!txt) {
      txt.close();
      return false;
    }
  }

  txt.close();
  return true;
}

gboolean ntext_expose(GtkWidget *widget, GdkEventExpose *ee,
		      gpointer data)
{
  NText *me = (NText *) data;

  if (!me->canvas) return FALSE;
  gdk_draw_pixmap(widget->window,widget->style->black_gc,
		  me->canvas, 
		  ee->area.x, ee->area.y,
		  ee->area.x, ee->area.y,
		  ee->area.width, ee->area.height);
  return TRUE;
}

gboolean ntext_configure(GtkWidget *widget, GdkEventConfigure *ee,
			 gpointer data)
{
  NText *me = (NText *) data;
  int i, j, ww, wh, fh, lb, ri;
  int nl, tl, tb;
  GdkGC *gc;
  GdkPixmap *g;

  bool havesel;
  int pw, qw;
  static TPoint a,b;

  gdk_window_get_size(widget->window, &ww, &wh);

  me->lw = ww - (me->leftb + me->rightb); 
  me->lh = wh;

  if (!me->canvas) {
    me->canvas = gdk_pixmap_new(widget->window, ww, wh, -1);
    me->cw = ww;
    me->ch = wh;
    me->cgc = gdk_gc_new(me->canvas);
  } else {
    if (ww > me->cw || wh > me->ch) {
      gdk_pixmap_unref(me->canvas);
      if (me->cgc) gdk_gc_destroy(me->cgc);
      me->canvas = gdk_pixmap_new(widget->window, ww, wh, -1);
      me->cw = ww;
      me->ch = wh;
      me->cgc = gdk_gc_new(me->canvas);
    }
  }

  if (me->fmtw != me->lw)
    me->formatBuffer();
  
  gc = me->cgc;
  g  = me->canvas;
  fh = me->fh;
  lb = me->leftb;
  havesel = me->havesel;

  tb = wh % fh;
  if (tb > 2) tb-=2;

  gdk_rgb_gc_set_foreground(gc, me->bgcolor);
  gdk_draw_rectangle(g,gc,TRUE,0,0,ww,wh);

  /* scrollbar calc */
  j = me->xlines.size();
  nl = wh / me->fh;
  
  if (nl >= j)
    tl = 0;
  else
    tl = (int) (me->vsa->value);

  // keep autoscrolling if seeing th bottom of the buffer
  if (me->WasAtBottom) {
    tl = j-nl;
    if (tl < 0) tl=0;
  }

  me->setScroll(tl,j,nl);
  if (!tl) tb=0;

  for(i=0;i<j;i++)
    me->xlines[i].valid = false;

  if (havesel)
    if (me->A < me->B) {
      a=me->A; b=me->B;
    } else {
      a=me->B; b=me->A;
    }

  for(i=0;i<nl;i++) {
    ri = i+tl;
    if (ri >= j)
      break;

    gdk_rgb_gc_set_foreground(gc, me->xlines[ri].Color);

    if (!havesel) {
      gdk_draw_text(g,me->font,gc,lb,tb+fh*(i+1)-2,
		    me->textbuffer + me->xlines[ri].Offset,
		    me->xlines[ri].Length);
    } else {
      
      
      if ( a>ri || b<ri ) { // outside the selection
	gdk_draw_text(g,me->font,gc,lb,tb+fh*(i+1)-2,
		      me->textbuffer + me->xlines[ri].Offset,
		      me->xlines[ri].Length);
      } else if ( a==ri && b==ri ) { // one-line selection

	pw = gdk_text_width(me->font,
			    me->textbuffer + me->xlines[ri].Offset,
			    a.Offset);

	qw = gdk_text_width(me->font,
			    me->textbuffer + me->xlines[ri].Offset + a.Offset,
			    b.Offset - a.Offset + 1);

	gdk_draw_text(g,me->font,gc,lb,tb+fh*(i+1)-2,
		      me->textbuffer + me->xlines[ri].Offset,
		      a.Offset);

	gdk_draw_text(g,me->font,gc,lb+pw+qw,tb+fh*(i+1)-2,
		      me->textbuffer + me->xlines[ri].Offset + b.Offset + 1,
		      me->xlines[ri].Length - b.Offset - 1);

	gdk_draw_rectangle(g,gc,TRUE,lb+pw,tb+fh*i,qw,fh);
	gdk_rgb_gc_set_foreground(gc, me->bgcolor);

	gdk_draw_text(g,me->font,gc,lb+pw,tb+fh*(i+1)-2,
		      me->textbuffer + me->xlines[ri].Offset + a.Offset,
		      b.Offset - a.Offset + 1);

      } else if ( a==ri ) {          // first line of multi-line selection

	pw = gdk_text_width(me->font,
			    me->textbuffer + me->xlines[ri].Offset,
			    a.Offset);

	qw = gdk_text_width(me->font,
			    me->textbuffer + me->xlines[ri].Offset + a.Offset,
			    me->xlines[ri].Length - a.Offset);

	gdk_draw_text(g,me->font,gc,lb,tb+fh*(i+1)-2,
		      me->textbuffer + me->xlines[ri].Offset,
		      a.Offset);

	gdk_draw_rectangle(g,gc,TRUE,lb+pw,tb+fh*i,qw,fh);
	gdk_rgb_gc_set_foreground(gc, me->bgcolor);

	gdk_draw_text(g,me->font,gc,lb+pw,tb+fh*(i+1)-2,
		      me->textbuffer + me->xlines[ri].Offset + a.Offset,
		      me->xlines[ri].Length - a.Offset);

      } else if ( b==ri ) {          // last line of multi-line selection

	pw = gdk_text_width(me->font,
			    me->textbuffer + me->xlines[ri].Offset,
			    b.Offset + 1);

	gdk_draw_text(g,me->font,gc,lb+pw,tb+fh*(i+1)-2,
		      me->textbuffer + me->xlines[ri].Offset + b.Offset + 1,
		      me->xlines[ri].Length - b.Offset - 1);

	gdk_draw_rectangle(g,gc,TRUE,lb,tb+fh*i,pw,fh);
	gdk_rgb_gc_set_foreground(gc, me->bgcolor);

	gdk_draw_text(g,me->font,gc,lb,tb+fh*(i+1)-2,
		      me->textbuffer + me->xlines[ri].Offset,
		      b.Offset + 1);

      } else {                       // middle line of multi-line selection

	pw = gdk_text_width(me->font,
			    me->textbuffer + me->xlines[ri].Offset,
			    me->xlines[ri].Length);
	gdk_draw_rectangle(g,gc,TRUE,lb,tb+fh*i,pw,fh);
	gdk_rgb_gc_set_foreground(gc, me->bgcolor);
	gdk_draw_text(g,me->font,gc,lb,tb+fh*(i+1)-2,
		      me->textbuffer + me->xlines[ri].Offset,
		      me->xlines[ri].Length);

      }

    }

    me->xlines[ri].valid = true;
    me->xlines[ri].X = lb;
    me->xlines[ri].Y = tb+fh*i;
    me->xlines[ri].H = fh;
  }

  me->WasAtBottom = (i+tl == j);
  return TRUE;
}

void ntext_sbchange(GtkAdjustment *adj, gpointer data) {
  NText *me = (NText *) data;
  if (me->IgnoreChg)
    me->IgnoreChg--;
  else {
    me->WasAtBottom = false;
    me->softRepaint();
  }
}

bool NText::calcTP(TPoint &t, int x,int y) {
  int i,j,lastvalid;
  int l,r; /* binary search limits of length */
  int p,q,w,v;

  if (!font)
    return false;

  j = xlines.size();
  lastvalid = -1;
  for(i=0;i<j;i++)
    if (xlines[i].valid) {
      lastvalid = i;
      if (y>=xlines[i].Y && y<(xlines[i].Y+xlines[i].H)) {

	t.SrcI = t.rs = i;

	if (x <= xlines[i].X) {
	  t.Offset = t.ro = 0;
	  t.X = x;
	  t.Y = y;
	  t.atEOL = false;
	  return true;
	}

	l = 0; r = xlines[i].Length - 1;

	while(l<=r) { // binary search for exact point
	  //cerr << "***** bsrch " << l << "-" << r << endl;

	  p = (l+r)/2;
	  q = p - 1;
	  if (q<0) q=0;

	  if (p)
	    w = gdk_text_width(font, textbuffer+xlines[i].Offset, p);
	  else
	    w = 0;

	  if (q)
	    v = gdk_text_width(font, textbuffer+xlines[i].Offset, q);
	  else
	    v = 0;

	  v += xlines[i].X;
	  w += xlines[i].X;

	  //cerr << "x=" << x << endl;
	  //cerr << "q=" << q << " v=" << v << endl;
	  //cerr << "p=" << p << " w=" << w << endl;

	  if (x>=v && x<=w) {
	    t.Offset = q;
	    t.X = x;
	    t.Y = y;
	    t.atEOL = false;
	    return true;
	  }
	  if (x>w)
	    l = p + 1;
	  if (x<v)
	    r = p;
	}

	t.X = x;
	t.Y = y;

	t.Offset = xlines[i].Length - 1;
	if (t.Offset < 0) t.Offset = 0;

	t.ro    = t.Offset;
	t.atEOL = true;

	return true;
      }
    }

  // set point at the end of text when the user clicks past the end */
  if (lastvalid >= 0) {
    t.SrcI = t.rs = lastvalid;
    t.Offset = t.ro = xlines[lastvalid].Length - 1;
    t.X = x;
    t.Y = y;
    t.atEOL = true;
    return true;
  }
  return false; /* no match (no visible lines at all ?!?) */
}

void NText::fixEOL() {

  if (A.atEOL && B.atEOL && A.rs == B.rs) {
    A.SrcI   = A.rs;
    A.Offset = A.ro;
    B.SrcI   = B.rs;
    B.Offset = B.ro;
    return;
  }
  
  if (A.atEOL) {
    if (B < A) {
      A.SrcI   = A.rs;
      A.Offset = A.ro;
    } else {
      if (A.rs+1 < (signed) xlines.size()) {
	A.SrcI   = A.rs + 1;
	A.Offset = 0;
      }
    }
  }

  if (B.atEOL) {
    if (A < B) {
      B.SrcI   = B.rs;
      B.Offset = B.ro;
    } else {
      if (B.rs+1 < (signed) xlines.size()) {
	B.SrcI   = B.rs + 1;
	B.Offset = 0;
      }
    }
  }
}

/* 
   1. clears the selection
   2. prepares A end point
*/
gboolean ntext_mdown(GtkWidget *widget, GdkEventButton *eb,
		     gpointer data) 
{
  NText *me = (NText *) data;
  TPoint c;
  int p,l,r,ml,mr;

  if (!eb) return FALSE;

  if (eb->button == 1) {

    switch(eb->type) {
    case GDK_2BUTTON_PRESS: // select word
      if (me->calcTP(c, (int)(eb->x), (int)(eb->y))) {
	p = me->textbuffer[me->xlines[c.SrcI].Offset + c.Offset];
	if (isspace(p))
	  return FALSE;
	me->A = c;
	me->B = c;
	me->A.atEOL = me->B.atEOL = false;
	l = r = c.Offset;
	ml = 0;
	mr = me->xlines[c.SrcI].Length - 1;

	// walk left
	while(l>=ml && !isspace(me->textbuffer[me->xlines[c.SrcI].Offset + l]))
	  --l;
	++l;

	// walk right
	while(r<=mr && !isspace(me->textbuffer[me->xlines[c.SrcI].Offset + r]))
	  ++r;
	--r;

	me->A.Offset = l;
	me->B.Offset = r;
	me->havesel = true;
	me->dropmup++;
	gtk_selection_owner_set(me->body, GDK_SELECTION_PRIMARY,eb->time);
	me->scheduleRepaint();
      }
      break;
    case GDK_3BUTTON_PRESS: // select whole line
      if (me->calcTP(c, (int)(eb->x), (int)(eb->y))) {
	me->havesel = true;
	c.atEOL = false;
	me->A = c;
	me->B = c;
	me->A.Offset = 0;
	me->B.Offset = me->xlines[c.SrcI].Length - 1;
	me->dropmup++;
	gtk_selection_owner_set(me->body, GDK_SELECTION_PRIMARY,eb->time);
	me->scheduleRepaint();
      }      
      break;
    default:
      me->havesel = me->calcTP(me->A, (int)(eb->x), (int)(eb->y));
      if (me->havesel) {
	me->B = me->A;
	gtk_selection_owner_set(me->body, GDK_SELECTION_PRIMARY,eb->time);
      }
    }
  }
  return TRUE;
}

/*
  1. prepare B end point
  2. if A==B (offset-wise), clear selection
*/
gboolean ntext_mup(GtkWidget *widget, GdkEventButton *eb,
		   gpointer data)
{
  NText *me = (NText *) data;
  TPoint c;
  bool   dirty = false;

  if (!eb) return FALSE;
  if (eb->button == 1) {
    if (!me->havesel) return FALSE;
    if (me->dropmup) {
      me->dropmup--;
      return FALSE;
    }
    if (me->calcTP(c, (int) (eb->x), (int) (eb->y))) {
      me->B = c;
      me->fixEOL();
      dirty = true;
    }
    if (me->A == me->B) {
      me->havesel = false;
      dirty = true;
    }
    if (dirty)
      me->softRepaint();
    if (me->havesel)
      gtk_selection_owner_set(me->body, GDK_SELECTION_PRIMARY,eb->time);
  }
  return TRUE;
}

/* 
   what it does:
   1. prepares B end point
*/
gboolean ntext_mdrag(GtkWidget *widget, GdkEventMotion *em,
		     gpointer data) 
{
  NText *me = (NText *) data;
  TPoint c;
  int x,y;

  if (!em) return FALSE;

  if (!me->havesel) return FALSE;
  if (em->state & GDK_BUTTON1_MASK) {

    x = (int)(em->x);
    y = (int)(em->y);
    if (y > me->ch) {
      me->lineDown(1);
      return TRUE;
    }
    if (y < 0) {
      me->lineUp(1);
      return TRUE;
    }
    if (me->calcTP(c, x, y)) {
      me->B = c;
      me->fixEOL();
      me->scheduleRepaint();
    }
  }

  return TRUE;
}

gboolean ntext_ksel(GtkWidget * widget,
		    GdkEventSelection * event, gpointer data)
{
  NText *me = (NText *) data;
  me->havesel = false;
  me->scheduleRepaint();
  return TRUE;
}

void     ntext_getsel(GtkWidget * widget,
		      GtkSelectionData * seldata,
		      guint info, guint time, gpointer data)
{
  NText *me = (NText *) data;
  int i,sz,l0,lf,off,len,e0,ef;
  char *txt,*p;
  TPoint a,b;

  if (me->havesel) {
    if (me->A < me->B) {
      a=me->A; b=me->B;
    } else {
      a=me->B; b=me->A;
    }
  
    sz = (me->xlines[b.SrcI].Offset + b.Offset) - 
      (me->xlines[a.SrcI].Offset + a.Offset) + 1;
    sz += (me->xlines[b.SrcI].SrcI - me->xlines[a.SrcI].SrcI + 1);
    sz+=4;

    txt = (char*) malloc(sz);
    memset(txt,0,sz);
    
    l0 = me->xlines[a.SrcI].SrcI;
    lf = me->xlines[b.SrcI].SrcI;

    e0 = me->xlines[a.SrcI].Offset + a.Offset;
    ef = me->xlines[b.SrcI].Offset + b.Offset;

    p = txt;
    for(i=l0;i<=lf;i++) {
      off = me->lines[i].Offset;
      len = me->lines[i].Length;

      if (off < e0) {
	len -= (e0-off);
	off = e0;
      }
      if (off+len-1 > ef) {
	len = ef - off + 1;
      }

      memcpy(p, me->textbuffer + off, len);
      p+=len;

      if (i!=lf) {
	*p = '\n';
	++p;
      }
    }

    gtk_selection_data_set (seldata, GDK_SELECTION_TYPE_STRING,
			    8, (guchar*)txt, strlen (txt));
  }

}

gboolean ntext_redraw(gpointer data) {
  NText *me = (NText *) data;
  me->toid = -1;
  if (me->canvas)
    me->softRepaint();
  return FALSE;
}

void NText::scheduleRepaint(int latency) {
  if (toid < 0)
    toid = gtk_timeout_add(latency,ntext_redraw,(gpointer) this);
}
