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

/* BZFlag common header */
#include "common.h"

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

/* system headers */
#include <assert.h>
#include <time.h>
#include <string.h>
#ifdef HAVE_PTHREADS
#  include <pthread.h>
#endif

/* common implementation headers */
#include "BZDBCache.h"
#include "FontManager.h"
#include "TextUtils.h"
#include "ErrorHandler.h"
#include "global.h"
#ifdef _WIN32
#  include <DirectoryNames.h>
#endif

/* local implementation headers */
#include "SceneRenderer.h"
#include "RadarRenderer.h"
#include "TimeKeeper.h"
#include "bzflag.h"

//
// ControlPanelMessage
//

ControlPanelMessage::ControlPanelMessage(const std::string& _string) :
  numlines(0), string(_string), year(0), month(0), day(0), hour(0), min(0), sec(0)
{
  TimeKeeper::localTime(&year, &month, &day, &hour, &min, &sec);
}

std::string ControlPanelMessage::getString() const {
  return formatTimestamp(BZDBCache::controlPanelTimestamp) + string;
}

std::string ControlPanelMessage::getString(bool withTimeDate) const {
  return formatTimestamp(withTimeDate?2:0) + string;
}

std::string ControlPanelMessage::formatTimestamp(int mode) const {
  // Just the date
  if (mode == 1)
    return TextUtils::format("[%02d:%02d:%02d] ", hour, min, sec);

  if (mode == 2)
    return TextUtils::format("[%04d-%02d-%02d %02d:%02d:%02d] ", year, month, day, hour, min, sec);

  return "";
}

void ControlPanelMessage::breakLines(float maxLength, int fontFace, float fontSize)
{
  FontManager &fm = FontManager::instance();

  std::string _string = getString();

  // get message and its length
  const char* msg = _string.c_str();
  int lineLen     = (int)_string.length();

  // if there are tabs in the message, find the last one
  int lastTab = (int)_string.rfind('\t');

  lines.clear();
  numlines=0;

  // in order for the new font engine to draw successive lines in the right
  // color, it needs to be fed the right ansi codes at the beginning of each
  // line.
  std::string previousANSICodes = "";
  std::string cumulativeANSICodes = "";

  // break lines
  while (lineLen > 0) {
    int lastWhitespace = 0;
    int n;

    // how many characters will fit?
    // the unprinted ANSI codes don't count
    if ((fm.getStrLength(fontFace, fontSize, msg) <= maxLength) && (lastTab <= 0)) {
      n = lineLen;
    } else {
      n = 0;
      while ((n < lineLen) &&
	     (fm.getStrLength(fontFace, fontSize, std::string(msg, n)) < maxLength)) {
	if (msg[n] == ESC_CHAR) {
	  // clear the cumulative codes when we hit a reset
	  // the reset itself will start the new cumulative string.
	  if ((strncmp(msg + n, ANSI_STR_RESET, strlen(ANSI_STR_RESET)) == 0)
	      || (strncmp(msg + n, ANSI_STR_RESET_FINAL, strlen(ANSI_STR_RESET_FINAL)) == 0))
	    cumulativeANSICodes = "";
	  // add this code to our cumulative string
	  cumulativeANSICodes += msg[n];
	  n++;
	  if ((n < lineLen) && (msg[n] == '[')) {
	    cumulativeANSICodes += msg[n];
	    n++;
	    while ((n < lineLen) && ((msg[n] == ';') ||
		  ((msg[n] >= '0') && (msg[n] <= '9')))) {
	      cumulativeANSICodes += msg[n];
	      n++;
	    }
	    // ditch the terminating character too
	    if (n < lineLen) {
	      cumulativeANSICodes += msg[n];
	      n++;
	    }
	  }
	} else {
	  n++;
	}
	if (TextUtils::isWhitespace(msg[n])) {
	  lastWhitespace = n;
	  // Tabs break out into their own message.  These get dealt with
	  // in ControlPanel::render, which will increment x instead of y.
	  if (msg[n] == '\t')
	    break;
	}
      }
    }

    if (lastWhitespace > 0)
      n = lastWhitespace;

    // message
    lines.push_back(previousANSICodes + std::string(msg,n));

    // Store cumulative ANSI codes to be prepended to the next line
    previousANSICodes = cumulativeANSICodes;

    if (msg[n] != '\t')
      numlines++;

    // account for portion broken
    msg += n;
    lineLen -= n;
    lastTab -= n;

  }
}


static std::deque<std::string> mutexMsgs;

#if defined(HAVE_PTHREADS)
static pthread_mutex_t controlpanel_mutex = PTHREAD_MUTEX_INITIALIZER;
#elif defined(_WIN32)
static CRITICAL_SECTION controlpanel_critical;
#endif

#if defined(HAVE_PTHREADS)
# define LOCK_CONTROLPANEL_MUTEX   pthread_mutex_lock(&controlpanel_mutex);
# define UNLOCK_CONTROLPANEL_MUTEX pthread_mutex_unlock(&controlpanel_mutex);
#elif defined(_WIN32)
# define LOCK_CONTROLPANEL_MUTEX   EnterCriticalSection(&controlpanel_critical);
# define UNLOCK_CONTROLPANEL_MUTEX LeaveCriticalSection(&controlpanel_critical);
#else
# define LOCK_CONTROLPANEL_MUTEX
# define UNLOCK_CONTROLPANEL_MUTEX
#endif


//
// ControlPanel
//
int			ControlPanel::messagesOffset = 0;

ControlPanel::ControlPanel(MainWindow& _mainWindow, SceneRenderer& _renderer) :
				tabsOnRight(true),
				tabs(NULL),
				totalTabWidth(0),
				window(_mainWindow),
				resized(false),
				numBuffers(2),
				changedMessage(0),
				radarRenderer(NULL),
				renderer(&_renderer),
				fontFace(0),
				dimming(1.0f),
				du(0),
				dv(0),
				messageMode(MessageAll),
				paused(false)
{
  setControlColor();

  // make sure we're notified when MainWindow resizes or is exposed
  window.getWindow()->addResizeCallback(resizeCallback, this);
  window.getWindow()->addExposeCallback(exposeCallback, this);
  BZDB.addCallback("displayRadar", bzdbCallback, this);
  BZDB.addCallback(StateDatabase::BZDB_RADARLIMIT, bzdbCallback, this);
  BZDB.addCallback("controlPanelTimestamp", bzdbCallback, this);

  // other initialization
  radarAreaPixels[0] = 0;
  radarAreaPixels[1] = 0;
  radarAreaPixels[2] = 0;
  radarAreaPixels[3] = 0;
  messageAreaPixels[0] = 0;
  messageAreaPixels[1] = 0;
  messageAreaPixels[2] = 0;
  messageAreaPixels[3] = 0;
  for (int i = 0; i < MessageModeCount; i++) {
    messages[i].clear();
    unRead[i] = false;
  }
  teamColor[0] = teamColor[1] = teamColor[2] = (GLfloat)0.0f;

  maxLines = 30;

  /* use pointer initialization to perform precomputation and
   * prestorage. eventually should move this data into constructor.
   */
  if (tabs == NULL) {
    tabs = new std::vector<const char *>;
    tabs->push_back("All");
    tabs->push_back("Chat");
    tabs->push_back("Server");
    tabs->push_back("Misc");
  }

  // construct critical section for threadsafe messages
#if defined(HAVE_PTHREADS)
#elif defined(_WIN32)
  InitializeCriticalSection(&controlpanel_critical);
#endif

  resize(); // need resize to set up font and window dimensions
}

ControlPanel::~ControlPanel()
{
  // don't notify me anymore (cos you can't wake the dead!)
  window.getWindow()->removeResizeCallback(resizeCallback, this);
  window.getWindow()->removeExposeCallback(exposeCallback, this);
  BZDB.removeCallback("displayRadar", bzdbCallback, this);
  BZDB.removeCallback(StateDatabase::BZDB_RADARLIMIT, bzdbCallback, this);
  BZDB.removeCallback("controlPanelTimestamp", bzdbCallback, this);

  // destroy critical section for threadsafe messages
#if defined(HAVE_PTHREADS)
#elif defined(_WIN32)
  DeleteCriticalSection(&controlpanel_critical);
#endif

  if (echoToConsole && echoAnsi) {
    std::cout << ColorStrings[FinalResetColor] << std::flush;
  }
  if (tabs != NULL) {
    tabs->clear();
    delete tabs;
    tabTextWidth.clear();
    totalTabWidth=0;
  }
}

void ControlPanel::bzdbCallback(const std::string& UNUSED(name), void* data)
{
  ((ControlPanel*)data)->resize();
  return;
}

void			ControlPanel::setControlColor(const GLfloat *color)
{
  if (color)
    memcpy(teamColor, color, 3 * sizeof(float));
  else
    memset(teamColor, 0, 3 * sizeof(float));
}

void			ControlPanel::render(SceneRenderer& _renderer)
{
  while (!mutexMsgs.empty()) {
    addMessage(mutexMsgs.front());
    mutexMsgs.pop_front();
  }

  if (!BZDB.isTrue("displayConsole")) {
    // always draw the console if its fully opaque
    const float opacity = RENDERER.getPanelOpacity();
    if (opacity != 1.0f) {
      return;
    }
  }

  if (!resized) resize();

  // optimization for software rendering folks
  if (!changedMessage && _renderer.getPanelOpacity() == 1.0f) {
    return;
  }

  int i, j;
  const int x = window.getOriginX();
  const int y = window.getOriginY();
  const int w = window.getWidth();
  const int tabStyle = BZDB.evalInt("showtabs");
  const bool showTabs = (tabStyle > 0);
  tabsOnRight = (tabStyle == 2);

  glMatrixMode(GL_PROJECTION);
  glLoadIdentity();
  glOrtho(0.0, (double)w, 0.0, window.getHeight(), -1.0, 1.0);
  glMatrixMode(GL_MODELVIEW);
  glPushMatrix();
  glLoadIdentity();
  OpenGLGState::resetState();

  FontManager &fm = FontManager::instance();
  fm.setOpacity(dimming);

  if (changedMessage > 0) {
    changedMessage--;
  }
  float fx = messageAreaPixels[0] + margin;
  float fy = messageAreaPixels[1] + margin + 1.0f;
  int   ay = (_renderer.getPanelOpacity() == 1.0f || !showTabs) ? 0
    : int(lineHeight + 4);

  glScissor(x + messageAreaPixels[0] - 1,
      y + messageAreaPixels[1],
      messageAreaPixels[2] + 1,
      messageAreaPixels[3] + ay);
  OpenGLGState::resetState();

  if (_renderer.getPanelOpacity() > 0.0f) {
    // nice blended messages background
    if (BZDBCache::blend && _renderer.getPanelOpacity() < 1.0f)
      glEnable(GL_BLEND);

    // clear the background
    glColor4f(0.0f, 0.0f, 0.0f, _renderer.getPanelOpacity());
    glRecti(messageAreaPixels[0] - 1, // clear an extra pixel column to simplify fuzzy float stuff later
	    messageAreaPixels[1],
	    messageAreaPixels[0] + messageAreaPixels[2],
	    messageAreaPixels[1] + messageAreaPixels[3]);

    // display tabs for chat sections
    if (showTabs) {
      long int drawnTabWidth = 0;
      for (unsigned int tab = 0; tab < tabs->size(); tab++) {

	// current mode is given a dark background to match the control panel
	if (messageMode == MessageModes(tab)) {
	  glColor4f(0.0f, 0.0f, 0.0f, _renderer.getPanelOpacity());
	} else {
	  glColor4f(0.10f, 0.10f, 0.10f, _renderer.getPanelOpacity());
	}

	if (tabsOnRight) {
	  // draw the tabs on the right side
	  glRecti(messageAreaPixels[0] + messageAreaPixels[2] - totalTabWidth + drawnTabWidth,
		  messageAreaPixels[1] + messageAreaPixels[3] - int(lineHeight + 4) + ay,
		  messageAreaPixels[0] + messageAreaPixels[2] - totalTabWidth + drawnTabWidth + int(tabTextWidth[tab]), //+ drawnTabWidth + int(tabTextWidth[tab]),
		  messageAreaPixels[1] + messageAreaPixels[3] + ay);
	} else {
	  // draw the tabs on the left side
	  glRecti(messageAreaPixels[0] + drawnTabWidth,
		  messageAreaPixels[1] + messageAreaPixels[3] - int(lineHeight + 4) + ay,
		  messageAreaPixels[0] + drawnTabWidth + int(tabTextWidth[tab]),
		  messageAreaPixels[1] + messageAreaPixels[3] + ay);
	}
	drawnTabWidth += long(tabTextWidth[tab]);
      } // end iteration over tabs
    }

    if (BZDBCache::blend && _renderer.getPanelOpacity() < 1.0f)
      glDisable(GL_BLEND);
  }

  // show scroll indicator if not at end
  if (messagesOffset > 0 && messageMode >= 0)
  {
    int lines = int(messages[messageMode].size());
    if (lines > 0) {

      const float size = std::max(float(maxLines) / lines, 0.02f);
      const float offset = float(messagesOffset) / lines;
      const int maxTop = messageAreaPixels[1] + messageAreaPixels[3];
      int top = messageAreaPixels[1] + int((offset + size) * (float)messageAreaPixels[3]);
      if (top > maxTop) {
	top = maxTop;
      }
      glColor3f(0.7f, 0.7f, 0.7f);
      glRecti(messageAreaPixels[0],
	      messageAreaPixels[1] + int(offset * (float)messageAreaPixels[3]),
	      messageAreaPixels[0] + 2, top);

    }
  }

  // Draw tab labels
  if (showTabs) {
    long int drawnTabWidth = 0;
    for (unsigned int tab = 0; tab < tabs->size(); tab++) {

      // current mode is bright, others are not so bright
      if (messageMode == MessageModes(tab)) {
	glColor4f(1.0f, 1.0f, 1.0f, dimming);
      } else if (unRead[MessageModes(tab)]) {
	glColor4f(0.5f, 0.0f, 0.0f, dimming);
      } else {
	glColor4f(0.5f, 0.5f, 0.5f, dimming);
      }

      if (tabsOnRight) {
	// draw the tabs on the right side (with one letter padding)
	fm.drawString(messageAreaPixels[0] + messageAreaPixels[2] - totalTabWidth + drawnTabWidth + floorf(fontSize),
		      messageAreaPixels[1] + messageAreaPixels[3] - floorf(lineHeight + 2.0f) + ay,
		      0.0f, fontFace, (float)fontSize, (*tabs)[tab]);
      } else {
	// draw the tabs on the left side (with one letter padding)
	fm.drawString(messageAreaPixels[0] + drawnTabWidth + floorf(fontSize),
		      messageAreaPixels[1] + messageAreaPixels[3] - floorf(lineHeight + 2.0f) + ay,
		      0.0f, fontFace, (float)fontSize, (*tabs)[tab]);
      }
      drawnTabWidth += long(tabTextWidth[tab]);
    }
  }

  /* draw messages
   *
   * It works by first breaking the string into a vector of strings
   * (done elsewhere), each of which will fit the control panel, and
   * tallying the number of lines, then moving up the proper number of
   * lines and displaying downward -- that is, it kinda backtracks for
   * each line that will wrap.
   *
   * messageAreaPixels[2] = Width of Message Window in Pixels.
   *
   * maxLines = Max messages lines that can be displayed at once per
   * page.  This COULD be a BZDB parameter (but isn't).
   *
   * maxScrollPages = This number * maxLines is the total maximum
   * lines of messages (and scrollback). It is stored as a BZDB
   * parameter.
   */

  glScissor(x + messageAreaPixels[0],
	    y + messageAreaPixels[1],
	    messageAreaPixels[2],
	    messageAreaPixels[3] - (showTabs ? int(lineHeight + 4) : 0) + ay);

  if (messageMode >= 0) {
    i = (int)messages[messageMode].size() - 1;
  } else {
    i = -1;
  }
  if (i >= 0 && messagesOffset > 0) {
    i -= messagesOffset;
    if (i < 0)
      i = 0;
  }

  const std::string highlightPattern = BZDB.get("highlightPattern");
  bool useHighlight = (highlightPattern.size() > 0);
  regex_t re;
  if (useHighlight) {
    if (regcomp(&re, highlightPattern.c_str(), REG_EXTENDED | REG_ICASE) != 0) {
      useHighlight = false; // bad regex
    }
  }

  for (j = 0; i >= 0 && j < maxLines; i--) {
    // draw each line of text
    int numLines = messages[messageMode][i].numlines;
    int numStrings = (int)messages[messageMode][i].lines.size();
    int msgy = numLines - 1;
    int msgx = 0;

    // see if this message need to be highlighted (check each line)
    bool highlight = false;
    if (useHighlight) {
      for (int l = 0; l < numStrings; l++)  {
	const std::string &msg = messages[messageMode][i].lines[l];
	std::string raw = stripAnsiCodes(msg);
	if (regexec(&re, raw.c_str(), 0, NULL, 0) == 0) {
	  highlight = true;
	}
      }
    }

    // default to drawing text in white
    GLfloat whiteColor[4] = {1.0f, 1.0f, 1.0f, dimming};
    glColor4fv(whiteColor);

    bool isTab = false;

    for (int l = 0; l < numStrings; l++)  {
      const std::string &msg = messages[messageMode][i].lines[l];

      // Tab chars move horizontally instead of vertically
      // It doesn't matter where in the string the tab char is
      // Usually it will be like <ansi><ansi><ansi>\ttext
      // We use 1 tabstop spaced 1/3 of the way across the controlpanel
      isTab = (msg.find('\t') != std::string::npos);
      if (isTab) {
	msgx += messageAreaPixels[2] / 3;
	msgy++;
      } else {
	msgx = 0;
      }

      assert(msgy >= 0);

      // only draw message if inside message area
      if (j + msgy < maxLines) {
	if (!highlight) {
	  fm.drawString(fx + msgx, fy + msgy * lineHeight, 0, fontFace, fontSize, msg);
	} else {
	  // highlight this line
	  std::string newMsg = ANSI_STR_PULSATING;
	  newMsg += ANSI_STR_UNDERLINE;
	  newMsg += ANSI_STR_FG_CYAN;
	  newMsg += stripAnsiCodes(msg);
	  fm.drawString(fx + msgx, fy + msgy * lineHeight, 0, fontFace, fontSize, newMsg);
	}
      }

      // next line
      msgy--;
    }
    j += numLines;
    fy += int(lineHeight * numLines);
  }

  // free the regex memory
  if (useHighlight) {
    regfree(&re);
  }

  glScissor(x + messageAreaPixels[0] - 2,
	    y + messageAreaPixels[1] - 2,
	    messageAreaPixels[2] + 3,
	    messageAreaPixels[3] + 33);
  OpenGLGState::resetState();

  // draw the lines around the console panel
  float outlineOpacity = RENDERER.getPanelOpacity();
  float fudgeFactor = BZDBCache::hudGUIBorderOpacityFactor;	// bzdb cache this maybe?
  if ( outlineOpacity < 1.0f )
	outlineOpacity = (outlineOpacity*fudgeFactor) + (1.0f - fudgeFactor);

  if (BZDBCache::blend)
	 glEnable(GL_BLEND);

  // nice border
  glColor4f(teamColor[0], teamColor[1], teamColor[2],outlineOpacity );
  glBegin(GL_LINE_LOOP);
  {
    long xpos;
    long ypos;

    // bottom left
    xpos = x + messageAreaPixels[0] - 1;
    ypos = y + messageAreaPixels[1] - 1;
    glVertex2f((float) xpos, (float) ypos);

    // bottom right
    xpos += messageAreaPixels[2] + 1;
    glVertex2f((float) xpos, (float) ypos);

    // top right
    ypos += messageAreaPixels[3] + 1;
    glVertex2f((float) xpos, (float) ypos);

    // over to panel on left
    if (!tabsOnRight) {
      xpos = x + messageAreaPixels[0] + totalTabWidth;
      glVertex2f((float) xpos, (float) ypos);
    }

    // across the top from right to left
    for (int tab = (int)tabs->size() - 1; tab >= 0; tab--) {

      if (messageMode == MessageModes(tab)) {
	ypos += ay;
	glVertex2f((float) xpos, (float) ypos);

	xpos -= long(tabTextWidth[tab]) + 1;
	glVertex2f((float) xpos, (float) ypos);

	ypos -= ay;
	glVertex2f((float) xpos, (float) ypos);
      } else {
	xpos -= long(tabTextWidth[tab]);
	glVertex2f((float) xpos, (float) ypos);
      }
    }

    // over from panel on right
    //    if (tabsOnRight) {
      xpos = x + messageAreaPixels[0] - 1;
      glVertex2f((float) xpos, (float) ypos);
      //    }

  } glEnd();

  if (0) {
    // some engines miss the corners
    glBegin(GL_POINTS); {
      glVertex2f((float) (x + messageAreaPixels[0] - 1),
		 (float) (y + messageAreaPixels[1] - 1));
      glVertex2f((float) (x + messageAreaPixels[0] + messageAreaPixels[2]),
		 (float) (y + messageAreaPixels[1] - 1));
      glVertex2f((float) (x + messageAreaPixels[0] + messageAreaPixels[2]),
		 (float) (y + messageAreaPixels[1] + messageAreaPixels[3]));
      glVertex2f((float) (x + messageAreaPixels[0] - 1),
		 (float) (y + messageAreaPixels[1] + messageAreaPixels[3]));
      long int tabPosition = 0;
      for (int tab = 0; tab < (int)tabs->size(); tab++) {
	if (messageMode == MessageModes(tab)) {
	  if (tabsOnRight) {
	    glVertex2f((float) (x + messageAreaPixels[0] + messageAreaPixels[2] - totalTabWidth + tabPosition),
		       (float) (y + messageAreaPixels[1] + messageAreaPixels[3] + ay));
	    glVertex2f((float) (x + messageAreaPixels[0] + messageAreaPixels[2] - totalTabWidth + tabPosition + long(tabTextWidth[tab])),
		       (float) (y + messageAreaPixels[1] + messageAreaPixels[3] + ay));
	  } else {
	    glVertex2f((float) (x + messageAreaPixels[0] + tabPosition),
		       (float) (y + messageAreaPixels[1] + messageAreaPixels[3] + ay));
	    glVertex2f((float) (x + messageAreaPixels[0] + tabPosition + long(tabTextWidth[tab])),
		       (float) (y + messageAreaPixels[1] + messageAreaPixels[3] + ay));
	  }
	}
	tabPosition += long(tabTextWidth[tab]);
      }
    } glEnd();
  }

  if (BZDBCache::blend)
	  glDisable(GL_BLEND);

glColor4f(teamColor[0], teamColor[1], teamColor[2],1.0f );

  glPopMatrix();

  fm.setOpacity(1.0f);
}

void			ControlPanel::resize()
{
  float radarSpace, radarSize, panelHeight;
  // get important metrics
  const float w = (float)window.getWidth();
  const float h = (float)window.getHeight();
  const float opacity = RENDERER.getPanelOpacity();
  if (opacity == 1.0f) {
    radarSize = float(window.getHeight() - window.getViewHeight());
    panelHeight = radarSize;
    radarSpace = 0.0f;
  } else {
    radarSize = h * (14 + RENDERER.getRadarSize()) / 60.0f;
    panelHeight = h * (14 + RENDERER.getPanelHeight()) / 60.0f;
    radarSpace = 3.0f * w / MinY;
  }

  // compute areas in pixels x,y,w,h
  // leave off 1 pixel for the border
  radarAreaPixels[1] = (int)radarSpace + 1;
  radarAreaPixels[2] = radarAreaPixels[3] = (int)(radarSize - (radarSpace * 2.0f)) - 2;
  if (BZDB.get("radarPosition") == "1") {	//radar on right
    radarAreaPixels[0] = (int)(w - radarSize + radarSpace); // radar X coord
    messageAreaPixels[0] = (int)radarSpace + 1;	// message box X coord
  } else {					//radar on left
    radarAreaPixels[0] = radarAreaPixels[1];	// radar X coord
    messageAreaPixels[0] = (int)radarSize + 1;	// message box X coord
  }
  messageAreaPixels[1] = radarAreaPixels[1];			// Y coord
  messageAreaPixels[2] = (int)(w - radarSize - radarSpace) - 2;	// Width
  messageAreaPixels[3] = (int)(panelHeight - (radarSpace * 2.0f)) - 2; // Height

  if (!BZDB.isTrue("displayRadar") || (BZDBCache::radarLimit <= 0.0f)) {
    messageAreaPixels[0] = (int)radarSpace + 1;
    messageAreaPixels[2] = (int)(w - (radarSpace * 2.0f)) - 2;
  }

  // if radar connected then resize it
  if (radarRenderer)
    radarRenderer->setShape(radarAreaPixels[0], radarAreaPixels[1],
			    radarAreaPixels[2], radarAreaPixels[3]);

  switch (static_cast<int>(BZDB.eval("cpanelfontsize"))) {
    case 0: { // auto
      const bool useBigFont = (messageAreaPixels[2] / 100.0f) > 10.0f;
      fontSize = useBigFont ? 12.0f : 8.0f;
      break;
      }
    case 1: // tiny
      fontSize = 6;
      break;
    case 2: // small
      fontSize = 8;
      break;
    case 3: // medium
      fontSize = 12;
      break;
    case 4: // big
      fontSize = 16;
      break;
  }

  FontManager &fm = FontManager::instance();
  fontFace = fm.getFaceID(BZDB.get("consoleFont"));

  // tab widths may have changed
  if (tabs) {
    tabTextWidth.clear();
    totalTabWidth = 0;
    const float charWidth = fm.getStrLength(fontFace, fontSize, "-");
    for (unsigned int tab = 0; tab < tabs->size(); tab++) {
      // add space for about 2-chars on each side for padding
      tabTextWidth.push_back(fm.getStrLength(fontFace, fontSize, (*tabs)[tab]) + (4.0f * charWidth));
      totalTabWidth += long(tabTextWidth[tab]);
    }
  }

  lineHeight = fm.getStrHeight(fontFace, fontSize, " ");

  maxLines = int(messageAreaPixels[3] / lineHeight);

  margin = lineHeight / 4.0f;

  // rewrap all the lines
  for (int i = 0; i < MessageModeCount; i++) {
    for (int j = 0; j < (int)messages[i].size(); j++) {
      messages[i][j].breakLines(messageAreaPixels[2] - 2 * margin, fontFace, fontSize);
    }
  }

  // note that we've been resized at least once
  resized = true;
  invalidate();
}

void			ControlPanel::resizeCallback(void* self)
{
  ((ControlPanel*)self)->resize();
}

void			ControlPanel::setNumberOfFrameBuffers(int n)
{
  numBuffers = n;
}

void ControlPanel::invalidate()
{
  if (numBuffers) {
    changedMessage = numBuffers;
  } else {
    changedMessage++;
  }
}

void			ControlPanel::exposeCallback(void* self)
{
  ((ControlPanel*)self)->invalidate();
}

void			ControlPanel::setMessagesOffset(int offset, int whence, bool paged)
{
  if (paged) {
    if (abs(offset) <= 1) {
      offset = offset * (maxLines - 1);
    } else {
      offset = offset * maxLines;
    }
  }

  // offset = offset from whence (offset of 0 is the bottom/most recent)
  // whence = 0, 1, or 2 (akin to SEEK_SET, SEEK_CUR, SEEK_END)

  switch (whence) {
    case 0:
      if (offset < (int)messages[messageMode].size())
	messagesOffset = offset;
      else
	messagesOffset = (int)messages[messageMode].size() - 1;
      break;
    case 1:
      if (offset > 0) {
	if (messagesOffset + offset < (int)messages[messageMode].size())
	  messagesOffset += offset;
	else
	  messagesOffset = (int)messages[messageMode].size() - 1;
      } else if (offset < 0) {
	if (messagesOffset + offset >= 0)
	  messagesOffset += offset;
	else
	  messagesOffset = 0;
      }
      break;
    case 2:
      if (offset < 0) {
	if ((int)messages[messageMode].size() >= offset)
	  messagesOffset += offset;
	else
	  messagesOffset = 0;
      }
      break;
  }
  invalidate();
}

void			ControlPanel::setMessagesMode(int _messageMode)
{
  messageMode = _messageMode;
  if (messageMode == MessageAll)
    for (int i = 0; i < MessageModeCount; i++)
      unRead[i] = false;
  else if (messageMode >= MessageChat)
    unRead[messageMode] = false;
  invalidate();
}

void ControlPanel::addMutexMessage(const std::string& msg)
{
  LOCK_CONTROLPANEL_MUTEX
  mutexMsgs.push_back(msg);
  UNLOCK_CONTROLPANEL_MUTEX
}

void			ControlPanel::addMessage(const std::string& line,
						 const int realmode)
{
  ControlPanelMessage item(line);
  item.breakLines(messageAreaPixels[2] - 2 * margin, fontFace, fontSize);

  int _maxScrollPages = BZDB.evalInt("scrollPages");
  if (_maxScrollPages <= 0) {
    _maxScrollPages = atoi(BZDB.getDefault("scrollPages").c_str());
    BZDB.setInt("scrollPages", _maxScrollPages);
  }

  // the effective tab
  const int tabmode = (realmode == MessageCurrent) ? messageMode : realmode;

  // add to the appropriate tabs
  for (int tab = MessageAll; tab < MessageModeCount; tab++) {

    if ((tab == tabmode) // add to its own mode
	// add to the All tab, unless using Current mode
	|| ((tab == MessageAll) && (realmode != MessageCurrent))
	// always add to all tabs
	|| (realmode == MessageAllTabs)) {

      // insert the message into the tab
      if ((int)messages[tab].size() < maxLines * _maxScrollPages) {
	// not full yet so just append it
	messages[tab].push_back(item);
      } else {
	// rotate list and replace oldest (in newest position after rotate)
	messages[tab].pop_front();
	messages[tab].push_back(item);
      }

      // visible changes, force a console refresh
      if (messageMode == tab) {
	// Don't scroll the messages unless we're at the bottom
	if ((BZDB.isTrue("pauseConsole") && messagesOffset > 0 && messageMode >= 0) ||
	    paused) {
	  setMessagesOffset(1, 1 /* current */, false);
	}

	invalidate();
      }

      // mark the tab as unread (if viewing tabs)
      const bool showTabs = (BZDB.evalInt("showtabs") > 0);
      if (showTabs && (messageMode != tab) && (messageMode != MessageAll)) {
	unRead[tab] = true;
      }
    }
  }

  if (echoToConsole) {
#ifdef _WIN32
    // this is cheap but it will work on windows
    std::string filename = getConfigDirName() + "stdout.txt";
    FILE *fp = fopen (filename.c_str(), "a+");
    if (fp) {
      fprintf(fp,"%s\n", stripAnsiCodes(line).c_str());
      fclose(fp);
    }
#else
    if (echoAnsi) {
      std::cout << line << ColorStrings[ResetColor] << std::endl;
    } else {
      std::cout << stripAnsiCodes(line) << std::endl;
    }
    fflush(stdout);
#endif
  }
}


void ControlPanel::saveMessages(const std::string& filename,
				bool stripAnsi, bool timestamp) const
{
  FILE* file = fopen(filename.c_str(), "a+");
  if (!file) {
    return;
  }

  const time_t nowTime = time (NULL);
  fprintf(file, "\n----------------------------------------\n");
  fprintf(file, "Messages saved: %s", ctime(&nowTime));
  fprintf(file, "----------------------------------------\n\n");

  // Iterate through the 'All' tab messages
  std::deque<ControlPanelMessage>::const_iterator msg = messages[MessageAll].begin()++;
  for (; msg != messages[MessageAll].end(); ++msg) {
    const std::string line = msg->getString(timestamp);
    if (stripAnsi) {
      fprintf(file, "%s\n", stripAnsiCodes(line).c_str());
    } else {
      fprintf(file, "%s%s\n", line.c_str(), ColorStrings[ResetColor].c_str());
    }
  }

  fclose(file);

  return;
}


void ControlPanel::setRadarRenderer(RadarRenderer* rr)
{
  radarRenderer = rr;
}


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