//
// "$Id: fl_file_chooser.cxx,v 1.10 1999/01/07 21:22:28 mike Exp $"
//
// File chooser widget for the Fast Light Tool Kit (FLTK).
//
// Copyright 1998-1999 by Bill Spitzak and others.
//
// This library is free software; you can redistribute it and/or
// modify it under the terms of the GNU Library General Public
// License as published by the Free Software Foundation; either
// version 2 of the License, or (at your option) any later version.
//
// This library 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
// Library General Public License for more details.
//
// You should have received a copy of the GNU Library General Public
// License along with this library; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
// USA.
//
// Please report all bugs and problems to "fltk-bugs@easysw.com".
//

#include <config.h>
#include <FL/fl_file_chooser.H>

#include <FL/Fl.H>
#include <FL/Fl_Window.H>
#include <FL/Fl_Box.H>
#include <FL/Fl_Button.H>
#include <FL/Fl_Return_Button.H>
#include <FL/Fl_Browser_.H>
#include <FL/Fl_Input.H>
#include <FL/fl_draw.H>
#include <FL/filename.H>

#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <ctype.h>

static void default_callback(const char*) {}
static void (*current_callback)(const char*) = default_callback;
void fl_file_chooser_callback(void (*cb)(const char*)) {
  current_callback = cb ? cb : default_callback;
}

// "File Chooser Browser" widget:
class FCB : public Fl_Browser_ {
  void* item_first() const ;
  void* item_next(void*) const ;
  void* item_prev(void*) const ;
  int item_height(const dirent*, int) const ;
  int item_height(void*) const ;
  int item_width(const dirent*) const ;
  int item_width(void*) const ;
  int item_quick_height(void*) const ;
  int incr_height() const ;
  void item_draw(void*, int, int, int, int) const ;
  int checkdir(const dirent*, char*) const ;
  void draw();
  void clear_prev();
public:
  char listed[FL_PATH_MAX];// current dir & starname
  int dirend;		// points after last / before starname
  int nameend;		// length to trailing '*' or '\0'
  const char* pattern;	// default pattern
  dirent** list;	// the file names
  dirent** last;	// pointer after end of list
  const char* message;	// message if no file names
  char preved[FL_PATH_MAX];// directory listed in prev
  dirent** prev;	// cached list of another directory
  dirent** prev_last;	// end of that list
  int prev_count;
  FCB(int x, int y, int w, int h) : Fl_Browser_(x, y, w, h, 0) {
    type(FL_HOLD_BROWSER);
    listed[0] = 0;
    dirend = nameend = 1;
    pattern = 0;
    list = prev = 0;
    message = 0;
  }
  // ~FCB nyi
  void clear();
  void set(const char*);
  int get(char*);
};

// "File Chooser Window" widget:
class FCW : public Fl_Window {
public:
  int handle(int);
  Fl_Input input;
  Fl_Button* ok_button;
  Fl_Button* cancel_button;
  Fl_Button* normal_button;
  FCB browser;
  FCW();
};

/* Files are marked as being directories by replacing the trailing null
   with a '/' if it is a directory, a '\001' if it is *not* a directory.
   An item has height (and is thus selectable) if it is either a directory
   or if it matches the pattern.  Quick-height assummes all unknown files
   are directories, and thus saves the time needed to do a stat().
*/

// return pointer to last character:
static const char* end_of_name(const dirent* d) {
#if HAVE_DIRENT_H
  const char* e;
  for (e = d->d_name; ;e++) switch (*e) {
  case 0: case 1: case '/': return e;
  }
#else
  // warning: clobbers byte after end of name
  return d->d_name + d->d_namelen;
#endif
}

// return true if item is directory, when given pointer to last character:
int FCB::checkdir(const dirent* d, char* e) const {
  if (*e == 1) return 0;
  if (*e == '/') return 1;
  char buf[FL_PATH_MAX];
  memcpy(buf, listed, dirend);
  memcpy(buf+dirend, d->d_name, e-d->d_name);
  *(buf+dirend+(e-d->d_name)) = 0;
  if (filename_isdir(buf)) {
    *e = '/'; return 1;
  } else {
    *e = 1; return 0;
  }
}

void* FCB::item_first() const {return list;}

void* FCB::item_next(void* p) const {
  if ((dirent**)p+1 >= last) return 0;
  return (dirent**)p+1;
}

void* FCB::item_prev(void* p) const {
  if ((dirent**)p <= list) return 0;
  return ((dirent**)p)-1;
}

static int ido_matching(const dirent* p, const char* e, const char* n) {
  // replace / or 1 at end with 0 and do match, then put back.  yukko
  int save = *e; *(char*)e = 0;
  int r = filename_match(p->d_name, n);
  *(char*)e = save;
  return(r);
}

int FCB::incr_height() const {return textsize()+2;}

int FCB::item_height(const dirent* p, int slow) const {
  const char* e = end_of_name(p);
  if (listed[dirend]) {
//  if (p->d_name[0]=='.' && listed[dirend]!='.') return 0;
    if (listed[nameend-1]=='/') {
      if (slow ? !checkdir(p, (char*)e) : *e==1) return 0;
      ((char*)listed)[nameend-1] = 0;
      int r = ido_matching(p, e, listed+dirend);
      ((char*)listed)[nameend-1] = '/';
      if (!r) return 0;
    } else {
      if (!ido_matching(p, e, listed+dirend)) return 0;
    }
  } else {
    if (p->d_name[0]=='.') return 0;
    if (pattern && (slow ? !checkdir(p, (char*)e) : *e==1) &&
	!ido_matching(p, e, pattern)) return 0;
  }
  return textsize()+2;
}

int FCB::item_height(void* x) const {
  return item_height(*(const dirent**)x, 1);
}

int FCB::item_quick_height(void* x) const {
  return item_height(*(const dirent**)x, 0);
}

void FCB::item_draw(void* v, int x, int y, int, int h) const {
  const dirent* p = *(const dirent**)v;
  const char* e = end_of_name(p);
  if (checkdir(p, (char*)e)) e++;
  if (v == selection()) fl_color(contrast(textcolor(), selection_color()));
  else fl_color(textcolor());
  fl_font(textfont(), textsize());
  fl_draw(p->d_name, e-p->d_name, x+4, y+h-3);
}

int FCB::item_width(const dirent* p) const {
  const char* e = end_of_name(p); if (*e == '/') e++;
  fl_font(textfont(), textsize());
  return (int)fl_width(p->d_name, e-p->d_name)+4;
}

int FCB::item_width(void* x) const {
  return item_width(*(const dirent**)x);
}

// "get" the current value by copying the name of the selected file
// or if none are selected, by copying as many common letters as
// possible of the matched file list:
int FCB::get(char* buf) {
  dirent** q = (dirent**)selection(); // the file to copy from
  int n = 0;	// number of letters
  if (q) {	// a file is selected
    const char* e = end_of_name(*q);
    n = e - (*q)->d_name;
    if (*e == '/') n++;
  } else {	// do filename completion
    for (q = list; q < last && !item_height(*q, 0); q++);
    if (q < last) {
      const char* e = end_of_name(*q);
      n = e - (*q)->d_name;
      if (*e == '/') n++;
      for (dirent** r = q+1; n && r < last; r++) {
	if (!item_height(*r, 0)) continue;
	int i;
	for (i=0; i<n && (*q)->d_name[i]==(*r)->d_name[i]; i++);
	n = i;
      }
    }
  }
  if (n) {
    memcpy(buf, listed, dirend);
    memcpy(buf+dirend, (*q)->d_name, n);
    buf[dirend+n]=0;
  }
  return n;
}

// "set" the current value by changing the directory being listed and
// changing the highlighted item, if possible:
void FCB::set(const char* buf) {

  int bufdirend;
  int ispattern = 0;
  const char* c = buf;
  for (bufdirend=0; *c;) switch(*c++) {
  case '?': case '[': case '*': case '{': ispattern = 1; goto BREAK;
#if defined(WIN32) || defined(__EMX__)
  case '\\':
#endif
  case '/': bufdirend=c-buf; break;
  }
#if defined(WIN32) || defined(__EMX__)
  if ((!bufdirend) && isalpha(buf[0]) && (buf[1]==':')) bufdirend = 2;
#endif
 BREAK:
  int bufend = strlen(buf);
  if (bufend<=bufdirend) ispattern = 1;

  // if directory is different, change list to xxx/ :
  if (bufdirend != dirend || strncmp(buf, listed, bufdirend)) {
    if (prev &&
	preved[bufdirend]==0 && !strncmp(buf, preved, bufdirend)) {
      strcpy(preved, listed); preved[dirend] = 0;
      dirent** t;
      t = prev; prev = list; list = t;
      t = prev_last; prev_last = last; last = t;
      strcpy(listed, buf);
      dirend = nameend = bufdirend;
      message = 0;
    } else {
      if (list) {
	clear_prev();
	strcpy(preved, listed); preved[dirend]=0;
	prev = list;
	prev_last = last;
      }
      list = last = 0;
      message = "reading..."; redraw(); Fl::flush(); redraw();
      strcpy(listed, buf);
      dirend = nameend = bufdirend;
      listed[dirend] = listed[dirend+1] = 0;
      int n = filename_list(dirend ? listed : ".", &list);
      if (n < 0) {
	if (errno==ENOENT) message = "No such directory";
	else message = strerror(errno);
	n = 0; list = 0;
      } else message = 0;
      last = list+n;
    }
    if (list && last <= list+2) message = "Empty directory";
    new_list();
  }

  dirent** q = 0; // will point to future selection
  int any = 0; // true if any names shown

  // do we match one item in the previous list?
  if (!ispattern && bufend >= nameend) {
    for (q = list; ; q++) {
      if (q >= last) {q = 0; break;}
      if (item_height(*q, 0)==0) continue;
      any = 1;
      const char* a = (*q)->d_name;
      const char* b = buf+bufdirend;
#ifdef WIN32
      while (*b && tolower(*a)==tolower(*b)) {a++; b++;}
#else
      while (*b && *a==*b) {a++; b++;}
#endif
      if (!*b && (*a==0 || /* *a=='/' ||*/ *a==1)) break;
    }
  }

  // no, change the list pattern to the new text + a star:
  if (!q) {
    strcpy(listed+dirend, buf+bufdirend);
    nameend = bufend;
    if (!ispattern) {listed[nameend]='*'; listed[nameend+1]=0;}
    any = 0;
    // search again for an exact match:
    for (q = list; ; q++) {
      if (q >= last) {q = 0; break;}
      if (item_height(*q, 0)==0) continue;
      any = 1;
      const char* a = (*q)->d_name;
      const char* b = buf+bufdirend;
#ifdef WIN32
      while (*b && tolower(*a)==tolower(*b)) {a++; b++;}
#else
      while (*b && *a==*b) {a++; b++;}
#endif
      if (!*b && (*a==0 || /* *a=='/' ||*/ *a==1)) break;
    }
    new_list();
  }

  if (any) message = 0;
  else if (!message) message = "No matching files";
  select_only(q);
  if (q) current_callback(buf);
}

void FCB::draw() {
  if (!message) {
    Fl_Browser_::draw();
    if (full_height() > 0) return;
    message = "No matching files";
  }
  Fl_Boxtype b = box(); if (!b) b = FL_DOWN_BOX;
  draw_box(b,color());
  fl_color(FL_INACTIVE_COLOR);
  fl_font(textfont(), textsize());
  fl_draw(message, x()+7, y()+3, w(), h()-3, FL_ALIGN_TOP_LEFT);
  // insure scrollbars are redrawn if error message goes away:
  scrollbar.redraw();
  hscrollbar.redraw();
}

void FCB::clear_prev() {
  if (prev) {
    for (dirent**p=prev_last-1; p>=prev; p--) free((void*)*p);
    free((void*)prev);
    prev = prev_last = 0;
  }
}

void FCB::clear() {
  if (list) {
    for (dirent**p=last-1; p>=list; p--) free((void*)*p);
    free((void*)list);
    list = last = 0;
  }
  clear_prev();
  listed[0] = 0; dirend = 1;
}

////////////////////////////////////////////////////////////////

static void fcb_cb(Fl_Widget*, void* v) {
  FCW* w = (FCW*)v;
  char buf[FL_PATH_MAX];
  if (w->browser.get(buf)) {
    w->input.value(buf);
    w->input.position(10000);
//  w->input.position(10000, w->browser.dirend);
    if (Fl::event_button()==1) {
      if (Fl::event_clicks()) w->ok_button->do_callback();
      else w->browser.set(buf);
    } else {
      current_callback(buf);
    }
  }
}

static void tab_cb(Fl_Widget*, void* v) {
  FCW* w = (FCW*)v;
  char buf[FL_PATH_MAX];
  if (w->browser.get(buf)) {
    w->input.value(buf);
    w->input.position(10000);
    w->browser.set(buf);
  }
}

#if defined(WIN32) || defined(__EMX__)
// ':' needs very special handling!
static inline int isdirsep(char c) {return c=='/' || c=='\\';}
#else
#define isdirsep(c) ((c)=='/')
#endif

static void input_cb(Fl_Widget*, void* v) {
  FCW* w = (FCW*)v;
  const char* buf = w->input.value();
  char localbuf[FL_PATH_MAX];
  if (buf[0] && isdirsep(buf[w->input.size()-1])
      && filename_expand(localbuf, buf)) {
    buf = localbuf;
    w->input.value(localbuf);
    w->input.position(10000);
  }
  w->browser.set(buf);
}

static void up_cb(Fl_Widget*, void* v) { // the .. button
  FCW* w = (FCW*)v;
  char* p;
  char* newname;
  char buf[FL_PATH_MAX];
  p = w->browser.listed+w->browser.dirend-1; // point right before last '/'
  if (p < w->browser.listed)
    newname = "../"; // go up from current directory
  else {
    for (; p>w->browser.listed; p--) if (isdirsep(*(p-1))) break;
    if (isdirsep(*p) || *p=='.' &&
	(isdirsep(p[1]) || p[1]=='.' && isdirsep(p[2]))) {
      p = w->browser.listed+w->browser.dirend;
      memcpy(buf, w->browser.listed, p-w->browser.listed);
      strcpy(buf+(p-w->browser.listed), "../");
    } else {
      memcpy(buf, w->browser.listed, p-w->browser.listed);
      buf[p-w->browser.listed] = 0;
    }
    newname = buf;
  }
  w->input.value(newname);
  w->input.position(10000);
  w->browser.set(newname);
}

static void dir_cb(Fl_Widget* obj, void* v) { // directory buttons
  FCW* w = (FCW*)v;
  const char* p = obj->label(); if (*p=='&') p++;
  char buf[FL_PATH_MAX];
  char* q; for (q=buf; *p && *p!=' '; *q++ = *p++); *q = 0;
  filename_expand(buf, buf);
  w->input.value(buf);
  w->input.position(10000);
  w->browser.set(buf);
}

static void working_cb(Fl_Widget*, void* v) { // directory buttons
  FCW*w = (FCW*)v;
  char buf[FL_PATH_MAX];
  filename_absolute(buf, "");
  w->input.value(buf);
  w->input.position(10000);
  w->browser.set(buf);
}

static void files_cb(Fl_Widget* obj, void* v) { // file pattern buttons
  FCW* w = (FCW*)v;
  char buf[FL_PATH_MAX];
  strcpy(buf, w->input.value());
  char* q = buf+w->browser.dirend;
  if (obj != w->normal_button) {	// tack on first word of label
    const char* p = obj->label(); if (*p=='&') p++;
    for (; *p && *p!=' '; *q++ = *p++);
  }
  *q = 0;
  w->input.value(buf);
  w->input.position(10000, w->browser.dirend);
  w->browser.set(buf);
}

/*----------------------- The Main Routine ----------------------*/
#define HEIGHT_BOX	(4*WIDTH_SPC+HEIGHT_BUT+HEIGHT_INPUT+HEIGHT_BROWSER)
#define HEIGHT_BUT	25
#define HEIGHT_INPUT	30
#define HEIGHT_BROWSER	(9*HEIGHT_BUT+2) // must be > buttons*HEIGHT_BUT
#define WIDTH_BOX	(3*WIDTH_SPC+WIDTH_BUT+WIDTH_BROWSER)
#define WIDTH_BROWSER	350
#define WIDTH_BUT	125
#define WIDTH_OK	70
#define WIDTH_SPC	5

int FCW::handle(int event) {
  if (Fl_Window::handle(event)) return 1;
  if (event==FL_KEYBOARD && Fl::event_key()==FL_Tab) {
    tab_cb(this, this);
    return 1;
  }
  return 0;
}

// set this to make extra directory-jumping button:
const char* fl_file_chooser_button;
extern const char* fl_ok;
extern const char* fl_cancel;

FCW::FCW() : Fl_Window(WIDTH_BOX, HEIGHT_BOX),
	input(WIDTH_SPC, HEIGHT_BOX-HEIGHT_BUT-2*WIDTH_SPC-HEIGHT_INPUT,
	      WIDTH_BOX-2*WIDTH_SPC, HEIGHT_INPUT, 0),
	browser(2*WIDTH_SPC+WIDTH_BUT, WIDTH_SPC,
		WIDTH_BROWSER, HEIGHT_BROWSER)
{
  int but_y = WIDTH_SPC;
  input.callback(input_cb, this);
  input.when(FL_WHEN_CHANGED);
  //  add(browser);
  browser.callback(fcb_cb, this);

  begin();
  Fl_Widget* obj;
  obj = ok_button = new Fl_Return_Button(
    WIDTH_BOX-2*(WIDTH_SPC+WIDTH_OK), HEIGHT_BOX-WIDTH_SPC-HEIGHT_BUT,
    WIDTH_OK, HEIGHT_BUT, fl_ok);
  obj = cancel_button = new Fl_Button(
    WIDTH_BOX-WIDTH_SPC-WIDTH_OK, HEIGHT_BOX-WIDTH_SPC-HEIGHT_BUT,
    WIDTH_OK, HEIGHT_BUT, fl_cancel);
  cancel_button->shortcut("^[");

  obj=new Fl_Button(WIDTH_SPC,but_y,WIDTH_BUT,HEIGHT_BUT, "&Up one directory");
  obj->callback(up_cb, this);
  but_y += HEIGHT_BUT;

  obj = new Fl_Button(WIDTH_SPC, but_y, WIDTH_BUT, HEIGHT_BUT, "&~/ Home");
  obj->callback(dir_cb, this);
  but_y += HEIGHT_BUT;

  obj = new Fl_Button(WIDTH_SPC, but_y, WIDTH_BUT, HEIGHT_BUT, "&/ Root");
  obj->callback(dir_cb, this);
  but_y += HEIGHT_BUT;

  obj=new Fl_Button(WIDTH_SPC, but_y, WIDTH_BUT, HEIGHT_BUT, "&Current dir");
  obj->callback(working_cb, this);
  but_y += HEIGHT_BUT;

  if (fl_file_chooser_button) {
    obj=new Fl_Button(WIDTH_SPC,but_y,WIDTH_BUT,HEIGHT_BUT,fl_file_chooser_button);
    obj->callback(dir_cb, this);
    but_y += HEIGHT_BUT;
  }

  normal_button = new Fl_Button(WIDTH_SPC, but_y, WIDTH_BUT, HEIGHT_BUT, "");
  normal_button->callback(files_cb, this);
  but_y += HEIGHT_BUT;

  obj = new Fl_Button(WIDTH_SPC,but_y, WIDTH_BUT, HEIGHT_BUT, "* &All files");
  obj->callback(files_cb, this);
  but_y += HEIGHT_BUT;

  obj = new Fl_Button(WIDTH_SPC,but_y,WIDTH_BUT,HEIGHT_BUT, ". &Hidden files");
  obj->callback(files_cb, this);
  but_y += HEIGHT_BUT;

  obj = new Fl_Button(WIDTH_SPC,but_y,WIDTH_BUT,HEIGHT_BUT, "*/ &Directories");
  obj->callback(files_cb, this);
  but_y += HEIGHT_BUT;

  resizable(new Fl_Box(browser.x(), but_y,
		       cancel_button->x()-browser.x(),
		       browser.y()+browser.h()-but_y));
  // add(input); // put last for better draw() speed
  end();
  set_modal();
}

char* fl_file_chooser(const char* message, const char* pat, const char* fname)
{
  static FCW* f; if (!f) f = new FCW();
  f->ok_button->label(fl_ok);
  f->cancel_button->label(fl_cancel);

  if (pat && !*pat) pat = 0;
  if (fname && *fname) {
    f->input.value(fname);
  } else if (f->browser.pattern != pat && (!pat || !f->browser.pattern ||
					   strcmp(pat,f->browser.pattern))) {
    // if pattern is different, remove name but leave old directory:
    const char* p = f->input.value();
    const char* q = filename_name(p);
    f->input.value(p, q-p);
  }
  f->browser.pattern = pat;
  f->normal_button->label(pat ? pat : "visible files");
  f->browser.set(f->input.value());
  f->input.position(10000, f->browser.dirend);

  f->label(message);
  f->hotspot(f);
  f->show();
  int ok = 0;
  for (;;) {
    Fl::wait();
    Fl_Widget* o = Fl::readqueue();
    if (o == f->ok_button) {ok = 1; break;}
    else if (o == f->cancel_button || o == f) break;
  }
  f->hide();
  f->browser.clear();

  if (!ok) return 0;
  const char* r = f->input.value();
  const char* p;
  for (p=r+f->browser.dirend; *p; p++)
    if (*p=='*' || *p=='?' || *p=='[' || *p=='{') return 0;
  return (char*)r;
}

//
// End of "$Id: fl_file_chooser.cxx,v 1.10 1999/01/07 21:22:28 mike Exp $".
//
