/*
** 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 macintosh
#	include "GUSIInternal.h"
#	include "GUSIFileSpec.h"
#	define S_IWRITE S_IWUSR
#endif

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

#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

static const char* cloneStr(const char* cp)
{
	if(cp && *cp)
	{
		cp = strdup(cp);
		if (!cp)
		{
			throw ENOMEM;
		}
	}
	else
	{
		cp = 0L;
	}
	return cp;
}

static void disposeStr(const char* cp) 
{
	if (cp)
	{
		free((void*)cp);
	}
}

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

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

const char* EntnodePath::GetFullPathName(UStr & resPath, const char* name) const
{
	resPath = fullpathname;
	if(!resPath.endsWith(kPathDelimiter))
		resPath << kPathDelimiter;
	resPath << name;

	return resPath;
}

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)
{
#ifdef macintosh
	macspec.vRefNum = 0;
	macspec.parID = 0;
	macspec.name[0] = '\0';
#endif /* macintosh */
	user = cloneStr(name);
	fullpathname = path->Ref();
}

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

void EntnodeData::SetDesc(const char *newdesc)
{
	desc = newdesc;
}

UStr EntnodeData::sbuffer;

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

EntnodeFile::EntnodeFile(const char* name, EntnodePath *path, const char *newvn,
	const char *newts, const char *newoptions, const char *newtag,
	const char *newdate, const char *newts_conflict) : 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);
}

EntnodeFolder::~EntnodeFolder()
{
}

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

/* Return the next real Entries line.  On end of file, returns NULL.
   On error, prints an error message and returns NULL.	*/

static EntnodeData * fgetentent(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);
	if(thePath == 0L)
		throw ENOMEM;

	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 macintosh
			&& (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(type == ENT_SUBDIR)
			ent = NEW EntnodeFolder(user, thePath);
		else
			ent = NEW EntnodeFile(user, thePath, vn, ts, options,
				tag, date, ts_conflict);
		if(ent == 0L)
		{
			errno = ENOMEM;
			cvs_err("Out of memory\n");
		}
		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.

   UPDATE_DIR is the name of the current directory, for use in error
   messages, or NULL if not known (that is, noone 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 = fgetentent (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 = fgetentent (fpin, &cmd)) != 0L)
		{
			ENTNODE newnode(ent);
			ent->UnRef();

			switch (cmd)
			{
			case 'A':
				if(entries.InsideAndInsert(newnode))
				{
					// dont care
				}
				break;
			case 'R':
				entries.Delete(newnode);
				break;
			default:
				/* Ignore unrecognized commands.  */
				break;
			}
		}
		fclose (fpin);
	}
	return true;
}

// mostly the cvs one...
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;

// [6/8/2001] Jerzy K. - the code has been replaced by the version from cvsnt 
//which seems simpler and giving the same effect
//keep it here for a while and remove eventually 
#if 0	
// Bug 432
	if( cp[8]=='0') cp[8]=' ';
//  The C Standard Library and the Unix/cygwin versions of CVS
//  write a padding space before days 1-9; Visual C++ libraries use
//  a leading 0.  This affects ctime() and asctime() calls.  Bug 432
//  adopts the padding space standard.  The following allows for backward
//  compatibility with a WinCVS working area checked out prior to the
//  Bug 432 fix.  (cp is the timestamp from the stat of the working file;
//  ts is the timestamp as read from CVS/Entries).
	char *ns = (char *)malloc( (strlen(ts) + 1) * sizeof( char ) );
	if ( ns == NULL ) {
		cvs_err("Could not allocate memory for string ns\n");
		return 1;
	}
	strcpy( ns, ts );
	if( ns[8]=='0') ns[8]=' ';
	int compare_result = strcmp(ns, cp);
	free( ns );
	return compare_result == 0;
// Bug 432
#else

	/* 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;
#endif
}

EntnodeData *Entries_SetVisited(const char *path, CSortList<ENTNODE> & entries, const char *name,
	const struct stat & finfo, bool isFolder, const std::vector<CStr> * ignlist)
{
	int index;
	bool isCvs = false;

	EntnodePath *thePath = new EntnodePath(path);
	if(thePath == 0L)
		throw ENOMEM;

	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
		{
			data->SetDesc(data->IsUnmodified() ? "File" : "Mod. File");
		}
	}

	return data;
}

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");
	}
}

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();
}
