/*
** This program is free software; 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 1, or (at your option)
** any later version.

** This program 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 General Public License for more details.

** You should have received a copy of the GNU General Public License
** along with this program; if not, write to the Free Software
** Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
*/

/*
 * Author : Alexandre Parenteau <aubonbeurre@hotmail.com> --- April 1998
 */

/*
 * CvsEntries.cpp --- adaptation from cvs/src/entries.c
 */

#include "stdafx.h"

#ifdef TARGET_OS_MAC
#	include "GUSIInternal.h"
#	include "GUSIFileSpec.h"
#	define S_IWRITE S_IWUSR
#endif

#include <stdlib.h>
#include <cstring>
#include <sys/types.h>
#include <sys/stat.h>

#include <sstream>
#include <algorithm>

#include "CvsEntries.h"
#include "AppConsole.h"
#include "getline.h"
#include "FileTraversal.h"

#ifdef WIN32
#	ifdef _DEBUG
#		define new DEBUG_NEW
#		undef THIS_FILE
		static char THIS_FILE[] = __FILE__;
#	endif
#	include "WinCvsDebug.h"
#endif /* WIN32 */

#if TIME_WITH_SYS_TIME
#	include <sys/time.h>
#	include <time.h>
#elif HAVE_SYS_TIME_H
#	include <sys/time.h>
#else
#	include <time.h>
#endif

#ifndef NEW
#define NEW new
#endif

#ifndef NAMESPACE
#       if defined(_MSC_VER) || defined(__MWERKS__) || (__GNUC__ > 2)
#               define NAMESPACE(w) w::
#       else
#               define NAMESPACE(w) ::
#       endif
#endif

//////////////////////////////////////////////////////////////////////////
// Helper functions

/// Months table used to parse the date string
NAMESPACE(std) string gStrMonths[] = {"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"};

/*!
	Extract the date-time information from the string
	\param strTime Date-time string as returned by <b>asctime</b> function (CVS/Entries file format)
	\return The date-time as time_t value or (time_t)-1 as returned by mktime
*/
time_t GetDateTime(const char* strTime)
{
	struct tm tmTime = { 0 };

	NAMESPACE(std) string stringTime = strTime;

	if( stringTime.empty() )
	{
		return -1;
	}

	// Replace the time separator with spaces to help with parsing
	replace(stringTime.begin(), stringTime.end(), ':', ' ');

	// Parse
	NAMESPACE(std) stringstream parser;
	parser << stringTime;
	
	NAMESPACE(std) string strDay;
	NAMESPACE(std) string strMonth;

	parser >> strDay;
	parser >> strMonth;
	{
		// Find out the month
		tmTime.tm_mon = -1;
		NAMESPACE(std) string* month = find(gStrMonths, gStrMonths + 12, strMonth);

		if( month != gStrMonths + 12 )
		{
			tmTime.tm_mon = month - gStrMonths;
		}
		else
		{
#ifdef WIN32
			ASSERT(FALSE);	// unknown month
#endif
			return -1;
		}
	}

	parser >> tmTime.tm_mday;
	parser >> tmTime.tm_hour;
	parser >> tmTime.tm_min;
	parser >> tmTime.tm_sec;
	
	parser >> tmTime.tm_year;
	tmTime.tm_year -= 1900;

    tmTime.tm_isdst = -1;

	return mktime(&tmTime);
}

/*!
	Clone the string	
	\param cp String to be cloned
	\return Pointer to the cloned string
	\note You must release memory allocated for the string
*/
static const char* CloneStr(const char* cp)
{
	if( cp && *cp )
	{
		cp = strdup(cp);
		if( !cp )
		{
			throw std::bad_alloc();
		}
	}
	else
	{
		cp = 0L;
	}

	return cp;
}

/*!
	Dispose cloned string
	\param cp Pointer to the cloned string to be released
*/
static void DisposeStr(const char* cp) 
{
	if( cp )
	{
		free((void*)cp);
	}
}

//////////////////////////////////////////////////////////////////////////
// EntnodePath

EntnodePath::EntnodePath(const char* path) : fullpathname(0L), ref(1)
{
	fullpathname = CloneStr(path);
}

EntnodePath::~EntnodePath()
{
	DisposeStr(fullpathname);
}

/*!
	Get the full path for a given name
	\param resPath Return full path
	\param name File name
	\return Full path
*/
const char* EntnodePath::GetFullPathName(UStr& resPath, const char* name) const
{
	resPath = fullpathname;
	if( !resPath.endsWith(kPathDelimiter) )
		resPath << kPathDelimiter;

	resPath << name;

	return resPath;
}

//////////////////////////////////////////////////////////////////////////
// EntnodeData

UStr EntnodeData::sbuffer;

EntnodeData::EntnodeData(const char* name, EntnodePath* path)
	: ref(1), missing(false), visited(false), unknown(false),
	  unmodified(true), ignored(false), locked(false), removed(false),
	  user(0L), desc(0L), fullpathname(0L)
{
#if TARGET_RT_MAC_MACHO
	memset(&macspec, 0, sizeof(macspec));
#endif

	user = CloneStr(name);
	fullpathname = path->Ref();
}

EntnodeData::~EntnodeData()
{
	DisposeStr(user);
	
	if( fullpathname )
		fullpathname->UnRef();
}

/// Operator []
const char* EntnodeData::operator[](int index) const
{
	switch( index )
	{
	case kName:
		return user;
	case kStatus:
		return desc;
	case kPath:
		return GetFullPathName(sbuffer);
	default:
		break;
	}

	return 0L;
}

//////////////////////////////////////////////////////////////////////////
// EntnodeFile

EntnodeFile::EntnodeFile(const char* name, EntnodePath* path, const char* newvn /*= 0L*/,
	const char* newts /*= 0L*/, const char* newoptions /*= 0L*/, const char* newtag /*= 0L*/,
	const char* newdate /*= 0L*/, const char* newts_conflict /*= 0L*/) : EntnodeData(name, path)
{
	vn = 0L;
	ts = 0L;
	option = 0L;
	tag = 0L;
	date = 0L;
	ts_conflict = 0L;

	vn = CloneStr(newvn);
	ts = CloneStr(newts);
	option = CloneStr(newoptions);
	tag = CloneStr(newtag);
	date = CloneStr(newdate);
	ts_conflict = CloneStr(newts_conflict);

	if( newvn != 0L && newvn[0] == '-' )
		SetRemoved(true);
}

EntnodeFile::~EntnodeFile()
{
	DisposeStr(vn);
	DisposeStr(ts);
	DisposeStr(option);
	DisposeStr(tag);
	DisposeStr(date);
	DisposeStr(ts_conflict);
}

/// Operator [] to access fields
const char* EntnodeFile::operator[](int index) const
{
	switch( index )
	{
	case kVN:
		return vn;
	case kTS:
		return ts;
	case kOption:
		return option;
	case kTag:
		return tag != 0L ? tag : date;
	case kConflict:
		return ts_conflict;
	default:
		break;
	}

	return EntnodeData::operator[](index);
}

/// Get the type
ent_type EntnodeFile::GetType() const
{
	return ENT_FILE;
}

/// Get the conflict marker
const char* EntnodeFile::GetConflict() const
{
	return ts_conflict;
}

//////////////////////////////////////////////////////////////////////////
// EntnodeFolder

EntnodeFolder::EntnodeFolder(const char* name, EntnodePath* path, const char* newvn /*= 0L*/,
	const char* newts /*= 0L*/, const char* newoptions /*= 0L*/, const char* newtag /*= 0L*/,
	const char* newdate /*= 0L*/, const char* newts_conflict /*= 0L*/) : EntnodeData(name, path)
{
}

EntnodeFolder::~EntnodeFolder()
{
}

/// Operator [] to access fields
const char* EntnodeFolder::operator[](int index) const
{
	return EntnodeData::operator[](index);
}

/// Get the type
ent_type EntnodeFolder::GetType() const
{
	return ENT_SUBDIR;
}

//////////////////////////////////////////////////////////////////////////
// ENTNODE

/// Operator =
ENTNODE& ENTNODE::operator=(const ENTNODE& anode)
{
	EntnodeData* oldData = shareData;
	shareData = anode.shareData;
	
	if( shareData != 0L )
	{
		shareData->Ref();
	}
	
	if( oldData != 0L )
	{
		oldData->UnRef();			
	}

	return *this;
}

//////////////////////////////////////////////////////////////////////////
// Static functions

/*!
	Get the next real Entries line
	\param fpin File
	\param path Path
	\param cmd Command
	\return The next real Entries line or NULL on end of file
	\note On error, prints an error message and returns NULL
*/
static EntnodeData* GetNextEntriesLine(FILE* fpin, const char* path, char* cmd = 0L)
{
	EntnodeData* ent;
	char* line = 0L;
	size_t line_chars_allocated = 0;
	register char* cp;
	ent_type type;
	char* l, *user, *vn, *ts, *options;
	char* tag_or_date, *tag, *date, *ts_conflict;
	int line_length;

	EntnodePath* thePath = new EntnodePath(path);

	ent = 0L;
	while( (line_length = getline(&line, &line_chars_allocated, fpin)) > 0 )
	{
		l = line;

		/* If CMD is not NULL, we are reading an Entries.Log file.
		Each line in the Entries.Log file starts with a single
		character command followed by a space.	For backward
		compatibility, the absence of a space indicates an add
		command.  */
		if( cmd != 0L )
		{
			if( l[1] != ' ' )
				*cmd = 'A';
			else
			{
				*cmd = l[0];
				l += 2;
			}
		}

		type = ENT_FILE;

		if( l[0] == 'D' )
		{
			type = ENT_SUBDIR;
			++l;
			/* An empty D line is permitted; it is a signal that this
			Entries file lists all known subdirectories.  */
		}

		if( l[0] != '/' )
			continue;

		user = l + 1;
		if( (cp = strchr (user, '/')) == 0L )
			continue;

		*cp++ = '\0';
		vn = cp;
		if( (cp = strchr (vn, '/')) == 0L )
			continue;

		*cp++ = '\0';
		ts = cp;
		if( (cp = strchr (ts, '/')) == 0L )
			continue;

		*cp++ = '\0';
		options = cp;
		if( (cp = strchr (options, '/')) == 0L )
			continue;

		*cp++ = '\0';
		tag_or_date = cp;

		if( (cp = strchr (tag_or_date, '\n')) == 0L
#ifdef TARGET_OS_MAC
			&& (cp = strchr (tag_or_date, '\r')) == 0L
#endif
		)
			continue;

		*cp = '\0';
		tag = (char*) 0L;
		date = (char*) 0L;
		if( *tag_or_date == 'T' )
			tag = tag_or_date + 1;
		else if( *tag_or_date == 'D' )
			date = tag_or_date + 1;

		if( (ts_conflict = strchr (ts, '+')) )
			*ts_conflict++ = '\0';

		if( user && strlen(user) )
		{
			if( type == ENT_SUBDIR )
				ent = NEW EntnodeFolder(user, thePath);
			else
				ent = NEW EntnodeFile(user, thePath, vn, ts, options, tag, date, ts_conflict);
		}
		else
		{
			continue;
		}

		break;
	}

	if( line_length < 0 && !feof(fpin) )
		cvs_err("Cannot read entries file (error %d)\n", errno);

	free(line);

	thePath->UnRef();

	return ent;
}

/*!
	Read the entries file into a list, hashing on the file name
	\param entries Return file list
	\param fullpath Path to read files from
	\return true on success, false otherwise
	\note UPDATE_DIR is the name of the current directory, for use in error
          messages, or NULL if not known (that is, no-one has gotten around
          to updating the caller to pass in the information)
*/
bool Entries_Open(CSortList<ENTNODE>& entries, const char* fullpath)
{
	EntnodeData* ent;

	UStr adminPath(fullpath);
	if( !adminPath.endsWith(kPathDelimiter) )
		adminPath << kPathDelimiter;

	adminPath << "CVS";
	adminPath << kPathDelimiter;
	
	entries.Reset();

	UStr filename = adminPath;
	filename << "Entries";
	
	FILE* fpin = fopen(filename, "r");
	if( fpin == 0L )
		return false;

	while( (ent = GetNextEntriesLine(fpin, fullpath)) != 0L )
	{
		ENTNODE newnode(ent);
		ent->UnRef();

		if( entries.InsideAndInsert(newnode) )
		{
			cvs_err("Warning : duplicated entry in the 'CVS/Entries' file in directory '%s'\n", fullpath);
		}
	}

	fclose(fpin);

	filename = adminPath;
	filename << "Entries.log";
	
	fpin = fopen(filename, "r");
	if( fpin != 0L )
	{
		char cmd;

		while( (ent = GetNextEntriesLine(fpin, &cmd)) != 0L )
		{
			ENTNODE newnode(ent);
			ent->UnRef();

			switch (cmd)
			{
			case 'A':
				if(entries.InsideAndInsert(newnode))
				{
					// don't care
				}
				break;
			case 'R':
				entries.Delete(newnode);
				break;
			default:
				/* Ignore unrecognized commands.  */
				break;
			}
		}

		fclose(fpin);
	}

	return true;
}

/*!
	Verify that file is not modified
	\param sb Stat information
	\param ts Timestamp
	\return true if file is unmodified, false otherwise
	\note Contains the work-around for DST bug
*/
static bool unmodified(const struct stat& sb, const char* ts)
{
	char* cp;
	struct tm* tm_p;
	struct tm local_tm;
	
	/* We want to use the same timestamp format as is stored in the
	st_mtime.  For unix (and NT I think) this *must* be universal
	time (UT), so that files don't appear to be modified merely
	because the timezone has changed.  For VMS, or hopefully other
	systems where gmtime returns NULL, the modification time is
	stored in local time, and therefore it is not possible to cause
	st_mtime to be out of sync by changing the timezone.  */
	
	tm_p = gmtime(&sb.st_mtime);
	if( tm_p )
	{
		memcpy(&local_tm, tm_p, sizeof (local_tm));
		cp = asctime(&local_tm);	/* copy in the modify time */
	}
	else
		cp = ctime(&sb.st_mtime);

	cp[24] = 0;

	/* Deal with the differences in timestamps - POSIX uses space filled, NT uses zero filled */
	if( cp[8]=='0' && ts[8]==' ' ) 
		cp[8]=' ';
	if( cp[8]==' ' && ts[8]=='0' ) 
		cp[8]='0';

	return strcmp(cp, ts) == 0;
}

/*!
	Fill an ENTNODE when the file appears on the disk
	\param path Path
	\param entries Entries list
	\param name Filename
	\param finfo File stat info
	\param isFolder true for folder, false otherwise
	\param ignlist Ignore list
	\return The reference to the node
	\note It sets some flags like "visited", "unknown", "ignored" etc.
*/
EntnodeData* Entries_SetVisited(const char* path, CSortList<ENTNODE>& entries, const char* name,
	const struct stat& finfo, bool isFolder, const std::vector<CStr>* ignlist /*= 0L*/)
{
	int index;
	bool isCvs = false;

	EntnodePath* thePath = new EntnodePath(path);

	if( isFolder )
	{
		EntnodeFolder* adata = NEW EntnodeFolder(name, thePath);
		ENTNODE anode(adata);
		adata->UnRef();
		isCvs = entries.InsideAndInsert(anode, &index);
	}
	else
	{
		EntnodeFile* adata = NEW EntnodeFile(name, thePath);
		ENTNODE anode(adata);
		adata->UnRef();
		isCvs = entries.InsideAndInsert(anode, &index);
	}

	thePath->UnRef();

	const ENTNODE& theNode = entries.Get(index);
	EntnodeData* data = theNode.Data();
	data->SetVisited(true);
	
	if( !isCvs )
	{
		data->SetUnknown(true);
		if( ignlist != 0L && MatchIgnoredList(name, *ignlist) )
			data->SetIgnored(true);

		// the folder may have some cvs informations in it, despite the fact
		// that it is not referenced by the parent directory, so try
		// to figure it.

		if( !data->IsIgnored() )
		{
			CStr cvsFile(path);
			if( !cvsFile.endsWith(kPathDelimiter) )
				cvsFile << kPathDelimiter;

			cvsFile << name;
			if( !cvsFile.endsWith(kPathDelimiter) )
				cvsFile << kPathDelimiter;

			cvsFile << "CVS";
			struct stat sb;
			if( stat(cvsFile, &sb) != -1 && S_ISDIR(sb.st_mode) )
			{
				data->SetUnknown(false);
			}
		}
	}

	if( isFolder )
	{
		if( data->IsIgnored() )
		{
			data->SetDesc("Ignored Folder");
		}
		else if( data->IsUnknown() )
		{
			data->SetDesc("NonCvs Folder");
		}
		else
		{
			data->SetDesc("Folder");
		}
	}
	else
	{
		const char* ts = (*data)[EntnodeFile::kTS];
		if( ts == 0L )
			data->SetUnmodified(true);
		else
			data->SetUnmodified(unmodified(finfo, ts));

#if defined(WIN32)
		if( GetWinCvsDebugMaskBit(wcvsdbg_trace_modified_checks_bit) )
		{
			char scratch[128];

			/* copied from unmodified... */
			char* cp;
			struct tm* tm_p;
			struct tm local_tm;
			tm_p = gmtime(&finfo.st_mtime);
			if( tm_p )
			{
				memcpy(&local_tm, tm_p, sizeof(local_tm));
				cp = asctime(&local_tm);	/* copy in the modify time */
			}
			else
			{
				cp = "NULL";
			}

			_snprintf(scratch, 128, 
				"Checking timestamps for %s: Entries: %s, file: %s --> %s\n",
                name, ts, cp, 
                data->IsUnmodified() ? "unmodified" : "modified");
			
			cvs_err(scratch);
			OutputDebugString(scratch);
        }
#endif

		data->SetLocked((finfo.st_mode & S_IWRITE) == 0);
		
		const char* info = 0L;
		if( data->IsIgnored() )
		{
			data->SetDesc("Ignored");
		}
		else if( data->IsUnknown() )
		{
			data->SetDesc("NonCvs file");
		}
		else if( data->GetConflict() != 0L )
		{
			data->SetDesc("Conflict");
		}
		else if( (info = (*data)[EntnodeFile::kOption]) != 0L && strcmp(info, "-kb") == 0 )
		{
			data->SetDesc(data->IsUnmodified() ? "Binary" : "Mod. Binary");
		}
		else if( (info = (*data)[EntnodeFile::kOption]) != 0L && strcmp(info, "-ku") == 0 )
		{
			data->SetDesc(data->IsUnmodified() ? "Unicode" : "Mod. Unicode");
		}
		else
		{
			data->SetDesc(data->IsUnmodified() ? "File" : "Mod. File");
		}
	}

	return data;
}

/*!
	Mark the missing entries
	\param entries Entries list
	\return true on success, false otherwise
	\note Called after the traversal marks the entries that didn't have "Entries_SetVisited" call associated
*/
void Entries_SetMissing(CSortList<ENTNODE>& entries)
{
	int numEntries = entries.NumOfElements();
	for(int i = 0; i < numEntries; i++)
	{
		const ENTNODE& theNode = entries.Get(i);
		EntnodeData* data = theNode.Data();
		
		if( data->IsVisited() )
			continue;

		data->SetMissing(true);
		data->SetDesc(data->IsRemoved() ? "Removed" : "Missing");
	}
}

/*!
	Read the tag file
	\param tag Return tag
	\param fullpath Path
	\return false if no CVS/Tag, true otherwise
*/
bool Tag_Open(CStr& tag, const char* fullpath)
{
	tag = "";
	CStr tagFile(fullpath);
	if( !tagFile.endsWith(kPathDelimiter) )
		tagFile << kPathDelimiter;
	
	tagFile << "Tag";

	FILE* fpin = fopen(tagFile, "r");
	if( fpin == 0L )
		return false;

	char* line = 0L;
	size_t line_chars_allocated = 0;
	int line_length;
	if( (line_length = getline(&line, &line_chars_allocated, fpin)) > 0 )
	{
		char* tmp = strchr(line, '\n');
		if( tmp != 0L )
			*tmp = '\0';
		
		if( line[0] == 'T' )
			tag = line + 1;
	}

	if( line != 0L )
		free(line);

	if( line_length < 0 && !feof(fpin) )
	{
		cvs_err("Cannot open Tag file in '%s' (error %d)\n", fullpath, errno);
		return false;
	}

	fclose(fpin);

	return !tag.empty();
}
