//<copyright>
// 
// Copyright (c) 1994,95,96
// Institute for Information Processing and Computer Supported New Media (IICM),
// Graz University of Technology, Austria.
// original version:
// Copyright (c) 1991 Stanford University
// Copyright (c) 1991 Silicon Graphics, Inc.
// 
//</copyright>
/*
 * Permission to use, copy, modify, distribute, and sell this software and 
 * its documentation for any purpose is hereby granted without fee, provided
 * that (i) the above copyright notices and this permission notice appear in
 * all copies of the software and related documentation, and (ii) the names of
 * Stanford and Silicon Graphics may not be used in any advertising or
 * publicity relating to the software without the specific, prior written
 * permission of Stanford and Silicon Graphics.
 * 
 * THE SOFTWARE IS PROVIDED "AS-IS" AND WITHOUT WARRANTY OF ANY KIND, 
 * EXPRESS, IMPLIED OR OTHERWISE, INCLUDING WITHOUT LIMITATION, ANY 
 * WARRANTY OF MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE.  
 *
 * IN NO EVENT SHALL STANFORD OR SILICON GRAPHICS BE LIABLE FOR
 * ANY SPECIAL, INCIDENTAL, INDIRECT OR CONSEQUENTIAL DAMAGES OF ANY KIND,
 * OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS,
 * WHETHER OR NOT ADVISED OF THE POSSIBILITY OF DAMAGE, AND ON ANY THEORY OF 
 * LIABILITY, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE 
 * OF THIS SOFTWARE.
 */

/*
 * FileChooser -- select a file
 */


//<file>
//
// File:        fchooser.C - implementation of class WFileChooser
//
// Created:      1 Jul 94   Michael Pichler
//
// $Id: fchooser.C,v 1.17 1996/09/10 10:34:27 jschipf Exp $
//
//</file>
//
// $Log: fchooser.C,v $
// Revision 1.17  1996/09/10 10:34:27  jschipf
// Bugfix: removed memory leak
//
// Revision 1.16  1996/02/21 13:33:43  bmarsch
// Removed warning
//
// Revision 1.15  1996/02/19 15:58:31  bmarsch
// Also accept callback for directories if flag is set
//
// Revision 1.14  1996/02/19 08:50:44  bmarsch
// Added flag to constructor to get a selection callback for
// directories
//
// Revision 1.13  1996/02/15 17:39:10  bmarsch
// Includes from OS/ were moved to hyperg/OS/
//
// Revision 1.12  1996/02/12 14:30:44  bmarsch
// Added members to get/change current directory
//
// Revision 1.11  1996/02/08 16:03:55  bmarsch
// Implemented members select_all() and deselect_all()
//
// Revision 1.10  1996/02/01 08:10:05  bmarsch
// Implemented multiselection
//
// Revision 1.9  1996/01/23 17:24:26  bmarsch
// Deleted all stuff associated with separate (motif-style)
// filter field editor
//
// Revision 1.8  1996/01/23 17:03:18  bmarsch
// Implemented file name completion
//


/* X-attributes     default value        meaning

   caption          ""
   subcaption       "Enter filename:"
   open             "Open"               button label open
   cancel           "Cancel"             "      "     cancel
   rows             10                   no. of rows in browser
   width            16*width('m')        natural browser width
   filterPattern    ""
   d.filterPattern  ""
   d.filterCaption  "Directory Filter:"
   background                            background of dialog
   completionchar   ' '                  character for filecompletion

   furthermore all attributes of Window, e.g. name
*/


#include "fbrowser.h"
#include "fchooser.h"
#include "field.h"
#include "msgbox.h"

#include <IV-look/choice.h>
#include <IV-look/kit.h>
#include <InterViews/action.h>
#include <InterViews/background.h>
#include <InterViews/event.h>
#include <InterViews/font.h>
#include <InterViews/hit.h>
#include <InterViews/input.h>
#include <InterViews/label.h>
#include <InterViews/layout.h>
#include <InterViews/scrbox.h>
#include <InterViews/style.h>
#include <InterViews/target.h>
#include <hyperg/OS/directory.h>
#include <hyperg/OS/string.h>
#include <hyperg/utils/str.h>

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


/*** WFileChooserImpl ***/

class WFileChooserImpl
{
  private:
    friend class WFileChooser;

    String* name_;
    WidgetKit* kit_;
    WFileChooser* fchooser_;
    WFileBrowser* fbrowser_;
    FieldEditor31* editor_;
    Glyph* browserglyph_;
    String* filterstr_;
    int* filter_map_;
    Directory* dir_;
    WFileChooserAction* action_;
    boolean dir_cb_;
    char* selected_;
    char* selstring_;
    boolean del_selected_;
    Style* style_;
    boolean multiselect_;

    WFileChooserImpl() { selected_ = nil; del_selected_ = false; }
    ~WFileChooserImpl() { if (del_selected_) delete[] selected_; }
    void init(WFileChooser*, Style*, WFileChooserAction*,
              boolean multiselect, boolean dir_callbacks);
    void free();
    void build();
    void clear();
    void load();
    boolean filtered(const String&, const String*);
    void accept_browser();
    void cancel_browser();
    void select_browser();
    void accept_editor (FieldEditor31*, char);
    void cancel_editor (FieldEditor31*, char);
    void special_editor (FieldEditor31*, char);
    boolean chdir(const String&);
    const char* selected (int i) const;
};

declareActionCallback(WFileChooserImpl)
implementActionCallback(WFileChooserImpl)

declareFieldEditorCallback31(WFileChooserImpl)
implementFieldEditorCallback31(WFileChooserImpl)


/*** WFileChooser ***/

WFileChooser::WFileChooser (
  const char* dir, WidgetKit* kit, Style* s,
  WFileChooserAction* a, const char* filter,
  boolean multiselect, boolean dir_callbacks
)
: Dialog (nil, s)
{
  impl_ = new WFileChooserImpl;
  WFileChooserImpl& fc = *impl_;
  fc.name_ = new CopyString (dir ? dir : "");
  fc.kit_ = kit;
  fc.filterstr_ = new CopyString (filter ? filter : "");
  fc.init (this, s, a, multiselect, dir_callbacks);
}


WFileChooser::~WFileChooser ()
{
  impl_->free ();
  delete impl_;
}


const char* WFileChooser::selected (int i) const
{
  return impl_->selected (i);
}


void WFileChooser::reread ()
{
  WFileChooserImpl& fc = *impl_;
  if (!fc.chdir (*fc.dir_->path ()))
  {
    cerr << "FileChooser: error: could not change to directory " << fc.dir_->path ()->string () << endl;
  }
}


void WFileChooser::rebuild ()
{
  impl_->build ();  // rebuild dialog
}


FieldEditor31* WFileChooser::editorField () const
{
  return impl_->editor_;
}


WFileBrowser* WFileChooser::fileBrowser () const
{
  return impl_->fbrowser_;
}


Glyph* WFileChooser::browserGlyph () const
{
  return impl_->browserglyph_;
}

Directory* WFileChooser::dir() const
{
  return impl_->dir_;
}

void WFileChooser::dir(const char* path)
{
  impl_->chdir(String(path));
}

void WFileChooser::dismiss (boolean accept)
{
  Dialog::dismiss (accept);  // close the dialog
  WFileChooserImpl& fc = *impl_;
  if (fc.action_)
    fc.action_->execute (this, accept);
}


int WFileChooser::num_selected() const
{
  return impl_->fbrowser_->num_marked();
}

void WFileChooser::select_all()
{
  impl_->fbrowser_->mark_all();
}

void WFileChooser::deselect_all()
{
  // clear browser
  impl_->fbrowser_->clear_all();

  // clear editor field
  impl_->editor_->field(impl_->dir_->path()->string());
}


/** class WFileChooserImpl **/

void WFileChooserImpl::init (
  WFileChooser* chooser, Style* s, WFileChooserAction* a,
  boolean multiselect, boolean dir_callbacks
)
{
  fchooser_ = chooser;
  fbrowser_ = nil;
  editor_ = nil;
  browserglyph_ = nil;
  filter_map_ = nil;
  selstring_ = nil;
  multiselect_ = multiselect;
  dir_cb_ = dir_callbacks;

  dir_ = Directory::open (*name_);
  if (!dir_)
  {
    dir_ = Directory::current ();
    if (dir_)  // "" also opens the current dir (otherwise remove this warning!)
      cerr << "Warning: FileChooser could not read directory " << name_->string () << endl;
    else  { // and what if we can't read the current directory?
      cerr << "Error: FileChooser could neither read " << name_->string ()
           << " nor current directory." << endl;
      dir_ = Directory::open("/");
      // and if we can't open slash, we can't help!
    }
  }

  Resource::ref (a);
  action_ = a;
  style_ = new Style (s);
  Resource::ref (style_);
  style_->alias ("FileChooser");
  style_->alias ("Dialog");

  build ();  // build dialog body
} // init


void WFileChooserImpl::free ()
{
  delete name_;
  delete dir_;
  delete filter_map_;
  delete filterstr_;
  delete[] selstring_;
  Resource::unref (action_);
  Resource::unref (style_);
}


void WFileChooserImpl::build ()
{
  WidgetKit& kit = *kit_;
  const LayoutKit& layout = *LayoutKit::instance();
  Style* s = style_;

  kit.push_style();
  kit.style(s);

  String caption ("");
  s->find_attribute ("caption", caption);

  String subcaption ("Enter filename:");
  s->find_attribute ("subcaption", subcaption);

  String open ("Open");
  s->find_attribute ("open", open);

  String close ("Cancel");
  s->find_attribute ("cancel", close);

  long rows = 10;
  s->find_attribute ("rows", rows);
  const Font* f = kit.font ();

  FontBoundingBox bbox;
  f->font_bbox (bbox);
  Coord height = rows * (bbox.ascent () + bbox.descent ()) + 3.0;

  Coord width;
  if (!s->find_attribute ("width", width))
    width = 16 * f->width ('m') + 3.0;

  Action* accept = new ActionCallback(WFileChooserImpl) (
    this, &WFileChooserImpl::accept_browser
  );
  Action* cancel = new ActionCallback(WFileChooserImpl) (
    this, &WFileChooserImpl::cancel_browser
  );

  Action* select = new ActionCallback(WFileChooserImpl) (
    this, &WFileChooserImpl::select_browser
  );

  RString comp(" ");
  s->find_attribute("completionchar", comp);
  if (comp[0] == '"' && comp[comp.length()-1] == '"')
    comp = comp.gSubstrIndex(1,comp.length()-2);

  if (!editor_)
  {
    const String* pathstr = dir_->path ();
    char buf [512];
    sprintf (
      buf, "%s%s",
      pathstr ? pathstr->string () : "",
      filterstr_ ? filterstr_->string () : ""
    );

    editor_ = new FieldEditor31 (
      &kit, s, buf,
      new FieldEditorCallback31(WFileChooserImpl)(
        this,
        &WFileChooserImpl::accept_editor,
        &WFileChooserImpl::cancel_editor,
        &WFileChooserImpl::special_editor
      ),
      comp
    );
    editor_->showModifications(0);
  }

  fbrowser_ = new WFileBrowser (kit_, accept, cancel, select, multiselect_);

  fchooser_->remove_all_input_handlers ();
  fchooser_->append_input_handler (editor_);
  fchooser_->append_input_handler (fbrowser_);

  // fancy label allows labelStyle chiseled or raised
  Glyph* g = layout.vbox ();
  if (caption.length () > 0)
    g->append (layout.rmargin (kit.fancy_label (caption), 5.0, fil, 0.0));
  if (subcaption.length() > 0)
    g->append(layout.rmargin (kit.fancy_label (subcaption), 5.0, fil, 0.0));
  g->append (layout.vglue (5.0, 0.0, 2.0));
  g->append (editor_);
  g->append (layout.vglue(15.0, 0.0, 12.0));
  browserglyph_ = layout.hbox ( // browser with scroll bar
    layout.vcenter (
      kit.inset_frame (
        layout.margin (
          layout.natural_span (fbrowser_, width, height), 1.0
        )
      ),
      1.0
    ),
    kit.vscroll_bar (fbrowser_->adjustable())
  );
  g->append (browserglyph_);
  g->append (layout.vspace (15.0));

  g->append (  // buttons
    layout.hbox (
      layout.hglue (10.0),
      layout.vcenter (kit.default_button (open, accept)),
      layout.hglue (10.0),
      layout.vcenter (kit.push_button (close, cancel)),
      layout.hglue(10.0)
    )
  );

  fchooser_->body (
    new Target (
      new Background (
        layout.margin (g, 10.0),
        kit.background ()
      ),
      TargetTransparentHit
    )
  );

  fchooser_->focus (editor_);
  kit.pop_style ();

  load ();

} // build


void WFileChooserImpl::clear ()
{
  Browser& b = *fbrowser_;
  b.select (-1);  // remove selection
  GlyphIndex n = b.count ();
  while (n--)
  { // remove telltale state and glyph
    b.remove_selectable (n);
    b.remove (n);
  }
}


void WFileChooserImpl::load ()
{
  Directory& d = *dir_;
  WFileBrowser& b = *fbrowser_;
  WidgetKit& kit = *kit_;

  kit.push_style ();
  kit.style (style_);
  const LayoutKit& layout = *LayoutKit::instance ();

  int dircount = d.count ();
  delete filter_map_;
  int* index = new int [dircount];
  filter_map_ = index;

  // directories
  int i;
  for (i = 0;  i < dircount;  i++)
  {
    if (!d.is_directory (i))
      continue;

    const String& f = *d.name (i);
    Glyph* name = layout.hbox (kit.label (f), kit.label ("/"));
    b.append_item(name);
    *index++ = i;
  } // for all directories

  // ordinary files
  for (i = 0; i < dircount; i++)
  {
    if (d.is_directory (i))
      continue;

    const String& f = *d.name (i);
    if (filtered (f, filterstr_))
    {
      Glyph* name = kit.label(f);
      b.append_item(name);
      *index++ = i;
    }
  } // for all ordinary files

  b.refresh ();  // scroll to first

  kit.pop_style ();

} // load


boolean WFileChooserImpl::filtered (const String& name, const String* flt)
{
  String str;
  if (flt)
    str = *flt;  // filter string

  if (!str.length ())
    return true;
  else
    return Directory::match (name, str);
}


void WFileChooserImpl::accept_browser() {
    int i = int(fbrowser_->selected());
    if (i == -1) {
        accept_editor(editor_, 0);
        return;
    }
    i = filter_map_[i];
    const String& path = *dir_->path();
    const String& name = *dir_->name(i);
    int length = path.length() + name.length();
    char* tmp = new char[length + 1];
    sprintf(
        tmp, "%.*s%.*s",
        path.length(), path.string(), name.length(), name.string()
    );
    editor_->field (tmp);
    if (del_selected_) delete[] selected_;
    selected_ = (char*) editor_->string ();
    del_selected_ = false;

    if (dir_->is_directory(i))
    {
      if (chdir (String (tmp, length)))
      {
        const String* pathstr = dir_->path ();
        char buf [512];

        sprintf (
          buf, "%s%s",
          pathstr ? pathstr->string () : "",
          filterstr_ ? filterstr_->string () : ""
        );
        editor_->field (buf);
        // mpichler, 19941107: do not give the focus to the field editor
        fbrowser_->focus_in ();
        // select first item
        fbrowser_->select(0);
        if (dir_cb_ && action_)
          action_->execute(fchooser_, true);
      }
      else  // should generate an error message
        cerr << "FileChooser: could not change to directory " << tmp << endl;
    }
    else  // ordinary file
      fchooser_->dismiss (true);

    delete tmp;
} // accept_browser


void WFileChooserImpl::cancel_browser()
{
  if (del_selected_) delete[] selected_;
  selected_ = nil;
  del_selected_ = false;
  fchooser_->dismiss (false);
}

void WFileChooserImpl::select_browser()
{
  if (!action_) return;

  int i = int(fbrowser_->selected());
  if (i < 0 || i >= fbrowser_->count()) return;

  i = filter_map_[i];
  if (dir_->is_directory(i) && !dir_cb_)
    return;

  const String& path = *dir_->path();
  const String& name = *dir_->name(i);
  int length = path.length() + name.length();
  if (del_selected_) delete[] selected_;
  selected_ = new char[length + 1];
  del_selected_ = true;
  sprintf(
    selected_, "%.*s%.*s",
    path.length(), path.string(), name.length(), name.string()
  );

  action_->selection(fchooser_);
}

void WFileChooserImpl::accept_editor (FieldEditor31* fe, char)
{
  // replace string by canonical form (expand ~ etc.)
  String* path = Directory::canonical (String (fe->string ()));
  const char* pathstr = path->string ();
  fe->field (pathstr);
  delete filterstr_;
  filterstr_ = nil;

  if (chdir (*path))  // directory
  { // chdir has copied the string
    delete path;
    if (dir_cb_ && action_)
      action_->execute(fchooser_, true);
  }
  else if (strchr (pathstr, '*') || strchr (pathstr, '?'))  // joker
  {
    const char* slpos = strrchr (pathstr, '/');

    if (slpos)
    {
      filterstr_ = new CopyString (slpos + 1);

      String directory (pathstr, slpos - pathstr + 1);  // +1 to be able to open "/"
      if (chdir (directory))  // directory may have changed
      { // chdir has copied the string
        delete path;
        return;  // clear and load done inside chdir
      }
      else  // error: directory does not exist
      { // (without filter the name is assumed to be name of a file to be created)
        char buf [512];
        sprintf (buf, "failed to open directory %.*s", directory.length (), directory.string ());
        MessageBox::message (
          HgLanguage::Default, // will be supported soon!!!
          nil, buf, "error", MessageBox::Ok
        );
      }
    }
    else  // small bug: changes to current directory
      filterstr_ = new CopyString (pathstr);

    clear ();
    load ();
  }
  else  // file name
  {
    if (del_selected_) delete[] selected_;
    selected_ = (char*) path->string ();
    del_selected_ = false;
    fchooser_->dismiss (true);
    // to get the plain file name?!:
    // fe->select (path->rindex ('/') + 1, path->length ());
  }
} // accept_editor


void WFileChooserImpl::cancel_editor (FieldEditor31*, char)
{
  fchooser_->dismiss (false);
}


void WFileChooserImpl::special_editor (FieldEditor31* fe, char)
{
  // replace directory string by canonical form (expand ~ etc.)
  String* path = Directory::canonical(String(fe->string()));
  const char* pathstring = path->string();

  // extract path and name
  const char* pos = strrchr(pathstring, '/');
  if (pos) {
    String dir(pathstring, pos-pathstring+1);
    chdir(dir);
  }

  String name(pos+1);
  const char* namestring = name.string();
  RString common = "";
  boolean prefix = false;
  clear();
  // search name* in directory
  for (int i = 0; i < dir_->count(); i++) {
    const char* entry = dir_->name(i)->string();
    const char* pos = strstr(entry, namestring);
    if (pos && pos == entry) {
      const char* rest = entry + name.length();
      if (common == "")
        common = rest;
      else {
        int rlen = strlen(rest);
        int max = (common.length() < rlen) ? rlen : common.length();
        int to = 0;
        while (common[to] == rest[to] && to++ < max);
        if (to == max) {
          common = "";
        }
        else {
          prefix = true;
          common = common.gSubstrIndex(0, to-1);
        }
        if (common == "")
          break;
      }
    }
  }

  if (!prefix && chdir(String(pathstring+common))) {
    // change to directory
    if (common[common.length()-1] != '/')
      common += "/";   // append slash to directory
  }
  else {
    // set filter string to <common>*
    RString s = namestring + common + "*";
    delete filterstr_;
    filterstr_ = new CopyString(s.string());
  }
  // set new matching file names
  clear();
  load();
  common = pathstring + common;
  // set new field
  fe->field(common);
  // delete current filter string
  delete filterstr_;
  filterstr_ = nil;
  delete path;
}


boolean WFileChooserImpl::chdir (const String& name)
{
  Directory* d = Directory::open (name);
  if (d != nil)
  {
    dir_->close ();
    delete dir_;
    dir_ = d;
    clear ();
    load ();
    return true;
  }
  return false;
}

const char* WFileChooserImpl::selected (int i) const
{
  if (!multiselect_ || i < 0 || i >= fbrowser_->num_marked())
    return selected_;

  else {
    WFileChooserImpl* This = (WFileChooserImpl*) this;  // need this since function is const
    int j = -1;
    for (int k = 0; k < fbrowser_->count(); k++) {
      if (fbrowser_->marked(k)) j++;
      if (j == i) {
        const String& path = *dir_->path();
        const String& name = *dir_->name(filter_map_[k]);
        delete[] selstring_;
        int length = path.length() + name.length();
        This->selstring_ = new char[length + 1];
        sprintf(
          selstring_, "%.*s%.*s",
          path.length(), path.string(), name.length(), name.string()
        );
        return selstring_;
      }
    }
  }

  return nil;
}


/** class WFileChooserAction **/

WFileChooserAction::WFileChooserAction() { }
WFileChooserAction::~WFileChooserAction() { }
void WFileChooserAction::execute(WFileChooser*, boolean) { }
void WFileChooserAction::selection(WFileChooser*) { }
