// tail.cpp
// 
// Copyright (c) 2005 - 2010 Accenture. All rights reserved.
// This component and the accompanying materials are made available
// under the terms of the "Eclipse Public License v1.0"
// which accompanies this distribution, and is available
// at the URL "http://www.eclipse.org/legal/epl-v10.html".
// 
// Initial Contributors:
// Accenture - Initial contribution
//

#include "character_converter.h"
#include "file_reader.h"
#include "tail.h"

const TInt KBlockSize = 512;
const TText KUnicodeParagraphSeparator = 0x2029;
const TText KUnicodeLineSeparator = 0x2028;
const TText KCarriageReturn = 0x000d;
const TText KLineFeed = 0x000a;


//
// CTrailingLineFinder.
//

class CTrailingLineFinder : public CBase, public MFileReaderObserver
	{
public:
	static CTrailingLineFinder* NewL(TInt aBlockSize, CCharacterConverter& aCharacterConverter);
	~CTrailingLineFinder();
	void Find(CFileReader& aFileReader, const TDesC& aFileName, TInt aNumLines, MTrailingLineFinderObserver& aObserver);
	void Cancel();
	const TDesC& operator[](TInt aIndex) const;
private:
	CTrailingLineFinder(CCharacterConverter& aCharacterConverter);
	void ConstructL(TInt aBlockSize);
	void HandleFileDataL(const TDesC8& aData, TReadType aType, TBool& aContinue);
	void FindNextLine(const TDesC& aData, TInt aOffset, TPtrC& aLine, TBool& aComplete);
	void AddLineL(const TDesC& aLine, TBool aComplete);
private:	// From MFileReaderObserver.
	virtual void HandleFileData(const TDesC8& aData, TReadType aType, TBool& aContinue);
	virtual void HandleFileReadError(TInt aError);
private:
	CFileReader* iFileReader;
	CCharacterConverter& iCharacterConverter;
	MTrailingLineFinderObserver* iObserver;
	HBufC8* iBuf;
	TPtr8 iPtr;
	RPointerArray<HBufC> iTrailingLines;
	TInt iCurrentLineIndex;
	TInt iNumLinesToFind;
	TInt iNumCompleteLinesFound;
	TInt iFilePos;
	TBool iPossiblySplitCrLf;
	TBool iLastLineNotComplete;
	};

CTrailingLineFinder* CTrailingLineFinder::NewL(TInt aBlockSize, CCharacterConverter& aCharacterConverter)
	{
	CTrailingLineFinder* self = new(ELeave) CTrailingLineFinder(aCharacterConverter);
	CleanupStack::PushL(self);
	self->ConstructL(aBlockSize);
	CleanupStack::Pop(self);
	return self;
	}

CTrailingLineFinder::~CTrailingLineFinder()
	{
	delete iBuf;
	iTrailingLines.ResetAndDestroy();
	}

void CTrailingLineFinder::Find(CFileReader& aFileReader, const TDesC& aFileName, TInt aNumLines, MTrailingLineFinderObserver& aObserver)
	{
	ASSERT(!iFileReader);
	iTrailingLines.ResetAndDestroy();
	iFileReader = &aFileReader;
	iObserver = &aObserver;
	iNumLinesToFind = aNumLines;
	iNumCompleteLinesFound = 0;
	iCurrentLineIndex = -1;
	iPossiblySplitCrLf = EFalse;
	iFileReader->Read(aFileName, *this);
	}

void CTrailingLineFinder::Cancel()
	{
	if (iFileReader)
		{
		iFileReader->Cancel();
		}
	}

const TDesC& CTrailingLineFinder::operator[](TInt aIndex) const
	{
	return *iTrailingLines[aIndex];
	}

void CTrailingLineFinder::HandleFileDataL(const TDesC8& aData, TReadType aType, TBool& aContinue)
	{
	iFilePos += aData.Length();
	aContinue = ETrue;
	const TDesC* convertedData = NULL;

	switch (aType)
		{
		case MFileReaderObserver::EFirst:
			{
			convertedData = &iCharacterConverter.ConvertChunkL(aData, CCharacterConverter::EFirst);
			break;
			}
		case MFileReaderObserver::EMiddle:
			{
			convertedData = &iCharacterConverter.ConvertChunkL(aData, CCharacterConverter::EMiddle);
			break;
			}
		case MFileReaderObserver::ELast:
			{
			convertedData = &iCharacterConverter.ConvertChunkL(aData, CCharacterConverter::ELast);
			break;
			}
		default:
			{
			ASSERT(FALSE);
			}
		}

	if (convertedData->Length() > 0)
		{
		TInt offset = 0;
		FOREVER
			{
			TPtrC linePtr;
			TBool completeLine;
			FindNextLine(*convertedData, offset, linePtr, completeLine);
			AddLineL(linePtr, completeLine);
			if (!completeLine)
				{
				break;
				}
			offset += linePtr.Length();
			}
		}

	if (aType == MFileReaderObserver::ELast)
		{
		iFileReader = NULL;
		iObserver->HandleTrailingLines(iCurrentLineIndex + 1, iFilePos);
		}
	}

void CTrailingLineFinder::AddLineL(const TDesC& aLine, TBool aComplete)
	{
	if (iLastLineNotComplete)
		{
		HBufC*& buf = iTrailingLines[iCurrentLineIndex];
		buf = buf->ReAllocL(buf->Length() + aLine.Length());
		buf->Des() += aLine;
		}
	else
		{
		HBufC* line = aLine.AllocLC();
		if (iNumCompleteLinesFound <= iNumLinesToFind)
			{
			++iCurrentLineIndex;
			User::LeaveIfError(iTrailingLines.Append(line));
			}
		else
			{
			delete iTrailingLines[0];
			iTrailingLines.Remove(0);
			iTrailingLines.Append(line); // Will always succeed due to the above removal.
			}
		CleanupStack::Pop(line);
		}

	iLastLineNotComplete = !aComplete;
	if (aComplete)
		{
		++iNumCompleteLinesFound;
		}
	}

CTrailingLineFinder::CTrailingLineFinder(CCharacterConverter& aCharacterConverter)
	: iCharacterConverter(aCharacterConverter), iPtr(NULL, 0)
	{
	}

void CTrailingLineFinder::ConstructL(TInt aBlockSize)
	{
	iBuf = HBufC8::NewL(aBlockSize);
	iPtr.Set(iBuf->Des());
	}

void CTrailingLineFinder::FindNextLine(const TDesC& aData, TInt aOffset, TPtrC& aLine, TBool& aComplete)
	{
	const TText* first = aData.Ptr() + aOffset;
	const TText* p = first;
	const TText* last = first + aData.Length() - aOffset - 1;
	aComplete = EFalse;
	TBool foundLineBreak = EFalse;
	while ((p <= last) && !foundLineBreak)
		{
		if (iPossiblySplitCrLf)
			{
			iPossiblySplitCrLf = EFalse;
			if (*p == KLineFeed)
				{
				aComplete = ETrue;
				foundLineBreak = ETrue;
				}
			}
		else if ((*p == KUnicodeParagraphSeparator) || (*p == KUnicodeLineSeparator) || (*p == KLineFeed))
			{
			aComplete = ETrue;
			foundLineBreak = ETrue;
			}
		else if (*p == KCarriageReturn)
			{
			if (p == last)
				{
				iPossiblySplitCrLf = ETrue;
				}
			else if (*(p + 1) == KLineFeed)
				{
				aComplete = ETrue;
				foundLineBreak = ETrue;
				++p;
				}
			else
				{
				aComplete = ETrue;
				foundLineBreak = ETrue;
				}
			}
		++p;
		}

	aLine.Set(first, p - first);
	}

void CTrailingLineFinder::HandleFileData(const TDesC8& aData, TReadType aType, TBool& aContinue)
	{
	TRAPD(err, HandleFileDataL(aData, aType, aContinue));
	if (err)
		{
		iObserver->HandleTrailingLineError(err);
		}
	}

void CTrailingLineFinder::HandleFileReadError(TInt aError)
	{
	iObserver->HandleTrailingLineError(aError);
	}


//
// CFileWatcher.
//

class CFileWatcher : public CActive
	{
public:
	static CFileWatcher* NewL(RFs& aFs);
	~CFileWatcher();
	void Start(const TDesC& aFileName, MFileChangeObserver& aObserver);
private:
	CFileWatcher(RFs& aFs);
	void Queue();
private:	// From CActive.
	virtual void DoCancel();
	virtual void RunL();
private:
	RFs& iFs;
	TFileName iFileName;
	MFileChangeObserver* iObserver;
	};


CFileWatcher* CFileWatcher::NewL(RFs& aFs)
	{
	return new(ELeave) CFileWatcher(aFs);
	}

CFileWatcher::~CFileWatcher()
	{
	Cancel();
	}

void CFileWatcher::Start(const TDesC& aFileName, MFileChangeObserver& aObserver)
	{
	ASSERT(!IsActive());
	iFileName = aFileName;
	iObserver = &aObserver;
	Queue();
	}

CFileWatcher::CFileWatcher(RFs& aFs)
	: CActive(CActive::EPriorityStandard), iFs(aFs)
	{
	CActiveScheduler::Add(this);
	}

void CFileWatcher::Queue()
	{
	ASSERT(!IsActive());
	iFs.NotifyChange(ENotifyWrite, iStatus, iFileName);
	SetActive();
	}

void CFileWatcher::DoCancel()
	{
	iFs.NotifyChangeCancel(iStatus);
	}

void CFileWatcher::RunL()
	{
	Queue();
	iObserver->HandleFileChange(iFileName);
	}


//
// CCmdTail.
//

CCommandBase* CCmdTail::NewLC()
	{
	CCmdTail* self = new(ELeave) CCmdTail();
	CleanupStack::PushL(self);
	self->ConstructL();
	return self;
	}

CCmdTail::~CCmdTail()
	{
	delete iCharacterConverter;
	delete iTrailingLineFinder;
	delete iFileWatcher;
	delete iFileReader;
	}

CCmdTail::CCmdTail()
	: CCommandBase(EManualComplete), iNumLines(10)
	{
	}

void CCmdTail::ConstructL()
	{
	BaseConstructL();
	}

void CCmdTail::WriteChunkToConsoleL(const TDesC8& aData, TReadType aType)
	{
	switch (aType)
		{
		case MFileReaderObserver::EFirst:
			{
			Write(iCharacterConverter->ConvertChunkL(aData, CCharacterConverter::EFirst));
			break;
			}
		case MFileReaderObserver::EMiddle: // Treat "middle" and "last" to same as with tailing it's not possible to know if "last" is really last.
		case MFileReaderObserver::ELast:
			{
			Write(iCharacterConverter->ConvertChunkL(aData, CCharacterConverter::EMiddle));
			break;
			}
		default:
			{
			ASSERT(FALSE);
			}
		}
	}

const TDesC& CCmdTail::Name() const
	{
	_LIT(KName, "tail");
	return KName;
	}

void CCmdTail::DoRunL()
	{
	iCharacterConverter = CCharacterConverter::NewL(KBlockSize, FsL());
	iTrailingLineFinder = CTrailingLineFinder::NewL(KBlockSize, *iCharacterConverter);
	iFileWatcher = CFileWatcher::NewL(Fs());
	iFileReader = CFileReader::NewL(KBlockSize, Fs(), ETrue);
	iTrailingLineFinder->Find(*iFileReader, iFileName, iNumLines, *this);
	}

void CCmdTail::OptionsL(RCommandOptionList& aOptions)
	{
	_LIT(KOptFollow, "follow");
	_LIT(KOptNumLines, "lines");
	aOptions.AppendBoolL(iFollow, KOptFollow);
	aOptions.AppendIntL(iNumLines, KOptNumLines);
	}

void CCmdTail::ArgumentsL(RCommandArgumentList& aArguments)
	{
	_LIT(KArgFileName, "file_name");
	aArguments.AppendFileNameL(iFileName, KArgFileName);
	}

void CCmdTail::HandleTrailingLines(TInt aNumLinesFound, TInt aEndFilePos)
	{
	if (iFollow)
		{
		iFilePos = aEndFilePos;
		iFileWatcher->Start(iFileName, *this);
		}
	for (TInt i = 0; i < aNumLinesFound; ++i)
		{
		Write((*iTrailingLineFinder)[i]);
		}
	if (!iFollow)
		{
		Complete();
		}
	}

void CCmdTail::HandleTrailingLineError(TInt aError)
	{
	PrintError(aError, _L("Problem finding trailing %d lines in %S: %d"), iNumLines, &iFileName, aError);
	Complete(aError);
	}

void CCmdTail::HandleFileChange(const TDesC& /*aFileName*/)
	{
	if (!iFileReader->IsActive())
		{
		iFileReader->Read(iFileName, iFilePos, *this);
		}
	}

void CCmdTail::HandleFileData(const TDesC8& aData, TReadType aType, TBool& aContinue)
	{
	aContinue = ETrue;
	iFilePos += aData.Length();
	TRAPD(err, WriteChunkToConsoleL(aData, aType));
	if (err)
		{
		aContinue = EFalse;
		PrintError(err, _L("Problem writing chunk of %S to console: %d"), &iFileName, err);
		}
	}

void CCmdTail::HandleFileReadError(TInt aError)
	{
	PrintWarning(_L("Problem reading %S: %d"), &iFileName, aError);
	}


#ifdef EXE_BUILD
EXE_BOILER_PLATE(CCmdTail)
#endif

