#include maps\mp\_utility;

//MMR = Match Making Rank (our metric for skill-based matchmaking in ranked playlist mode)

DIVISION_TABLE_NAME	= "mp/divisiontable.csv";
DIVISION_TABLE_TIER_COL = 0;
DIVISION_TABLE_MIN_PT_COL = 1;
DIVISION_TABLE_MAX_PT_COL = 2;
DIVISION_TABLE_WIN_COL = 1;
DIVISION_TABLE_TIE_COL = 2;
DIVISION_TABLE_LOSS_COL = 3;
DIVISION_TABLE_RESET_COL = 1;

NUM_CHALLENGE_BITS = 3;

// These BIT flags must match those defined in g_rankedplaylist.cpp
DIVISION_PREDICTION_WIN_BIT			= 1;
DIVISION_PREDICTION_LOSE_BIT		= 2;
DIVISION_PREDICTION_RANKED_BIT		= 8;

MATCH_RESULT_TIE = "tie";
MATCH_RESULT_WIN = "win";
MATCH_RESULT_LOSS = "loss";

MAX_USHORT = 65535;
MIN_SHORT = -32768;

init()
{
	if ( InVirtualLobby() )
		return;

	if ( GetDvarInt( "scr_game_division" ) == 1 )
	{
		level.teamMMR["allies"] = 0;
		level.teamMMR["axis"] = 0;

		thread onMatchEnd();
	}
	else
	{
		thread onNonRankedMatchStart();
	}
}

isValidPlayer()	//self == player
{
	return ( !IsBot( self ) && !IsAgent( self ) && !IsTestClient( self ) );
}

onNonRankedMatchStart()
{
	level endon( "game_win" );
	level endon( "exitLevel_called" );
	
	level waittill ( "prematch_over" );
	waitframe();
	
	foreach( player in level.players )
	{
		player clearNonRankedMatchPrediction();
	}
		
	while( true )
	{
		level waittill( "connected", player );
		player clearNonRankedMatchPrediction();
	}
}


onMatchEnd()
{
	level endon( "exitLevel_called" );
	
	for(;;)
	{
		level waittill( "game_win", winners );
/#
		PrintLn( "[RP] final game winner = " + winners );
#/				
		
		if ( !IsDefined( winners ) || ( winners != "axis" && winners != "allies" && winners != "tie" ) )
			continue;
			
		calculateTeamMMRs();
		
		foreach( player in level.players )
		{
			if ( !player isValidPlayer() )
				continue;
			
			player calculateMatchPrediction();
						
			startDP = player GetRankedPlayerData( "division" );
			
			if ( player.team == winners )
				player onDivisionWin();
			else if ( winners == "tie" )
				player onDivisionTie();
			else
				player onDivisionLoss();
			
			endDP = player GetRankedPlayerData( "division" );
/#			
			PrintLn( "[RP]Saving player matchDp as ( " + endDP + " - " + startDP + " ) = " + ( endDP - startDP ) + ", encoded as " + ( endDP - startDP + 128 ) );
#/
			player SetCommonPlayerData( "round", "matchDp", ( endDP - startDP + 128 /*encode signed byte into unsigned byte*/ ) );
		}
		
		break;
	}
}

onDivisionWin()	//self == player
{
	dp = self GetRankedPlayerData( "division" );
	
	self getDivisionPoints( dp );
	
	if ( !IsDefined( self.minDP ) )
		return;
	
	deltaDP = getDivisionPointsDelta( self.matchPrediction, "win" );
/#
	PrintLn( "[RP]Division Play win, delta DP = " + deltaDP );
#/	
	self setPlayerMMR( deltaDP );
	
	if ( IsDefined( self.maxDP ) )
	{
		if ( self.maxDP == dp )
		{
			//in division progression challenge
			self getDivisionChallengeStatus();
			self.divisionPlays++;
			self.divisionWins++;
			
			if ( self.divisionWins >= 2 )
			{
				//progress to next division
				self clearDivisionChallengeStatus();
/#
				PrintLn( "[RP]Jumping to next division in challenge state after win. division -> " + ( dp + 1 ) );
#/
				self SetRankedPlayerData( "division", dp + 1 );
			}
			else
			{
/#
				PrintLn( "[RP]Remaining in challenge state after win." );
#/
				self updateDivisionChallengeStatus( true /*win*/ );
			}
		}
		else	
		{
			//not in challenge state
/#
			PrintLn( "[RP]Pushing division in normal state after win.  min (" + self.maxDP + ", " + (dp + deltaDP) + ")" );
#/
			self SetRankedPlayerData( "division", int( min( float(self.maxDP), float(dp + deltaDP) ) ) );
		}
	}
	else	
	{
		//in masters division
/#
		PrintLn( "[RP]Pushing division in masters division after win.  division -> " + (dp + deltaDP) );
#/
		self SetRankedPlayerData( "division", dp + deltaDP );
	}
}

onDivisionLoss()	//self == player
{
	dp = self GetRankedPlayerData( "division" );
	
	self getDivisionPoints( dp );
	
	if ( !isDefined( self.minDP ) )
		return;
	
	deltaDP = getDivisionPointsDelta( self.matchPrediction, "loss" );
/#
	PrintLn( "[RP]Division Play loss, delta DP = " + deltaDP );
#/	
	self setPlayerMMR( deltaDP );

	if ( isDefined( self.maxDP ) && self.maxDP == dp )
	{
		//in division progression challenge
		self getDivisionChallengeStatus();
		self.divisionPlays++;
		self.divisionLosses++;
		
		if( self.divisionLosses >= 2 )
		{
			//failed challenge
			self clearDivisionChallengeStatus();
/#
			PrintLn( "[RP]Dropping division after second loss in challenge state, division -> " + (self.minDP + getDivisionResetOffset()) );
#/
			self SetRankedPlayerData( "division", self.minDP + getDivisionResetOffset() );
		}
		else
		{
/#
			PrintLn( "[RP]Remaining in division challenge after loss." );
#/
			self updateDivisionChallengeStatus( false /*lost*/ );
		}
	}
	else if ( self.minDP == dp )
	{
		//in division relegation state
		if ( self.minDP > 0 )
		{	
			relegationLosses = self GetRankedPlayerData( "divisionRelegationCounter" );
			relegationLosses++;
	
			if ( relegationLosses >= 3 )
	{
				//relegating
				self getDivisionPoints( dp - 1 );
/#
				PrintLn( "[RP]Dropping division after loss during relegation, division -> " + (self.minDP + getDivisionResetOffset() ) );
#/
				self SetRankedPlayerData( "division", self.minDP + getDivisionResetOffset() );
				self SetRankedPlayerData( "divisionRelegationCounter", 0 );
	}
			else
	{
/#
				PrintLn( "[RP]Ticking relegation counter after loss." );
#/
				self SetRankedPlayerData( "divisionRelegationCounter", relegationLosses );
			}
		}
	}
	else
	{
		//not in division challenge
/#
		PrintLn( "[RP]Setting division after loss while in normal state.  max (" + self.minDP + ", " + (dp + deltaDP) + ")" );
#/
		self SetRankedPlayerData( "division", int( max( float( self.minDP ), float( dp + deltaDP ) ) ) );
	}
}

onDivisionTie()
{
	dp = self GetRankedPlayerData( "division" );
	
	self getDivisionPoints( dp );
	
	if ( !isDefined( self.minDP ) )
		return;
	
	deltaDP = getDivisionPointsDelta( self.matchPrediction, "tie" );
/#
	PrintLn( "[RP]Division Play tie, delta DP = " + deltaDP );
#/
	self setPlayerMMR( deltaDP );
	
	if ( isDefined( self.maxDP ) )
	{
		if ( self.maxDP == dp )
		{
			//ignore ties while in progression challenge
/#
			PrintLn( "[RP]Ignoring results for tie while in challenge state." );
#/
			return;
		}
		else if ( deltaDP > 0 )
		{
			//sub-master divisions
/#
			PrintLn( "[RP]Setting division after tie in normal state.  min (" + self.maxDP + ", " + (dp + deltaDP) + ")" );
#/
			self SetRankedPlayerData( "division", int( min( float( self.maxDP ), float( dp + deltaDP ) ) ) );
			return;
		}
	}
	
/#
	PrintLn( "[RP]Setting division after tie while in masters division. max (" + self.minDP + ", " + (dp + deltaDP) + ")" );
#/
	self SetRankedPlayerData( "division", int( max( float( self.minDP ), float(dp + deltaDP ) ) ) );
}

getDivisionPoints( dp )		//self == player
{
	numRows = TableGetRowCount( DIVISION_TABLE_NAME );
	firstRow = TableLookupRowNum( DIVISION_TABLE_NAME, DIVISION_TABLE_TIER_COL, "bronze" );
	for( row = firstRow; row < numRows; row++ )
	{
		maxPts = TableLookupByRow( DIVISION_TABLE_NAME, row, DIVISION_TABLE_MAX_PT_COL );
		if ( !IsDefined( maxPts ) || maxPts == "" )
		{
			//master division
			self.minDP = int( TableLookupByRow( DIVISION_TABLE_NAME, row, DIVISION_TABLE_MIN_PT_COL ) );
			self.maxDP = undefined;
			return;
		}
		
		iMaxPts = int( maxPts );
		if ( dp <= iMaxPts )
		{
			//below master division
			self.minDP = int( TableLookupByRow( DIVISION_TABLE_NAME, row, DIVISION_TABLE_MIN_PT_COL ) );
			self.maxDP = iMaxPts;
			return;
		}
	}
	
	/#
	AssertMsg( "Failed to get division point min/max for division points " + dp + " in " + DIVISION_TABLE_NAME );
	#/
	
	self.minDP = undefined;
	self.maxDP = undefined;
}

getDivisionChallengeStatus()	//self == player
{
	playBits = self GetRankedPlayerData( "divisionChallengePlayed" );
	winBits = self GetRankedPlayerData( "divisionChallengeWon" );
	
	plays = 0;
	wins = 0;
	losses = 0;
	
	for( i = 0; i < NUM_CHALLENGE_BITS; i++ )
	{
		if ( (playBits >> i) & 1 )
		{
			plays++;
			if ( (winBits >> i) & 1 )
				wins++;
			else
				losses++;
		}
		else
		{
			break;
		}
	}
	
	self.divisionPlays = plays;
	self.divisionWins = wins;
	self.divisionLosses = losses;
}

clearDivisionChallengeStatus()	//self == player
{
/#
	PrintLn( "[RP]Clearning division challenge flags." );
#/
	self SetRankedPlayerData( "divisionChallengePlayed", 0 );
	self SetRankedPlayerData( "divisionChallengeWon", 0 );	
}

updateDivisionChallengeStatus( won )	//self == player
{
	playBits = self GetRankedPlayerData( "divisionChallengePlayed" );
	winBits = self GetRankedPlayerData( "divisionChallengeWon" );
	
	playBits = ( playBits << 1 ) | 1;
	winBits = ( winBits << 1 );
	
	if( won )
	{
		winBits = winBits | 1;
	}
	
/#
	PrintLn( "[RP]Setting divisionChallengePlayed bitfield to " + playBits );
#/
	self SetRankedPlayerData( "divisionChallengePlayed", playBits );
/#
	PrintLn( "[RP]Setting divisionChallengeWon bitfield to " + winBits );
#/
	self SetRankedPlayerData( "divisionChallengeWon", winBits );
}

getMMR()	//self == player
{
	//MMR is stored in signed short range, convert to unsigned for script usage
	self.MMR = self GetRankedPlayerData( "mmr" );
	self.MMR -= MIN_SHORT;
}

saveMMR()	//self == player
{
	//convert MMR to signed short range for storage
	mmr = self.MMR + MIN_SHORT;
	self SetRankedPlayerData( "mmr", mmr );
}

saveMatchPrediction()	//self == player
{
	//add 4 to prediction value, this is a sneaky way to tell on the runtime side if the player has left early w/o adding extra field to playerdata
	predictionFlags = DIVISION_PREDICTION_RANKED_BIT;
	
/#
	PrintLn( "[RP]Saving match prediction.  prediction = " + self.matchPrediction + ", predictionFlags = " + predictionFlags );
#/
	if ( self.matchPrediction == "win" )
		predictionFlags = predictionFlags | DIVISION_PREDICTION_WIN_BIT;
	else if( self.matchPrediction == "loss" )
		predictionFlags = predictionFlags | DIVISION_PREDICTION_LOSE_BIT;
	// "tie" is 0, nothing to add
	
	self SetCommonPlayerData( "round", "matchPrediction", predictionFlags );
}

clearNonRankedMatchPrediction()	//self == player
{
	if ( !self isValidPlayer() )
			return;

/#
	PrintLn( "[RP]Clearing match prediction." );
#/
	
	self SetCommonPlayerData( "round", "matchPrediction", 0 );
}

calculateTeamMMRs()	
{
	numPlayers["axis"] = 0;
	numPlayers["allies"] = 0;
	level.teamMMR["axis"] = 0;
	level.teamMMR["allies"] = 0;
	
	foreach( player in level.players )
	{
		if ( !player isValidPlayer() )
			continue;
		
		if ( !IsDefined( player.team ) || ( player.team != "allies" && player.team != "axis" ) )
			continue;
		
		if ( !IsDefined( player.MMR ) )
			player getMMR();
		
		numPlayers[ player.team ]++;
		level.teamMMR[ player.team ] += player.MMR;
	}
	
	if ( numPlayers[ "axis" ] > 0 )
		level.teamMMR[ "axis" ] /= numPlayers[ "axis" ];
	if ( numPlayers[ "allies" ] > 0 )
		level.teamMMR[ "allies" ] /= numPlayers[ "allies" ];
}

calculateMatchPrediction()		//self == player
{
	otherMMR = self.MMR;
	
	self.matchPrediction = MATCH_RESULT_TIE;

	if ( !IsDefined( self.team ) )
		return;
	else if ( self.team == "axis" )
		otherMMR = level.teamMMR[ "allies" ];
	else if ( self.team == "allies" )
		otherMMR = level.teamMMR[ "axis" ];
	else
		return;
	
	if ( !IsDefined( self.MMR ) )
		return;
	
	if ( self.MMR < (otherMMR - 10) )
		self.matchPrediction = MATCH_RESULT_LOSS;
	
	if ( self.MMR > (otherMMR + 10) )
		self.matchPrediction = MATCH_RESULT_WIN;
}

getDivisionPointsDelta( prediction, result )
{
	predictionRow = TableLookupRowNum( DIVISION_TABLE_NAME, DIVISION_TABLE_TIER_COL, prediction );
	if ( result == "win" )
		return int( TableLookupByRow( DIVISION_TABLE_NAME, predictionRow, DIVISION_TABLE_WIN_COL ) );
	if ( result == "loss" )
		return int( TableLookupByRow( DIVISION_TABLE_NAME, predictionRow, DIVISION_TABLE_LOSS_COL ) );
	
	return int( TableLookupByRow( DIVISION_TABLE_NAME, predictionRow, DIVISION_TABLE_TIE_COL ) );
}

getDivisionResetOffset()
{
	return int( TableLookup( DIVISION_TABLE_NAME, DIVISION_TABLE_TIER_COL, "resetOffset", DIVISION_TABLE_RESET_COL ) );
}

setPlayerMMR( deltaDP )		//self == player
{
	if ( !IsDefined( self.MMR ) )
		self getMMR();
	
	if ( !IsDefined( self.MMR ) )
		return;
	
	//clamp [0, MAX_USHORT]
	if ( deltaDP < 0 && self.MMR < (0 - deltaDP) )
		deltaDP = 0 - self.MMR;
	
	if ( (MAX_USHORT - self.MMR) < deltaDP )
		deltaDP = MAX_USHORT - self.MMR;
	
	self.MMR += deltaDP;
	self saveMMR();
	self saveMatchPrediction();
}
