/*
* Copyright 2011, 2014 Range Networks, Inc.
*
* This software is distributed under multiple licenses;
* see the COPYING file in the main directory for licensing
* information for this specific distribution.
*
* This use of this software may be subject to additional restrictions.
* See the LEGAL file in the main directory for details.

    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.
*/

#define LOG_GROUP LogGroup::GPRS		// Can set Log.Level.GPRS for debugging

#include "MSInfo.h"
#include "TBF.h"
#include "FEC.h"
#include "RLCMessages.h"
#include "BSSG.h"
#include "RLCEngine.h"
#include "Sgsn.h"
#include <string.h>		// for strchr
#include <iomanip>		// For fmtflags, setprecision

namespace GPRS {

   
// This stores the multislot class info that we need from 3GPP 45.002 annex B
// We dont save Ttb or Trb because:
// Trb is 1 or less for all multislot classes >= 2, so we satisfy the constraint
// by leaving one tn timeslot between transmit and receive timeslots.
// Ttb is 1 or less for all multislot classes with Rx > 2.
// Also see 3GPP 45.002 table 6.4.2.2.1
struct MultislotClass {
    uint8_t mMultislotClass;
    uint8_t mMultislotRx;   // Max downlink channels.
    uint8_t mMultislotTx;   // Max uplink channels.
    uint8_t mMultislotSum;  // Max Rx + Tx
};

// The MS uses the idle frames in the 52-multiframe to make measurements.
// I think the Tta and Tra apply to the frames surrounding the idle frame.
// 45.008 10.2.3.2.1 says that if the MS does not have time to perform
// the measurement it can skip a transmit or receive timeslot
// adjacent to the idle frame.

static const int sMultislotMax = 45;	// Table entries numbered 1-45.
static MultislotClass sMultislotInfo[sMultislotMax] = {
	{1, 	1,	1,	2},
	{2, 	2,	1,	3},
	{3, 	2,	2,	3},
	{4, 	3,	1,	4},
	{5, 	2,	2,	4},
	{6, 	3,	2,	4},
	{7, 	3,	3,	4},
	{8, 	4,	1,	5},
	{9, 	3,	2,	5},
	{10,	4,	2,	5},
	{11,	4,	3,	5},
	{12,	4,	4,	5},
	{13,	3,	3,	16},	// These ones with sum=16 are class 2 MS,
	{14,	4,	4,	16},	// ie, send and receive simultaneously.
	{15,	5,	5,	16},
	{16,	6,	6,	16},
	{17,	7,	7,	16},
	{18,	8,	8,	16},
	{19,	6,	2,	16},
	{20,	6,	3,	16},
	{21,	6,	4,	16},
	{22,	6,	4,	16},
	{23,	6,	6,	16},
	{24,	8,	2,	16},
	{25,	8,	3,	16},
	{26,	8,	4,	16},
	{27,	8,	4,	16},
	{28,	8,	6,	16},
	{29,	8,	8,	16},
	{30,	5,	1,	6},
	{31,	5,	2,	6},
	{32,	5,	3,	6},
	{33,	5,	4,	6},
	{34,	5,	5,	6},
	{35,	5,	1,	6},
	{36,	5,	2,	6},
	{37,	5,	3,	6},
	{38,	5,	4,	6},
	{39,	5,	5,	6},
	{40,	6,	1,	7},
	{41,	6,	2,	7},
	{42,	6,	3,	7},
	{43,	6,	4,	7},
	{44,	6,	5,	7},
	{45,	6,	6,	7}
};


// Return an entry from the 45.002 Annex B table.
// We use T(rb) and T(tb) instead of T(ra), T(ta) because the latter are only used
// when there is a requirement for adjacent cell measurements.
// We dont need to worry much about T(rb) and T(tb) because we will
// always just keep Trb >= 1.  This constraint is satisfied by allocating
// adjacent channels when Rx <= 2 or Tx <= 2.
static MultislotClass getMultislotClass(MSInfo *ms)
{
	int multislotclass = ms->sgsnGetMultislotClass(ms->msTlli);
	GPRSLOG(1) << ms <<LOGVAR(multislotclass);
	// -1 means not specified and 0 or >45 are illegal.
	if (multislotclass <= 0 || multislotclass > sMultislotMax) {
		return sMultislotInfo[0];	// No multislot capability.
	} else {
		MultislotClass result = sMultislotInfo[multislotclass-1];
		devassert((int)result.mMultislotClass == multislotclass);
		return result;
	}
}

MSInfo::MSInfo(uint32_t tlli)
	: msDebugId(++Stats.countMSInfo),
	msTlli(tlli),
	msPacch(0),
	//msMode(RROperatingMode::PacketIdle),
	msT3191(gL2MAC.macT3191Value),
	msT3193(gL2MAC.macT3193Value),
	msT3168(gL2MAC.macT3168Value)
	//msTxxxx(5000)		// Needs initialization to prevent abort when we test it,
						// but we will set it again to the real value when we use it.
{
	gReports.incr("GPRS.MSInfo");
	gL2MAC.macAddMS(this);
}

bool MSInfo::msIsSuspended()
{
	SGSN::GmmState::state state = sgsnGetRegistrationState(this->msTlli);
	return state == SGSN::GmmState::GmmRegisteredSuspsended;
}

// Extended dynamic mode means # channels up > # channels down.
bool MSInfo::msCanUseExtendedUplink()
{
	// The Blackberry and iphone set the GeranFeaturePackI bit, but
	// the danged Multitech modems truncate the MS capabilities before
	// the GeranFeaturePackI bit, even though they do support extended uplink TBF
	// They are multislot class 12, so if the MS multislot class > 10, assume ok.
	return gL2MAC.macUplinkPersist > 0 && this->msIsRegistered()
		&& (this->sgsnGetGeranFeaturePackI(this->msTlli) ||
		    this->sgsnGetMultislotClass(this->msTlli) > 10);
}

bool MSInfo::msIsRegistered()
{
	SGSN::GmmState::state state = sgsnGetRegistrationState(this->msTlli);
	return state == SGSN::GmmState::GmmRegisteredNormal ||
		       state == SGSN::GmmState::GmmRegisteredSuspsended;
}

void MSInfo::msCountUSFGrant(bool penalize)
{
	// It does not matter that this is not the exact slot when the USF was granted,
	// because the msLastUsfGrant is used only to compare the same value in other MS.
	msLastUsfGrant = gBSNNext;
	msNumDataUSFGrants++;
	// Note: We are supposed to set N3101 if the uplink USF block
	// is unused, not when we grant it - so add a bit to
	// the N3101 max count to account for this.
	if (penalize) {
		msN3101++;
	}
}


const char *RROperatingMode::name(RROperatingMode::type mode)
{
	switch (mode) {
		CASENAME(PacketIdle)
		CASENAME(PacketTransfer)
		//CASENAME(DualTransfer)
		//CASENAME(Camped)
	}
	return "unrecognized";	// Not reached, but makes gcc happy.
}
std::ostream& operator<<(std::ostream& os, const RROperatingMode::type &mode)
{
	os << RROperatingMode::name(mode);
	return os;
}


void MSInfo::msDelete(bool forever)
{
	gL2MAC.macForgetMS(this,forever); // MS destruction happens in here.
}

// Relinquish any USFs belonging to this MS, if they are no longer in use by any TBFs.
// This function is in the MS, not the TBF, because there could be multiple TBFs
// sharing the same USFs.  (Each TBF has a unique TFI, but share the USF.)
void MSInfo::msCleanUSFs()
{
	bool anyactiveuplinks = false;
	TBF *tbf;
	RN_MS_FOR_ALL_TBF(this,tbf) {
		//if (tbf->mtDir == RLCDir::Up && tbf->mtGetState() != TBFState::Deleting)
		// Update 8-13: Dead TBFs are no longer needed to reserve USFs, but
		// they are still attached, so ignore them for this purpose.
		if (tbf->mtDir == RLCDir::Up && tbf->mtAttached && tbf->mtGetState() != TBFState::Dead) {
			anyactiveuplinks = true;
			break;
		}
	}

	int chcnt = 0;
	int freedusf = 0;
	if (!anyactiveuplinks) {
		// Relinquish any USFs we may have.
		PDCHL1Uplink *up;
		RN_FOR_ALL(PDCHL1UplinkList_t,msPCHUps,up) {
			chcnt++;
			freedusf = up->mchParent->freeUSF(this,false);
		}
		msNumDataUSFGrants = 0;
		msAckNackUSFGrant = -1;
		for (int u = 0; u < USFMAX; u++) { msUSFs[u] = 0; }	// Not used, but be tidy.
	}
	GPRSLOG(1) <<"CleanUSFs"<<this<<LOGVAR(anyactiveuplinks)<<LOGVAR(chcnt)<<LOGVAR(freedusf);
}

// The TBF died, so reserve the USF resources for 5 seconds.
void MSInfo::msFailUSFs()
{
	PDCHL1Uplink *up;
	RN_FOR_ALL(PDCHL1UplinkList_t,msPCHUps,up) {
		up->mchParent->freeUSF(this,true);
	}
}

//void MSInfo::setUSFGrantBSN(int usf)
//{
	//devassert(usf >= USFMIN && usf <= USFMAX);
	//msLastUSFGrantBSN = gBSNNext;
//}


// How many timeslots assigned so far?
//static int chsum(MSInfo *ms)
//{
//	return ms->msPCHDowns.size() + ms->msPCHUps.size();
//}

// The multitech modem supports:
// 	o precendence class (1,2,3,0)
//  o mean throughput, which is not precise enough:
// mean=9 	50,000 (~111 bit/s)
// mean=10 100,000 (~0.22 kbit/s)
// mean=11 200 000(~0.44 kbit/s)
// mean=12 500 000(~1.11 kbit/s)
// mean=13 1,000,000 (~2.2 kbit/s)  <- class 2 shared x 8
// mean=14 2,000,000 (~4.4 kbit/s)  <- class 2 shared x 4
// mean=15 5,000,000 (~11.1 kbit/s)	<- class 2 shared x 2
// mean=16 10,000,000 (~22 kbit/s)	<- class 2 dedicated.
// mean=17 20,000,000 (~44 kbit/s)	<- map to class 3 dedicated
// mean=18 50,000,000 (~111 kbit/s) <- map to class 4 dedicated.  What about 4U,4D?
//
// So that leaves guaranteed and maximum UL/DL values from 1-63Kbps:
// The max UL/DL will determine the class, and guaranteed the sharing factor.
// That doesnt work very well because class 2 and class 4 have the same max UL/DL.
// An sql option can determine how to handle 'best effort', either as the best
// available or a specified class.


// This should always succeed unless there is a logical bug during startup channel allocation.
// 45.008 10.1.1.2 describes what the MS will do if the multislot config does not give
// it sufficient time to take measurements.
// There is a BA(GPRS) list of BCCH carriers to monitor - where?
// 45.008 10.1.1.2: The MS shall perform the measurements during the block
// period where the polling response is sent.
//
// Here are pictures of the multislot assignments we make, the numbers are TN.
// Tr is the number of receive set-up timeslots;
// Tt is the number of transmit set-up timeslots.
// We can use either Tra and Ttb or Trb and Ttb.
// For multislot classes less than 10, Tra < Tta so we must leave the extra timeslots
// between receive and transmit to utilize the maximum number of slots.
// We are trying to avoid moving PACCH; in the pictures below TN1 is the PACCH.
// GSM uplink trails downlink separated by two empty slots.
// Normal single channel, Tr = 2, Tt = 4
// 1D | 2  | 3  | 4  | 5  | 6  | 7  | 8
// 6  | 7  | 8  | 1U | 2  | 3  | 4  | 5
// 2-down/2-up, Tr = 1, Tt = 3:  Uses TN1,2 PACCH on TN1 or 2, chs: P+1 or P-1
// 1D | 2D | 3  | 4  | 5  | 6  | 7  | 8
// 6  | 7  | 8  | 1U | 2U | 3  | 4  | 5
// 3-down/2-up, Tr = 1, Tt = 2:  Uses TN8,1,2 PACCH on TN1 or 2, chs: P+1&-1 or P-1&-2
// 1D | 2D | 3  | 4  | 5  | 6  | 7  | 8D
// 6  | 7  | 8  | 1U | 2U | 3  | 4  | 5
// 4-down/1-up, Tr = 1, Tt = 2:  Uses TN7,8,1,2 PACCH on TN1, chs: P+1&-1&-2
// 1D | 2D | 3  | 4  | 5  | 6  | 7D | 8D
// 6  | 7  | 8  | 1U | 2  | 3  | 4  | 5

// The multislot class 12 and higher support 1-down/4-up, and they also have
// Tta = Tra so we can put the extra empty slots on either side, but that doesnt help.
// 3GPP 45.002 table 6.4.2.2.1 specifies that Tra applies in these cases,
// so we dont have a choice.
// For extended dynamic uplink the lowest numbered uplink must be the downlink.
// Note: There is something called "Shifted USF" but it only applies to super
// high multislot classes.
// 2-down/3-up, uses TN1,2,3 PACCH on TN1 or 2, chs: P+1&+2 or P+1&-1.
// 1D | 2D | 3  | 4  | 5  | 6  | 7  | 8
// 6  | 7  | 8  | 1U | 2U | 3U | 4  | 5
// 1-down/4-up, Tra=2, Ttb=1 uses TN1,2,3,4, ie P+1&P+2&P+3
// 1D | 2  | 3  | 4  | 5  | 6  | 7  | 8
// 6  | 7  | 8  | 1U | 2U | 3U | 4U | 5


// The following are illegal because for extended dynamic uplink
// the lowest numbered uplink must be the downlink.
// Also, 3GPP 45.002 table 6.4.2.2.1 specifies that Tra applies in these cases, not Tta.
// 2-down/3-up, Tr = 1, Tt = 2:  Uses TN8,1,2 PACCH on TN1 or 8, chs: P+1&-1 or P+1&+2
// 1D | 2  | 3  | 4  | 5  | 6  | 7  | 8D
// 6  | 7  | 8U | 1U | 2U | 3  | 4  | 5
// 1-down/4-up, Tr=1 Tt=2 uses TN8,1,2,3 PACCH on TN1, chs: P-1&P+1&P+2
// 1D | 2  | 3  | 4  | 5  | 6  | 7  | 8
// 6  | 7  | 8U | 1U | 2U | 3U | 4  | 5

// Here are Tt=1 alternatives:
// Aternative 4-down/1-up for class 12, Tr = 2, Tt = 1:  Uses TN6,7,8,1 PACCH on TN1
// chs: P-1&P-2&P-3
// 1D | 2  | 3  | 4  | 5  | 6D | 7D | 8D
// 6  | 7  | 8  | 1U | 2  | 3  | 4  | 5
// So we can do 3-down/2-up and 2-down/3-up on 3 adjacent channels,
// but if we want to do 4-down/1-up and 1-down/4-up without moving PACCH
// we need 6 adjacent channels.
//
// GPRS Classes of service.
// Full bandwidth full duplex (4D/1U to 1D/4U), requires 5xTN or 2 per ARFCN.
// Full bandwidth assymetric only takes 4 adjacent TN, ex: 3D/1U to 1D/4U
// These may be shared or not.  Strategically we may move the PACCHs around later.
// The more typical 2D/2U and uses PACCH on every other ARFCN, has a guaranteed
// bandwidth of 2x because even though the neighbor may encroach on it, it can
// also encroach on its neighbor.
// How about Guaranteed Bandwidth and Minimum Bandwidth in up/down.
// The ones that make sense, assuming 1 channel = 12Kpbs, are:
// class 5 adjacent TN: 48D/48U half-duplex,
// class 4 adjacent TN: 48D/36U, 36D/48U half-duplex,  <- must specify which
// Or we could call the above class 4, 4D and 4U.
// class 3 adjacent TN: 36D/36U half-duplex, no need to specify, can dynamically
//				change between 36D/24U or 24D/36U full-duplex
// class 2 adjacent TN: 24D/24U full-duplex
// class 1, uses same as above but shared: 12U/12U.
// If you allocate 4 TNs on an ARFCN, if they are both class 2, then the bandwidth
// of the upper one can borrow from the lower one only when it is not busy,
// otherwise they share as follows:
//		one shared D TN:  18D/24U  and 30D/24U
//		two shared D TN:  12D/24U  and 36D/24U
//
// The sql options can specify reserved allocation of any number of classes, 
// for example: '2x4,3x3,1x2'.
// For dynamic allocation, it could be based on one or more of the following:
//		the QoS from the modem;
//		the class to allocate, or maybe max and min classes.  (default class 2)
//		or maybe the only thing we support for dynamic is class 2.
//		total number of dynamic gprs channels to allocate;
//		number of voice TCH to leave open;
//		
// We can let the multitech modem specify the QoS.


// Until we support per-imsi bandwidth, it would be first-come first serve, or we
// could have the customer program the QoS in the multitech modem.
// If more phones register than will fit, we need to know if we are allowed to share,
// so we really need the sharing number, too.
// Or we could just use the priority: 
// Allowing multiple classes makes no sense at all until we support per-imsi bandwidth,
// so for now I guess it is just a single static and a single dynamic class:
// StaticAllocation '2x4', DynamicAllocationClass.Max '4' DynamicAllocationClass.Min '2'
// Or we could specify the bandwidth, for example: 2x48D/48U or 4x24D/24U
// 

const unsigned cTnPerArfcn = 8;
static unsigned addTn(unsigned tn, int offset)
{
	return (unsigned) (tn + offset) % cTnPerArfcn;
}
static bool tnavail(unsigned chmask,unsigned tn)
{
	return chmask & (1 << (tn % cTnPerArfcn));
}

// The tnlist describes the tn slots used in this multislot configuration.
bool MSInfo::msAddCh(unsigned chmask, const char *tnlist)
{
	const char *cp, *pp = strchr(tnlist,'P');
	if (!pp) {
		devassert(pp);	// Caller goofed.  tnlist must include a 'P' for PACCH location.
		return false;	// And thats a failure.
	}
	int before = pp-tnlist;					// Number of channels needed before PACCH.
	// Check for channel availability.
	unsigned tn, tnfirst = addTn(msPacch->TN(),- before);
#if 0
	unsigned tn = msPacch->TN();
	int after = strlen(tnlist)-before-1;	// Number of channels needed after PACCH.
	if (before && !tnavail(chmask,tn-1)) {return false;}
	if (before>1 && !tnavail(chmask,tn-2)) {return false;}
	if (after && !tnavail(chmask,tn+1)) {return false;}
	if (after>1 && !tnavail(chmask,tn+2)) {return false;}
#endif
	GPRSLOG(4) << "msAddCh" <<LOGVAR(tnlist)<<LOGVAR(before)<<LOGVAR(tnfirst);
	for (tn=tnfirst,cp=tnlist; *cp; cp++, tn=addTn(tn,1)) {
		if (*cp == 'P') { continue; }	// PACCH already allocated, so it wont look avail.
		if (!tnavail(chmask,tn)) {return false;}
	}

	// Allocate the channels.
	tn = addTn(msPacch->TN(),- before);
	for (tn=tnfirst,cp=tnlist; *cp; cp++, tn=addTn(tn,1)) {
		GPRSLOG(4) << "msAddCh loop" <<LOGVAR(tnlist)<<LOGVAR(tnfirst)<<LOGVAR(*cp)<<LOGVAR(tn);
		PDCHL1FEC *ch = gL2MAC.macFindChannel(msPacch->ARFCN(),tn);
		switch (*cp) {
		case 'D':	// downlink only timeslot.
			msPCHDowns.push_back(ch->downlink());
			break;
		case 'U':	// uplink only timeslot.
			msPCHUps.push_back(ch->uplink());
			break;
		case 'B':	// bidirection timeslot other than PACCH.
			msPCHDowns.push_back(ch->downlink());
			msPCHUps.push_back(ch->uplink());
			break;
		case 'P':	// PACCH, already added so skip.
			break;
		}
	}
	return true; // finished
}

// Try for a specific number of down and up timeslots.
// If there are multiple ways to satisfy the request, try them all.
// The PACCH has already been added to the channels.
// The letters correspond to timeslots and mean:
// D: downlink, U=uplink, B=bidir, P=PACCH (which is also bidir.)
// Any bidir timeslot could be the PACCH.
bool MSInfo::msTrySlots(unsigned chmask,int down,int up)
{
	// For extended dynamic uplink (ie, if number uplink channels > number downlink)
	// then the first channel must be bidirectional, and the USF of the first
	// channel allocates the uplink for all TNs.
	switch ((down<<4)+up) {
	case 0x41: return msAddCh(chmask,"DDPD");
	case 0x14: return msAddCh(chmask,"PUUU");
	case 0x32: return msAddCh(chmask,"DPB") || msAddCh(chmask,"DBP");
	case 0x23: return msAddCh(chmask,"PBU") || msAddCh(chmask,"BPU");
	case 0x22: return msAddCh(chmask,"PB") || msAddCh(chmask,"BP");
	// These are oddballs that would only be used for testing:
	case 0x31: return msAddCh(chmask,"DPD") || msAddCh(chmask,"DDP");
	case 0x13: return msAddCh(chmask,"PUU");
	case 0x21: return msAddCh(chmask,"PD") || msAddCh(chmask,"DP");
	case 0x12: return msAddCh(chmask,"PU");
	default: devassert(0); return false;
	}
}

// Assign channels for the requested timeslots.
// If the multislot request cannot be satisfied, try others.
bool MSInfo::msAssignChannels2(int maxdown, int maxup, int sum)
{
	PDCHL1FEC *pdch1;
	if (msPacch) {
		pdch1 = msPacch;
	} else {	// Does this happen?
		pdch1 = gL2MAC.macPickChannel();
	}
	if (pdch1 == NULL) {
		GPRSLOG(1) << "msAssignChannels failed" <<this;
		return false;
	}

	msPCHUps.push_back(pdch1->uplink());
	msPCHDowns.push_back(pdch1->downlink());
	msPacch = pdch1;

#if 0
	if (maxdown <= 1 && maxup <= 1) { return; }

	// Look for adjacent channels before (minus) and after (plus) our PACCH tn.
	// We look up to two channels in both directions.
	int plus = 0, minus = 0;
	PDCHL1FEC *cplus[2]; cplus[0] = cplus[1] = 0;
	PDCHL1FEC *cminus[2]; cminus[0] = cminus[1] = 0;

	unsigned tn = pdch1->TN();
	for (plus = 0; plus < 2; plus++) {
		cplus[plus] = gL2MAC.macFindChannel(pdch1->ARFCN(),(unsigned)(tn+plus)%8);
		if (!cplus[plus]) break;
	}
	for (minus = 0; minus < 2; minus++) {
		cminus[minus] = gL2MAC.macFindChannel(pdch1->ARFCN(),(unsigned)(tn-minus)%8);
		if (!cminus[minus]) break;
	}
#endif

	unsigned mask = gL2MAC.macFindChannels(pdch1->ARFCN());
	GPRSLOG(2)<<format("AssignChannels3, down/up=%d/%d sum=%d mask=0x%x",
		maxdown,maxup,sum,mask);

	// Try to match the available channels to the request.
	// If we cannot fulfill the request, try for something else.
	// If the up and down specs are incompatible we give the down priority.
	// If a 4x1 or 1x4 request cannot be satisfied, should we go ahead
	// and do 3x2 or 2x3 instead of 3x1 or 1x3?
	// The way the user does that is ask for 4x2 or 2x4, and they'll get
	// 4x1 or 1x4 if avail, else 3x2 or 2x3.
	// (pat) Update: We settled on using only 3-down/2-up, or 2-down/2-up if we dont have 3 adjacent ch;
	// for these cases the additional bidir channel can be on either side of the pacch.  (Which fact is encapsulated in the msTrySlots code.)
	int up21 = maxup >= 2 ? 2 : 1;
	int down21 = maxdown >= 2 ? 2 : 1;
	if (maxdown >= 4) {
		msTrySlots(mask,4,1) || msTrySlots(mask,3,up21) || msTrySlots(mask,2,up21);
	} else if (maxup >= 4) {
		msTrySlots(mask,1,4) || msTrySlots(mask,down21,3) || msTrySlots(mask,down21,2);
	} else if (maxdown == 3) {
		msTrySlots(mask,3,up21) || msTrySlots(mask,2,up21);
	} else if (maxup == 3) {
		msTrySlots(mask,down21,3) || msTrySlots(mask,down21,2);
	} else if (maxdown == 2) {
		// The sum test is only needed for multislot class 3,
		// which is the only class that can not do 2-down/2-up.
		if (sum == 3) up21 = 1;
		msTrySlots(mask,2,up21);
	} else if (maxup == 2) {
		if (sum == 3) down21 = 1;
		msTrySlots(mask,down21,2);
	}

	msPCHUps.sort(chCompareFunc);
	msPCHDowns.sort(chCompareFunc);

#if 0
	if (pdch2) {
		if (maxdown > 1) { msPCHDowns.push_back(pdch2->downlink()); }

		// Do we want to add a third or fourth channel?  They will be downlink only.
		// We can satisfy multislot constraints (Tra=1 and Ttb=2) using
		// either or both of the timeslots immediately prior to the two
		// channels already allocated, but note that these ones cannot be PACCH.
		if ((int)msPCHDowns.size() < maxdown && chsum(this) < slots.mMultislotSum) {
			int tn2 = pdch2->TN();
			int tn = tn1<tn2 ? tn1 : tn2;	// tn is the lower of the two ch assigned.
			if (--tn < 0) { tn = 7; }
			PDCHL1FEC *pdch3 = gL2MAC.macFindChannel(pdch1->ARFCN(),tn);
			if (pdch3) {
				msPCHDowns.push_back(pdch3->downlink());

				// Can we do a 4-down config?
				// In that case we will sacrifice an uplink TN.
				if ((int)msPCHDowns.size() < maxdown && 5 <= slots.mMultislotSum) {
					if (--tn < 0) { tn = 7; }
					PDCHL1FEC *pdch4 = gL2MAC.macFindChannel(pdch1->ARFCN(),tn);
					if (pdch4) {
						msPCHDowns.push_back(pdch4->downlink());
						maxup = 1;	// Limit to one channel up now.
					}
				}
			}
		}

		// The chsum test is only needed for multislot class 3,
		// which is the only class that can not do 2-down/2-up.
		if (maxup > 1 && chsum(this) < slots.mMultislotSum) {
			msPCHUps.push_back(pdch2->uplink());
		}
	} // have pdch2.
#endif
	return true;
}

bool MSInfo::msAssignChannels()
{
	// Get the channels we will use...
	// For now, just use the one the request came in on.
	// It would be better to make sure we pick a channel that has free USFs,
	// but I'm not going to worry about it.
	if (msPCHDowns.size() == 0) {
		devassert(msPCHUps.size() == 0);

		// Check for multislot.  Limit to sql options.
		// User can control via sql: Multislot.Max, which can be over-ridden
		// by: Multislot.Max.Uplink, Multislot.Max.Downlink.
		int maxdown=1, maxup = 1;
		maxdown = configGprsMultislotMaxDownlink();
		maxup = configGprsMultislotMaxUplink();
		// Defend against garbage input:
		if (maxdown < 1) {maxdown = 1;}
		if (maxup < 1) {maxup = 1;}

		MultislotClass slots;
		if (maxdown > 1 || maxup > 1) {
			// Check the multislot class of the phone:
			slots = getMultislotClass(this);

			// Limit to phone capabilities.
			if (maxdown > (int)slots.mMultislotRx) { maxdown = slots.mMultislotRx; }
			if (maxup > (int)slots.mMultislotTx) { maxup = slots.mMultislotTx; }
			GPRSLOG(1)<<format("Multislot mscap=%d/%d max,down/up=%d/%d",
				slots.mMultislotRx,slots.mMultislotTx,maxdown,maxup);

			// Dont bother with this test.  Would only occur if you set
			// maxup < maxdown on a multislot class phone, and so far all of
			// them support extended dynamic.  The user should never set the
			// config options this way except for multiclass 12, and it has to support this.
			//if (maxup > maxdown && ! msCanUseExtendedDynamic()) {
			//	maxdown = maxup;
			//}

			// Update: This test moved into msAssignChannels2
			// This test is only needed for multislot class 3,
			// which is the only class that can not do 2-down/2-up,
			// and needs to be converted to 2-down/1-up,
			// but we'll go ahead and do an exhaustive check.
			//while (maxdown + maxup > slots.mMultislotSum) {
			//	if (maxup > 1) {
			//		maxup--;
			//	} else if (maxdown > 1) {
			//		maxdown--;
			//	} else {
			//		break;		// This is impossible, but be safe.
			//	}
			//}
		}

		msAssignChannels2(maxdown,maxup,slots.mMultislotSum);

		LOGWATCHF("Channel Assign, max:down/up=%d/%d ch down/up=%d/%d\n",
			maxdown,maxup,msPCHDowns.size(),msPCHUps.size());

        // If we are multislot, log a message:
        if (msPCHDowns.size() > 1) {
            std::ostringstream os;
            msDumpChannels(os);
            LOG(INFO) << "Multislot assignment for "<<this<<os;
        }

	} else {
		devassert(msPCHUps.size() != 0);
	}
	return true;
}

void MSInfo::msDeassignChannels()
{
	TBF *tbf;
	RN_MS_FOR_ALL_TBF(this,tbf) {
		if (tbf->isTransmitting()) {
			GLOG(ERR) << "DeassignChannels while TBF transmitting:"<<tbf;
			// The caller was suppsed to prevent this.
			// No recovery is possible; just kill it.
			tbf->mtCancel(MSStopCause::Goof,TbfNoRetry);
		}
	}
	// We dont call this if there are any active TBFs, but
	// there could be attached TBFs that have not started yet.
	// We must de-attach them to release the channels.
	msPCHDowns.clear();
	msPCHUps.clear();
}

// TODO:
void MSInfo::msReassignChannels()
{
	msDeassignChannels();
	// TODO?
	// tbf->mtDeReattach();
}

unsigned MSInfo::msGetDownlinkQueuedBytes()
{
	// Figure out how many bytes stacked up in the queue for this MS.
	unsigned nbytes = 0;
	TBF *tbf;
	RN_MS_FOR_ALL_TBF(this,tbf) {
		if (tbf->mtDir != RLCDir::Down) continue;
		nbytes += tbf->engineDownPDUSize();
	}
	return nbytes;
}

// The MS is in PacketTransfer mode if any TBFs are currently running, stalled or not.
RROperatingMode::type MSInfo::getRROperatingMode()
{
	if (msCountTBF2(RLCDir::Either,TbfMTransmitting,NULL)) {
		return RROperatingMode::PacketTransfer;
	} else {
		return RROperatingMode::PacketIdle;
	}
}

// This is used from the CCCH service code to find the TBF that requested service.
// It is possible that the TBF was destroyed while waiting for the paging opportunity, in which case return NULL.
//TBF *findTBFForCCCH()
//{
//	RN_MS_FOR_ALL_TBF(this,tbf) {
//		// Gosh, the tbf could be destroyed right now.
//		if (tbf->mtDir == RLCDir::Down && tbf->mtGetState() == TBFState::DataWaiting1) { return tbf; }
//	}
//	return NULL;
//}


// Count how many TBFs exist in the specified direction (which may be Either)
// are in the specified TbfMacroState.
// Normally there will only be one TBF.
// We also have to prevent starting a bunch of redundant nearly identical
// TBFs when the phone sends us a bunch of RACHes in a row.
int MSInfo::msCountTBF1(RLCDir::type dir, TbfMacroState tbfmstate, TBF**ptbf) const
{
	int count = 0;
	TBF *tbf;
	RN_MS_FOR_ALL_TBF(this,tbf) {
		if (dir == RLCDir::Either || tbf->mtDir == dir) {
			if (tbfmstate == TbfMActive && !tbf->isActive()) continue;
			if (tbfmstate == TbfMTransmitting && !tbf->isTransmitting()) continue;
			// We always ignore tbfs that are in the process of being deleted.
			if (tbf->mtGetState() == TBFState::Deleting) continue;
			if (tbf->mtGetState() == TBFState::Unused) continue;	// shouldnt happen
			if (ptbf) { *ptbf = tbf; }
			count++;
		}
	}
	return count;
}

// Count TBFs for this real MS, which means all MSInfo belonging to the real MS.
int MSInfo::msCountTBF2(RLCDir::type dir, TbfMacroState tbfmstate, TBF**ptbf)
{
	int count = msCountTBF1(dir,tbfmstate,ptbf);
	if (msAltTlli) {
		MSInfo *ms2 = gL2MAC.macFindMSByTlli(msAltTlli,false);
		if (ms2 == NULL) {
			// The old MSInfo expired naturally, and we will never have to worry about it again.
			msAltTlli = 0;
		} else {
			count += ms2->msCountTBF1(dir,tbfmstate,ptbf);
		}
	}
	return count;
}

int MSInfo::msCountActiveTBF(RLCDir::type dir, TBF**ptbf)
{
	return msCountTBF2(dir,TbfMActive,ptbf);
}

int MSInfo::msCountTransmittingTBF(RLCDir::type dir, TBF**ptbf)
{
	return msCountTBF2(dir,TbfMTransmitting,ptbf);
}

// Downlink time slots as defined by GSM04.60 12.18
// And I quote: Bit 8 indicates the status of timeslot 0,
// bit 7 indicates the status of timeslot 1, etc.
// Note that TN() runs 0..7 (see Time::incTN())
unsigned char MSInfo::msGetDownlinkTimeslots(MultislotSymmetry sym)
{
	unsigned char result = 0;
	//if (sym == MultislotSymmetric && msPCHUps.size() < msPCHDowns.size()) {
	//	// If the uplink and downlink tn size are  assymetric,
	//	// use the smaller array, which is always valid in both directions.
	//	PDCHL1Uplink *up;
	//	RN_FOR_ALL(PDCHL1UplinkList_t,msPCHUps,up) {
	//		result |= (1 << (7 - up->TN()));
	//	}
	//} else {
		PDCHL1Downlink *down;
		RN_FOR_ALL(PDCHL1DownlinkList_t,msPCHDowns,down) {
			result |= (1 << (7 - down->TN()));
		}
	//}
	return result;
}


//std::ostream& operator<<(std::ostream& os, const Statistic<ChannelCodingType> &stat) { stat.text(os); return os; }

void SignalQuality::dumpSignalQuality(std::ostream&os) const
{
	ios_base::fmtflags savedfoobarflags = os.flags();
	os.precision(2);
	os << "\t" << fixed;
	os << LOGVAR2("TimingError",msTimingError);
	os << LOGVAR2("RSSI",msRSSI);
	os << LOGVAR2("CV",msCValue);
	os << LOGVAR2("ILev",msILevel);
	os << LOGVAR2("RXQual",msRXQual);
	os << LOGVAR2("SigVar",msSigVar);
	os << LOGVAR2("ChCoding",msChannelCoding);
	os.flags(savedfoobarflags);		// What were these guys thinking?

	//ChannelCodingType ccup = msGetChannelCoding(RLCDir::Up);
	//ChannelCodingType ccdown = msGetChannelCoding(RLCDir::Down);
	//int cc = min((int)ccup,(int)ccdown);
	//if (ccup == ccdown) {
	//	os << LOGVAR2("ChannelCoding",(int)ccup);
	//} else {
	//	os << format(" ChannelCoding=%dup/%ddown",ccup,ccdown);
	//}
	os << "\n";
}


void SignalQuality::setRadData(RadData &rd)
{
	msRSSI.addPoint((int)rd.mRSSI);
	msTimingError.addPoint(rd.mTimingError);
}

void SignalQuality::setRadData(float wRSSI,float wTimingError)
{
	msRSSI.addPoint((int)wRSSI);
	msTimingError.addPoint(wTimingError);
}

// Determine whether we should use slow or fast channel coding for the specified direction.
ChannelCodingType MSInfo::msGetChannelCoding(RLCDirType wdir) const
{
	// Initial channel coding is determined from RSSI from most recent burst from MS.
	// If the signal strength was low (less than -40db) then use the slow speed.
	// TODO: For subsequent TBFs we should use statistics from previous TBFs.
	// BEGINCONFIG
	// 'GPRS.ChannelCodingControl.RSSI',-40,0,0,'If the initial signal strength is less than this amount in DB GPRS uses a lower bandwidth but more robust encoding CS-1'
	// ENDCONFIG

	// Allow user full control over the codecs with these options:
	const char *option = (wdir == RLCDir::Up) ? "GPRS.Codecs.Uplink" : "GPRS.Codecs.Downlink";
	const char *codecs = gConfig.getStr(option).c_str();
	// We only support CS1 and CS4.
	bool cs1allowed = strchr(codecs,'1');
	bool cs4allowed = strchr(codecs,'4');
	if (cs1allowed && cs4allowed) {
		// Choose codec based on initial signal strength:
		int fastRSSI = gConfig.getNum("GPRS.ChannelCodingControl.RSSI");
		return (msRSSI.getCurrent() < fastRSSI) ? ChannelCodingCS1 : ChannelCodingCS4;
	} else if (cs4allowed) {
		return ChannelCodingCS4;
	} else {
		return ChannelCodingCS1;
	}
}

// UNUSED
// Not a MSInfo member function, but still related to MSInfo.
// This function is (was) used to implement CHANGE-TLLI from the BSSG interface.
// 3-2012: (pat) removed as procedurally incorrect.  See notes at MSInfo struct.
// We need to keep a separate MSInfo for each TLLI.
// We might want to move some of the running timer info from the MSInfo for the old-TLLI
// to the MSInfo for the new-TLLI, but probably not even that because at the time this
// happens, the MSInfo for the new-TLLI will be the most recently used one, because
// we just received the attach-accept message.
// 6-2012: (pat) Changed my mind and replaced with msChangeTlli, msAliasTlli.
MSInfo *bssgMSChangeTLLI(uint32_t oldTLLI,uint32_t newTLLI)
{
    MSInfo *ms = NULL;
    GPRSLOG(1) << "MSChangeTLLI"<<LOGHEX(oldTLLI)<<LOGHEX(newTLLI);
	// (pat) 6-16-2012: I modified this code without testing,
	// since we longer use the BSSG interface.
    if ((ms = gL2MAC.macFindMSByTlli(newTLLI, false))) {
		ms->msChangeTlli(newTLLI);
	} else if ((ms = gL2MAC.macFindMSByTlli(oldTLLI, false))) {
		ms->msAliasTlli(newTLLI);
		ms->msChangeTlli(newTLLI);
	}
    //if ((ms = gL2MAC.macFindMSByTlli(oldTLLI, false))) {
    //    if (oldTLLI == ms->msTLLI) {
    //        ms->msSetTLLI(newTLLI);
    //    }
    //}
    return ms;
}


// This code is used with the integrated SGSN.
// The MS is identified by multilple TLLIs.
// The sgsn places the current tlli to be used in downlink in each message.
// This function makes sure that newTlli is the current one.
void MSInfo::msChangeTlli(uint32_t newTlli)
{
	if (! tlliEq(newTlli,msTlli)) {
		// If the message is in the queue for this MS, the MS must have been
		// identified by either msTlli or msOldTlli.
		devassert(tlliEq(newTlli,msOldTlli));
		MSInfo *ms2 = gL2MAC.macFindMSByTlli(newTlli,false);
		if (ms2 != this) {
			// This is kind of a serious problem.
			// It would only happen if the MS just happens to use a TLLI that is assigned by the SGSN.
			LOG(ERR) << "Changing TLLI of"<<this<<" from"<<LOGHEX(msTlli)<< " to"<<LOGHEX(newTlli)<<" MS already exists:"<<ms2;
			ms2->msDelete(false);
		}

		msOldTlli = msTlli;
		msDeprecated = false;
		if (msAltTlli) {
			MSInfo *ms3 = gL2MAC.macFindMSByTlli(msAltTlli,false);
			if (ms3 && ms3 != this) { msDeprecated = true; }
		}
	}
	// The newTlli may differ by the TLLI_LOCAL_BIT, so always set msTlli.
	msTlli = newTlli;
}

// In addition to alias tllis, we also accept foreign TLLIs, see macFindMSByTlli.
void MSInfo::msAliasTlli(uint32_t otherTlli)
{
	if (otherTlli == 0) return;
	if (!tlliEq(otherTlli,msTlli) && !tlliEq(otherTlli, msOldTlli)) {
		MSInfo *ms2 = gL2MAC.macFindMSByTlli(otherTlli,false);
		if (ms2) {
			devassert(ms2 != this);
			// Set the AltTlli so these two MSInfo structs reference each other,
			// since they are the same MS, and wont try to start simultaneous TBFs.
			// Conceivably there could be more than just two TLLIs, but I think it
			// is ok because the MS uses them serially and it will all just work out.
			// IMPORTANT:  The alt tlli is NOT the old tlli.
			// See the comments at MSinfo
			ms2->msAltTlli = this->msTlli;
			this->msAltTlli = ms2->msTlli;
		} else {
			// This code is processed by MAC when this message is first seen.
			// Set oldTlli so that we will know the TLLI is the same MS.
			// This may be switched by msChangeTlli when the message is processed.
			this->msOldTlli = otherTlli;
		}
	}
}

// Return index in data history arrays arrays, which is the current 48-block-multiframe
unsigned StatHits::histind()
{
	// There are approx 48 blocks per second.
	unsigned now = (gBSNNext / 48);	// Current 48-block-multiframe, modulo the hyperframe.
	unsigned i = now % cNumHist;
	if (now != mWhen[i]) {
		// Saved data is over 10 seconds old; clear it.
		mHistory[i].clear();
	}
	mWhen[i] = now;
	return i;
}

void StatHits::getStats(float *pER, int *pTotal, float *pWorstER, int *pWorstTotal)
{
	*pWorstER = 0.0;
	*pWorstTotal = 0;
	int total = 0, good = 0;
	int now = (gBSNNext / 48);	// Current 48-block-multiframe, modulo the hyperframe.

	StatTotalHits *hp = &mHistory[0];
	for (int i = 0; i < cNumHist; i++, hp++) {
		int age = (int)now - (int)mWhen[i];	// age of data in history bucket.
//printf(" i=%d now=%d when=%d age=%d",i,now,(int)mWhen[i],age);
		if (age < 0) { age += RLCBSN_t::BSNPeriodicity / 48; } // Account for BSN wrap.
		if (age >= cNumHist) { hp->clear(); continue; }	// Data too old.
		if (!hp->mTotal) {continue;}	// empty
		good += hp->mGood;
		total += hp->mTotal;
		float thisER = (float)(hp->mTotal-hp->mGood) / hp->mTotal;
		if (thisER > *pWorstER) {
			*pWorstER = thisER;
			*pWorstTotal = hp->mTotal;
		}
	}
	*pTotal = total;
	*pER = total ?  (float)(total-good)/total : 0;
//printf(" total=%d ER=%g\n",total,*pER);
}

// Format a number with up to 2 significant digits if < 1,
// but dont go less than .01 or to exponential notation.
// Kind of amazing there is no default format for this.
std::string fmtfloat2(float num)
{
	if (num < 0.005) {			// < 0.01 is 0
		return string("0");
	} else if (num < 0.2) {		// .01 - .19, 2 sig digit
		return format("%.2f",num).substr(1);
	} else if (num < 1) {		// .2 - .9, 1 sig digit ok.
		return format("%.1f",num).substr(1);
	} else if (num < 10) {		// 1.0 - 9.9, 2 sig digit
		return format("%.1f",num);
	} else {					// 10 - infinity, whatever digits needed.
		return format("%.0f",num);
	}
}


static void	putER(std::ostream&os, const char*label, float er, int total)
{
	// Like this: "99.9% (7) low: 4% (2)"
	os << label << fmtfloat2(er) << "% (" << total << ")";
}

// Print the average for the last N seconds and worst second.
void StatHits::textRecent(std::ostream &os)
{
	float avgER, worstER;
	int total, worstTotal;
	getStats(&avgER,&total,&worstER,&worstTotal);
	putER(os,"",avgER,total);
	// Dont bother to print worst if there is none.
	if (worstER) { putER(os," low:",worstER,worstTotal); }
}

void StatHits::textTotal(std::ostream&os)
{
	float er = mTotal.mTotal ? ((double)mTotal.mTotal - mTotal.mGood) / mTotal.mTotal : 0;
	putER(os,"",er,(int)mTotal.mTotal);
}

void MSInfo::msDumpCommon(std::ostream&os) const
{
	os << "\t";
	os << LOGVAR(msNumDataUSFGrants);
	os << LOGVAR(msAckNackUSFGrant);
	if (msOldTlli) os << LOGHEX(msOldTlli);
	if (msAltTlli) os << LOGHEX(msAltTlli);
	if (msDeprecated) os << LOGVAR(msDeprecated);
	if (msPacch) { os << " Pacch="; msPacch->shortId(); }
	os << LOGVAR2("idle",msIdleCounter);
	//os << LOGVAR(msTimingErrorCount);
	os << "\n";
}

void MSInfo::msDumpChannels(std::ostream &os) const
{
	PDCHL1Uplink *up; PDCHL1Downlink *down;
	os <<" channels:";
	int howmany = 0;
	RN_FOR_ALL_CONST(PDCHL1DownlinkList_t,msPCHDowns,down) {
		if (howmany++ == 0) os << " down=(";
		os << format(" %d:%d",down->CN(),down->TN());
	}
	if (howmany) os << ")";
	howmany = 0;
	RN_FOR_ALL_CONST(PDCHL1UplinkList_t,msPCHUps,up) {
		if (howmany++ == 0) os << " up=(";
		int tn = up->TN();
		os << format(" %d:%d,usf=%d",up->CN(),tn,(int)msUSFs[tn]);
	}
	if (howmany) os << ")";
}

void MSStat::msStatDump(const char *indent, std::ostream &os)
{
	os << indent;
	os << " dataER:"; msCountBlocks.textTotal(os);
	os << " recent:"; msCountBlocks.textRecent(os);
	{
		int fails = msCountTbfFail + msCountTbfNoConnect;
		float tbfER = (float)fails/msCountTbfs;
		//os << format(" tbfER:%.1f%% (%d)", 100.0*tbfER,(int)msCountTbfs);
		os << " tbfER:";
		putER(os,"",tbfER,(int)msCountTbfs);
	}
	os << "\n" << indent;
	os << " rrbpER:"; msCountRbbpReservations.textTotal(os);
	os << " recent:"; msCountRbbpReservations.textRecent(os);
	os << " ccchER:"; msCountCcchReservations.textTotal(os);
	os << " recent:"; msCountCcchReservations.textRecent(os);
	os << "\n";
}

void MSInfo::msDump(std::ostream&os, SGSN::PrintOptions options)
{
	int i;
	RROperatingMode::type rrmode = getRROperatingMode();
	os << this		// Dumps the operator<< value, which is sprintf(MS#%d,msDebugId)
		//<< LOGHEX(msTlli)		// The TLLI is in the default id now, so do not reprint it.
		<< LOGVAR(rrmode)
		<< " Bytes:" << msBytesUp << "up/" << msBytesDown << "down"
		// The TrafficMetric is total number of blocks sent and received,
		// decayed by 1/2 every 24 blocks, so max is 48/channel.
		// The metric will be > 100% if multiple channels are used simultaneously.
		// Note: channel = uplink or downlink, so single slot MS with uplink and downlink TBFs
		// could reach 200%.  Even a one-way TBF uses some of the other direction so can exceed 100%.
		//<< format(" Utilization=%.1f%%",100.0 * msTrafficMetric / 48.0)
		<< " Utilization=" << fmtfloat2(100.0 * msTrafficMetric / 48.0) << "%"
		<< "\n";
	os << "\t"; sgsnPrint(msTlli,options | SGSN::printNoMsId,os);
	dumpSignalQuality(os);
	msStatDump("\t",os);

	if (!(options & SGSN::printVerbose)) {return;}

	// In case the queue is completely stalled or suspended, add a new data point for
	// the current max delay if it is greater.
	if (msDownlinkQueue.size()) {
		double curage = msDownlinkQOldest.elapsed()/1000.0;
		if (curage > msDownlinkQDelay.getCurrent()) { msDownlinkQDelay.addPoint(curage); }
	}
	os << "\t" << LOGVAR2("DownlinkQ:bytes",msDownlinkQStat)
		<< LOGVAR2("delay",msDownlinkQDelay)
		<< "\n";

	os << "\t TBFs:" <<LOGVAR2("total",msCountTbfs);
	if (msCountTbfFail) { os<<LOGVAR2("failed",msCountTbfFail); }
	if (msCountTbfNoConnect) { os<<LOGVAR2("NC",msCountTbfNoConnect); }
	os <<"\n";

	os << "\t current=(";
		TBF *tbf;
		RN_MS_FOR_ALL_TBF(this,tbf) {
			//os << " "<<tbf << (tbf->mtDir == RLCDir::Up ? "(up)" : "(down)");
			os << " "<<tbf <<tbf->mtDir;
		}
	os << ")\n";

	os << "\t USFs=(";
		for (i = 0; i < 8; i++) { os << " " << msUSFs[i]; }
	os << " )\n";

	os << "\t"; msDumpChannels(os); os << "\n";
	msDumpCommon(os);
}

string MSInfo::id() const
{
	char buf[100];
	sprintf(buf," MS#%d,TLLI=%x", msDebugId,(uint32_t)msTlli);
	if (msOldTlli) { sprintf(buf+strlen(buf),",%x",(uint32_t)msOldTlli); }
	return string(buf);
}
std::ostream& operator<<(std::ostream& os, const MSInfo*ms)
{
	if (ms) {
		os << ms->id();	// not efficient, but only for debugging.
	} else {
		os << " MS#(null)";
	}
	return os;
}

};	// namespace GPRS
