/******************************************************************************
 JUNIXDirInfo.cc

	Class that contains information about everything in a given directory.

	Derived classes can override IsVisible() to perform extra filtering
	of the list of files.  Note that since this class has a constructor
	function, derived classes must also enforce their own constructor
	functions.

	If the contents will merely change due to filtering or updating,
	instead of changing directories, we broadcast ContentsWillBeUpdated
	before changing anything.  This gives others a chance to save state
	that can be restored after ContentsChanged.  Note that PathChanged
	cancels the effect of ContentsWillBeUpdated.

	The content filter is applied in BuildInfo() because it is so
	expensive and is not likely to change very often since it should not
	be under user control.

	BASE CLASS = JContainer

	Copyright © 1996 by Glenn W. Bach. All rights reserved.
	Copyright © 1997-98 by John Lindal. All rights reserved.

 ******************************************************************************/

#include <JUNIXDirInfo.h>
#include <JUNIXDirEntry.h>

#include <JString.h>
#include <JRegex.h>
#include <JLatentPG.h>
#include <JStdError.h>

#ifndef __MWERKS__
#include <sys/stat.h>
#include <sys/types.h>
#include <dirent.h>
#endif

#include <unistd.h>
#include <limits.h>
#include <jDirUtil.h>
#include <jTime.h>
#include <jGlobals.h>
#include <jAssert.h>

// Broadcaster messages types

const JCharacter* JUNIXDirInfo::kContentsWillBeUpdated = "ContentsWillBeUpdated::JUNIXDirInfo";
const JCharacter* JUNIXDirInfo::kContentsChanged       = "ContentsChanged::JUNIXDirInfo";
const JCharacter* JUNIXDirInfo::kPathChanged           = "PathChanged::JUNIXDirInfo";
const JCharacter* JUNIXDirInfo::kPermissionsChanged    = "PermissionsChanged::JUNIXDirInfo";

/******************************************************************************
 Constructor function (static)

	By forcing everyone to use this function, we avoid having to worry
	about BuildInfo() succeeding within the class itself.

	Note that this prevents one from creating derived classes unless one
	creates a similar constructor function that checks OKToCreateUNIXDirInfo().

 ******************************************************************************/

JBoolean
JUNIXDirInfo::Create
	(
	const JCharacter*	dirName,
	JUNIXDirInfo**		obj
	)
{
	if (OKToCreateUNIXDirInfo(dirName))
		{
		*obj = new JUNIXDirInfo(dirName);
		assert( *obj != NULL );
		return kTrue;
		}
	else
		{
		*obj = NULL;
		return kFalse;
		}
}

JBoolean
JUNIXDirInfo::OKToCreateUNIXDirInfo
	(
	const JCharacter* dirName
	)
{
	return JConvertToBoolean( JDirectoryExists(dirName) &&
							  JDirectoryReadable(dirName) &&
							  JCanEnterDirectory(dirName) );
}

/******************************************************************************
 Constructor (protected)

 ******************************************************************************/

JUNIXDirInfo::JUNIXDirInfo
	(
	const JCharacter* dirName
	)
	:
	JContainer()
{
	itsCWD = new JString(dirName);
	assert( itsCWD != NULL );
	JAppendDirSeparator(itsCWD);

	itsIsWritableFlag = kFalse;
	itsModTime        = 0;
	itsStatusTime     = 0;

	itsShowHiddenFlag = kFalse;
	itsShowDirsFlag   = kTrue;
	itsShowFilesFlag  = kTrue;
	itsShowOthersFlag = kFalse;

	itsNameRegex    = NULL;
	itsContentRegex = NULL;
	itsPG           = NULL;

	itsDirEntries = new JPtrArray<JUNIXDirEntry>;
	assert( itsDirEntries != NULL);
	itsDirEntries->SetCompareFunction(JUNIXDirEntry::CompareNames);
	itsDirEntries->SetSortOrder(JOrderedSetT::kSortAscending);

	itsVisEntries = new JPtrArray<JUNIXDirEntry>;
	assert( itsVisEntries != NULL);

	itsAlphaEntries = new JPtrArray<JUNIXDirEntry>;
	assert( itsAlphaEntries != NULL);
	itsAlphaEntries->SetCompareFunction(JUNIXDirEntry::CompareNames);
	itsAlphaEntries->SetSortOrder(JOrderedSetT::kSortAscending);

	InstallOrderedSet(itsVisEntries);

	const JError err = BuildInfo();
	assert( err.OK() );
}

/******************************************************************************
 Copy constructor

 ******************************************************************************/

JUNIXDirInfo::JUNIXDirInfo
	(
	const JUNIXDirInfo& source
	)
	:
	JContainer(source)
{
	itsCWD = new JString(*(source.itsCWD));
	assert( itsCWD != NULL );

	itsIsWritableFlag = source.itsIsWritableFlag;
	itsModTime        = source.itsModTime;
	itsStatusTime     = source.itsStatusTime;

	itsShowHiddenFlag = source.itsShowHiddenFlag;
	itsShowDirsFlag   = source.itsShowDirsFlag;
	itsShowFilesFlag  = source.itsShowFilesFlag;
	itsShowOthersFlag = source.itsShowOthersFlag;

	itsNameRegex    = NULL;
	itsContentRegex = NULL;
	itsPG           = NULL;

	itsDirEntries = new JPtrArray<JUNIXDirEntry>;
	assert( itsDirEntries != NULL);

	itsVisEntries = new JPtrArray<JUNIXDirEntry>;
	assert( itsVisEntries != NULL);

	itsAlphaEntries = new JPtrArray<JUNIXDirEntry>;
	assert( itsAlphaEntries != NULL);
	itsAlphaEntries->SetCompareFunction(JUNIXDirEntry::CompareNames);
	itsAlphaEntries->SetSortOrder(JOrderedSetT::kSortAscending);

	CopyLists(source);
	InstallOrderedSet(itsVisEntries);
}

/******************************************************************************
 Destructor

 ******************************************************************************/

JUNIXDirInfo::~JUNIXDirInfo()
{
	itsDirEntries->DeleteAll();
	delete itsDirEntries;
	delete itsVisEntries;
	delete itsAlphaEntries;

	delete itsCWD;
	delete itsNameRegex;
	delete itsContentRegex;
	delete itsPG;
}

/******************************************************************************
 Assignment operator

 ******************************************************************************/

const JUNIXDirInfo&
JUNIXDirInfo::operator=
	(
	const JUNIXDirInfo& source
	)
{
	if (this == &source)
		{
		return *this;
		}

	JContainer::operator=(source);

	*itsCWD = *(source.itsCWD);

	itsIsWritableFlag = source.itsIsWritableFlag;
	itsModTime        = source.itsModTime;
	itsStatusTime     = source.itsStatusTime;

	itsShowHiddenFlag = source.itsShowHiddenFlag;
	itsShowDirsFlag   = source.itsShowDirsFlag;
	itsShowFilesFlag  = source.itsShowFilesFlag;
	itsShowOthersFlag = source.itsShowOthersFlag;

	CopyLists(source);

	return *this;
}

/*****************************************************************************
 CopyLists (private)

 ******************************************************************************/

void
JUNIXDirInfo::CopyLists
	(
	const JUNIXDirInfo& source
	)
{
JIndex i;

	// copy name filter

	delete itsNameRegex;
	itsNameRegex = NULL;

	if (source.itsNameRegex != NULL)
		{
		itsNameRegex = new JRegex(*(source.itsNameRegex));
		assert( itsNameRegex != NULL );
		}

	// copy content filter

	delete itsContentRegex;
	itsContentRegex = NULL;

	if (source.itsContentRegex != NULL)
		{
		itsContentRegex = new JRegex(*(source.itsContentRegex));
		assert( itsContentRegex != NULL );
		}

	// copy file info

	assert( itsDirEntries != NULL );
	itsDirEntries->DeleteAll();

	JSize entryCount = (source.itsDirEntries)->GetElementCount();
	for (i=1; i<=entryCount; i++)
		{
		JUNIXDirEntry* origEntry = (source.itsDirEntries)->NthElement(i);
		JUNIXDirEntry* entry = new JUNIXDirEntry(*origEntry);
		assert( entry != NULL );
		itsDirEntries->Append(entry);
		}

	itsDirEntries->SetCompareFunction((source.itsDirEntries)->GetCompareFunction());
	itsDirEntries->SetSortOrder((source.itsDirEntries)->GetSortOrder());

	// update other file lists

	assert( itsVisEntries != NULL && itsAlphaEntries != NULL );

	ApplyFilters(kFalse);
}

/******************************************************************************
 ShowHidden

 ******************************************************************************/

void
JUNIXDirInfo::ShowHidden
	(
	const JBoolean showHidden
	)
{
	if (showHidden != itsShowHiddenFlag)
		{
		itsShowHiddenFlag = showHidden;
		ApplyFilters(kTrue);
		}
}

/******************************************************************************
 ShowDirs

 ******************************************************************************/

void
JUNIXDirInfo::ShowDirs
	(
	const JBoolean showDirs
	)
{
	if (showDirs != itsShowDirsFlag)
		{
		itsShowDirsFlag = showDirs;
		ApplyFilters(kTrue);
		}
}

/******************************************************************************
 ShowFiles

 ******************************************************************************/

void
JUNIXDirInfo::ShowFiles
	(
	const JBoolean showFiles
	)
{
	if (showFiles != itsShowFilesFlag)
		{
		itsShowFilesFlag = showFiles;
		ApplyFilters(kTrue);
		}
}

/******************************************************************************
 ShowOthers

 ******************************************************************************/

void
JUNIXDirInfo::ShowOthers
	(
	const JBoolean showOthers
	)
{
	if (showOthers != itsShowOthersFlag)
		{
		itsShowOthersFlag = showOthers;
		ApplyFilters(kTrue);
		}
}

/******************************************************************************
 ChangeSort

 ******************************************************************************/

void
JUNIXDirInfo::ChangeSort
	(
	JCompareUNIXEntries*			f,
	const JOrderedSetT::SortOrder	order
	)
{
	Broadcast(ContentsWillBeUpdated());

	itsDirEntries->SetCompareFunction(f);
	itsDirEntries->SetSortOrder(order);
	itsDirEntries->Sort();

	itsVisEntries->SetCompareFunction(f);
	itsVisEntries->SetSortOrder(order);
	itsVisEntries->Sort();

	Broadcast(ContentsChanged());
}

/******************************************************************************
 ChangeProgressDisplay

	We take ownership of the object and will delete it when appropriate.

 ******************************************************************************/

void
JUNIXDirInfo::ChangeProgressDisplay
	(
	JProgressDisplay* pg
	)
{
	assert( pg != NULL );

	delete itsPG;
	itsPG = pg;
}

/******************************************************************************
 UseDefaultProgressDisplay

 ******************************************************************************/

void
JUNIXDirInfo::UseDefaultProgressDisplay()
{
	delete itsPG;
	itsPG = NULL;
}

/******************************************************************************
 FindEntry

	Returns kTrue if an entry with the given name exists.

 ******************************************************************************/

JBoolean
JUNIXDirInfo::FindEntry
	(
	const JCharacter*	name,
	JIndex*				index
	)
	const
{
	JUNIXDirEntry target(name, 0);
	return itsAlphaEntries->SearchSorted(&target, JOrderedSetT::kFirstMatch, index);
}

/******************************************************************************
 ClosestMatch

	Returns the index of the closest match for the given name prefix.

 ******************************************************************************/

JBoolean
JUNIXDirInfo::ClosestMatch
	(
	const JCharacter*	prefixStr,
	JIndex*				index
	)
	const
{
	JUNIXDirEntry target(prefixStr, 0);
	JBoolean found;
	*index = itsAlphaEntries->SearchSorted1(&target, JOrderedSetT::kFirstMatch, &found);
	if (*index > itsAlphaEntries->GetElementCount())		// insert beyond end of list
		{
		*index = itsAlphaEntries->GetElementCount();
		}
	return JConvertToBoolean( *index > 0 );
}

/*****************************************************************************
 GoUp

 ******************************************************************************/

JError
JUNIXDirInfo::GoUp()
{
	JString theCWD = *itsCWD;

	// strip trailing slashes

	JStripTrailingDirSeparator(&theCWD);
	if (JIsRootDirectory(theCWD))
		{
		return JBadPath();
		}

	// change directory

	JString newCWD, name;
	if (JSplitPathAndName(theCWD, &newCWD, &name))
		{
		return GoTo(newCWD);
		}
	else
		{
		return JBadPath();
		}
}

/*****************************************************************************
 GoDown

 ******************************************************************************/

JError
JUNIXDirInfo::GoDown
	(
	const JCharacter* dirName
	)
{
	const JString theCWD = *itsCWD + dirName;
	return GoTo(theCWD);
}

/*****************************************************************************
 GoToClosest

	If the directory exists, go to it.  Otherwise, go as far down the
	directory tree as possible towards the specified directory.

	As an example, /usr/include/junk doesn't normally exist, so it will
	go to /usr/include instead.

 ******************************************************************************/

void
JUNIXDirInfo::GoToClosest
	(
	const JCharacter* origDirName
	)
{
	const JString dirName = JGetClosestDirectory(origDirName);
	const JError err      = GoTo(dirName);
	assert( err.OK() );
}

/*****************************************************************************
 GoTo

 ******************************************************************************/

JError
JUNIXDirInfo::GoTo
	(
	const JCharacter* origDirName
	)
{
	if (JStringEmpty(origDirName))
		{
		return JBadPath();
		}

	JString dirName = origDirName;
	JAppendDirSeparator(&dirName);
	if (dirName.GetFirstCharacter() != '/')
		{
		dirName.Prepend(JGetCurrentDirectory());
		}

	if (dirName != *itsCWD)
		{
		const JString origCWD = *itsCWD;

		*itsCWD = dirName;
		const JError err = BuildInfo();
		if (err.OK())
			{
			Broadcast(PathChanged());
			}
		else
			{
			*itsCWD = origCWD;
			}

		return err;
		}

	else
		{
		return JNoError();
		}
}

#ifndef __MWERKS__

/*****************************************************************************
 BuildInfo (private)

	If you add error conditions, remember to update OKToCreateUNIXDirInfo().

 ******************************************************************************/

JError
JUNIXDirInfo::BuildInfo()
{
	if (!JDirectoryReadable(*itsCWD))
		{
		return JAccessDenied();
		}

	const JString origDir = JGetCurrentDirectory();

	JError err = JChangeDirectory(*itsCWD);
	if (!err.OK())
		{
		return err;
		}

	// clear old information

	itsDirEntries->DeleteAll();

	// update instance variables

	struct stat stbuf;
	stat(*itsCWD, &stbuf);
	itsIsWritableFlag = JDirectoryWritable(*itsCWD);
	itsModTime        = stbuf.st_mtime;
	itsStatusTime     = stbuf.st_ctime;

	// process files in the directory

	JLatentPG pg(itsContentRegex == NULL ? 100 : 10);
	if (itsPG != NULL)
		{
		pg.SetPG(itsPG, kFalse);
		}
	pg.VariableLengthProcessBeginning("Scanning directory...", kTrue, kFalse);

	DIR* dir = opendir(".");
	assert( dir != NULL );

	#ifdef J_USE_READDIR_R
	struct dirent* data = (struct dirent*)
		calloc(1, sizeof(struct dirent) + _POSIX_PATH_MAX);
	assert( data != NULL );
	#endif

	struct dirent* direntry;
	#ifdef J_USE_READDIR_R
	while ((direntry = readdir_r(dir, data)) != NULL)
	#else
	while ((direntry = readdir(dir)) != NULL)
	#endif
		{
		if (strcmp(direntry->d_name, ".") == 0 ||
			strcmp(direntry->d_name, "..") == 0)
			{
			continue;
			}

		JUNIXDirEntry* newEntry = new JUNIXDirEntry(*itsCWD, direntry->d_name);
		assert( newEntry != NULL );
		if (MatchesContentFilter(*newEntry))
			{
			itsDirEntries->InsertSorted(newEntry, kTrue);
			}
		else
			{
			delete newEntry;
			}

		if (!pg.IncrementProgress())
			{
			break;
			}
		}

	#ifdef J_USE_READDIR_R
	free(data);
	#endif

	pg.ProcessFinished();

	closedir(dir);
	err = JChangeDirectory(origDir);
	assert( err.OK() );

	ApplyFilters(kFalse);
	return JNoError();
}

/*****************************************************************************
 Update

	Returns kTrue if anything needed to be updated.

 ******************************************************************************/

JBoolean
JUNIXDirInfo::Update()
{
	struct stat info;
	if (lstat(*itsCWD, &info) != 0 || stat(*itsCWD, &info) != 0 ||
		itsModTime != (time_t) info.st_mtime)
		{
		ForceUpdate();
		return kTrue;
		}
	else if (itsStatusTime != (time_t) info.st_ctime &&
			 JDirectoryReadable(*itsCWD))
		{
		itsStatusTime     = info.st_ctime;
		itsIsWritableFlag = JDirectoryWritable(*itsCWD);
		Broadcast(PermissionsChanged());
		return kTrue;
		}
	else if (itsStatusTime != (time_t) info.st_ctime)
		{
		ForceUpdate();
		return kTrue;
		}
	else
		{
		return kFalse;
		}
}

#endif

/*****************************************************************************
 ForceUpdate

 ******************************************************************************/

void
JUNIXDirInfo::ForceUpdate()
{
	if (JDirectoryExists(*itsCWD))
		{
		Broadcast(ContentsWillBeUpdated());

		const JError err = BuildInfo();
		if (err.OK())
			{
			return;
			}
		}

	// directory doesn't exist or BuildInfo() failed

	JString dir;
	if (!JGetHomeDirectory(&dir))
		{
		dir = "/";
		}
	GoTo(dir);
}

/******************************************************************************
 SetWildcardFilter

 ******************************************************************************/

void
JUNIXDirInfo::SetWildcardFilter
	(
	const JCharacter* filterStr
	)
{
	JString regexStr;
	if (!BuildRegexFromWildcardFilter(filterStr, &regexStr))
		{
		ClearWildcardFilter();
		}
	else if (itsNameRegex == NULL || regexStr != itsNameRegex->GetPattern())
		{
		delete itsNameRegex;
		itsNameRegex = new JRegex(regexStr);
		assert( itsNameRegex != NULL );

		ApplyFilters(kTrue);
		}
}

/******************************************************************************
 ClearWildcardFilter

 ******************************************************************************/

void
JUNIXDirInfo::ClearWildcardFilter()
{
	if (itsNameRegex != NULL)
		{
		delete itsNameRegex;
		itsNameRegex = NULL;
		ApplyFilters(kTrue);
		}
}

/******************************************************************************
 BuildRegexFromWildcardFilter (static)

	Converts a wildcard filter ("*.cc *.h") to a regex ("^.*\.cc$|^.*\.h$").
	Returns kFalse if the given wildcard filter string is empty.

 ******************************************************************************/

JBoolean
JUNIXDirInfo::BuildRegexFromWildcardFilter
	(
	const JCharacter*	origFilterStr,
	JString*			regexStr
	)
{
	regexStr->Clear();

	JString filterStr = origFilterStr;
	filterStr.TrimWhitespace();

	if (filterStr.IsEmpty())
		{
		return kFalse;
		}

	JIndex index;
	while (filterStr.LocateSubstring(" ", &index))
		{
		assert( index > 1 );
		const JString str = filterStr.GetSubstring(1, index-1);

		AppendRegex(str, regexStr);

		filterStr.RemoveSubstring(1, index);
		filterStr.TrimWhitespace();
		}

	assert( !filterStr.IsEmpty() );
	AppendRegex(filterStr, regexStr);
	return kTrue;
}

/******************************************************************************
 AppendRegex (static private)

 ******************************************************************************/

void
JUNIXDirInfo::AppendRegex
	(
	const JCharacter*	origStr,
	JString*			regexStr
	)
{
JIndex i;

	JString str = origStr;

	// Convert wildcard multiples (*) to regex multiples (.*)
	// and wildcard singles (?) to regex singles (.)

	for (i = str.GetLength(); i>=1; i--)
		{
		const JCharacter c = str.GetCharacter(i);
		if (c == '*')
			{
			str.InsertSubstring(".", i);
			}
		else if (c == '?')
			{
			str.SetCharacter(i, '.');
			}
		else if (JRegex::NeedsBackslashToBeLiteral(c))
			{
			str.InsertSubstring("\\", i);
			}
		}

	// Add instructions that it must match the entire file name.

	str.Prepend("^");
	str.Append("$");

	// append to regexStr

	if (!regexStr->IsEmpty())
		{
		*regexStr += "|";
		}

	*regexStr += str;
}

/******************************************************************************
 ApplyFilters (private)

	update should be kTrue if the contents are merely being updated.

 ******************************************************************************/

void
JUNIXDirInfo::ApplyFilters
	(
	const JBoolean update
	)
{
	if (update)
		{
		Broadcast(ContentsWillBeUpdated());
		}

	itsVisEntries->RemoveAll();
	itsAlphaEntries->RemoveAll();

	const JSize count = itsDirEntries->GetElementCount();
	for (JIndex i=1; i<=count; i++)
		{
		JUNIXDirEntry* entry = itsDirEntries->NthElement(i);
		if (IsVisible(*entry))
			{
			itsVisEntries->Append(entry);
			itsAlphaEntries->InsertSorted(entry, kTrue);
			}
		}

	Broadcast(ContentsChanged());
}

/******************************************************************************
 IsVisible (virtual protected)

 ******************************************************************************/

JBoolean
JUNIXDirInfo::IsVisible
	(
	const JUNIXDirEntry& entry
	)
	const
{
	const JUNIXDirEntry::Type type = entry.GetType();
	const JString name             = entry.GetName();

	if (name.GetFirstCharacter() == '.' && name != ".." && !itsShowHiddenFlag)
		{
		return kFalse;
		}

	if (type == JUNIXDirEntry::kDir || type == JUNIXDirEntry::kDirLink)
		{
		return itsShowDirsFlag;
		}
	else if (type == JUNIXDirEntry::kFile || type == JUNIXDirEntry::kFileLink ||
			 type == JUNIXDirEntry::kBrokenLink)
		{
		return JI2B( itsShowFilesFlag && MatchesNameFilter(entry) );
		}
	else if (type == JUNIXDirEntry::kUnknown || type == JUNIXDirEntry::kUnknownLink)
		{
		return JI2B( itsShowOthersFlag && MatchesNameFilter(entry) );
		}
	else if (type == JUNIXDirEntry::kDoesNotExist)
		{
		return kFalse;
		}
	else
		{
		assert( 0 );	// this should never happen
		return kFalse;
		}
}

/******************************************************************************
 MatchesNameFilter (protected)

 ******************************************************************************/

JBoolean
JUNIXDirInfo::MatchesNameFilter
	(
	const JUNIXDirEntry& entry
	)
	const
{
	if (itsNameRegex != NULL)
		{
		const JString name = entry.GetName();
		return itsNameRegex->Match(name);
		}
	else
		{
		return kTrue;
		}
}

/******************************************************************************
 SetContentFilter

 ******************************************************************************/

JError
JUNIXDirInfo::SetContentFilter
	(
	const JCharacter* regexStr
	)
{
	if (itsContentRegex != NULL && regexStr == itsContentRegex->GetPattern())
		{
		return JNoError();
		}

	JBoolean hadFilter = kTrue;
	JString prevPattern;
	if (itsContentRegex == NULL)
		{
		hadFilter       = kFalse;
		itsContentRegex = new JRegex;
		assert( itsContentRegex != NULL );
		itsContentRegex->SetSingleLine();
		}
	else
		{
		prevPattern = itsContentRegex->GetPattern();
		}

	JError err = itsContentRegex->SetPattern(regexStr);
	if (err.OK())
		{
		ForceUpdate();
		}
	else if (hadFilter)
		{
		err = itsContentRegex->SetPattern(prevPattern);
		assert( err.OK() );
		}
	else
		{
		delete itsContentRegex;
		itsContentRegex = NULL;
		}
	return err;
}

/******************************************************************************
 ClearContentFilter

 ******************************************************************************/

void
JUNIXDirInfo::ClearContentFilter()
{
	if (itsContentRegex != NULL)
		{
		delete itsContentRegex;
		itsContentRegex = NULL;
		ForceUpdate();
		}
}

/******************************************************************************
 MatchesContentFilter (protected)

 ******************************************************************************/

JBoolean
JUNIXDirInfo::MatchesContentFilter
	(
	const JUNIXDirEntry& entry
	)
	const
{
	if (itsContentRegex == NULL || entry.IsDirectory())
		{
		return kTrue;
		}
	else
		{
		return entry.MatchesContentFilter(*itsContentRegex);
		}
}

#include <JArray.tmpls>
#define JTemplateType JUNIXDirEntry
#include <JPtrArray.tmpls>
#undef JTemplateType
