/*

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

// standard library includes ...

#include <stdexcept>
#include <vector>
#include <fstream>
#include <iostream>
#include <iterator>
#include <iomanip>

#ifdef WIN32
#include <time.h>
#endif

using namespace std; 

#include "utils.h"
#include "globals.h"
#include "exceptions.h"
#include "gameutils.h"
#include "timer.h"
#include "player.h"

const string FIGHTING = "fighting";           // player is fighting flag
const string FIGHT_TARGET = "fight_target";   // who they are fighting
const string FIGHT_PHASE = "fight_phase";     // what phase are we in? - see below

void PlayerEnteredGame (Player * p, const string & why)
{

  // we have to do this now *after* we have loaded their file
  // or it will be overwritten by the old data

  time_t t = time (NULL);
    
  p->sets ("last_connected_ip", p->address_);
  p->seti ("last_connected_time", t);
  p->sets ("last_connected_at", stringDate (t));
  p->seti ("times_on", p->geti ("times_on") + 1); // count times they were on
   
  serverInfo ().sets ("last_player_started", p->GetName ());
  serverInfo ().sets ("last_player_started_date", stringDate (t));
  serverInfo ().seti ("player_on_count", serverInfo ().geti ("player_on_count") + 1);

  p->sets ("last_command", ""); // so they don't immediately get kicked off
  p->seti ("command_count", 1); 

  p->connstate_ = ePlaying;    // now normal player

  // make sure they have an ID
  if (p->gets ("id").empty ())
    p->sets ("id", UniqueID ());

  // add to playing map
  playing () [p->gets ("id")] = p;
  ThingMap subs;
  subs ["player"] = *p;
  
  p->prompt_ = p->message ("default_prompt");         // default prompt
  p->send ("connect_welcome", messagemap (), subs);
  p->send (why, messagemap (), subs);
  p->send ("motd", messagemap (), subs);
  
  if (p->getb (FIGHTING))
    {
    p->send ("not_fighting_now", messagemap ());
    p->setb (FIGHTING, false);  
    }
  
  p->sets (FIGHT_PHASE, "");   // they are no longer doing whatever it was
  p->sets (FIGHT_TARGET, "");   // they are no longer fighting this person
  
  p->DoCommand ("look");     // new player looks around 

  ThingMap sub;
  sub ["player"] = *p;
  sub ["address"].sets ("short", p->gets ("last_connected_ip"));
                  
  // tell other players
  SendToAll ("new_player_joined", messagemap (), sub, p);
  
  // log to log file
  gamelog (p->GetName () + " has joined the game from " + 
           p->gets ("last_connected_ip"));
  
  // log it
  cout << "Player " << p->GetName () << " has joined the game." << endl;
} // end of PlayerEnteredGame

void ProcessPlayerName (Player * p, istream & sArgs)
{
  string playername;
  sArgs >> playername;

  /* name can't be blank */
  if (playername.empty ())
    throw msg ("connect_name_not_blank");
  
  /* don't allow two of the same name */
  if (FindPlayer (playername))
    throw msg ("connect_already_playing");

  if (playername.find_first_not_of (valid_player_name) != string::npos)
    throw msg ("connect_disallowed_characters");
        
  if (tolower (playername) == "new")
    {
    if (!control ().getb ("permit_new_players"))
      throw msg ("connect_no_new_players");
    
    ObjectList & languages = control ().items_ ["languages"];    
    
    if (languages.size () < 2)
      {
      p->connstate_ = eAwaitingNewName;
      p->prompt_ = p->message ("connect_choose_a_name");
      }
    else
      {
      p->connstate_ = eAwaitingLangage;
      int count = 0;
      p->prompt_.clear ();
      
      for (ObjectList::const_iterator i = languages.begin ();
           i != languages.end ();
            ++i)
        {
        p->prompt_ += MAKE_STRING (++count << ". " << i->gets ("prompt") << " ");
        }
      p->prompt_ += "... ";
      }
    }   // end of new player
  else
    {   // old player
  
    p->sets ("name", tocapitals (playername));
    p->Load ();   // load player so we know the password etc.
    
    p->sub_ ["player"] = *p;
    p->sub_ ["vnum"].sets ("short", MAKE_STRING (p->geti ("room")));
        
    // check his room exists, if not, he'll have to try another name
    FindRoom (p->geti ("room"));
    
    p->connstate_ = eAwaitingPassword;
    p->prompt_ = p->message ("connect_choose_password");
    p->badPasswordCount_ = 0;
    } // end of old player
        
} /* end of ProcessPlayerName */

void ProcessNewPlayerLanguage (Player * p, istream & sArgs)
  {
   ObjectList & languages = control ().items_ ["languages"];    
 
  // find which number they selected
  int which;
  sArgs >> ws >> which >> ws;
  if (sArgs.fail () || !sArgs.eof () || 
      which > languages.size () ||
      which <= 0)
    {
    p->send (":(\n");   // sad face, language non-specific
    }
  else
    {
    ObjectList::const_iterator lang_iter = get_n (ISEQ (languages), which - 1);
    // this must succeed, surely!
    if (lang_iter != languages.end ())
      {
      p->sets ("language", lang_iter->gets ("name")); // remember language
      p->sets ("prefix", lang_iter->gets ("prefix")); // and description prefix
      p->connstate_ = eAwaitingNewName;
      p->prompt_ = p->message ("connect_choose_a_name");
      } // end of language found
    } // end of language in range
  
  } // end of ProcessNewPlayerLanguage

void ProcessNewPlayerName (Player * p, istream & sArgs)
{
  string playername;
  sArgs >> playername;

  int minLength = control ().geti ("name_length_minimum", 4);
  int maxLength = control ().geti ("name_length_maximum", 10);
  
  /* name can't be blank */
  if (playername.empty ())
    throw msg ("connect_name_not_blank");

  if (playername.find_first_not_of (valid_player_name) != string::npos)
    throw msg ("connect_disallowed_characters");
        
  if (playername.size () < minLength)
    throw msg ("connect_name_too_short");
      
  if (playername.size () > maxLength)
    throw msg ("connect_name_too_long");

  // disallow bad names in our control file
  vector<string> badnames;
  stringToContainer (control ().gets ("bad_player_names"), badnames);
  vector<string>::const_iterator i = 
    find_if (ISEQ (badnames), stringContains (tolower (playername)));
  if (i != badnames.end ())
    throw msg ("connect_name_not_permitted");

  // see if player exists
  ifstream f ((PLAYER_DIR + tocapitals (playername) + PLAYER_EXT).c_str (), ios::in);
  if (f || FindPlayer (playername))  // player file on disk, or playing without saving yet
    throw msg ("connect_player_already_exists");
  
  p->sets ("name", tocapitals (playername));
  
  p->connstate_ = eAwaitingNewPassword;
  p->prompt_ = p->message ("connect_choose_new_password");  
  p->badPasswordCount_ = 0;
    
} /* end of ProcessNewPlayerName */

void ProcessNewPassword (Player * p, istream & sArgs)
{
   string password;
   sArgs >> password;
  
  int minLength = control ().geti ("password_length_minimum", 6);
  int maxLength = control ().geti ("password_length_maximum", 20);
  int uniqueLength = control ().geti ("password_length_unique", 4);

  /* password can't be blank */
  if (password.empty ())
    throw msg ("connect_no_password");
  
  if (password.size () < minLength)
    throw msg ("connect_password_too_short");
 
  if (password.size () > maxLength)
    throw msg ("connect_password_too_long");
  
  // for password checks
  string temp = tolower (password);  // case doesn't really matter

  // password cannot be inside name (eg. name: nick, password: nic)
  if (tolower (p->gets ("name")).find (temp) != string::npos)
     throw msg ("connect_password_too_obvious");

  // name cannot be inside password (eg. name: nick, password: nick1234)  
  if (temp.find (tolower (p->gets ("name"))) != string::npos)
     throw msg ("connect_password_too_obvious");

  // we will guard against passwords like "aaaaaa", "aaabbb" or "ababababa"
  sort (ISEQ (temp));     // get into order, eg. ababab becomes aaabbb
  temp.erase (unique (ISEQ (temp)), temp.end ());   // remove duplicates, so aaabbb becomes ab
  
  // check we have enough uniqueness
  if (temp.size () < uniqueLength)
    throw msg ("connect_password_too_easy_to_guess");
  
  // rather than storing the password in plain text, we will store a hash of it
  // then, we later hash what they enter and compare the hashes.
  
  p->sets ("password", SecureHash (password).str ()); // remember for confirmation
  p->connstate_ = eConfirmPassword;
  p->prompt_ = p->message ("connect_reenter_password");
    
} /* end of ProcessNewPassword */

struct fShowOneClass
{
  Player * p_;
  set<string> classset_;

  // ctor
  fShowOneClass (Player * p) : p_ (p) {}
  void operator() (const Object & o) 
    {
    p_->send (MAKE_STRING (setw (15) << firstLetterCaps (o.gets ("name")) 
            << " - " << o.gets ("summary")));
    classset_.insert (firstLetterCaps (o.gets ("name")));
    } // end of operator()
  
  const string GetList () const
    {
    ostringstream os;
    copy (ISEQ (classset_), ostream_iterator<string> (os, " "));
    return os.str ();    
    }
  
};  // end of fShowOneClass

void ProcessConfirmPassword (Player * p, istream & sArgs)
{
  string password;
  sArgs >> password;
  
  // password must agree
  if (SecureHash (password).str () != p->gets ("password"))
    {
    p->connstate_ = eAwaitingNewPassword;
    p->prompt_ = p->message ("connect_choose_new_password");
    throw msg ("connect_password_not_confirmed");
    }
  
  // that player might have been created while we were choosing a password, so check again
  ifstream f ((PLAYER_DIR + tocapitals (password) + PLAYER_EXT).c_str (), ios::in);
  if (f || FindPlayer (password))  // player file on disk, or playing without saving yet
    {
    p->connstate_ = eAwaitingNewName;
    p->prompt_ = p->message ("connect_choose_a_name");  // re-prompt_ for name
    throw msg ("connect_player_already_exists");
    }

  // classes are in control file
  ObjectList & classes = control ().items_ ["classes"];

  p->send ("connect_class_list_follows", messagemap ());
  
  // show each one, build a list for the prompt
  string classList = for_each (ISEQ (classes), fShowOneClass (p)).GetList ();
  
  // now get their class
  p->connstate_ = eAwaitingClass;
  p->prompt_ = MAKE_STRING (p->message ("connect_classes_are") 
                << classList << "\n" << p->message ("connect_choose_class"));
             
} /* end of ProcessNewPassword */

struct copyNumericAttribute
  {
  const Object & from_;
  Player & p_;  
  // ctor
  copyNumericAttribute (const Object & from, Player & p) : from_ (from), p_ (p) {}
  // operator ()
  void operator () (const string & which) { p_.seti (which, from_.geti (which)); }
  };  // end of copyNumericAttribute

struct fsetFlag
  {
  Player & p_;  
  // ctor
  fsetFlag (Player & p) : p_ (p) {}
  // operator ()
  void operator () (const string & which)  { p_.setb (which, true); }
  };  // end of fsetFlag


void ProcessClass (Player * p, istream & sArgs)
{
   string playerclass;
   sArgs >> playerclass;
  
  if (playerclass.empty ())
    return;   // can't be empty, this will make it reprompt for it
  
  ObjectList & classes = control ().items_ ["classes"];
  
  StringMap wildcards;
  
  ObjectList::const_iterator iClass = FindObjectRegexp (classes, "match", playerclass, wildcards);
  if (iClass == classes.end ())
    throw msg ("connect_no_such_class");
  
  p->sets ("class", playerclass);
  
  // copy class attributes from class to player
  set<string> whatSet;
  // make into a stringset (actual list of things is in the control file)
  stringToContainer (control ().gets ("class_copy_numbers"), whatSet);
  // copy them
  for_each (ISEQ (whatSet), copyNumericAttribute (*iClass, *p));
    
  // find what flags to set (eg. guest)
  stringToContainer (control ().gets ("new_player_flags"), whatSet);
  // set them
  for_each (ISEQ (whatSet), fsetFlag (*p));
  
  // New player now in the game
  PlayerEnteredGame (p, "new_player");
         
} /* end of ProcessClass */

void ProcessPlayerPassword (Player * p, istream & sArgs)
{
  try
    {
    string password;
    sArgs >> password;

    /* password can't be blank */
    if (password.empty ())
      throw msg ("connect_no_password");
            
    if (SecureHash (password).str () != p->gets ("password"))
      {
      badPlayerPasswords () [p->address_]++;   // count bad attempts from this IP
      throw msg ("connect_password_incorrect");
      }

    // check for "blocked" flag on this player
    if (p->HaveFlag ("blocked"))
      {
      p->ClosePlayer ();
      p->prompt_ = p->message ("connect_goodbye");
      throw msg ("connect_blocked");
      }
      
    badPlayerPasswords () [p->address_] = 0;    // this IP is OK
    // OK, they're in!
    PlayerEnteredGame (p, "existing_player");
 
    } // end of try block
    
  // detect too many password attempts
  catch (exception & e)
    {
    if (++p->badPasswordCount_ >= control ().geti ("password_attempts_maximum", 3))
      {
      p->send ("connect_too_many_password_guesses", messagemap ());
      p->Init ();
      }
      throw;
    }
    
} /* end of ProcessPlayerPassword */
    
int LoadStates ()
{
static map<tConnectionStates, tStateHandler> & statemap = getStateMap ();

  cout << "Loading connection states ..." << endl;

// connection states
  statemap [eAwaitingName]        = ProcessPlayerName;    // existing player
  statemap [eAwaitingPassword]    = ProcessPlayerPassword;

  statemap [eAwaitingLangage]     = ProcessNewPlayerLanguage; // new player
  statemap [eAwaitingNewName]     = ProcessNewPlayerName; 
  statemap [eAwaitingNewPassword] = ProcessNewPassword;
  statemap [eConfirmPassword]     = ProcessConfirmPassword;
  statemap [eAwaitingClass]       = ProcessClass;

  statemap [ePlaying]             = ProcessCommand;   // playing

  return 0;
} // end of LoadStates

static int loadThemUp = LoadStates ();
