//<copyright>
// 
// Copyright (c) 1994,95
// Institute for Information Processing and Computer Supported New Media (IICM),
// Graz University of Technology, Austria.
// 
//</copyright>



//<file>
//
// File:        textb.C - browser for some lines of text
//
// Created:     10 Oct 94   Michael Pichler
//
// Changed:     13 Apr 95   Michael Pichler
//
//
//</file>


#include "textb.h"

#include "editlabel.h"
#include "glyphutil.h"

#ifdef SMOOTHSCROLL
#include "sscrbox.h"
#else
#include <InterViews/scrbox.h>
#endif

#include <IV-look/kit.h>
#include <InterViews/color.h>
#include <InterViews/display.h>
#include <InterViews/event.h>
#include <InterViews/font.h>
#include <InterViews/hit.h>
#include <InterViews/layout.h>
#include <InterViews/selection.h>
#include <InterViews/session.h>
#include <InterViews/style.h>

#include <X11/keysym.h>

#include <string.h>
#include <ctype.h>
#include <iostream.h>



/*** TxtBrAdjustable ***/
// horicontal adjustable of a TextBrowser


class TxtBrAdjustable: public Adjustable
{
  public:
    TxtBrAdjustable (TextBrowser* tbrowser);

    void scrollRange (Coord allocationwidth, Coord maxglyphwidth);

    // Adjustable
    virtual Coord lower (DimensionName) const;
    virtual Coord upper (DimensionName) const;
    virtual Coord length (DimensionName) const;
    virtual Coord cur_lower (DimensionName) const;
    virtual Coord cur_upper (DimensionName) const;
    virtual Coord cur_length (DimensionName) const;
    virtual void scroll_to (DimensionName, Coord);

  private:
    TextBrowser* tbrowser_;
    Coord maxrange_, currange_, curlower_;
};


TxtBrAdjustable::TxtBrAdjustable (TextBrowser* tbrowser)
{
  tbrowser_ = tbrowser;
  maxrange_ = currange_ = 1.0;
  curlower_ = 0.0;
}


void TxtBrAdjustable::scrollRange (Coord allocationwidth, Coord maxglyphwidth)
{
  if (maxglyphwidth > allocationwidth)  // have something to scroll
  { maxrange_ = maxglyphwidth;
    currange_ = allocationwidth;
  }
  else
  { maxrange_ = currange_ = allocationwidth;
  }

  Coord space = maxrange_ - currange_;  // space to scroll
  small_scroll (Dimension_X, space / 16);  // might use average or maximum char width here
  large_scroll (Dimension_X, space / 4);

  notify(Dimension_X);
  if (curlower_ > space)  // (curlower_ + currange_ > maxrange_)
    scroll_to (Dimension_X, space);  // scroll to right border

} // scrollRange


Coord TxtBrAdjustable::lower (DimensionName) const
{ return 0.0;
}

Coord TxtBrAdjustable::upper (DimensionName) const
{ return maxrange_;
}

Coord TxtBrAdjustable::length (DimensionName) const
{ return maxrange_;
}

Coord TxtBrAdjustable::cur_lower (DimensionName) const
{ return curlower_;
}

Coord TxtBrAdjustable::cur_upper (DimensionName) const
{ return (curlower_ + currange_);
}

Coord TxtBrAdjustable::cur_length (DimensionName) const
{ return currange_;
}


void TxtBrAdjustable::scroll_to (DimensionName, Coord newlower)
{
//cerr << "I was told to scroll to " << newlower;

  if (newlower < 0)
    newlower = 0;
  Coord maxnewlower = maxrange_ - currange_;
  if (newlower > maxnewlower)  // newlower + currange_ !<= maxrange_
    newlower = maxnewlower;

//cerr << " and I will scroll to " << newlower << endl;

  curlower_ = newlower;
  tbrowser_->scrollTo (newlower);

} // scroll_to



/*** TextEditLabel ***/
// EditLabel that behaves slightly different in allocate

class TextEditLabel: public EditLabel
{
  public:
    TextEditLabel (
      const char* string, const Font* font, const Color* color,
      const Color* csrcol, const Color* selcol,
      const Color* invcol, const Color* chgcol,
      int hide, char hidechar
    )
    : EditLabel (string, font, color, csrcol, selcol, invcol, chgcol, hide, hidechar)
    { fillSelection (1);
    }

    virtual void allocate (Canvas* c, const Allocation& a, Extension& ext)
    { do_allocate (c, a, ext);
    }
};



/*** TextBrowser ***/

declarePtrList(TextBrowserLines,EditLabel)
implementPtrList(TextBrowserLines,EditLabel)

declareSelectionCallback(TextBrowser)
implementSelectionCallback(TextBrowser)


TextBrowser::TextBrowser (WidgetKit& kit, int size,
                          const char* stylename, const char* aliasname)
: InputHandler (nil, kit.style ())
{
  kit_ = &kit;
  lines_ = new TextBrowserLines (size);
  maxwidth_ = 0;
  horoffset_ = 0;

  cursorline_ = markerline_ = 0;
  cursorcol_ = markercol_ = 0;

  copy_ = lose_ = 0;

  // will have to use specific attribute names
  // like in TextBrowser instead of foreground/background
  kit.begin_style (stylename ? stylename : "TextBrowser",
                   aliasname ? aliasname : "FieldBrowser");
  Style* style = kit.style ();
  style->alias ("EditLabel");
  Display* dis = Session::instance ()->default_display ();

  font_ = kit.font ();
  Resource::ref (font_);
  fgcolor_ = kit.foreground ();
  Resource::ref (fgcolor_);

  if (!(csrcolor_ = lookupColor (style, dis, "cursorColour", "cursorColor", "cursorcolour", "cursorcolor")))
    csrcolor_ = kit.foreground ();
  Resource::ref (csrcolor_);

  if (!(selcolor_ = lookupColor (style, dis, "selectionColour", "selectionColor", "selectioncolour", "selectioncolor")))
    selcolor_ = kit.foreground ();
  Resource::ref (selcolor_);

  if (!(invcolor_ = lookupColor (style, dis, "inverseColour", "inverseColor", "inversecolour", "inversecolor")))
    invcolor_ = kit.background ();
  Resource::ref (invcolor_);

  hadj_ = new TxtBrAdjustable (this);
  allwidth_ = 0;

  box_ = new TBSCROLLBOX (size);  // ref'ed as body

  Glyph* target = new TransparentTarget (
    kit.inset_frame (box_)
  );

  kit.end_style ();  // "TextBrowser"/"FieldBrowser"/"EditLabel"

  hscrollbar_ = kit.hscroll_bar (hadj_);
  vscrollbar_ = kit.vscroll_bar (box_);

  Requisition req;
  vscrollbar_->request (req);
  const Requirement& xreq = req.x_requirement ();
  float scrollbarwidth = xreq.natural ();

  const LayoutKit& layout = *LayoutKit::instance();

  Glyph* buttonfiller = layout.hfixed (
    kit.outset_frame (
      layout.flexible (nil)
    ),
    scrollbarwidth
  );

  hbox1_ = layout.hbox (
    layout.vcenter (
      target,
      1.0
    ),
    vscrollbar_
  );
  hbox2_ = layout.hbox (
    hscrollbar_,
    buttonfiller
  );
  vbox_ = layout.vbox (
    hbox1_,
    hbox2_
  );
  body (vbox_);

  insertmode_ = true;
}


TextBrowser::~TextBrowser ()
{
  clearText ();  // clear list of all lines
  delete lines_;

  Resource::unref (font_);
  Resource::unref (fgcolor_);
  Resource::unref (csrcolor_);
  Resource::unref (selcolor_);
  Resource::unref (invcolor_);

  // inhibit selection callbacks on this text browser
  if (copy_)
  { copy_->detach ();
    copy_->unref ();
  }
  if (lose_)
  { lose_->detach ();
    lose_->unref ();
  }
}


Adjustable* TextBrowser::adjustableY ()
{
  return box_;
}

Adjustable* TextBrowser::adjustableX ()
{
  return hadj_;
}


void TextBrowser::appendLine (const char* text)
{
  EditLabel* line = newLine (text);
  box_->append (line);  // ref's line
  lines_->append (line);

  reallocate();
  redraw();
}


void TextBrowser::prependLine (const char* text)
{
  EditLabel* line = newLine (text);
  box_->prepend (line);  // ref's line
  lines_->prepend (line);

  reallocate();
  redraw();
}


void TextBrowser::appendText (const char* text)
{
  if (!text)
    return;

  char* str = (char*) text;  // don't worry: the string will remain the same
  EditLabel* line;
  int len;
  char* nlpos;

  int curlen = 256;  // extends if necessary
  char* linebuf = new char [curlen];

  while (*str)
  {
    nlpos = strchr (str, '\n');

    if (nlpos)
    {
      len = nlpos - str;
      if (len+1 > curlen)
      { delete[] linebuf;
        linebuf = new char [curlen = len+1];  // incl. '\0'
      }
      strncpy (linebuf, str, len);
      linebuf [len] = '\0';

      line = newLine (linebuf);
      box_->append (line);
      lines_->append (line);

      str = nlpos + 1;  // behind '\n'
    }
    else
    {
      line = newLine (str);
      box_->append (line);
      lines_->append (line);
      break;  // "incomplete last line"
    }

  } // for each line

  delete[] linebuf;

  reallocate();
  redraw();
} // appendText


EditLabel* TextBrowser::newLine (const char* text)
{
  // create a new line
  EditLabel* line = new TextEditLabel (
    text, font_,
    fgcolor_, csrcolor_, selcolor_, invcolor_,
    nil, 0, 0  // no modified background, no text hiding
  );
  line->insertMode(insertmode_);

  // update maximum line width (see also listbox.C)
  Requisition req;
  line->request (req);
  const Requirement& xreq = req.x_requirement ();
  float nat = xreq.natural ();
  if (nat > maxwidth_)
    maxwidth_ = nat;

  return line;
} // newLine


void TextBrowser::breakLine()
{
  EditLabel* curlabel = lines_->item(cursorline_);
  EditLabel* newlabel = newLine(curlabel->string() + cursorcol_);
  curlabel->deleteToEndOfLine();

  box_->insert(cursorline_+1, newlabel);
  lines_->insert(cursorline_+1, newlabel);
  cursorPosition(cursorline_+1, 0, 0);
  
  // update maxwidth_ and reallocate scrollbar
  maxwidth_ = 0;
  for (int i=0; i<numLines(); i++) {
    Coord width = lines_->item(i)->width();
    if (width > maxwidth_)
      maxwidth_ = width;
  }
  hadj_->scrollRange(allwidth_, maxwidth_ + 4.0);
  // magic 4.0 makes up for border of inset frame, sorry

  reallocate();
}


void TextBrowser::reallocate()
{
  Extension ext;
  if (canvas()) {
    // modify (change) all boxes; otherwise they do no allocate on
    // their children!!!
    hscrollbar_->change(0);
    vscrollbar_->change(0);
    vbox_->modified(0);
    hbox1_->modified(0);
    hbox2_->modified(0);

    allocate(canvas(), allocation(), ext);
  }
}


void TextBrowser::notifyX ()
{
  hadj_->scrollRange(allwidth_, maxwidth_ + 4.0);
  // magic 4.0 makes up for border of inset frame, sorry

  // update scrollbar
  float offset = lines_->item(cursorline_)->getOffset();
  hadj_->scroll_to(Dimension_X, offset);
}


void TextBrowser::clearText ()
{
  long n = lines_->count ();

  while (n--)
    box_->remove (n);  // unref's line

  lines_->remove_all ();
  maxwidth_ = 0;
}


GlyphIndex TextBrowser::numLines () const
{
  return lines_->count ();
}


void TextBrowser::scrollTo (Coord horoffset)
{
  if (horoffset == horoffset_)  // nothing to do
    return;

  horoffset_ = horoffset;

  TextBrowserLines* lines = lines_;
  long n = lines->count ();
  while (n--)
    lines->item (n)->setOffset (horoffset);

  redraw ();
  hadj_->notify (Dimension_X);  // update scroll bar
}


void TextBrowser::cursorPosition (long line, int col, int sel)
{
  long min, max;  // touched lines
  if (cursorline_ < markerline_)
    min = cursorline_,  max = markerline_;
  else
    min = markerline_,  max = cursorline_;
  if (line < min)
    min = line;
  if (line > max)
    max = line;

  // set range of col to [0;len)
  int len = lines_->item(line)->strlen();
  if (col > len) col = len;

  hideCursor (cursorline_);
  lines_->item (line)->cursorPosition (col, sel && line == cursorline_);
  if (!sel)
  { cursorline_ = line;
    cursorcol_ = col;
  }
  showCursor (cursorline_);
  markerline_ = line;
  markercol_ = col;
  // assert: cursorline_ and markerline_ in valid range

  long from, to;  // new selection range
  if (cursorline_ < markerline_)
    from = cursorline_,  to = markerline_;
  else
    from = markerline_,  to = cursorline_;

//cerr << "new selection range from " << from << " to " << to << endl;
//cerr << "updating selection in lines " << min << " through " << max << endl;

  for (long i = min;  i <= max;  i++)
  { // enusure not to call functions of Editlabel
    // that change the scrolling offset here
    EditLabel* line = lines_->item (i);
    if (i < from || i > to)  // outside range
      line->stopSelecting ();
    else if (i != from && i != to)  // inside range
      line->selectAll ();
    else if (sel)
    {
      if (i == from && i != to)  // first line
        line->cursorPosition (line->strlen (), 1);
      else if (i == to && i != from)  // last line
        line->cursorPosition (0, 1);
    }
  }

  scrollToCursor();
  notifyX();

  // caller responsible for redraw
} // cursorPosition


void TextBrowser::scrollToCursor()
{
  // scroll line
  if (cursorline_ <= box_->first_shown()) {  // scroll up
    box_->scrollOnTop(cursorline_);
  }
  else if (cursorline_ >= box_->last_shown()) { // scroll down
    box_->scrollOnBottom(cursorline_);
  }

  // todo: scroll column
  EditLabel* curlabel = lines_->item(cursorline_);
  curlabel->scrollToCursor();
  float offset = curlabel->getOffset();
  horoffset_ = offset;
  for (int i=0; i<numLines(); i++)
    lines_->item(i)->setOffset(offset);
}


static long labs (long l)  { return (l < 0) ? -l : l; }

void TextBrowser::attractMark (long line, int col)
{
  // make markline_ be nearer line than cursorline_

  if (labs (line - cursorline_) < labs (line - markerline_))
  {
    line = cursorline_;
    cursorline_ = markerline_;
    markerline_ = line;
    int temp = cursorcol_;
    cursorcol_ = markercol_;
    markercol_ = temp;
  }
  else if (line == cursorline_ && line == markerline_)
    lines_->item (line)->attractMark (col);
}


void TextBrowser::paste(SelectionManager* s)
{
  String* type;
  void* data;
  int nbyte, format;

  s->get_value(type, data, nbyte, format);

  if (nbyte <= 0) return;

  char* text = (char*) data;
  while (*text) {
    char* nlpos = strchr(text, '\n');
    if (nlpos) {
      int len = nlpos - text;
      text[len]= '\0';
      breakLine();
      lines_->item(cursorline_-1)->insertString(text, len);
      text[len] = '\n';
      text = nlpos + 1;
    }
    else {
      EditLabel* curlabel = lines_->item(cursorline_);
      curlabel->insertString(text, strlen(text));

      // update maxwidth_ and reallocate
      float width = curlabel->width();
      if (width > maxwidth_)
        maxwidth_ = width;

      cursorcol_ = curlabel->strlen();
      scrollToCursor();
      notifyX();
      break;
    }
  }

  reallocate();
  redraw();
}


void TextBrowser::copySelection (const Event& e)
{
  SelectionManager* s = e.display ()->primary_selection ();
  Resource::unref (copy_);
  if (lose_)
  { lose_->detach ();  // otherwise I'd lose my new selection
    lose_->unref ();
  }
  // offer a copy
  copy_ = new SelectionCallback(TextBrowser) (this, &TextBrowser::copy);
  Resource::ref (copy_);
  lose_ = new SelectionCallback(TextBrowser) (this, &TextBrowser::lose);
  Resource::ref (lose_);
  s->own (copy_, lose_);
}


void TextBrowser::copy (SelectionManager* s)  // do copy operation
{
//cerr << "TextBrowser::copy" << endl;

  long from, to;  // line no.
  if (cursorline_ < markerline_)
    from = cursorline_,  to = markerline_;
  else
    from = markerline_,  to = cursorline_;

  if (from == to)  // part of single line
  {
    EditLabel* line = lines_->item (from);
    int mark = line->markPosition ();
    int cursor = line->cursorPosition ();
    const char* data = line->string ();

    if (cursor < mark)
      s->put_value (data + cursor, mark - cursor);
    else
      s->put_value (data + mark, cursor - mark);
  }
  else  // several lines
  {
    EditLabel* line = lines_->item (from);  // tail of first line
    int len = line->strlen () - line->cursorPosition () + 1;  // with '\n'

    long i;
    for (i = from + 1;  i < to;  i++)  // lines in between
    { line = lines_->item (i);
      len += line->strlen () + 1;  // with '\n'
    }

    line = lines_->item (to);  // head of last line
    int completelastline = (line->cursorPosition () == line->strlen ());
    len += line->cursorPosition () + completelastline;  // poss. with '\n'

    char* data = new char [len + 1];  // strcpy adds '\0'...

    char* dptr = data;
    line = lines_->item (from);  // tail of first line
    strcpy (dptr, lines_->item (from)->string () + line->cursorPosition ());
    dptr += line->strlen () - line->cursorPosition ();
    *dptr++ = '\n';

    for (i = from + 1;  i < to;  i++)  // lines in between
    { line = lines_->item (i);
      strcpy (dptr, line->string ());
      dptr += line->strlen ();
      *dptr++ = '\n';
    }

    line = lines_->item (to);  // head of last line
    strncpy (dptr, line->string (), line->cursorPosition ());
    if (completelastline)
      dptr [line->cursorPosition ()] = '\n';

    //cerr << "[putting string with " << len << " byte(s) into clipboard]" << endl;
    s->put_value (data, len);

    delete data;
  }

} // copy


void TextBrowser::lose (SelectionManager*)  // losing selection
{
  stopSelecting ();
  redraw ();
}


void TextBrowser::stopSelecting ()
{
  long min, max;
  if (cursorline_ < markerline_)
    min = cursorline_, max = markerline_;
  else
    min = markerline_, max = cursorline_;

  for (long i = min; i <= max; i++)
    lines_->item(i)->stopSelecting();

  markerline_ = cursorline_;
  markercol_ = cursorcol_;
}


void TextBrowser::allocate (Canvas* c, const Allocation& a, Extension& e)
{
  InputHandler::allocate (c, a, e);
//   cerr << "got allocation. left: " << a.left () << ", right: " << a.right ()
//        << "; max. line width: " << maxwidth_ << endl;

  allwidth_ = a.right () - a.left ();

  // subtract space for vscrollbar
  Requisition vreq;
  vscrollbar_->request(vreq);
  allwidth_ -= vreq.x_requirement().natural();

  hadj_->scrollRange (allwidth_, maxwidth_ + 4.0);
  // magic 4.0 makes up for border of inset frame, sorry
}


void TextBrowser::press (const Event& e)
{
  if (numLines() == 0) return;

  dragged_ = false;

  Hit hit (&e);
  repick (0, hit);

  button_ = e.pointer_button ();

  if (hit.any ())
  {
    long li = hit.index (2);
    int index = (int) hit.index (3);
//cerr << "(hit character " << index << " of line " << li << ")";

    switch (button_)
    {
      case Event::left:  // set text cursor
      case Event::middle:
        cursorPosition (li, index, 0);  // move cursor (and mark)
      break;

      case Event::right:  // expand selection (at nearer endpoint)
        attractMark (li, index);
        cursorPosition (li, index, 1);  // move mark
      break;
    }

    redraw ();
  }

} // press


void TextBrowser::drag (const Event& e)
{
//cerr << ".";
  Hit hit (&e);
  repick (0, hit);

  if (hit.any ())
  {
    long li = hit.index (2);
    int index = (int) hit.index (3);
    long oldmarkline = markerline_;
    int oldmarkcol = markercol_;

    switch (button_)
    {
      case Event::left:  // expand selection
      case Event::right:
        cursorPosition (li, index, 1);  // move mark
        // TODO: equivalent for scrollToMark
        if (markerline_ != oldmarkline || markercol_ != oldmarkcol)
          redraw ();
      break;
      // TODO: move text around with middle mouse button
    }
  }

  dragged_ = true;
} // drag


void TextBrowser::release (const Event& e)
{
//cerr << " release (" << button_ << ")" << endl;
  if (button_ == Event::middle) {
    if (!dragged_) {
      pasteSelection(e);
      redraw();
    }
  }
  else {
    if (dragged_)
      copySelection (e);
  }
}


void TextBrowser::double_click (const Event& e)
{
//cerr << "double click" << endl;
  Hit hit (&e);
  repick (0, hit);
  static Coord oldx = -1;
  static Coord oldy = -1;
  Coord newx = e.pointer_x ();
  Coord newy = e.pointer_y ();

  if (hit.any () && button_ == Event::left)
  {
    int li = (int) hit.index (2);
    int index = (int) hit.index (3);

    EditLabel* line = lines_->item (li);
    if (newx == oldx && newy == oldy)
      line->selectAll ();
    else
      line->selectWord (index);
    redraw ();
  }
  oldx = newx;
  oldy = newy;
} // double_click


void TextBrowser::keystroke (const Event& e)
{
  unsigned long keysym = e.keysym ();
  int shift = e.shift_is_down ();
  int ctrl = e.control_is_down ();

// TODO: home/end/ctrl-home/ctrl-end

  switch (e.keysym ())
  {
    case XK_Tab:
      if (shift)
        prev_focus ();
      else
        next_focus ();
    return;

    // vertical scrolling
    // the scrollbox has its minimum at the lower and the maximum at the upper end
    case XK_Up:
      box_->scroll_forward (Dimension_Y);
    break;
    case XK_Down:
      box_->scroll_backward (Dimension_Y);
    break;
    case XK_Prior:
      box_->page_forward (Dimension_Y);
    break;
    case XK_Next:
      box_->page_backward (Dimension_Y);
    break;

    // horicontal scrolling
    case XK_Left:
      if (ctrl)
        hadj_->page_backward (Dimension_X);
      else
        hadj_->scroll_backward (Dimension_X);
    break;
    case XK_Right:
      if (ctrl)
        hadj_->page_forward (Dimension_X);
      else
        hadj_->scroll_forward (Dimension_X);
    break;
  }

  redraw ();

} // keystroke


// should have visual cue for focus!
