/* ==================================================== ======== ======= *
 *
 *  ufilebx.cc
 *  Ubit Project  [Elc][beta1][2001]
 *  Author: Elc + Bruno Andrillon & Aline chevrel
 *
 *  Part of the Ubit Toolkit: A Brick Construction Game Model for Creating GUIs
 *
 *  (C) 1999-2001 Eric Lecolinet @ ENST Paris
 *  WWW: http://www.enst.fr/~elc/ubit   Email: elc@enst.fr (subject: ubit)
 *
 * ***********************************************************************
 * COPYRIGHT NOTICE : 
 * THIS PROGRAM IS DISTRIBUTED WITHOUT ANY WARRANTY AND WITHOUT EVEN THE 
 * IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. 
 * 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.
 * SEE FILES 'COPYRIGHT' AND 'COPYING' FOR MORE DETAILS.
 * ***********************************************************************
 *
 * ==================================================== [Elc:01] ======= *
 * ==================================================== ======== ======= */

//pragma ident	"@(#)uufilebox.cc	ubit:b1.7.0"

#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>  
#include <dirent.h>
#include <time.h>

#include <ubrick.hh>
#include <ucolor.hh>
#include <ustr.hh>
#include <upix.hh>
#include <uborder.hh>
#include <ucall.hh>
#include <ubox.hh>
#include <uwin.hh>
#include <uscroll.hh>
#include <uview.hh>
#include <ugadgets.hh>
#include <ustyle.hh>
#include <uselect.hh>
#include <ufilebox.hh>
#include <ucall.hh>
#include <usymbol.hh>

#include <uobs.hh>

static const int LINE_COUNT = 10;
static const int DIR_QUANTUM = 100;

const UClass UFilebox::uclass("UFilebox");
const UClass UFileDialog::uclass("UFileDialog");

UFilebox& ufilebox(UArgs l) {return *(new UFilebox(l));}
UFileDialog& ufileDialog(UArgs l) {return *(new UFileDialog(l));}

/* ==================================================== [Elc:01] ======= */
/* ==================================================== ======== ======= */

UFilebox::~UFilebox() {
  // other objects are implicitely deleted as they are filebox children
  if (fpath) delete fpath;
}

UFilebox::UFilebox(UArgs a) : UBox(a) {
  viewport = null;
  //ex: setCmodes(UMode::CAN_CLOSE_MENU, false);
  setCmodes(UMode::HAS_AUTOCLOSE_BHV, true);
  setCmodes(UMode::AUTOCLOSE_BHV, false);

  fdir   = &ustr("./");  // => scan du directory courant
  fname  = &ustr("");
  ffilter= &ustr("");
  fpath  = &ustr("");

  UButton &ok = 
    ubutton(UPix::ray + UFont::bold + "OK" 
	    + UOn::action / ucall(this,&UFilebox::OkBehavior));
  ok.setCmodes(UMode::HAS_AUTOCLOSE_BHV|UMode::AUTOCLOSE_BHV, true); 

  UButton &cancel = 
    ubutton(UPix::cross + "Cancel"
	    + UOn::action / ucall(this,&UFilebox::CancelBehavior));
  cancel.setCmodes(UMode::HAS_AUTOCLOSE_BHV|UMode::AUTOCLOSE_BHV, true); 

  show_attributes = 
    &ubutton(UMode::canSelect  + UOn::select/UBgcolor::orange
	     + UPix::eyes
	     + UOn::action / ucall(this,&UFilebox::rescan)
	     );
  show_dotfiles = 
    &ubutton(UMode::canSelect  + UOn::select/UBgcolor::orange
	     + UPix::rball
	     + UOn::action / ucall(this,&UFilebox::rescan)
	     );
  show_dirs_only = 
    &ubutton(UMode::canSelect + UOn::select/UBgcolor::orange
	     + UPix::folder
	     + UOn::action / ucall(this,&UFilebox::rescan)
	     );

  UBox &tbar = ubar
    (
     uleft() + uhspacing(4) + " "
     + ubutton(uvcenter() + UPix::raise   // PARENT Dir
	       + UOn::action / ucall(this,&UFilebox::changeDir, &ustr(".."))
	       )
     + ubutton(uvcenter() + UPix::right   // HOME Dir
	       //+ UOn::action / uset(fdir, &ustr(".")))
	       + UOn::action / ucall(this, &UFilebox::setDir, 
				     const_cast<const UStr*>(&ustr("./")))
	       )
     + uhflex()
     + utextbox(UPix::ofolder + uedit() + " " + uhflex()
		+ fdir //CURRENT Dir
		//ca va pas: on reappellerait rescan a tort et a travers
		// des qu'on reformatte fdir
		//+ ucall(this, &UFilebox::rescan, UOn::change)
		+ UOn::ktype / ucall(this, &UFilebox::rescan)
		)
     + uright()
     + show_attributes
     + show_dotfiles
     + show_dirs_only
     );

  // horizontal scrollbar only
  scrollpane = &uscrollpane
    (
     //UScrollbar::never, UScrollbar::always,
     uwidth(400) + uheight(UHeight::keepSize)
     + UBorder::etchedIn
     + UBgcolor::white
     // les add() ulterieurs placeront
     // le Viewport en zone centrale
     + uvcenter() + uhcenter()
    );

  /**** pas flexible!
  UTable &controls = utable
    (
     uhflex() +
     utrow( uhflex() +utcell("Name:")
	   + utcell(uhflex() +utextbox(uhflex() +uedit() + fname))
	   + utcell(uhflex() +ok)
	   )
     + utrow( uhflex() +utcell("Filter:")
	   + utcell(uhflex() +utextbox(uhflex() +uedit() + ffilter))
	   + utcell(uhflex() +cancel)
	   )
     );
  ***/

  UBox &controls = uhbox
    (
     uvflex()
     + uvbox(UValign::bottom
	     + ulabel("File Name:")
	     + ulabel("Extension:")
	     )
     + uhflex() 
     + uvbox(uvspacing(3)
	     + utextbox(uedit() + fname 
			+ UOn::action / ucall(this,&UFilebox::OkBehavior))
	     + utextbox(uedit() + ffilter
			//marche plus: bug textf + UOn::change / ucall(this,&UFilebox::rescan))
			+ UOn::krelease / ucall(this,&UFilebox::rescan))
	     )
     + uright()
     + uvbox(ok + cancel)
     );

  // add to the Filebox 
  addlist
    (
     UOrient::vertical + uhmargin(4) + uvmargin(3) + uvspacing(4)
     + uhflex()
     + utop() + tbar
     + uvflex()  + scrollpane
     + ubottom() + controls
     );
  
  // sert a calcluer la taille verticale du scrollpane (on lui ajoute
  // LINE_COUNT lignes
  // horizontale, et comme il est defini:  uheight(UHeight::keepSize)
  // sa taille verticale ne variera pas ensuite

  viewport = &uhbox(UBgcolor::white
		    + uhmargin(3) + uvmargin(3)
		    + uhspacing(3)+ uvspacing(3)
		    + "     ");

  for (int k = 0; k < LINE_COUNT; k++)
    viewport->add(uline(UPix::doc + " "));
  scrollpane->add(viewport);

  rescan();   //devrait etre a l'apparition du dialog
}


/* ==================================================== [Elc:01] ======= */
/* ==================================================== ======== ======= */

struct D_ENTRY {
  UStr   *name;
  u_bool is_dir;
  UBrick *icon;
  UStr   *attr;
};

static u_bool init_entry(D_ENTRY &entry, struct stat &statbuf, char *dname, 
			  const char* s_filter, u_bool want_attributes) {

  entry.name = new UStr(dname);
  entry.is_dir = false;
  entry.icon = null;
  entry.attr = null;
  char modes[11];

  if (S_ISDIR(statbuf.st_mode)) {       // directory
    entry.is_dir = true;
    entry.icon = &UPix::folder;
    modes[0] = 'd';
  }

  else if (S_ISREG(statbuf.st_mode)) {   // regular file
    if (statbuf.st_mode & (S_IXUSR | S_IXGRP | S_IXOTH))
      entry.icon = &UPix::ray;     // si executable
    else entry.icon = &UPix::doc;  // si pas executable
    modes[0] = '-';
  }

  else if (S_ISLNK(statbuf.st_mode)) {     // link
    entry.icon = &USymbol::right;
    modes[0] = 'l';
  }

  else {
    entry.icon = &UPix::question;	 // whatever
    if (statbuf.st_mode & S_IFIFO) //fifo (or "named pipe") special file
      modes[0] = 'p';
    else if (statbuf.st_mode & S_IFCHR) //character special file;
      modes[0] = 'c';
    else if (statbuf.st_mode & S_IFBLK) //block special file;
      modes[0] = 'b';
    else   // reste:  D (door) et s (AF_UNIX address family socket)
      modes[0] = '?';
  }

  if (want_attributes) {
    modes[1] = (statbuf.st_mode & S_IRUSR) ? 'r' : '-';
    modes[2] = (statbuf.st_mode & S_IWUSR) ? 'w' : '-';
    modes[3] = (statbuf.st_mode & S_IXUSR) ? 'x' : '-';

    modes[4] = (statbuf.st_mode & S_IRUSR) ? 'r' : '-';
    modes[5] = (statbuf.st_mode & S_IWUSR) ? 'w' : '-';
    modes[6] = (statbuf.st_mode & S_IXGRP) ? 'x' : '-';

    modes[7] = (statbuf.st_mode & S_IRUSR) ? 'r' : '-';
    modes[8] = (statbuf.st_mode & S_IWUSR) ? 'w' : '-';
    modes[9] = (statbuf.st_mode & S_IXOTH) ? 'x' : '-';

    modes[10] = 0; //!!


    //que sous Solaris: cftime(s_date, "%h/%d/%y %H:%M", &statbuf.st_mtime);
    //struct tm *gmtime(const time_t *timep);  
    char s_date[30];
    struct tm *timebuf = localtime(&statbuf.st_mtime);
    strftime(s_date, sizeof(s_date), "%h/%d/%Y %H:%M", timebuf);

    char s_attr[200];
    sprintf(s_attr, "%s %9ld %s ", modes, statbuf.st_size, s_date);
    entry.attr = new UStr(s_attr);

    /* reste
	    statbuf.st_uid, 
	    statbuf.st_gid, 
    */
  }
  return true;
}

/* ==================================================== ======== ======= */

static int compare_entries(const void *p1, const void *p2) {
  D_ENTRY *pe1 = (D_ENTRY*)p1;
  D_ENTRY *pe2 = (D_ENTRY*)p2;
  return strcmp(pe1->name->chars(), pe2->name->chars());
}

/* ==================================================== ======== ======= */
// separer le chemin et du repertoire et le reste (remain) qui
// servira de filtre
// -- reduit s_dir si necessaire : enleve filter et / final
// -- renvoie nouvelle longueur de s_dir
// -- met le filter dans *sremain si sremain!= null
//    (il faudra faire un free de *sremain si != null)

static int separ(char *s_dir, char **s_remain) {
  int l_dir = strlen(s_dir);
  if (s_remain) *s_remain = null;

  //chercher si dir se termine par un / (et l'enlever)
  if (s_dir[l_dir-1] == '/') {
    if (l_dir > 1) {   // ne pas enlever le / de la racine
      s_dir[l_dir-1] = 0;  //securite pour opendir() si / final pas admis
      l_dir--;
    }
  }
  else {  // chercher le / du parent
    char *p = strrchr(s_dir, '/');
    //s_remain = ce qui reste apres le dernier /
    if (p) {
      if (s_remain) *s_remain = u_strdup(p+1);
      //!att: ne pas enlever le / de la racine
      if (p > s_dir) *p = 0;
      else *(p+1) = 0;
      l_dir = strlen(s_dir);  // la taille a change!
    }
  }

  return l_dir;
}

/* ==================================================== [Elc:01] ======= */
/* ==================================================== ======== ======= */
// new_path is relative except if starting by a /
// cette fonction reinitialise fdir sauf si arg null ou == fdir

void UFilebox::changeDir(UStr *argpath_str) {  
  const char* argpath = argpath_str ? argpath_str->chars() : null;

  if (argpath && argpath[0] == '/') {      // new_path is absolute
    setDir(argpath_str);
  }
  else {			// new_path is relative
    char *newpath = fdir->get();
    if (!newpath) newpath = u_strdup("/"); // securite
    
    if (!argpath || !argpath[0])
      /*nop*/;
    else if (strcmp(argpath, "..")==0 || strcmp(argpath, "../")==0) {
      // trouver le path du parent (et skip du dernier /)
      int ll = strlen(newpath);
      char *pc = null;
      if (ll > 0) {
	newpath[ll-1] = 0;
	pc = strrchr(newpath, '/');
      }
      if (pc) *(pc+1) = '\0';       // garder le /
      else strcpy(newpath, "/");    // racine
    }

    else { //ajouter le subdirectory
      //concatener (mais apres avoir enleve un filtre eventuel)

      int ll = separ(newpath, null);

      char *pc = (char*)malloc(ll + strlen(argpath) + 4);
      sprintf(pc, "%s/%s/", newpath, argpath);
      free(newpath);
      newpath = pc;
    }
    
    // ne pas oublier de remettre a jour fdir
    fdir->set(newpath);
    free(newpath);
    rescan();
  }
}

/* ==================================================== ======== ======= */

static void parse_dir(UStr *&fdir, char *&s_dir, int &l_dir,
		      char *&s_remain) {

  // FAUDRAIT VIRER LES BLANCS au debut et a la fin !!!!!

  if (!fdir) {   //securite (ne doit jamais arriver)
    fdir = new UStr();
    fdir->set("./");
  }

  if (!fdir->chars() || !*fdir->chars()) {
    fdir->set("/");
  }

  else if (fdir->equals(".") || fdir->equals("./")) {
    //NB: ne PAS faire de free() sur un getenv!
    char *pwd = getenv("PWD");
    if (pwd) {
      fdir->set(pwd);
      fdir->append("/");
    }
    else fdir->set("/");
  }

  s_dir = fdir->get();  //copie
  s_remain = null;
  l_dir = separ(s_dir, &s_remain);
}

/* ==================================================== ======== ======= */
//NOTE: la fonction Unix 'scandir' n'est pas standard (que BSD)!

static int my_scandir(UStr *&fdir, const char *s_filter,
		      u_bool want_dotfiles, u_bool want_attributes,
		      D_ENTRY *&entries) {
  char *s_dir = null;
  int  l_dir  = 0;
  char *s_remain = null;
  //!att: faire un free final de s_dir mais PAS de s_remain 
  //(qui est en fait a la suite de s_dir dans la memem char*)

  parse_dir(fdir, s_dir, l_dir, s_remain);
  if (!s_dir) return 0;

  DIR *dirp = opendir(s_dir); 
  if (!dirp) {			//ce directory n'existe pas
    free(s_dir);
    return 0;
  }

  // fullpath = chemin d'acces complet des fichiers
  char *fullpath = (char*)malloc((l_dir + 1000) * sizeof(char));
  strcpy(fullpath, s_dir);

  // pointe sur le debut du nom de fichier dans le fullpath
  char *namepos = null;;
  if (fullpath[l_dir-1] != '/') {
    fullpath[l_dir]   = '/';
    fullpath[l_dir+1] = 0;
    namepos = fullpath + l_dir+1;
  }
  else namepos = fullpath + l_dir;

  int l_filter = s_filter ? strlen(s_filter) : 0;
  int l_remain = s_remain ? strlen(s_remain) : 0;
  if (s_remain && s_remain[0]=='.') want_dotfiles = true;

  struct stat statbuf;
  int count = 0;
  int tab_size = DIR_QUANTUM;
  entries = (D_ENTRY*) malloc(tab_size * sizeof(D_ENTRY));
  struct dirent *de;

  while (dirp) {
    if (!(de = readdir(dirp)))	// erreur ou fin de liste
      break;		
    else {
      char *dname = de->d_name;

      if (dname[0] == 0) continue;
      else if (dname[0]=='.') {
	// skip . 
	if (dname[1]==0) continue;
	// skip ..
	else if (dname[1]=='.' && dname[2]==0) continue;
	// skip files starting by . depending on mode
	else if (!want_dotfiles) continue;
      }

      if (count >= tab_size) { // agrandir tab si necessaire
	tab_size += DIR_QUANTUM;
	entries = (D_ENTRY*) realloc(entries, tab_size * sizeof(D_ENTRY));
      }

      // copier nom du fichier dans fullpath pour faire stat()
      strcpy(namepos, dname);
      stat(fullpath, &statbuf);

      // SKIP si ne correspond pas au filtre (sauf ..)
      if (s_remain) {
	if (strncmp(dname, s_remain, l_remain) != 0) continue;
      }

      // SKIP si pas directory et ne correspond pas au filtre
      // NOTE: on ne filtre PAS les directories (sinon resultat illisible!)
      if (s_filter && !S_ISDIR(statbuf.st_mode)) {
	const char *p = u_strext(dname);
	if (!p || strncmp(p, s_filter, l_filter) != 0) continue;
      }

      init_entry(entries[count], statbuf, dname, s_filter, want_attributes);
      count++;
    }
  }

  qsort(entries, count, sizeof(entries[0]), compare_entries);

  closedir(dirp);
  free(s_dir);
  free(fullpath);
  return count;
}

/* ==================================================== [Elc:01] ======= */
/* ==================================================== ======== ======= */

void UFilebox::rescan() {
  u_bool want_attributes  = show_attributes->isSelected();
  u_bool want_dotfiles    = show_dotfiles->isSelected();
  u_bool want_normalfiles = ! show_dirs_only->isSelected();

  // 'filtrage' du filtre:
  // !!! FAUDRAIT VIRER LES BLANCS ET ACCEPTER PLUSIUERS FORMATS!!!!
  const char *fil = ffilter ? ffilter->chars() : null;
  if (fil) {
    if (*fil == ' ') fil++;   //skip white space
    if (fil[0]==0) fil = null;
    else if (fil[0]=='*' || fil[0]=='.') {  //skip 1 * or 1 .
      if (fil[1]==0) fil = null;
      else fil++;  //skip the * or the .
    }
  }

  // on repositionne les items dans tous les cas
  scrollpane->scroll(0,0);

  D_ENTRY *entries = null;
  int count = my_scandir(fdir, fil, 
			 want_dotfiles, want_attributes,
			 entries);

  // si le scandir ne renvoie rien on laisse l'affichage inchange
  //OBS:  if (count <= 0) return;

  // detruire l'ancien viewport
  if (viewport) udelete(viewport);

  // creer le nouveau contenu du Filebox
  UVbox *col;  //premiere colonne verticale du viewport

  viewport = &uhbox(UBgcolor::white
		    + uhmargin(3) + uvmargin(3)
		    + uhspacing(3)+ uvspacing(3)
		    +  (col = &uvbox()));
  scrollpane->add(viewport);

  //nouveau selecteur (l'ancien a ete detruit par: delete viewport)
  URadioSelect *new_sel = &uradioSelect();

  int n = 0;
  for (int k=0; k < count; k++) {
    D_ENTRY &e = entries[k];

    if (e.is_dir) {	// cas directory
      if (e.attr)
	col->add(uline(*new_sel + e.icon 
		       + ugroup(UFont::fixed + e.attr)
		       + UFont::bold + e.name
		       + UOn::action / ucall(this,&UFilebox::changeDir, e.name)));
      else
	col->add(uline(*new_sel + e.icon 
		       + e.name
		       + UOn::action / ucall(this,&UFilebox::changeDir, e.name)));
      n++; 
    }
    
    else if (want_normalfiles) {    // cas ordinary file or whatever
      if (e.attr)
	col->add(uline(*new_sel + e.icon 
		       + ugroup(UFont::fixed + e.attr)
		       + UFont::bold + e.name
		       + UOn::mclick / uset(fname, e.name)
		       + UOn::m2click / ucall(this, &UFilebox::OkBehavior)
		       )
		 );
      else
	col->add(uline(*new_sel + e.icon
		       + e.name
		       + UOn::mclick / uset(fname, e.name)
		       + UOn::m2click /ucall(this, &UFilebox::OkBehavior)
		       )
		 );
      n++; 
    }
    
    // nouvelle colonne verticale (sauf si want_attributes auquel cas
    // l'affichage est toujours vertical pour afficher les attributs)
    if (!want_attributes && n % LINE_COUNT == 0) {
      col = &uvbox();
      viewport->add(col);
    }
  }

  /**
     col->addlist
     ("" +
     UFont::large + UFont::bold + UFont::italic + UColor::red
     + "Can't open directory: not found or permission denied"
     );
     }
  ***/
  free(entries);
  scrollpane->update();
}

/* ==================================================== ======== ======= */

void UFilebox::OkBehavior(UEvent* e) {
  closeWin(e);
  fire(*e, UOn::action);
}

void UFilebox::CancelBehavior(UEvent* e) {
  closeWin(e);
}

/* ==================================================== ======== ======= */

UFilebox& UFilebox::setName(const UStr& s) {
  fname->set(s);
  return *this;
}
void UFilebox::setName(const UStr*s) {
  fname->set(s);
}

UFilebox& UFilebox::setDir(const UStr&s) {
  fdir->set(s);
  // rajouter un / a la fin si necessaire
  if (fdir->charAt(-1) != '/') fdir->append("/");
  rescan();
  return *this;
}
void UFilebox::setDir(const UStr*s) {
  if (s) setDir(*s);
  else setDir("");
}

UFilebox& UFilebox::setFilter(const UStr&s) {
  ffilter->set(s);
  return *this;
}
void UFilebox::setFilter(const UStr*s) {
  ffilter->set(s);
}

const UStr *UFilebox::getFilter() const {
  return ffilter;
}
const UStr *UFilebox::getDir() const {
  return fdir;
}
const UStr *UFilebox::getName() const {
  return fname;
}
const UStr *UFilebox::getPath() const {
  fpath->set(fdir);
  fpath->insert(-1, '/');
  fpath->append(*fname);
  return fpath;
}

/* ==================================================== [Elc:01] ======= */
/* ==================================================== ======== ======= */
//OBSOLETE

UFileDialog::UFileDialog(UArgs a) : UDialog(a) {
  // BUGGY: il faudrait une reference pour eviter pbms si removeAll

  // action sur filebox appelera OkBehavior sur filedialog qui appelera
  // action sur filedialog CQFD!
  filebox = &ufilebox( UOn::action / ucall(this, &UFileDialog::OkBehavior) );

  addlist(uhflex() + uvflex() + filebox);
}

UFilebox *UFileDialog::getFilebox() {return filebox;}

void UFileDialog::OkBehavior(UEvent* e) {
  //show(false);
  fire(*e, UOn::action);
}

char *UFileDialog::getFileName() {  // name of the file (without directory)
  return filebox->getName()->get();
}
char *UFileDialog::getFileDir() {    // directory of the file (without name)
  return filebox->getDir()->get();
}
char *UFileDialog::getFilterDir() {  // current directoy in the filter
  return filebox->getFilter()->get();
}

char *UFileDialog::getFilePath() {  // full pathname:  directory/name
  return filebox->getPath()->get();
}

/* ==================================================== [TheEnd] ======= */
/* ==================================================== [Elc:01] ======= */

