/**
 * \file FormFiledialog.C
 * This file is part of LyX, the document processor.
 * Licence details can be found in the file COPYING.
 *
 * \author unknown
 * \author John Levon
 *
 * Full author contact details are available in file CREDITS.
 */

#include <config.h>

#include "FormFiledialog.h"
#include "forms/form_filedialog.h"

#include "forms_gettext.h"
#include "globbing.h"
#include "xforms_helpers.h"

#include "frontends/Dialogs.h"

#include "support/filefilterlist.h"
#include "support/filetools.h"
#include "support/lstrings.h"
#include "support/lyxlib.h"
#include "support/package.h"

#include "lyx_forms.h"

#include <boost/bind.hpp>
#include <boost/filesystem/operations.hpp>
#include <boost/tokenizer.hpp>

#include <algorithm>
#include <sstream>

using lyx::support::AbsolutePath;
using lyx::support::AddName;
using lyx::support::ExpandPath;
using lyx::support::FileFilterList;
using lyx::support::getcwd;
using lyx::support::MakeAbsPath;
using lyx::support::OnlyFilename;
using lyx::support::package;
using lyx::support::split;
using lyx::support::suffixIs;
using lyx::support::trim;

using std::max;
using std::sort;
using std::ostringstream;
using std::string;
using std::vector;

using namespace lyx::frontend;
namespace fs = boost::filesystem;

namespace {

/** Given a string "<glob> <glob> <glob>", expand each glob in turn.
 *  Any glob that cannot be expanded is ignored silently.
 *  Invokes \c convert_brace_glob and \c glob internally, so use only
 *  on systems supporting the Posix function 'glob'.
 *  \param mask the string "<glob> <glob> <glob>".
 *  \param directory the current working directory from
 *  which \c glob is invoked.
 *  \returns a vector of all matching file names.
 */
vector<string> const expand_globs(string const & mask,
				  string const & directory)
{
	// Split into individual globs and then call 'glob' on each one.
	typedef boost::tokenizer<boost::char_separator<char> > Tokenizer;
	boost::char_separator<char> const separator(" ");

	vector<string> matches;
	Tokenizer const tokens(mask, separator);
	Tokenizer::const_iterator it = tokens.begin();
	Tokenizer::const_iterator const end = tokens.end();
	for (; it != end; ++it)
		lyx::support::glob(matches, *it, directory);

	return matches;
}


extern "C" {

	static
	int C_LyXFileDlg_CancelCB(FL_FORM *fl, void *xev)
	{
		return FileDialog::Private::CancelCB(fl, xev);
	}

	static
	void C_LyXFileDlg_DoubleClickCB(FL_OBJECT * ob, long data)
	{
		FileDialog::Private::DoubleClickCB(ob, data);
	}

	static
	void C_LyXFileDlg_FileDlgCB(FL_OBJECT * ob, long data)
	{
		FileDialog::Private::FileDlgCB(ob, data);
	}

}


// compares two LyXDirEntry objects content (used for sort)
class comp_direntry : public std::binary_function<DirEntry, DirEntry, bool> {
public:
	bool operator()(DirEntry const & r1, DirEntry const & r2) const
	{
		bool const r1d = suffixIs(r1.name_, '/');
		bool const r2d = suffixIs(r2.name_, '/');
		if (r1d && !r2d)
			return true;
		if (!r1d && r2d)
			return false;
		return r1.name_ < r2.name_;
	}
};

} // namespace anon



// *** FileDialog::Private class implementation

// static members
FD_filedialog * FileDialog::Private::file_dlg_form_ = 0;
FileDialog::Private * FileDialog::Private::current_dlg_ = 0;
int FileDialog::Private::minw_ = 0;
int FileDialog::Private::minh_ = 0;


// Reread: updates dialog list to match class directory
void FileDialog::Private::Reread()
{
	// Opens directory
	if (!fs::exists(directory_) || !fs::is_directory(directory_)) {
// FIXME: re-add ...
#if 0
		Alert::err_alert(_("Warning! Couldn't open directory."),
			directory_);
#endif
		directory_ = getcwd();
	}

	// Clear the present namelist
	dir_entries_.clear();

	// Updates display
	fl_hide_object(file_dlg_form_->List);
	fl_clear_browser(file_dlg_form_->List);
	fl_set_input(file_dlg_form_->DirBox, directory_.c_str());

	// Splits complete directory name into directories and compute depth
	depth_ = 0;
	string line;
	string Temp;
	string File = directory_;
	if (File != "/")
		File = split(File, Temp, '/');

	while (!File.empty() || !Temp.empty()) {
		string const dline = "@b" + line + Temp + '/';
		fl_add_browser_line(file_dlg_form_->List, dline.c_str());
		File = split(File, Temp, '/');
		line += ' ';
		++depth_;
	}

	vector<string> const glob_matches = expand_globs(mask_, directory_);

	fs::directory_iterator beg(directory_);
	fs::directory_iterator end;
	for (; beg != end; ++beg) {
		string const fname = beg->leaf();

		// If the pattern doesn't start with a dot, skip hidden files
		if (!mask_.empty() && mask_[0] != '.' && fname[0] == '.')
			continue;

		bool const isDir = fs::exists(*beg) && fs::is_directory(*beg);

		// filters files according to pattern and type
			typedef vector<string>::const_iterator viterator;
			viterator gbegin = glob_matches.begin();
			viterator const gend = glob_matches.end();

		if (!isDir && std::find(gbegin, gend, fname) == gend)
			continue;

		DirEntry tmp;
		// creates used name
		tmp.name_ = fname;

		if (isDir)
			tmp.name_ += '/';

		// creates displayed name
		tmp.displayed_ = fname;
		dir_entries_.push_back(tmp);
	}

	// Sort the names
	sort(dir_entries_.begin(), dir_entries_.end(), comp_direntry());

	// Add them to directory box
	for (DirEntries::const_iterator cit = dir_entries_.begin();
	     cit != dir_entries_.end(); ++cit) {
		string const temp = line + cit->displayed_;
		fl_add_browser_line(file_dlg_form_->List, temp.c_str());
	}
	fl_set_browser_topline(file_dlg_form_->List, depth_);
	fl_show_object(file_dlg_form_->List);
	last_sel_ = -1;
}


// SetDirectory: sets dialog current directory
void FileDialog::Private::SetDirectory(string const & path)
{
	string tmp;
	if (path.empty())
		tmp = getcwd();
	else
		tmp = MakeAbsPath(ExpandPath(path), directory_);

	// must check the directory exists
	if (!fs::exists(tmp) || !fs::is_directory(tmp)) {
// FIXME: re-add ...
#if 0
		Alert::err_alert(_("Warning! Couldn't open directory."), tmp);
#endif
	} else {
		directory_ = tmp;
	}
}


void FileDialog::Private::SetFilters(string const & mask)
{
	SetFilters(FileFilterList(mask));
}


void FileDialog::Private::SetFilters(FileFilterList const & filters)
{
	if (filters.empty())
		return;

	// Just take the first one for now.
	typedef FileFilterList::Filter::glob_iterator glob_iterator;
	glob_iterator const begin = filters[0].begin();
	glob_iterator const end = filters[0].end();
	if (begin == end)
		return;

	ostringstream ss;
	for (glob_iterator it = begin; it != end; ++it) {
		if (it != begin)
			ss << ' ';
		ss << *it;
	}

	mask_ = ss.str();
	fl_set_input(file_dlg_form_->PatBox, mask_.c_str());
}


FileDialog::Private::Private()
{
	directory_ = MakeAbsPath(string("."));

	// Creates form if necessary.
	if (!file_dlg_form_) {
		file_dlg_form_ = build_filedialog(this);
		minw_ = file_dlg_form_->form->w;
		minh_ = file_dlg_form_->form->h;
		// Set callbacks. This means that we don't need a patch file
		fl_set_object_callback(file_dlg_form_->DirBox,
				       C_LyXFileDlg_FileDlgCB, 0);
		fl_set_object_callback(file_dlg_form_->PatBox,
				       C_LyXFileDlg_FileDlgCB, 1);
		fl_set_object_callback(file_dlg_form_->List,
				       C_LyXFileDlg_FileDlgCB, 2);
		fl_set_object_callback(file_dlg_form_->Filename,
				       C_LyXFileDlg_FileDlgCB, 3);
		fl_set_object_callback(file_dlg_form_->Rescan,
				       C_LyXFileDlg_FileDlgCB, 10);
		fl_set_object_callback(file_dlg_form_->Home,
				       C_LyXFileDlg_FileDlgCB, 11);
		fl_set_object_callback(file_dlg_form_->User1,
				       C_LyXFileDlg_FileDlgCB, 12);
		fl_set_object_callback(file_dlg_form_->User2,
				       C_LyXFileDlg_FileDlgCB, 13);

		// Make sure pressing the close box doesn't crash LyX. (RvdK)
		fl_set_form_atclose(file_dlg_form_->form,
				    C_LyXFileDlg_CancelCB, 0);
		// Register doubleclick callback
		fl_set_browser_dblclick_callback(file_dlg_form_->List,
						 C_LyXFileDlg_DoubleClickCB,
						 0);
	}
	fl_hide_object(file_dlg_form_->User1);
	fl_hide_object(file_dlg_form_->User2);

	r_ = Dialogs::redrawGUI().connect(boost::bind(&FileDialog::Private::redraw, this));
}


FileDialog::Private::~Private()
{
	r_.disconnect();
}


void FileDialog::Private::redraw()
{
	if (file_dlg_form_->form && file_dlg_form_->form->visible)
		fl_redraw_form(file_dlg_form_->form);
}


// SetButton: sets file selector user button action
void FileDialog::Private::SetButton(int index, string const & name,
			   string const & path)
{
	FL_OBJECT * ob;
	string * tmp;

	if (index == 0) {
		ob = file_dlg_form_->User1;
		tmp = &user_path1_;
	} else if (index == 1) {
		ob = file_dlg_form_->User2;
		tmp = &user_path2_;
	} else {
		return;
	}

	if (!name.empty()) {
		fl_set_object_label(ob, idex(name).c_str());
		fl_set_button_shortcut(ob, scex(name).c_str(), 1);
		fl_show_object(ob);
		*tmp = path;
	} else {
		fl_hide_object(ob);
		tmp->erase();
	}
}


// GetDirectory: gets last dialog directory
string const FileDialog::Private::GetDirectory() const
{
	if (!directory_.empty())
		return directory_;
	else
		return string(".");
}

namespace {
	bool x_sync_kludge(bool ret)
	{
		XSync(fl_get_display(), false);
		return ret;
	}
} // namespace anon

// RunDialog: handle dialog during file selection
bool FileDialog::Private::RunDialog()
{
	force_cancel_ = false;
	force_ok_ = false;

	// event loop
	while (true) {
		FL_OBJECT * ob = fl_do_forms();

		if (ob == file_dlg_form_->Ready) {
			if (HandleOK())
				return x_sync_kludge(true);

		} else if (ob == file_dlg_form_->Cancel || force_cancel_)
			return x_sync_kludge(false);

		else if (force_ok_)
			return x_sync_kludge(true);
	}
}


// XForms objects callback (static)
void FileDialog::Private::FileDlgCB(FL_OBJECT *, long arg)
{
	if (!current_dlg_)
		return;

	switch (arg) {

	case 0: // get directory
		current_dlg_->SetDirectory(fl_get_input(file_dlg_form_->DirBox));
		current_dlg_->Reread();
		break;

	case 1: // get mask
		current_dlg_->SetFilters(fl_get_input(file_dlg_form_->PatBox));
		current_dlg_->Reread();
		break;

	case 2: // list
		current_dlg_->HandleListHit();
		break;

	case 10: // rescan
		current_dlg_->SetDirectory(fl_get_input(file_dlg_form_->DirBox));
		current_dlg_->SetFilters(fl_get_input(file_dlg_form_->PatBox));
		current_dlg_->Reread();
		break;

	case 11: // home
		current_dlg_->SetDirectory(package().home_dir());
		current_dlg_->SetFilters(fl_get_input(file_dlg_form_->PatBox));
		current_dlg_->Reread();
		break;

	case 12: // user button 1
		current_dlg_->SetDirectory(current_dlg_->user_path1_);
		current_dlg_->SetFilters(fl_get_input(file_dlg_form_->PatBox));
		current_dlg_->Reread();
		break;

	case 13: // user button 2
		current_dlg_->SetDirectory(current_dlg_->user_path2_);
		current_dlg_->SetFilters(fl_get_input(file_dlg_form_->PatBox));
		current_dlg_->Reread();
		break;

	}
}


// Handle callback from list
void FileDialog::Private::HandleListHit()
{
	// set info line
	int const select_ = fl_get_browser(file_dlg_form_->List);
       string line = (select_ > depth_ ?
			    dir_entries_[select_ - depth_ - 1].name_ :
			    string());
       if (suffixIs(line, '/'))
	       line.clear();
       fl_set_input(file_dlg_form_->Filename, line.c_str());
}


// Callback for double click in list
void FileDialog::Private::DoubleClickCB(FL_OBJECT *, long)
{
	// Simulate click on OK button
	if (current_dlg_->HandleDoubleClick())
		current_dlg_->Force(false);
}


// Handle double click from list
bool FileDialog::Private::HandleDoubleClick()
{
	string tmp;

	// set info line
	bool isDir = true;
	int const select_ = fl_get_browser(file_dlg_form_->List);
	if (select_ > depth_) {
		tmp = dir_entries_[select_ - depth_ - 1].name_;
		if (!suffixIs(tmp, '/')) {
			isDir = false;
			fl_set_input(file_dlg_form_->Filename, tmp.c_str());
		}
	} else if (select_ == 0)
		return true;

	// executes action
	if (isDir) {
		string Temp;

		// builds new directory name
		if (select_ > depth_) {
			// Directory deeper down
			// First, get directory with trailing /
			Temp = fl_get_input(file_dlg_form_->DirBox);
			if (!suffixIs(Temp, '/'))
				Temp += '/';
			Temp += tmp;
		} else {
			// Directory higher up
			Temp.erase();
			for (int i = 0; i < select_; ++i) {
				string const piece = fl_get_browser_line(file_dlg_form_->List, i + 1);
				// The '+2' is here to count the '@b' (JMarc)
				Temp += piece.substr(i + 2);
			}
		}

		// assigns it
		SetDirectory(Temp);
		Reread();
		return false;
	}
	return true;
}


// Handle OK button call
bool FileDialog::Private::HandleOK()
{
	// mask was changed
	string tmp = fl_get_input(file_dlg_form_->PatBox);
	if (tmp != mask_) {
		SetFilters(tmp);
		Reread();
		return false;
	}

	// directory was changed
	tmp = fl_get_input(file_dlg_form_->DirBox);
	if (tmp != directory_) {
		SetDirectory(tmp);
		Reread();
		return false;
	}

	// Handle return from list
	int const select = fl_get_browser(file_dlg_form_->List);
	if (select > depth_) {
		string const temp = dir_entries_[select - depth_ - 1].name_;
		if (!suffixIs(temp, '/')) {
			// If user didn't type anything, use browser
			string const name = fl_get_input(file_dlg_form_->Filename);
			if (name.empty())
				fl_set_input(file_dlg_form_->Filename, temp.c_str());
			return true;
		}
	}

	// Emulate a doubleclick
	return HandleDoubleClick();
}


// Handle Cancel CB from WM close
int FileDialog::Private::CancelCB(FL_FORM *, void *)
{
	// Simulate a click on the cancel button
	current_dlg_->Force(true);
	return FL_IGNORE;
}


// Simulates a click on OK/Cancel
void FileDialog::Private::Force(bool cancel)
{
	if (cancel) {
		force_cancel_ = true;
		fl_set_button(file_dlg_form_->Cancel, 1);
	} else {
		force_ok_ = true;
		fl_set_button(file_dlg_form_->Ready, 1);
	}
	// Start timer to break fl_do_forms loop soon
	fl_set_timer(file_dlg_form_->timer, 0.1);
}


// Select: launches dialog and returns selected file
string const FileDialog::Private::Select(string const & title,
					 string const & path,
					 FileFilterList const & filters,
					 string const & suggested)
{
	// handles new mask and path
	SetFilters(filters);
	SetDirectory(path);
	Reread();

	// highlight the suggested file in the browser, if it exists.
	int sel = 0;
	string const filename = OnlyFilename(suggested);
	if (!filename.empty()) {
		for (int i = 0; i < fl_get_browser_maxline(file_dlg_form_->List); ++i) {
			string s = fl_get_browser_line(file_dlg_form_->List, i + 1);
			s = trim(s);
			if (s == filename) {
				sel = i + 1;
				break;
			}
		}
	}

	if (sel != 0)
		fl_select_browser_line(file_dlg_form_->List, sel);
	int const top = max(sel - 5, 1);
	fl_set_browser_topline(file_dlg_form_->List, top);

	// checks whether dialog can be started
	if (current_dlg_)
		return string();
	current_dlg_ = this;

	// runs dialog
	setEnabled(file_dlg_form_->Filename, true);
	fl_set_input(file_dlg_form_->Filename, suggested.c_str());
	fl_set_button(file_dlg_form_->Cancel, 0);
	fl_set_button(file_dlg_form_->Ready, 0);
	fl_set_focus_object(file_dlg_form_->form, file_dlg_form_->Filename);
	fl_deactivate_all_forms();
	// Prevent xforms crashing if the dialog gets too small by preventing
	// it from being shrunk beyond a minimum size.
	// calls to fl_set_form_minsize/maxsize apply only to the next
	// fl_show_form(), so this comes first.
	fl_set_form_minsize(file_dlg_form_->form, minw_, minh_);

	fl_show_form(file_dlg_form_->form,
		     FL_PLACE_MOUSE | FL_FREE_SIZE, 0,
		     title.c_str());

	bool const isOk = RunDialog();

	fl_hide_form(file_dlg_form_->form);
	fl_activate_all_forms();
	current_dlg_ = 0;

	// Returns filename or string() if no valid selection was made
	if (!isOk || !fl_get_input(file_dlg_form_->Filename)[0])
		return string();

	file_name_ = fl_get_input(file_dlg_form_->Filename);

	if (!AbsolutePath(file_name_))
		file_name_ = AddName(fl_get_input(file_dlg_form_->DirBox), file_name_);
	return file_name_;
}


// SelectDir: launches dialog and returns selected directory
string const FileDialog::Private::SelectDir(string const & title,
					 string const & path,
					 string const & suggested)
{
	SetFilters("*/");
	// handles new path
	bool isOk = true;
	if (!path.empty()) {
		// handle case where path does not end with "/"
		// remerge path+suggested and check if it is a valid path
		if (!suggested.empty()) {
			string tmp = suggested;
			if (!suffixIs(tmp, '/'))
				tmp += '/';
			string const full_path = path + tmp;
			// check if this is really a directory
			if (fs::exists(full_path)
			    && fs::is_directory(full_path))
				SetDirectory(full_path);
			else
				SetDirectory(path);
		} else
			SetDirectory(path);
		isOk = false;
	}
	if (!isOk)
		Reread();

	// checks whether dialog can be started
	if (current_dlg_)
		return string();
	current_dlg_ = this;

	// runs dialog
	fl_set_input(file_dlg_form_->Filename, "");
	setEnabled(file_dlg_form_->Filename, false);
	fl_set_button(file_dlg_form_->Cancel, 0);
	fl_set_button(file_dlg_form_->Ready, 0);
	fl_set_focus_object(file_dlg_form_->form, file_dlg_form_->DirBox);
	fl_deactivate_all_forms();
	fl_show_form(file_dlg_form_->form,
		     FL_PLACE_MOUSE | FL_FREE_SIZE, 0,
		     title.c_str());

	isOk = RunDialog();

	fl_hide_form(file_dlg_form_->form);
	fl_activate_all_forms();
	current_dlg_ = 0;

	// Returns directory or string() if no valid selection was made
	if (!isOk)
		return string();

	file_name_ = fl_get_input(file_dlg_form_->DirBox);
	return file_name_;
}
