/* bzflag
 * Copyright (c) 1993-2017 Tim Riker
 *
 * This package is free software;  you can redistribute it and/or
 * modify it under the terms of the license found in the file
 * named COPYING that should have accompanied this file.
 *
 * THIS PACKAGE IS PROVIDED ``AS IS'' AND WITHOUT ANY EXPRESS OR
 * IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.
 */

/* interface header */
#include "ScoreboardRenderer.h"

/* common implementation headers */
#include "Bundle.h"
#include "Team.h"
#include "FontManager.h"
#include "BZDBCache.h"
#include "OpenGLGState.h"
#include "TextUtils.h"
#include "TimeKeeper.h"

/* local implementation headers */
#include "LocalPlayer.h"
#include "World.h"
#include "sound.h"

// because of the 'player' crap, we can't  #include "Roaming.h"  easily
extern Player* getRoamTargetTank();

#define DEBUG_SHOWRATIOS 1

std::string ScoreboardRenderer::scoreSpacingLabel("88% 8888 888-888 [88]");
std::string ScoreboardRenderer::scoreLabel("Score");
std::string ScoreboardRenderer::killSpacingLabel("888~888 Hunt->");
std::string ScoreboardRenderer::killLabel(" Kills");
std::string ScoreboardRenderer::teamScoreSpacingLabel("88 (888-888) 88");
std::string ScoreboardRenderer::teamCountSpacingLabel("888");
std::string ScoreboardRenderer::playerLabel("Player");

// NOTE: order of sort labels must match SORT_ consts
const char* ScoreboardRenderer::sortLabels[] = {
  "[Score]",
  "[Normalized Score]",
  "[Reverse Score]",
  "[Callsign]",
  "[Team Kills]",
  "[TK ratio]",
  "[Team]",
  "[1on1]",
  NULL
};

int ScoreboardRenderer::sortMode = 0;

bool ScoreboardRenderer::alwaysShowTeamScore = 0;


ScoreboardRenderer::ScoreboardRenderer() :
				winX(),
				winY(),
				winWidth (0.0),
				winHeight(),
				dim(false),
				huntPosition(0),
				huntSelectEvent(false),
				huntPositionEvent(0),
				huntState(HUNT_NONE),
	huntAddMode(false),
	teamScoreYVal(0.0f),
	roaming(false),
	minorFontFace(),
	minorFontSize(),
	labelsFontFace(),
	labelsFontSize(),
	scoreLabelWidth(),
	killsLabelWidth(),
	teamScoreLabelWidth(),
	teamCountLabelWidth(),
	huntArrowWidth(),
	huntPlusesWidth(),
	huntedArrowWidth(),
	tkWarnRatio(),
	numHunted(0)
{
  // initialize message color (white)
  messageColor[0] = 1.0f;
  messageColor[1] = 1.0f;
  messageColor[2] = 1.0f;
  sortMode = BZDB.getIntClamped("scoreboardSort", 0, SORT_MAXNUM);
  alwaysShowTeamScore = (BZDB.getIntClamped("alwaysShowTeamScore", 0, 1) != 0);
}


/*  Set window size and location to be used to render the scoreboard.
 *  Values are relative to .... when render() is invoked.
*/
void  ScoreboardRenderer::setWindowSize (float x, float y, float width, float height)
{
  winX = x;
  winY = y;
  winWidth = width;
  winHeight = height;
  setMinorFontSize (winHeight);
  setLabelsFontSize (winHeight);
}



ScoreboardRenderer::~ScoreboardRenderer()
{
  // no destruction needed
}


const char  **ScoreboardRenderer::getSortLabels ()
{
  return sortLabels;
}


void ScoreboardRenderer::setSort (int _sortby)
{
  sortMode = _sortby;
  BZDB.setInt ("scoreboardSort", sortMode);
}

int ScoreboardRenderer::getSort ()
{
  return sortMode;
}

void    ScoreboardRenderer::setAlwaysTeamScore (bool _onoff)
{
  alwaysShowTeamScore = _onoff;
  BZDB.set ("alwaysShowTeamScores", _onoff ? "1" : "0");
}

bool    ScoreboardRenderer::getAlwaysTeamScore ()
{
  return alwaysShowTeamScore;
}

void ScoreboardRenderer::setMinorFontSize(float height)
{
  FontManager &fm = FontManager::instance();
  minorFontFace = fm.getFaceID(BZDB.get("consoleFont"));

  switch (static_cast<int>(BZDB.eval("scorefontsize"))) {
  case 0: { // auto
    const float s = height / 72.0f;
    minorFontSize = floorf(s);
    break;
  }
  case 1: // tiny
    minorFontSize = 6;
    break;
  case 2: // small
    minorFontSize = 8;
    break;
  case 3: // medium
    minorFontSize = 12;
    break;
  case 4: // big
    minorFontSize = 16;
    break;
  }

  huntArrowWidth = fm.getStrLength(minorFontFace, minorFontSize, "->");
  huntPlusesWidth = fm.getStrLength(minorFontFace, minorFontSize, "@>");
  huntedArrowWidth = fm.getStrLength(minorFontFace, minorFontSize, "Hunt->");
  scoreLabelWidth = fm.getStrLength(minorFontFace, minorFontSize, scoreSpacingLabel);
  killsLabelWidth = fm.getStrLength(minorFontFace, minorFontSize, killSpacingLabel);
  teamScoreLabelWidth = fm.getStrLength(minorFontFace, minorFontSize, teamScoreSpacingLabel);
  teamCountLabelWidth = fm.getStrLength(minorFontFace, minorFontSize, teamCountSpacingLabel);
  const float spacing = fm.getStrLength(minorFontFace, minorFontSize, " ");
  scoreLabelWidth += spacing;
  killsLabelWidth += spacing;
}


void ScoreboardRenderer::setLabelsFontSize(float height)
{
  const float s = height / 96.0f;
  FontManager &fm = FontManager::instance();
  labelsFontFace = fm.getFaceID(BZDB.get("consoleFont"));
  labelsFontSize = floorf(s);
}


void ScoreboardRenderer::setDim(bool _dim)
{
  dim = _dim;
}


static const float dimFactor = 0.2f;

void ScoreboardRenderer::hudColor3fv(const GLfloat* c)
{
  if (dim)
    glColor3f(dimFactor * c[0], dimFactor * c[1], dimFactor * c[2]);
  else
    glColor3fv(c);
}




void    ScoreboardRenderer::exitSelectState (void) {
  playLocalSound(SFX_HUNT_SELECT);
  if (numHunted > 0) {
    setHuntState(HUNT_ENABLED);
  } else {
    setHuntState(HUNT_NONE);
  }
}




void ScoreboardRenderer::render(bool forceDisplay)
{
  FontManager &fm = FontManager::instance();
  if (dim) {
    fm.setOpacity(dimFactor);
  }

  if (BZDB.isTrue("displayScore") || forceDisplay) {
    OpenGLGState::resetState();
    renderScoreboard();
  } else {
    if (getHuntState() == HUNT_SELECTING) {	// 'S' pressed while selecting ...
      exitSelectState ();
    }
    if (BZDB.isTrue("alwaysShowTeamScores") && World::getWorld()->allowTeams()) {
      OpenGLGState::resetState();
      renderTeamScores(winWidth, winY,
	    FontManager::instance().getStrHeight(minorFontFace, minorFontSize, " "));
    }
  }

  if (dim) {
    fm.setOpacity(1.0f);
  }
}


int ScoreboardRenderer::teamScoreCompare(const void* _c, const void* _d)
{
  Team* c = World::getWorld()->getTeams()+*(const int*)_c;
  Team* d = World::getWorld()->getTeams()+*(const int*)_d;
  return (d->getWins()-d->getLosses()) - (c->getWins()-c->getLosses());
}

// invoked by playing.cxx when 'prev' is pressed
void ScoreboardRenderer::setHuntPrevEvent()
{
  huntPositionEvent = -1;
  --huntPosition;
}

// invoked by playing.cxx when 'next' is pressed
void ScoreboardRenderer::setHuntNextEvent()
{
  huntPositionEvent = 1;
  ++huntPosition;
}

// invoked by playing.cxx when select (fire) is pressed
void ScoreboardRenderer::setHuntSelectEvent ()
{
  huntSelectEvent = true;
}

// invoked by clientCommands.cxx when '7' or 'U' is pressed
void ScoreboardRenderer::huntKeyEvent (bool isAdd)
{
  if (getHuntState() == HUNT_ENABLED) {
    if (isAdd) {
      setHuntState(HUNT_SELECTING);
      playLocalSound(SFX_HUNT_SELECT);
    } else {
      setHuntState(HUNT_NONE);
      playLocalSound(SFX_HUNT);
    }
    huntAddMode = isAdd;

  } else if (getHuntState() == HUNT_SELECTING) {
    exitSelectState ();

  } else {
    setHuntState(HUNT_SELECTING);
    playLocalSound(SFX_HUNT_SELECT);
    huntAddMode = isAdd;
    if (!BZDB.isTrue("displayScore"))
      BZDB.set("displayScore", "1");
  }
}


void ScoreboardRenderer::setHuntState (int _huntState)
{
  if (huntState == _huntState)
    return;
  if (_huntState != HUNT_SELECTING)
    huntAddMode = false;
  if (_huntState==HUNT_NONE) {
    clearHuntedTanks();
  } else if (_huntState==HUNT_SELECTING) {
    huntPosition = 0;
  }
  huntState = _huntState;
}


int ScoreboardRenderer::getHuntState() const
{
  return huntState;
}


// invoked when joining a server
void ScoreboardRenderer::huntReset()
{
    huntState = HUNT_NONE;
    numHunted = 0;
}

void ScoreboardRenderer::renderTeamScores (float x, float y, float dy) {
  // print teams sorted by score
  int teams[NumTeams];
  int teamCount = 0;
  int i;
  float xn, xl;
  std::string label;

  if (teamScoreYVal > 0.0)
	  y = teamScoreYVal;

  if (World::getWorld()->allowRabbit())
    return;

  Bundle *bdl = BundleMgr::getCurrentBundle();
  FontManager &fm = FontManager::instance();
  hudColor3fv(messageColor);

  label = bdl->getLocalString("Team Score");
  xl = xn = x - teamScoreLabelWidth - teamCountLabelWidth;
  fm.drawString(xl, y, 0, minorFontFace, minorFontSize, label);

  for (i = RedTeam; i < NumTeams; i++) {
    if (!Team::isColorTeam(TeamColor(i))) continue;
    const Team* team = World::getWorld()->getTeams() + i;
    if (team->size == 0) continue;
    teams[teamCount++] = i;
  }
  qsort(teams, teamCount, sizeof(int), teamScoreCompare);
  y -= dy;

  char score[44];
  for (i = 0 ; i < teamCount; i++) {
    Team& team = World::getWorld()->getTeam(teams[i]);
    sprintf(score, "%3d (%3d-%-3d) %3d", team.getWins() - team.getLosses(), team.getWins(), team.getLosses(), team.size);
    hudColor3fv(Team::getTankColor((TeamColor)teams[i]));
    fm.drawString(xn, y, 0, minorFontFace, minorFontSize, score);
    y -= dy;
  }
}


// not used yet - new feature coming
void ScoreboardRenderer::renderCtfFlags () {
  int i;
  RemotePlayer* player;
  const int curMaxPlayers = World::getWorld()->getCurMaxPlayers();
  char flagColor[200];

  FontManager &fm = FontManager::instance();
  const float x = winX;
  const float y = winY;
  const float dy = fm.getStrHeight(minorFontFace, minorFontSize, " ");
  float y0 = y - dy;

  hudColor3fv(messageColor);
  fm.drawString(x, y, 0, minorFontFace, minorFontSize, "Team Flags");

  fm.setDimFactor(dimFactor);

  for (i=0; i < curMaxPlayers; i++) {
    if ((player = World::getWorld()->getPlayer(i))) {
      FlagType* flagd = player->getFlag();
      TeamColor teamIndex = player->getTeam();
      if (flagd!=Flags::Null && flagd->flagTeam != NoTeam) {	// if player has team flag ...
	std::string playerInfo = Team::getAnsiCode(flagd->flagTeam);
	snprintf(flagColor, 200, "%-12s", flagd->flagName.c_str());
	playerInfo += flagColor;
	playerInfo += Team::getAnsiCode(teamIndex);
	playerInfo += player->getCallSign();

	fm.drawString(x, y0, 0, minorFontFace, minorFontSize, playerInfo);
	y0 -= dy;
      }
    }
  }
  renderTeamScores (winWidth, y, dy);
}


void ScoreboardRenderer::clearHuntedTanks ()
{
  World *world = World::getWorld();
  if (!world)
    return;
  const int curMaxPlayers = world->getCurMaxPlayers();
  Player *p;
  for (int i=0; i<curMaxPlayers; i++) {
    if ((p = world->getPlayer(i)))
      p->setHunted (false);
  }
  numHunted = 0;
}


void ScoreboardRenderer::renderScoreboard(void)
{
  int i=0;
  int numPlayers;
  int mottoLen;
  Player** players;
  Player*  player;
  bool haveObs = false;

  if ( (players = newSortedList (sortMode, true, &numPlayers)) == NULL)
    return;

  LocalPlayer* myTank = LocalPlayer::getMyTank();
  Bundle *bdl = BundleMgr::getCurrentBundle();
  FontManager &fm = FontManager::instance();

  const float x1 = winX;
  const float x2 = x1 + scoreLabelWidth;
  const float x3 = x2 + killsLabelWidth;
  const float y0 = winY;
  hudColor3fv(messageColor);

  std::string psLabel = bdl->getLocalString(playerLabel);
  if (sortMode != SORT_SCORE) {
    psLabel += "  ";
    psLabel += sortLabels[sortMode];
  }
  fm.drawString(x1, y0, 0, minorFontFace, minorFontSize, bdl->getLocalString(scoreLabel));
  fm.drawString(x2, y0, 0, minorFontFace, minorFontSize, bdl->getLocalString(killLabel));
  fm.drawString(x3, y0, 0, minorFontFace, minorFontSize, psLabel);
  const float dy = fm.getStrHeight(minorFontFace, minorFontSize, " ");
  float y = y0 - dy;

  // make room for the status marker
  const float xs = x3 - fm.getStrLength(minorFontFace, minorFontSize, "+|");

  if (huntState == HUNT_SELECTING) {
    std::string huntStr = ColorStrings[YellowColor];
    huntStr += ColorStrings[PulsatingColor];
    huntStr += " *SEL*";
    fm.drawString(xs - huntedArrowWidth, y0, 0, minorFontFace, minorFontSize, huntStr);
  }

  // grab the tk warning ratio
  tkWarnRatio = BZDB.eval("tkwarnratio");

  if (huntState == HUNT_SELECTING) {
    if (numPlayers<1 || (numPlayers==1 && players[0]==myTank)) {
      setHuntState (HUNT_NONE);
    } else {
      if (players[huntPosition] == myTank) {
	if (huntPositionEvent < 0)
	  --huntPosition;
	else
	  ++huntPosition;
      }
      if (huntPosition>=numPlayers) {
	  huntPosition = 0;
	if (players[0] == myTank)
	  huntPosition = 1;
      }
      if (huntPosition<0) {
	huntPosition = numPlayers-1;
	if (players[huntPosition] == myTank)
	  --huntPosition;
      }
      if (huntSelectEvent) {	// if 'fire' was pressed ...
	if (!huntAddMode)
	  clearHuntedTanks ();
	if (huntAddMode && players[huntPosition]->isHunted()) {	// UNselect
	      players[huntPosition]->setHunted(false);
	  if (--numHunted != 0)
		playLocalSound(SFX_HUNT_SELECT);
	} else {						// else select
	      players[huntPosition]->setHunted(true);
	  if (++numHunted == 1)
		playLocalSound(SFX_HUNT);
	  else
		playLocalSound(SFX_HUNT_SELECT);
	}
	huntState = HUNT_ENABLED;
      }
    }
  }

  mottoLen = BZDB.getIntClamped ("mottoDispLen", 0, 128);
  huntSelectEvent = false;
  huntPositionEvent = 0;
  numHunted = 0;

  const int maxLines = BZDB.evalInt("maxScoreboardLines");
  int lines = 0;
  int hiddenLines = 0;
  while ((player = players[i]) != NULL) {
    if ((maxLines > 0) && (lines >= maxLines)) {
      hiddenLines++;
    }
    else {
      if (player->isHunted()) {
	++numHunted;
      }
      if (player->getTeam()==ObserverTeam && !haveObs) {
	y -= dy;
	haveObs = true;
      }
      if (huntState==HUNT_SELECTING && i==huntPosition) {
	drawPlayerScore(player, x1, x2, x3, xs, (float)y, mottoLen, true);
      } else {
	drawPlayerScore(player, x1, x2, x3, xs, (float)y, mottoLen, false);
      }
      y -= dy;
    }
    ++i;
    ++lines;
  }

  if (hiddenLines > 0) {
    char buf[64];
    snprintf(buf, sizeof(buf), "...%i...", hiddenLines);
    fm.drawString(x1, y, 0, minorFontFace, minorFontSize, buf);
    fm.drawString(x2, y, 0, minorFontFace, minorFontSize, buf);
    fm.drawString(x3, y, 0, minorFontFace, minorFontSize, buf);
  }

  if (huntState==HUNT_ENABLED && !numHunted) {
    huntState = HUNT_NONE;	// last hunted player must have left the game
    huntAddMode = false;
    playLocalSound(SFX_HUNT);
  }

  delete[] players;

  if (World::getWorld()->allowTeams())
    renderTeamScores(winWidth, y0, dy);
}


void ScoreboardRenderer::stringAppendNormalized (std::string *s, float n)
{
  char fmtbuf[10];
  sprintf (fmtbuf, "  [%4.2f]", n);
  *s += fmtbuf;
}


void ScoreboardRenderer::drawRoamTarget(float _x0, float _y0,
					float _x1, float UNUSED(_y1))
{
  static const TimeKeeper startTime = TimeKeeper::getCurrent();

  const float x0 = floorf(_x0) + 0.5f;
  const float y0 = floorf(_y0) + 0.5f;
  const float x1 = floorf(_x1) + 0.5f;
  const float y1 = y0 + 1.0f;

  const float black[4] = {
      0.0f * (dim ? dimFactor : 1.0f),
      0.0f * (dim ? dimFactor : 1.0f),
      0.0f * (dim ? dimFactor : 1.0f),
      1.0f
    };
  const float white[4] = {
      1.0f * (dim ? dimFactor : 1.0f),
      1.0f * (dim ? dimFactor : 1.0f),
      1.0f * (dim ? dimFactor : 1.0f),
      1.0f
    };
  const float* c0 = black;
  const float* c1 = white;

  const double diff = (TimeKeeper::getCurrent() - startTime);
  if (fmod(diff, 0.5) > 0.25) {
    c0 = white;
    c1 = black;
  }

  glPushAttrib(GL_ALL_ATTRIB_BITS);
  glDisable(GL_BLEND);
  glDisable(GL_LIGHTING);
  glDisable(GL_TEXTURE_2D);
  glBegin(GL_LINES);
  glColor4fv(c0); glVertex2f(x0, y1); glVertex2f(x1, y1);
  glColor4fv(c1); glVertex2f(x0, y0); glVertex2f(x1, y0);
  glEnd();
  glPopAttrib();
}


void ScoreboardRenderer::drawPlayerScore(const Player* player,
			    float x1, float x2, float x3, float xs, float y,
			    int mottoLen, bool huntCursor)
{
  // score
  char score[40], kills[40];

  bool highlightTKratio = false;
  if (tkWarnRatio > 0.0) {
    if (((player->getWins() > 0) && (player->getTKRatio() > tkWarnRatio)) ||
	((player->getWins() == 0) && (player->getTeamKills() >= 3))) {
      highlightTKratio = true;
    }
  }

  if (World::getWorld()->allowRabbit()) {
    sprintf(score, "%2d%% %4d %3d-%-3d%s[%2d]", player->getRabbitScore(),
	    player->getScore(), player->getWins(), player->getLosses(),
	    highlightTKratio ? ColorStrings[CyanColor].c_str() : "",
	    player->getTeamKills());
  } else if (World::getWorld()->allowTeams()) {
    sprintf(score, "%4d %4d-%-4d%s[%2d]", player->getScore(),
	    player->getWins(), player->getLosses(),
	    highlightTKratio ? ColorStrings[CyanColor].c_str() : "",
	    player->getTeamKills());
  } else {
    sprintf(score, "%4d %4d-%-4d%s", player->getScore(),
	    player->getWins(), player->getLosses(),
	    highlightTKratio ? ColorStrings[CyanColor].c_str() : "");
  }

  // kills
  if (LocalPlayer::getMyTank() != player) {
    sprintf(kills, "%3d~%-3d", player->getLocalWins(), player->getLocalLosses());
  } else {
    sprintf(kills, "%4d", player->getSelfKills());
  }


  // team color
  TeamColor teamIndex = player->getTeam();
  std::string teamColor;
  if (player->getId() < 200) {
    teamColor = Team::getAnsiCode(teamIndex);
  } else {
    teamColor = ColorStrings[CyanColor];	// replay observers
  }

  // authentication status
  std::string statusInfo;
  if (BZDBCache::colorful) {
    statusInfo += ColorStrings[CyanColor];
  } else {
    statusInfo += teamColor;
  }
  if (player->isAdmin()) {
    statusInfo += '@';
  } else if (player->isVerified()) {
    statusInfo += '+';
  } else if (player->isRegistered()) {
    statusInfo += '-';
  } else {
    statusInfo = "";	// don't print
  }

  std::string playerInfo;
  // team color
  playerInfo += teamColor;
  // Slot number only for admins (playerList perm check, in case they have
  // hideAdmin)
  LocalPlayer* localPlayer = LocalPlayer::getMyTank();
  if (localPlayer->isAdmin() || localPlayer->hasPlayerList()) {
    char slot[10];
    sprintf(slot, "%3d",player->getId());
    playerInfo += slot;
    playerInfo += " - ";
  }

  if (roaming && BZDB.isTrue("showVelocities")) {
    float vel[3] = {0};
    memcpy(vel,player->getVelocity(),sizeof(float)*3);

    float linSpeed = sqrt(vel[0]*vel[0]+vel[1]*vel[1]);

    float badFactor = 1.5f;
    if (linSpeed > player->getMaxSpeed()*badFactor)
	    playerInfo += ColorStrings[RedColor];
    if (linSpeed > player->getMaxSpeed())
	    playerInfo += ColorStrings[YellowColor];
    else if (linSpeed < 0.0001f)
	    playerInfo += ColorStrings[GreyColor];
    else
	    playerInfo += ColorStrings[WhiteColor];
    playerInfo += TextUtils::format("%5.2f ",linSpeed);
    playerInfo += teamColor;
  }

  // callsign
  playerInfo += player->getCallSign();

  // motto in parentheses
  if (player->getMotto()[0] != '\0' && mottoLen>0) {
    playerInfo += " (";
    playerInfo += TextUtils::str_trunc_continued (player->getMotto(), mottoLen);
    playerInfo += ")";
  }
  // carried flag
  bool coloredFlag = false;
  FlagType* flagd = player->getFlag();
  if (flagd != Flags::Null) {
    // color special flags
    if (BZDBCache::colorful) {
      if ((flagd == Flags::ShockWave)
      ||  (flagd == Flags::Genocide)
      ||  (flagd == Flags::Laser)
      ||  (flagd == Flags::GuidedMissile)) {
	      playerInfo += ColorStrings[WhiteColor];
      } else if (flagd->flagTeam != NoTeam) {
	// use team color for team flags
	playerInfo += Team::getAnsiCode(flagd->flagTeam);
      }
      coloredFlag = true;
    }
    playerInfo += "/";
    playerInfo += (flagd->endurance == FlagNormal ? flagd->flagName : flagd->flagAbbv);
    // back to original color
    if (coloredFlag) {
      playerInfo += teamColor;
    }
  }

  // status
  if (player->isPaused())
    playerInfo += "[p]";
  else if (player->isNotResponding())
    playerInfo += "[nr]";
  else if (player->isAutoPilot())
    playerInfo += "[auto]";

#if DEBUG_SHOWRATIOS
  if (player->getTeam() != ObserverTeam) {
    if (sortMode == SORT_NORMALIZED)
      stringAppendNormalized (&playerInfo, player->getNormalizedScore());
    else if (sortMode == SORT_MYRATIO && LocalPlayer::getMyTank() != player)
      stringAppendNormalized (&playerInfo, player->getLocalNormalizedScore());
    else if (sortMode == SORT_TKRATIO)
      stringAppendNormalized (&playerInfo, player->getTKRatio());
  }
#endif

  FontManager &fm = FontManager::instance();
  fm.setDimFactor(dimFactor);

  if (player == getRoamTargetTank()) {
    const float w = fm.getStrLength(minorFontFace, minorFontSize, playerInfo);
    const float h = fm.getStrHeight(minorFontFace, minorFontSize, playerInfo);
    drawRoamTarget(x3, y, x3 + w, y + h);
  }

  // draw
  if (player->getTeam() != ObserverTeam) {
    hudColor3fv(Team::getTankColor(teamIndex));
    fm.drawString(x1, y, 0, minorFontFace, minorFontSize, score);
    hudColor3fv(Team::getTankColor(teamIndex));
    fm.drawString(x2, y, 0, minorFontFace, minorFontSize, kills);
  }
  fm.drawString(x3, y, 0, minorFontFace, minorFontSize, playerInfo);
  if (statusInfo.size() > 0) {
    fm.drawString(xs, y, 0, minorFontFace, minorFontSize, statusInfo);
  }
  if (BZDB.isTrue("debugHud")) {
    printf ("playerInfo: %s\n", playerInfo.c_str());	// FIXME
  }

  // draw huntEnabled status
  if (player->isHunted()) {
    std::string huntStr = ColorStrings[WhiteColor];
    huntStr += "Hunt->";
    fm.drawString(xs - huntedArrowWidth, y, 0, minorFontFace, minorFontSize,
		  huntStr.c_str());
  } else if (huntCursor && !huntAddMode) {
    std::string huntStr = ColorStrings[WhiteColor];
    huntStr += ColorStrings[PulsatingColor];
    huntStr += "->";
    fm.drawString(xs - huntArrowWidth, y, 0, minorFontFace, minorFontSize,
		  huntStr.c_str());
  }
  if (huntCursor && huntAddMode) {
    std::string huntStr = ColorStrings[WhiteColor];
    huntStr += ColorStrings[PulsatingColor];
    huntStr += "@>";
    fm.drawString(xs - huntPlusesWidth, y, 0, minorFontFace, minorFontSize,
		  huntStr.c_str());
  }
}


// get current 'leader' (NULL if no player)
Player*   ScoreboardRenderer::getLeader(std::string *label) {
  int sortType=sortMode;

  if (sortMode==SORT_CALLSIGN || sortMode==SORT_MYRATIO || sortMode==SORT_TEAM) {
    sortType = SORT_SCORE;
  }
  if (label != NULL) {
    if (sortMode==SORT_TKS)
      *label = "TK Leader ";
    else if (sortMode==SORT_TKRATIO)
      *label = "TK Ratio Leader ";
    else
      *label = "Leader ";
  }

  Player** list = newSortedList (sortType, true);

  Player* top;
  if (!list)
    top = NULL;
  else
    top = list[0];

  delete[] list;

  if (top==NULL || top->getTeam()==ObserverTeam)
    return NULL;
  return top;
}

/************************ Sort logic follows .... **************************/

struct st_playersort{
  Player *player;
  int i1;
  int i2;
  const char *cp;
};
typedef struct st_playersort sortEntry;


int       ScoreboardRenderer::sortCompareCp(const void* _a, const void* _b)
{
  const sortEntry *a = (const sortEntry *)_a;
  const sortEntry *b = (const sortEntry *)_b;
  return strcasecmp (a->cp, b->cp);
}


int       ScoreboardRenderer::sortCompareI2(const void* _a, const void* _b)
{
  const sortEntry *a = (const sortEntry *)_a;
  const sortEntry *b = (const sortEntry *)_b;
  if (a->i1 != b->i1 ) {
    return b->i1 - a->i1;
  }
  return b->i2 - a->i2;
}


// creates (allocates) a null-terminated array of Player*
Player **  ScoreboardRenderer::newSortedList (int sortType, bool obsLast, int *_numPlayers)
{
  LocalPlayer *myTank = LocalPlayer::getMyTank();
  World *world = World::getWorld();

  if (!myTank || !world)
    return NULL;

  const int curMaxPlayers = world->getCurMaxPlayers() +1;
  int i,j;
  int numPlayers=0;
  int numObs=0;
  Player* p;
  sortEntry* sorter = new sortEntry [curMaxPlayers];

  // fill the array with remote players
  for (i=0; i<curMaxPlayers-1; i++) {
    if ((p = world->getPlayer(i))) {
      if (obsLast && p->getTeam()==ObserverTeam)
	sorter[curMaxPlayers - (++numObs)].player = p;
      else
	sorter[numPlayers++].player = p;
    }
  }
  // add my tank
  if (obsLast && myTank->getTeam()==ObserverTeam)
    sorter[curMaxPlayers - (++numObs)].player = myTank;
  else
    sorter[numPlayers++].player = myTank;

  // sort players ...
  if (numPlayers > 0) {
    for (i=0; i<numPlayers; i++) {
      p = sorter[i].player;
      switch (sortType) {
	case SORT_TKS:
	  sorter[i].i1 = p->getTeamKills();
	  sorter[i].i2 = 0 - (int)(p->getNormalizedScore() * 100000);
	  break;
	case SORT_TKRATIO:
	  sorter[i].i1 = (int)(p->getTKRatio() * 1000);
	  sorter[i].i2 = 0 - (int)(p->getNormalizedScore() * 100000);
	  break;
	case SORT_TEAM:
	  sorter[i].i1 = p->getTeam();
	  sorter[i].i2 = (int)(p->getNormalizedScore() * 100000);
	  break;
	case SORT_MYRATIO:
	  if (p == myTank)
	    sorter[i].i1 = -100001;
	  else
	    sorter[i].i1 = 0 - (int)(p->getLocalNormalizedScore() * 100000);
	  sorter[i].i2 = (int)(p->getNormalizedScore() * 100000);
	  break;
	case SORT_NORMALIZED:
	  sorter[i].i1 = (int)(p->getNormalizedScore() * 100000);
	  sorter[i].i2 = 0;
	  break;
	case SORT_CALLSIGN:
	  sorter[i].cp = p->getCallSign();
	  break;
	default:
	  if (world->allowRabbit())
	    sorter[i].i1 = p->getRabbitScore();
	  else
	    sorter[i].i1 = p->getScore();
	  sorter[i].i2 = 0;
	  if (sortType == SORT_REVERSE)
	    sorter[i].i1 *= -1;
      }
    }
    if (sortType == SORT_CALLSIGN)
      qsort (sorter, numPlayers, sizeof(sortEntry), sortCompareCp);
    else
      qsort (sorter, numPlayers, sizeof(sortEntry), sortCompareI2);
  }

  // TODO: Sort obs here (by time joined, when that info is available)

  Player** players = new Player *[numPlayers + numObs + 1];
  for (i=0; i<numPlayers; i++)
    players[i] = sorter[i].player;
  for (j=curMaxPlayers-numObs; j<curMaxPlayers; j++)
    players[i++] = sorter[j].player;
  players[i] = NULL;

  if (_numPlayers != NULL)
    *_numPlayers = numPlayers;

  delete[] sorter;
  return players;
}


void ScoreboardRenderer::getPlayerList(std::vector<Player*>& players)
{
  players.clear();

  int playerCount;
  Player** pList = newSortedList(getSort(), true, &playerCount);
  if (pList == NULL) {
    return;
  }
  for (int i = 0; i < playerCount; i++) {
    Player* p = pList[i];
    if (p && (p->getTeam() != ObserverTeam)) {
      players.push_back(p);
    }
  }
  delete[] pList;
}


// Local Variables: ***
// mode: C++ ***
// tab-width: 8 ***
// c-basic-offset: 2 ***
// indent-tabs-mode: t ***
// End: ***
// ex: shiftwidth=2 tabstop=8
