// Copyright (c) 2004-2009 Nokia Corporation and/or its subsidiary(-ies).
// All rights reserved.
// This component and the accompanying materials are made available
// under the terms of "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:
// Nokia Corporation - initial contribution.
//
// Contributors:
//
// Description:
// record.cpp - the queries and replies from the server
// This file contains the implementations for the CDndRecord and TDndRecordList objects. 
//

#include "record.h"
#include "node.h"
#include <networking/dnd_err.h>
#include "inet6log.h"
#include "engine.h"

#ifdef EXCLUDE_SYMBIAN_DNS_PUNYCODE
#undef SYMBIAN_DNS_PUNYCODE
#endif //EXCLUDE_SYMBIAN_DNS_PUNYCODE

const TInt CDndRecord::iOffset = _FOFF(CDndRecord, iDlink);
const TInt CDndRecord::iOffsetLRU = _FOFF(CDndRecord, iLRU);

// Default constructor
CDndRecord::CDndRecord(TDndRecordLRU &aLRU, CDndNode &aOwner, const EDnsQType aType, const EDnsQClass aClass)
 : iOwner(aOwner), iList(aLRU), iType(aType), iClass(aClass), iErrorCode(KErrNotFound)
	{
	//
	// Maintain LRU list state
	//
	iList.AddFirst(*this);
	iList.iCount += 1;
	}

CDndRecord::~CDndRecord()
	{
	//
	// Maintain LRU list state
	//
	iLRU.Deque();	// All records are always in LRU
	iList.iCount -= 1;
	}

// CndRecord::Unlock
// *****************
/**
//	Unlocks the record (decrement reference count).
//
//	When the reference count reaches ZERO, and if record has no TTL set or
//	has expired, the record is removed.
//
//	If iHasTTL == 0, then RR has been created, but no query results
//	have been received. If the querier quits before any answers become
//	available, there is no point in keeping the empty record in cache.
//
//	If iExpired == 1, then RR has been detected as expired, but could
//	not be released due to being locked.
//
// @param aWorker	Unlocking instance
*/
void CDndRecord::Unlock(const void *aWorker)
	{
	ASSERT(iLocks > 0);
	// Cancel "being worked on" state, if this unlocking
	// instance had registered as working on request.
	if (iWorker == aWorker)
		iWorker = NULL;

	if (--iLocks == 0)
		{
		ASSERT(iWorker == NULL);
		// The last user exited. If the record has no data (initial state)
		// or has been marked as expired, then delete the record.
		if (iHasTTL == 0 || iExpired == 1)
			Delete();
		}
	}


// CndRecord::AssignWork
// *********************
/**
// Assign a "worker" to record, if not already present.
//
// The caller (= aWorker) wants to start the DNS querying process for
// this record.
//
// @return
//	@li ETrue, if worker has been assigned (and queries can be started)
//	@li EFalse, if some other worker has already been assigned (no queries need to be started)
*/
TBool CDndRecord::AssignWork(const void *aWorker)
	{
	ASSERT(iLocks > 0);
	if (iWorker == NULL)
		{
		iWorker = aWorker;
		return ETrue;
		}
	// Return also true, if this same worker is already
	// assigned, and otherwise false (= someone else is working on this).
	return iWorker == aWorker;
	}


// CDndRecord::HitLRU
// ******************
/**
//	Moves the record to first position in the LRU list.
*/
void CDndRecord::HitLRU()
	{
	//
	// Maintain LRU list state
	//
	iLRU.Deque();
	iList.AddFirst(*this);
	}

// CDndRecord::Delete
// ******************
void CDndRecord::Delete()
	{
	iOwner.DeleteRecord(this);
	}

// CDndRecord::Expiretime
// **********************
/**
// @returns	a reference to expiration time of this record
*/
const TTime &CDndRecord::ExpireTime() const
	{
	return iTTL;
	}


// CDndRecord::Invalidate
// **********************
/**
// "Invalid record" is just a record that contains no actual
// data (yet). It is the initial state of the newly created
// records.
*/
void CDndRecord::Invalidate()
	{
#ifdef _LOG
	if (iErrorCode != KErrNotFound)
		{
		TBuf<KDnsMaxName> name;
		Reply().GetName(sizeof(TInet6HeaderDNS), name);
		Log::Printf(_L("%S: QType: %d, QClass: %d [%d] Invalidated\r\n"), &name, iType, iClass, (TInt)(Header().ID()));
		}
#endif
	delete iReply.iBuf;
	iReply.iBuf = NULL;
	iHasTTL = 0;
	iErrorCode = KErrNotFound;
	}

// CDndRecord::FillErrorCode
// *************************
/**
// @param	aErrorCode	then new error code for the record
// @param	aTTL
//		the "Time To Live" time for the record in seconds. This
//		converted into real expiration time by adding it into
//		current time.
*/
void CDndRecord::FillErrorCode(const TInt aErrorCode, const TUint aTTL)
	{
	if (aErrorCode == KErrNotFound)
		Invalidate();
	else
		{
		// Transfer the TTL to absolute time.
		const TTimeIntervalSeconds interval = aTTL;
		iTTL.UniversalTime();
		iTTL += interval;
		iErrorCode = aErrorCode;
		iHasTTL = 1;
		}

	}


// CDndRecord::FillData
// ********************
/**
// Store the reply into record and compute the TTL for the record
// from the TTL's of contained RR's (which match the query). If the
// reply has no matching RR's, then the KDndDefaultTTL is used.
//
// The record is marked as "valid" with error code KErrNone.
//
// @param	aReply
//		contains the reply for the query represented by this record
// @param	aAnswerOffset
//		indication the starting position of the answer section in the
//		reply.
// @param	aServer
//		id of the server from which reply comes (note that for the Multicast DNS, this
//		id identifies the multicast group, and replies from different hosts are tagged
//		with the same id).
// @param	aErrorCode
//		The error code
// @param	aTTL
//		The default time to live, if RR's don't specify otherwise.
*/
void CDndRecord::FillData(const TMsgBuf &aReply, const TInt aAnswerOffset, const TUint32 aServer, TInt aErrorCode, TUint aTTL)
	{
	TUint32 max_ttl = KDndMaxTTL;

	if (aErrorCode == KErrNone)
		{
		// Compute TTL of the record from the minimum TTL
		// of the RR's that match exactly the iQType and iQClass
		TDndRR rr(aReply);
		TInt next = 0;
		TInt answerCount = ((TInet6HeaderDNS *)(aReply.Ptr()))->ANCOUNT();
		for (;;++next)
			{
			const TInt ret = rr.FindRR(aAnswerOffset, answerCount, iType, iClass, next);

			if (ret == KErrDndCache)
				// The reply is corrupt, do not store into cache, do
				// not modify the currently cached value, if any exists.
				return;
			if (ret < 0)
				break;	// No more rr's
			if (EDnsQClass(rr.iClass) != iClass || EDnsQType(rr.iType) != iType)
				break;	// No more matching RR's (FindRR returns first all matching ones)
			if (max_ttl > rr.iTTL)
				max_ttl = rr.iTTL;
			}
		if (next == 0)
			{
			// No RR's that matched the query or there was no anser RR's.
			// Use the supplied default TTL.
			max_ttl = aTTL;
			}
		}
	else
		max_ttl = aTTL;	// Just use the default for any error codes.

	delete iReply.iBuf;
	iReply.iBuf = HBufC8::New(aReply.Length());
	if (iReply.iBuf)
		{
		TPtr8 buf = iReply.iBuf->Des();
		buf = aReply;
		iReply.iOffset = aAnswerOffset;
		iServer = aServer;
		FillErrorCode(aErrorCode, max_ttl);
		}
	else
		{
		// This is internal memory allocation problem,
		// use just default TTL
		FillErrorCode(KErrNoMemory, KDndDefaultTTL);
		}
	}

#ifdef _LOG
// CDndRecord::Print
// *****************
/**
// (This method is currently only used in DEBUG compile)
//
// @param aControl	provides the actual "output device"
*/
#ifdef SYMBIAN_DNS_PUNYCODE
void CDndRecord::Print(CDndEngine &aControl, TBool aIdnEnabled) const
#else
void CDndRecord::Print(CDndEngine &aControl) const
#endif //SYMBIAN_DNS_PUNYCODE
	{
	TInt err;
	TBuf<KDnsMaxName> buf;
	if (iErrorCode == KErrNotFound)
		{
		aControl.ShowText(_L("Invalid Record."));
		return;
		}
#ifdef SYMBIAN_DNS_PUNYCODE
		Reply().GetName(sizeof(TInet6HeaderDNS), buf, aIdnEnabled);
#else
		Reply().GetName(sizeof(TInet6HeaderDNS), buf);
#endif //SYMBIAN_DNS_PUNYCODE

	const TInt answerCount = Header().ANCOUNT();

	aControl.ShowTextf(_L("%S: QType: %d, QClass: %d, Err: %d [%d %d/%d/%d AA=%d TC=%d RA=%d RCODE=%d]"),
		&buf,
		iType, iClass, iErrorCode,
		(TInt)(Header().ID()),
		answerCount, (TInt)Header().NSCOUNT(), (TInt)Header().ARCOUNT(),
		(TInt)(Header().AA() != 0),
		(TInt)(Header().TC() != 0),
		(TInt)(Header().RA() != 0),
		(TInt)(Header().RCODE()));

	TRAP(err, iTTL.FormatL(buf, _L(" | Expires: %F%D.%M.%Y %H:%T:%S")));
	aControl.ShowText(buf);

	TDndRR rr(TMsgBuf::Cast(*iReply.iBuf));
#ifdef SYMBIAN_DNS_PUNYCODE
	rr.iIdnEnabled = aIdnEnabled;
#endif //SYMBIAN_DNS_PUNYCODE
	TInt next = 0;
	for (;;++next)
		{
		if (rr.FindRR(iReply.iOffset, answerCount, iType, iClass, next) < 0)
			break;	// No more rr's

		TInetAddr addr;
		rr.GetResponse(buf, addr);
		TBuf<50> tmp;
		addr.OutputWithScope(tmp);
		aControl.ShowTextf(_L("   + RR: %S Type: %d, Class: %d, TTL: %d [%S]"), &buf, (int)rr.iType, (int)rr.iClass, (int)rr.iTTL,
			&tmp);
		}
	}
#endif

TDndRecordLRU::TDndRecordLRU() : TDblQue<CDndRecord>(CDndRecord::iOffsetLRU)
	{
	}

// TDndRecordLRU::Cleanup
// **********************
/**
//	Limit the number of records in the LRU to the
//	specified aMaxRecords. Deletes excess records
//	from the end of the list.
//
//	Locked records are not deleted and are left
//	in the list.
//
// @param	aMaxRecords
//		the maximum number of records that should be
//		on the list. This is "advisory", if there are
//		locked records, Cleanup may not be able to
//		comply fully with the request.
*/
void TDndRecordLRU::Cleanup(const TUint aMaxRecords)
	{
	LOG(Log::Printf(_L("LRU Cleanup(max=%d) records in cache=%d"), aMaxRecords, iCount));
	TUint locked = 0;
	while (locked < iCount && iCount > aMaxRecords)
		{
		CDndRecord *const record = Last();
		if (record->IsLocked())
			{
			// Oops... the last record is locked, cannot remove!
			// Shunt to first, and try next one...
			record->HitLRU();
			locked += 1;
			// A special kludge: if aMaxRecords == 0, all records really
			// should be deleted. Can't delete locked ones now, so just
			// mark them as 'expired', so that new queries don't use them.
			if (aMaxRecords == 0)
				record->MarkExpired();
			}
		else
			{
			// This Delete *WILL* decrease iCount!
			record->Delete();
			}
		}
	LOG(Log::Printf(_L("LRU Cleanup finished, records in cache=%d (at least %d locked)"), iCount, locked));
	}


TDndRecordList::TDndRecordList() : TDblQue<CDndRecord>(CDndRecord::iOffset)
	{}

#ifdef _LOG
// TDndRecordList::Print
// *********************
/**
// (This method is currently only used in DEBUG compile)
//
// @param aControl	provides the actual "output device"
*/
void TDndRecordList::Print(CDndEngine &aControl) 
	{
	if (IsEmpty())
		aControl.ShowText(_L("RecordList: Empty List"));
	else
		{
		aControl.ShowText(_L("RecordList: "));
		CDndRecord *record;
		TDblQueIter<CDndRecord> iter(*this);
		while((record = iter++) != NULL)
			{
#ifdef SYMBIAN_DNS_PUNYCODE
			record->Print(aControl); // by default the name shall be displayed in ASCII format. 
#else
			record->Print(aControl);
#endif // SYMBIAN_DNS_PUNYCODE
			}
		}
	}
#endif

// TDndRecordList::Find
// ********************
/**
// Locate a record of matching type and class.
//
// Search the record list for a matching type and class. If
// an expired record is found, empty and reuse it.
//
// Note: this does not release expired record with no users. Just
// let the normal LRU cleanup handle them.
//
// @param	aType of the record to be located
// @param	aClass of the record to be located
// @param	aReqTime of the query (current time or very close to it)
// @returns
//	@li	NULL, if no matching record exists
//	@li non-NULL record pointer, if a matching record exists
*/
CDndRecord * TDndRecordList::Find(const EDnsQType aType, const EDnsQClass aClass, const TTime &aReqTime)
	{
	if (IsEmpty())
		return NULL;
	
	CDndRecord *record;
	TDblQueIter<CDndRecord> iter(*this);
	while ((record = iter++) != NULL)
		{
		if (record->iExpired == 0 && record->iType == aType && record->iClass == aClass)
			{
			if (record->iHasTTL && record->ExpireTime() < aReqTime)
				{
				// The record has expired,
				if (record->IsLocked())
					{
					// The record is in use by someone, cannot reuse it.
					// Mark it as "expired", so that it will be automaticly
					// deleted when the last current user releases it.
					record->iExpired = 1;
					// A new duplicate RR will be created.
					continue;
					}
				// The record has expired, but nobody is using it for now.
				// Just reset to empty and use the existing RR.
				record->Invalidate();
				}
			break;
			}
		}
	return record;
	}

// TDndRecordList::Add
// *******************
/**
// @param	aRecord to be insersted into list.
*/
void TDndRecordList::Add(CDndRecord &aRecord)
	{
	AddFirst(aRecord);
	}

// TDndRecordList::Delete
// **********************
/**
//	@param	aRecord to be deleted (must be a member of this list)
*/
void TDndRecordList::Delete(CDndRecord *aRecord)
	{
	aRecord->iDlink.Deque();
	delete aRecord;
	}
