/*

 BabbleMud - A MUD server

 Author:  Nick Gammon 
          http://www.gammon.com.au/ 

(C) Copyright Nick Gammon 2004. Permission to copy, use, modify, sell and
distribute this software is granted provided this copyright notice appears
in all copies. This software is provided "as is" without express or implied
warranty, and with no claim as to its suitability for any purpose.
 
*/

#include "commands.h"

#include <stack>

/*

Colour converter. I initially had something somewhat more complex than this,
but it was difficult to understand, and of questionable use.

We have at present a simple stateless system, based around the fact that most
people will use ANSI codes in their client. Thus it is up to you to
reset colours to defaults after changing them. Each of the main colour codes
(normal, bold) incorporates a reset into them, so they cancel any previous values.

However the background and underline codes do not, so to have, say, green on blue
you would do:  `g`!b   (green first, which does an ANSI reset, followed by a background
change).
  
There is a simple system of push (`^) and recall (`v) to save and restore the most
recently outputted colour. The current "colour stack" is stored in the caller's frame
(eg. the player) so that you can in fact push and recall over consecutive calls.
If you are planning to keep switching between a saved colour and various other ones,
you can push with `^, change colours (eg. `R) then recall it with `=, change colours
again, and eventually pop the saved colour with `v.

eg.  `^ save original `G change to green `= recall original `B change to blue `v pop original

It also uses the "current colour" sequence to cache output somewhat, so that if you do
multiple things of the same colour (eg. ANSI reset on the prompt) then it will not
keep outputting the same codes.

Unless you are trying to output a batch of lines in the same colour, you should finish
off with a reset, so the colour is back to normal.

eg.  `R this is in red `X  (now reset)

At present unused codes are just echoed, however they should be considered reserved for
future use. If you really want to show the ` character just use two of them, ie. ``

Normal colours (darker)

`s  - silver (actually, black)
`r  - red
`g  - green
`y  - yellow
`b  - blue
`m  - magenta
`c  - cyan
`w  - white

Bold colours (bright)

`S  - silver (grey)
`R  - red
`G  - green
`Y  - yellow
`B  - blue
`M  - magenta
`C  - cyan
`W  - white

``  - ` itself
`_  - underline subsequent words
`x  - reset 
`X  - reset (also empties stack)

Background colours

`!s  - silver (black)
`!r  - red
`!g  - green
`!y  - yellow
`!b  - blue
`!m  - magenta
`!c  - cyan
`!w  - white

Save/recall

`^   - push current colour(s)    (for recall by `v or `=)
`=   - use last pushed colour(s) (use but leave on stack)
`v   - pop last pushed colour(s) (remove from stack)

`[   - use text in "colour_macro_start" in control file
`]   - use text in "colour_macro_end" in control file

The "macro" colours are intended to let you easily use a frequent sequence, one
that is tedious to do otherwise. For instance, to emphasise a word in help text,
like:  "To take something use the verb `[take`]."

*/

#define ANSI_RESET             0 
#define ANSI_BOLD              1
#define ANSI_BLINK             3 
#define ANSI_UNDERLINE         4 
#define ANSI_SLOW_BLINK        5
#define ANSI_FAST_BLINK        6 
#define ANSI_INVERSE           7 

#define ANSI_CANCEL_BOLD      22 
#define ANSI_CANCEL_BLINK     23 
#define ANSI_CANCEL_UNDERLINE 24 
#define ANSI_CANCEL_SLOW_BLINK  25
#define ANSI_CANCEL_INVERSE   27 

#define ANSI_TEXT_BLACK       30 
#define ANSI_TEXT_RED         31 
#define ANSI_TEXT_GREEN       32 
#define ANSI_TEXT_YELLOW      33 
#define ANSI_TEXT_BLUE        34 
#define ANSI_TEXT_MAGENTA     35 
#define ANSI_TEXT_CYAN        36 
#define ANSI_TEXT_WHITE       37 
         
#define ANSI_SET_FOREGROUND_DEFAULT 39
                     
#define ANSI_BACK_BLACK       40 
#define ANSI_BACK_RED         41 
#define ANSI_BACK_GREEN       42 
#define ANSI_BACK_YELLOW      43 
#define ANSI_BACK_BLUE        44 
#define ANSI_BACK_MAGENTA     45 
#define ANSI_BACK_CYAN        46 
#define ANSI_BACK_WHITE       47 

#define ANSI_SET_BACKGROUND_DEFAULT 49

// straight ANSI code
static inline string AnsiCode (short Code) 
{
return MAKE_STRING ("\x1B[" << Code << "m"); 
}

// ANSI code, preceded by reset to known state
static inline string AnsiNormalCode (short Code) 
{
return MAKE_STRING ("\x1B[0;" << Code << "m"); 
}

// ANSI bold code, preceded by reset to known state
static inline string AnsiBoldCode (short Code) 
{
return MAKE_STRING ("\x1B[0;1;" << Code << "m"); 
}

// map what follows the ` symbol to a string
static map<char, string> AnsiMap;
// ditto for background colours (ie. `!x)
static map<char, string> BackMap;
// set of characters that reset the code
static set<char> ResetSet;

// this is done once at program startup
static int OutputterSetup ()
  {
  cout << "Setting up colour codes ..." << endl;
  
  // text colour - normal
  AnsiMap ['s']  = AnsiNormalCode (ANSI_TEXT_BLACK);    
  AnsiMap ['r']  = AnsiNormalCode (ANSI_TEXT_RED);    
  AnsiMap ['g']  = AnsiNormalCode (ANSI_TEXT_GREEN);  
  AnsiMap ['y']  = AnsiNormalCode (ANSI_TEXT_YELLOW); 
  AnsiMap ['b']  = AnsiNormalCode (ANSI_TEXT_BLUE);   
  AnsiMap ['m']  = AnsiNormalCode (ANSI_TEXT_MAGENTA);
  AnsiMap ['c']  = AnsiNormalCode (ANSI_TEXT_CYAN);   
  AnsiMap ['w']  = AnsiNormalCode (ANSI_TEXT_WHITE);  

  // text colour - bold
  AnsiMap ['S']  = AnsiBoldCode (ANSI_TEXT_BLACK); 
  AnsiMap ['R']  = AnsiBoldCode (ANSI_TEXT_RED);    
  AnsiMap ['G']  = AnsiBoldCode (ANSI_TEXT_GREEN);  
  AnsiMap ['Y']  = AnsiBoldCode (ANSI_TEXT_YELLOW); 
  AnsiMap ['B']  = AnsiBoldCode (ANSI_TEXT_BLUE);   
  AnsiMap ['M']  = AnsiBoldCode (ANSI_TEXT_MAGENTA);
  AnsiMap ['C']  = AnsiBoldCode (ANSI_TEXT_CYAN);   
  AnsiMap ['W']  = AnsiBoldCode (ANSI_TEXT_WHITE);  
  
  // colourcode itself (so `` becomes ` )
  AnsiMap [COLOURCODE]  = COLOURCODE;  

  // underline
  AnsiMap ['_']  = AnsiCode (ANSI_UNDERLINE);  

  // clear (reset)
  AnsiMap ['x']  = AnsiCode (ANSI_RESET);  
  AnsiMap ['X']  = AnsiCode (ANSI_RESET);  
  
  // background colour
  BackMap ['s']  = AnsiCode (ANSI_BACK_BLACK); 
  BackMap ['r']  = AnsiCode (ANSI_BACK_RED);    
  BackMap ['g']  = AnsiCode (ANSI_BACK_GREEN);  
  BackMap ['y']  = AnsiCode (ANSI_BACK_YELLOW); 
  BackMap ['b']  = AnsiCode (ANSI_BACK_BLUE);   
  BackMap ['m']  = AnsiCode (ANSI_BACK_MAGENTA);
  BackMap ['c']  = AnsiCode (ANSI_BACK_CYAN);   
  BackMap ['w']  = AnsiCode (ANSI_BACK_WHITE);  

 // these codes have an ANSI reset in them
 // we use this to know when to clear out our "saved colours" sequence
 
  // normal colours
  ResetSet.insert ('s');  
  ResetSet.insert ('r');   
  ResetSet.insert ('g');   
  ResetSet.insert ('y'); 
  ResetSet.insert ('b'); 
  ResetSet.insert ('m'); 
  ResetSet.insert ('c'); 
  ResetSet.insert ('w'); 
  // bold colours  
  ResetSet.insert ('S'); 
  ResetSet.insert ('R'); 
  ResetSet.insert ('G'); 
  ResetSet.insert ('Y'); 
  ResetSet.insert ('B'); 
  ResetSet.insert ('M'); 
  ResetSet.insert ('C'); 
  ResetSet.insert ('W');
  // reset itself
  ResetSet.insert ('x');
  ResetSet.insert ('X');

  return 0;
  }  // end of  OutputterSetup

// convert a string so that imbedded colour codes are replaced by ANSI
// sequences. If wantAnsi is false, then the colour sequences are just dropped
const string ConvertColour (const string & source, 
                            stack<string> & colourStack, // for push/pop
                            string & currentColour,      // for stacking
                            const bool wantAnsi)
  {
// set up maps - once only
static int loadThemUp = OutputterSetup ();
// get our macros for `[ and `] use  
static string macroStart = control ().gets ("colour_macro_start");
static string macroEnd = control ().gets ("colour_macro_end");
  
  // save a bit of time by not doing a lot of work on string which are
  // empty, or do not contain the colour-code escape character
  if (source.empty () || source.find (COLOURCODE) == string::npos)
    return source;
 
// look for `X imbedded in the string

const char * p;
const char * pStart = source.c_str ();   // where buffer starts
map<char, string>::const_iterator iter;   // for finding in the maps
string sFixed;    // the output

  // allow for same size as source so we do less reallocations
  sFixed.reserve (source.size ());
  
  while ((p = strchr (pStart, COLOURCODE)))
    {
    // string before `
    sFixed += string (pStart, p++ - pStart);

    // p now points to the code
    pStart = p; // where it starts

    switch (*p)
      {
      
      case '[': // macro start sequence
        sFixed += ConvertColour (macroStart, colourStack, currentColour, wantAnsi);
        break;
      
      case ']': // macro end sequence
        sFixed += ConvertColour (macroEnd, colourStack, currentColour, wantAnsi);
        break;
      
      case '^': // push current colour sequence
        if (wantAnsi)
          colourStack.push (currentColour);
        break;
      
      case 'v': // pop saved colour sequence and output it
      case 'V': // ditto, so you don't have to remember the case
        if (wantAnsi && !colourStack.empty ())
          {
          string savedColour = colourStack.top ();
          colourStack.pop ();
          if (savedColour != currentColour)
            {
            currentColour = savedColour;
            sFixed += currentColour;  // output saved colour
            }
          }
        break;
      
     case '=': // recall saved sequence and output it (but leave on stack
        if (wantAnsi && !colourStack.empty ())
          {
          string savedColour = colourStack.top ();
          if (savedColour != currentColour)
            {
            currentColour = savedColour;
            sFixed += currentColour;  // output saved colour
            }
          }
        break;
        
      case '!': // background change  (eg. `!G )
        iter = BackMap.find (*++p);
        if (iter == BackMap.end ())
          { // not found, just echo it
          sFixed += COLOURCODE;
          sFixed += '#';
          sFixed += *p;
          }
        else
          if (wantAnsi)
            {
            // don't output a colour code identical to what we last sent
            if (iter->second != currentColour)
              {
              sFixed += iter->second;
              // remember our current code for stacking if wanted
              if (ResetSet.find (*p) != ResetSet.end ())
                currentColour.clear (); // if it resets, don't need old ones
              currentColour += iter->second;  // add to list
              } // end of change of colour
            }
        break;
          
      // capital X discards any stacked colour sequences
      case 'X':
        if (!colourStack.empty ())
          colourStack = stack<string> ();   // empty it
        currentColour.clear ();
        // ... now fall through and do default stuff
      
      default:  // normal colour
        iter = AnsiMap.find (*p);
        if (iter == AnsiMap.end ())
          {
          sFixed += COLOURCODE;
          sFixed += *p;   // not found, just echo it
          }
        else
          if (wantAnsi)
            {
            // don't output a colour code identical to what we last sent
            if (iter->second != currentColour)
              {
              sFixed += iter->second;
              // remember our current code for stacking if wanted
              if (ResetSet.find (*p) != ResetSet.end ())
                currentColour.clear (); // if it resets, don't need old ones
              currentColour += iter->second;  // add to list
              } // end of change of colour
            }
        break;
      
      } // end of switch on code which followed the `
    

    pStart = p + 1;   // move on past the `X thing
                 
    } // end of loop
  
  return sFixed + string (pStart);
  } // end of ConvertColour
