/*

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

// uncomment to debug incoming headers

// #define DEBUG_HTTP 1

// uncomment to immediately write to control file

#define COMMIT_CHANGES 1

#include "commands.h"
#include "webclient.h"
#include "save.h"

// Actions can be:

static const string ADD_ACTION        = "add";        //  add record (which is now on form)
static const string SEARCH_ACTION     = "search";     //  search for a record by match of some sort
static const string LIST_ACTION       = "list";       //  list existing records 
static const string NEW_ACTION        = "new";        //  start a new, blank record
static const string DUPLICATE_ACTION  = "duplicate";  //  duplicate the current record
static const string DELETE_ACTION     = "delete";     //  delete the current record
static const string AMEND_ACTION      = "change";     //  change the current record
static const string EDIT_ACTION       = "edit";       // change to editing new thing (ie. change tables)

// (blank)     - just show action buttons

static const int MAXRECORDS = 200;    // maximum to display for long lists, searches
static const int BOXCOLS = 78;        // columns for input boxes

                                                
// reserved names for using on the form
//  by using underscores they shouldn't clash with actual data field names

static const string NAMEFIELD     = "_name";      // name of table (eg. spells)
static const string KEYFIELD      = "_key";       // which key they are changing (eg. heal)
static const string ACTIONFIELD   = "_action";    // what action (eg. add)
static const string NEWNAME       = "_newname";   // the name of the new table
static const string HASHFIELD     = "_hash";      // the hash of the original record 
static const string SEARCHFIELD   = "_searchfield";  // what to search on  (eg. "hp")
static const string SEARCHVALUE   = "_searchvalue";  // what to search for  (eg. 10)
static const string CONFIRMDELETE = "_confirmdelete";  // true to confirm deleting

// gets a reference to the correct list (eg. spells, rooms etc.)
// this is for main dtds, sub-dtds are selected later on
ObjectList * getObjectList (WebClient & w, const string & name)
  {  
  if (name == "rooms")    // rooms are in a map of TopThings, unfortunately
    return NULL;          // and must be handled differently
  else if (name == "spells")
    return & control ().items_ ["spells"];
  else if (name == "languages")
    return & control ().items_ ["languages"];
  else if (name == "dtd")
    return & control ().items_ ["dtd"];   // the DTD itself
  else
    throw runtime_error (name + " not implemented");
  }

class EditorData
  {
  private:
    ObjectList * itemList_;    // the list the item is found in (eg. the spell list)
    ObjectList::iterator itemListIterator_;   // for listing items
    RoomMapIterator  roomMapIterator_;              // for listing rooms
  public:
    string name_;      // what to edit (eg. spell)
    string action_;    // add, change, delete etc.
    string keyname_;   // the name of the key (eg. "name")
    string key_;       // what the key is (eg. "heal")
    Object dtd_;       // dtd for this table
    thing item_;       // a copy of the thing being edited (eg. the spell)
    list<pair<string, string> > languages_;    // which languages for multi-lingual
    StringMap errors_;  // error messages, per field  (eg. "field required", "not numeric" etc.)
    Regexp regexp_;     // regexp for search
    string searchField_;  // field name to look in for search
    string searchValue_;  // what to look for
    bool addingAction_;   // true if this is adding a new record
    string baseType_;     // top-level key type (eg. rooms, spells)
  
  // ctor
  EditorData () : itemList_ (NULL) 
    {
    roomMapIterator_ = rooms ().end ();
    }
  
  // for setting the itemlist
  void setItemList (ObjectList * itemList)  
    { 
    itemList_ = itemList; 
    if (itemList_)
      itemListIterator_ = itemList_->end ();
    }

  ObjectList * getItemList () const { return itemList_; }

  // finds key_ (with key name keyname_) in the relevant list/map
  thing * findRecord ()
    {
    // items in an ObjectList, like control data
    if (itemList_)
      {
      ObjectIter i = FindObject (*itemList_, keyname_, key_);
      if (i == itemList_->end ())
        return NULL;
      return &(*i);   // return pointer to item ;)
      }
    
    // rooms are different
    int vnum = convertString<int> (key_, "Bad vnum");
    RoomMapIterator i = rooms ().find (vnum);
    if (i == rooms ().end ())
      return NULL;
    return &(i->second);    // pointer to room  
    }
  
  // insert a new record into the appropriate list/map
  void insert ()
    {
    if (itemList_)
      {
      Object obj;
      CopyThing (item_, obj);
      itemList_->push_back (obj);
      }
    else  // end of ObjectList
      {
      // rooms are different
      int vnum = convertString<int> (key_, "Bad vnum");
      Room room;
      CopyThing (item_, room);
      rooms () [vnum] = room;  
      }   // end of doing room
    } // end of insert
  
  // erase the keyed record from the appropriate list/map
  void erase ()
    {
    // items in an ObjectList, like control data
    if (itemList_)
      {
      ObjectIter i = FindObject (*itemList_, keyname_, key_);
      if (i != itemList_->end ())
        itemList_->erase (i);
      } // end of ObjectList
    else
      {
      // rooms are different
      int vnum = convertString<int> (key_, "Bad vnum");
      rooms () .erase (vnum);
      } // end of doing room
    } // end of erase
  
  thing * getNextItem (const bool first = false)
    {
    thing * result = NULL;
    if (itemList_)
      {
      if (first)
        itemListIterator_ = itemList_->begin ();
      if (itemListIterator_ != itemList_->end ())
        {
        result = & (*itemListIterator_);
        itemListIterator_++;
        }
      }   // end of ObjectList
    else
      {
      // rooms are different
      if (first)
        roomMapIterator_ = rooms ().begin ();
      if (roomMapIterator_ != rooms ().end ())
        {        
        result = static_cast<thing*>  (&(roomMapIterator_->second));
        roomMapIterator_++;
        }
      } // end of room
    return result;
    } // end of getNextItem
    
  // true if form has errors on it
  const bool haveError () const { return !errors_.empty (); }

  // return true if a particular field had an error on it
  const bool fieldHasError (const string & fullName)
    {
    StringMap::const_iterator error_iter = errors_.find (fullName);
    return error_iter != errors_.end ();
    }
  
  };

// combine one thing
template <typename T>
struct concatenateAttribute
{
  string total_;
   
  void operator() (const pair<string, T> & data)
    {
    total_ += data.first + MAKE_STRING (data.second) + ";";  
    }
  
  const string GetTotal () const { return total_; }

};  // end of concatenateAttribute

// hash a thing, to see if someone changed it
string HashThing (const thing & o)
  {
  string total;

  total += for_each (ISEQ (o.get_sdata ()), concatenateAttribute<string> ()).GetTotal ();  
  total += for_each (ISEQ (o.get_idata ()), concatenateAttribute<int> ()).GetTotal ();  
  total += for_each (ISEQ (o.get_fdata ()), concatenateAttribute<double> ()).GetTotal ();  
  total += for_each (ISEQ (o.get_bdata ()), concatenateAttribute<bool> ()).GetTotal ();  
  
  return SecureHash (total).str ();
  
  } // end of HashThing

// put a button on the form
void Button (WebClient & w, const string & name, const string & label)
  {
  w << "<input Type=\"submit\" "
    << "Name=\"_" << name << "\" "
    << "Value=\"" << label << "\">\n";
  } // end of Button

// hyperlink, name is the key (eg. spell=fireblast) description is what they see
void Hyperlink (WebClient & w, const EditorData & data,
                const string & value, const string & description)
  {
  w << "<a href=\"/" << w.URL_ << "?" << NAMEFIELD << "=" << data.name_ 
    << "&" << KEYFIELD << "=" << value << "\">"
    << FixHTML (description) << "</a>\n";
  } // end of Hyperlink

// put hidden text on the form
void Hidden (WebClient & w, const string & name, const string & contents)
  {
  w << "<input Type=\"hidden\" "
    << "Name=\"" << name << "\" "
    << "Value=\"" << contents << "\">\n";
   } // end of Hidden

void InputField (WebClient & w, const string & name, const string & contents,
                 const int size, const int maxlength, const bool right = false)
 {
 string align;
 if (right)
   align = " align=\"right\" style=\"text-align:right;\" ";
 
  w << "<input Type=\"text\" "
    << "Name=\"" << name << "\" "
    << "Value=\"" << FixHTML (contents) << "\" " 
    << "size=\"" << size << "\" "
    << "maxlength=\"" << maxlength << "\" "
    << align
    << ">\n"; 
 }  // end of InputField

void InputBox (WebClient & w, const string & name, const string & contents,
                 const int lines)
 {
  w << "<textarea "
    << "Name=\"" << name << "\" "
    << "rows=\"" << lines << "\" "
    << "cols=\"" << BOXCOLS << "\" >\n"; 

  w << FixHTML (contents);
  w << "</textarea>\n";
 }  // end of InputBox

void ComboBox (WebClient & w, const string & name, const string & contents,
                 const string matchlist)
 {
    w << "<select Name=\"" << name << "\" size=\"1\">\n";
  
  set<string> matches;
  stringToContainer (matchlist, matches);
 
  for (set<string>::const_iterator i = matches.begin ();
       i != matches.end ();
       ++i)
    {
    // indicate one currently selected
    string selected;
    if (*i == contents)
      selected = "selected";
    w << "<option Value=\"" << *i << "\" " << selected << ">"
      << FixHTML (*i) << "\n";
    }
  w << "</select>\n";
  
 }  // end of ComboBox

void CheckBox (WebClient & w, const string & name, const bool contents)
 {
  w << "<input Type=\"checkbox\" "
    << "Name=\"" << name << "\" "
    << "Value=\"1\" ";
  if (contents)
    w << "checked ";
  w << ">\n"; 
 }  // end of CheckBox

void ShowButtons (WebClient & w, EditorData & data)
  {  
  // can only show main buttons if they have selected something to edit
  if (!data.name_.empty ())
    {
    
    pair<string, string> s = GetWord (data.searchField_, ":");
    string searchField = s.first;
    
    // generate search list - show all possible fields in the DTD
    w << "<p><p>Search: &nbsp;\n";
    w << "<select Name=\"" << SEARCHFIELD << "\" size=\"1\">\n";
    // list all dtds
    for (ObjectList::const_iterator dtditer = data.dtd_.contents_.begin ();
         dtditer !=  data.dtd_.contents_.end ();
         ++dtditer)
      {
      string name = dtditer->gets ("name");
        
      // mucking around for multiple languages
      string language;
      string prefix;
      bool multiLanguage = dtditer->getb ("multi_language");
      list<pair<string, string> >::const_iterator lang_iter = data.languages_.begin ();
      string fullName = name;   // name with language prefix  
      
     // repeat for each supported language - I do it this way because I want to
      // go through the loop at least once
      do
        {
        // get the current language
        if (multiLanguage)
          {
          if (lang_iter != data.languages_.end ())
            {
            language = lang_iter->first;
            prefix = lang_iter->second;     
            fullName = prefix + name;   // prefix name so success_room becomes fr_success_room
            } // end of another language to go
          }   // end of multi-languages on *this* field
      
          // indicate one we are currently searching for
          string selected;
          if (fullName == searchField)
            selected = "selected";
          w << "<option Value=\"" << fullName << ":"    // name:type
            << dtditer->gets ("type") << "\" " << selected << ">"
            << FixHTML (fullName) << "\n";
          
          // onto next language
         if (lang_iter != data.languages_.end ())
            ++lang_iter;
        } while (multiLanguage && lang_iter != data.languages_.end ());
        
      }   // end of each dtd item
    w << "</select>\n";
    
    w << "&nbsp; for match on: &nbsp;";
    InputField (w, SEARCHVALUE, data.searchValue_, 30, 255);
    Button (w, SEARCH_ACTION, "Search");
  
        // start new line for rest of them
    w << "<p>\n";

    // List All button
    Button (w, LIST_ACTION, "List All");
      
    // New record button (unless already doing a new record)
    if (data.action_ != NEW_ACTION)
      Button (w, NEW_ACTION, "New");
    
    // we can only add if we started with a blank record, or a faulty add
  
    if (data.addingAction_)
      Button (w, ADD_ACTION, "Add");
      
    // we can only change or delete if we have a key to do it to
      
    if (!data.key_.empty () && 
        data.action_ != DUPLICATE_ACTION && 
        data.action_ != DELETE_ACTION)
      {
      Button (w, AMEND_ACTION, "Change");
      Button (w, DELETE_ACTION, "Delete");
      Button (w, DUPLICATE_ACTION, "Duplicate");
      // safety checkbox
      w << "<p>Check To Confirm Delete: <input type=\"checkbox\" name=\""
        << CONFIRMDELETE << "\" Value=\"1\"> (if using 'Delete' button)</p>\n";
        
      // put primary key into form as a hidden field for updating or deleting purposes
      Hidden (w, KEYFIELD, data.key_);
      }  // end of having primary key
    
    w << "</p>\n";
    
    // what we are editing -- see list now
    Hidden (w, NAMEFIELD, data.name_);
    } // end of having a table to edit
  
  // show list of things to edit
  w << "<hr />\n";
  w << "Edit: <select Name=\"" << NEWNAME << "\" size=\"1\">\n";
  
  ObjectList alldtd = control ().items_ ["dtd"];
  // list all dtds
  for (ObjectList::const_iterator dtditer = alldtd.begin ();
       dtditer != alldtd.end ();
       ++dtditer)
    {
    
    // exclude sub-dtds
    if (dtditer->getb ("sub_dtd"))
      continue;
    
    // indicate one we are currently editing
    string selected;
    if (dtditer->gets ("name") == data.name_)
      selected = "selected";
    w << "<option Value=\"" << dtditer->gets ("name") << "\" " << selected << ">"
      << FixHTML (dtditer->gets ("name")) << "\n";
    }
  w << "</select>\n";
  
  Button (w, EDIT_ACTION, "Edit new thing");
  
  w << "<p><a href=\"editor.htm\">Online editor help</a></p>\n";
  
  } // end of ShowButtons


void FindRecord (WebClient & w, EditorData & data)
  {
  
  // find base record 
  thing * t = data.findRecord ();
  if (t == NULL)
    throw runtime_error ("There is no '" + data.key_ + "' in '" + data.name_ + "'");
 
  // get copy of record
  data.item_ = *t;
    
  } // end of FindRecord

void ValidateString (WebClient & w, EditorData & data, 
                  const Object & dtditem, const string & fullName)
  {
  // what they entered
  string enteredData = trim (w.postData_ [fullName]);

  // most checks are bypassed for empty fields
  // if you want them applied check "required" first
  
  if (enteredData.empty ())
    {
    data.item_.erases (fullName);  // erase altogether
    return; // all done!
    }

  // not empty
  
  if (dtditem.hasi ("min_length") && 
      enteredData.size () < dtditem.geti ("min_length"))
     {
     data.errors_ [fullName] = 
        MAKE_STRING ("Value too short, minimum is " << dtditem.geti ("min_length") << " characters.");
     return;
     }
    
  if (dtditem.hasi ("max_length") && 
      enteredData.size () > dtditem.geti ("max_length"))
     {
     data.errors_ [fullName] = 
        MAKE_STRING ("Value too long, maximum is " << dtditem.geti ("max_length") << " characters.");
     return;
     }
       
  Regexp re;
  string s;

  // check it matches the regular expression
  s = dtditem.gets ("match_regexp");
  if (!s.empty ())
    {
    re.Compile (s);
    if (!re.Execute (enteredData))
       {
       data.errors_ [fullName] = 
          MAKE_STRING ("Value does not match required 'match' regular expression.");
       return;
       }
    }   // end of testing match_regexp

  // check it does not match the regular expression
  s = dtditem.gets ("nomatch_regexp");
  if (!s.empty ())
    {
    re.Compile (s);
    if (re.Execute (enteredData))
       {
       data.errors_ [fullName] = 
          MAKE_STRING ("Value matches required 'no match' regular expression.");
       return;
       }
    }   // end of testing nomatch_regexp
  
  // check it matches the set of values
  // note - it accepts an empty string, if this will not do make the field "required"
  s = dtditem.gets ("match_set");
  if (!s.empty ())
    {
    set<string> matches;
    stringToContainer (s, matches);
    if (matches.find (enteredData) == matches.end ())
      {
      ostringstream os;
      const string delimiter = ", ";
      copy (ISEQ (matches), ostream_iterator<string> (os, delimiter.c_str ()));
      string wanted = os.str ();
      wanted = wanted.substr (0, wanted.size () - delimiter.size ()); // drop trailing ", "
      data.errors_ [fullName] = 
         MAKE_STRING ("Value is not in required list (" + wanted + ")");
      return;
      }
    }   // end of testing match_set
  
  // if "regexp" flag is set the field is itself a regexp - make sure it compiles
  if (dtditem.getb ("regexp"))
    {
    try
      {
      re.Compile (enteredData);
      } // end of try
    catch (exception & e)
      {
       data.errors_ [fullName] = 
         MAKE_STRING ("Bad regular expression: " << e.what ());
      return;
      } // end of catch
    } // end of field being a regexp
  
  
  // copy into object, so it is displayed when we show the form
  data.item_.sets (fullName, enteredData);
   
  }   // end of ValidateString

void ValidateInt (WebClient & w, EditorData & data, 
                  const Object & dtditem, const string & fullName)
  {
  // what they entered
  string enteredData = w.postData_ [fullName];
  int value = 0;
      
  // most checks are bypassed for empty fields
  // if you want them applied check "required" first
  
  if (enteredData.empty ())
    {
    data.item_.erasei (fullName);  // erase altogether
    return; // all done!
    }

// not empty

  istringstream is (enteredData);

  is >> ws >> value >> ws;
  if (is.fail () || !is.eof ())
     {
     data.errors_ [fullName] = "Entry not numeric";
     return;
     }
  
  if (dtditem.hasi ("min_value") && 
      value < dtditem.geti ("min_value"))
     {
     data.errors_ [fullName] = MAKE_STRING ("Value too low, minimum is " << dtditem.geti ("min_value"));
     return;
     }
    
  if (dtditem.hasi ("max_value") && 
      value > dtditem.geti ("max_value"))
     {
     data.errors_ [fullName] = MAKE_STRING ("Value too high, maximum is " << dtditem.geti ("max_value"));
     return;
     }
  
   // check room exists
  if (dtditem.getb ("room"))
    {
    RoomMapIterator i = rooms ().find (value);
    if (i == rooms ().end ())
      {
      data.errors_ [fullName] = "This room does not exist.";
      return;
      }
    }   // end of room check
  
  // copy into object, so it is displayed when we show the form
  data.item_.seti (fullName, value);
   
  }   // end of ValidateInt

void ValidateFloat (WebClient & w, EditorData & data, 
                  const Object & dtditem, const string & fullName)
  {
  // what they entered
  string enteredData = w.postData_ [fullName];
  double value = 0.0;
  
  // most checks are bypassed for empty fields
  // if you want them applied check "required" first
  
  if (enteredData.empty ())
    {
    data.item_.erasef (fullName);  // erase altogether
    return; // all done!
    }

// not empty    
  
  istringstream is (enteredData);

  is >> ws >> value >> ws;
  if (is.fail () || !is.eof ())
     {
     data.errors_ [fullName] = "Entry not numeric";
     return;
     }
  
  if (dtditem.hasf ("min_value") && 
      value < dtditem.getf ("min_value"))
     {
     data.errors_ [fullName] = MAKE_STRING ("Value too low, minimum is " << dtditem.getf ("min_value"));
     return;
     }
    
  if (dtditem.hasf ("max_value") && 
      value > dtditem.getf ("max_value"))
     {
     data.errors_ [fullName] = MAKE_STRING ("Value too high, maximum is " << dtditem.getf ("max_value"));
     return;
     }
   
  // copy into object, so it is displayed when we show the form
  data.item_.setf (fullName, value);
   
  }   // end of ValidateFloat

void ValidateBool (WebClient & w, EditorData & data, 
                  const Object & dtditem, const string & fullName)
  {
  // what they entered
  string enteredData = w.postData_ [fullName];
      
  if (enteredData.empty ())
    enteredData = "0";
  
  bool value;
  istringstream is (enteredData);

  is >> ws >> value >> ws;
  if (is.fail () || !is.eof ())
     {
     data.errors_ [fullName] = "Entry not boolean";
     return;
     }
  
  // copy into object, so it is displayed when we show the form
  data.item_.setb (fullName, value);
   
  }   // end of ValidateBool


struct validateField
{
  WebClient & w_;
  EditorData & data_;
  int count_;
  char type_;

  // ctor
  validateField (WebClient & w, EditorData & data) 
      : w_ (w), data_ (data), count_ (0) {}
   
  void operator() (Object & dtditem)
    {
   
    string name = dtditem.gets ("name");    // field name
    string type = dtditem.gets ("type");    // type of data: s, n, f, b
    
    if (type.empty ())
      throw runtime_error ("No data type in DTD for " + name);
    
    type_ = tolower (type [0]);
        
    // the key field is always required
    if (name == data_.keyname_)
      dtditem.setb ("required", true);
    
    // lengths
    int max_length = dtditem.geti ("max_length", 60);
    int min_length = dtditem.geti ("min_length", 0);
    
    string language;
    string prefix;
    bool multiLanguage = dtditem.getb ("multi_language");
    
    list<pair<string, string> >::const_iterator lang_iter = data_.languages_.begin ();
    
    string fullName = name;   // name with language prefix
      
    // repeat for each supported language - I do it this way because I want to
    // go through the loop at least once
    do
      {
      // get the current language
      if (multiLanguage)
        {
        if (lang_iter != data_.languages_.end ())
          {
          language = lang_iter->first;
          prefix = lang_iter->second;     
          fullName = prefix + name;   // prefix name so success_room becomes fr_success_room
          }// end of another language to go
        }  // end of multi-languages on *this* field
           
      // if field not there, error, otherwise validate it
      if (dtditem.getb ("required") && trim (w_.postData_ [fullName]).empty ())
        data_.errors_ [fullName] = "This field must be entered.";
      else
        switch (type_)
          {
          case 's': ValidateString  (w_, data_, dtditem, fullName);   break;
          case 'n': ValidateInt     (w_, data_, dtditem, fullName);   break;
          case 'f': ValidateFloat   (w_, data_, dtditem, fullName);   break;
          case 'b': ValidateBool    (w_, data_, dtditem, fullName);   break;
          default:
            throw runtime_error ("Unknown data type '" + type + "' in DTD for " + name);
            
          } // end of switch on type_
      
      // onto next language
     if (lang_iter != data_.languages_.end ())
        ++lang_iter;
    } while (multiLanguage && lang_iter != data_.languages_.end ());
  }   // end of operator() 
};  // end of validateField

void ValidateInputFields (WebClient & w, EditorData & data)
  {
  // validate everything in the DTD  
  for_each_noreturn (ISEQ (data.dtd_.contents_), validateField (w, data));
  
  } // end of ValidateInputFields

void AddRecord (WebClient & w, EditorData & data)
  {
  // now is the time to generate the new key
  if (data.dtd_.getb ("auto_id"))
    data.item_.sets (data.keyname_, UniqueID ());
  
  // get value of new key
  if (data.item_.hasi (data.keyname_))  // rooms will have numeric keys
    data.key_ = MAKE_STRING (data.item_.geti (data.keyname_));
  else
    data.key_ = data.item_.gets (data.keyname_);

  w << "<h2>Insert item '" << FixHTML (data.key_) << "' in " << FixHTML (data.name_) << "</h2>\n";
  
  // check for duplicates
  
  // find base record 
  thing * t = data.findRecord ();
  if (t != NULL)
    throw runtime_error ("Duplicate item '" + data.key_ + "' in '" + data.name_ + "'");

  // add to relevant list
  data.insert ();
    
  w << "<p><b>Item added.</b></p>";

#ifdef COMMIT_CHANGES
    if (data.baseType_ == "rooms")
      WriteRooms ();
    else
      WriteTopThing (CONTROL_FILE, "control", control ());
#endif   
    
  } // end of AddRecord

void ChangeRecord (WebClient & w, EditorData & data)
  {
  w << "<h2>Change item '" << FixHTML (data.key_) << "' in " << FixHTML (data.name_) << "</h2>\n";
  
  // find base record 
  thing * t = data.findRecord ();
  if (t == NULL)
    throw runtime_error ("There is no '" + data.key_ + "' in '" + data.name_ + "'");
  
  // if the hash has changed, then someone has edited it while we were looking at it
  if (HashThing (*t) != w.postData_ [HASHFIELD])
    throw runtime_error ("That data has been changed by someone else. Please re-locate it and try again.");
  
  // replace it
  CopyThing (data.item_, *t);
  
  // if the hash is the same, they didn't really change anything ;)
  if (HashThing (data.item_) == w.postData_ [HASHFIELD])
    w << "<p><b>Nothing changed.</b></p>";
  else
    {
    w << "<p><b>Item updated.</b></p>";
#ifdef COMMIT_CHANGES
    if (data.baseType_ == "rooms")
      WriteRooms ();
    else
      WriteTopThing (CONTROL_FILE, "control", control ());
#endif      
    }
  
  } // end of ChangeRecord

void DeleteRecord (WebClient & w, EditorData & data)
  {
  w << "<h2>Delete item '" << FixHTML (data.key_) << "' in " << FixHTML (data.name_) << "</h2>\n";
    
  // find base record 
  thing * t = data.findRecord ();
  if (t == NULL)
    throw runtime_error ("There is no '" + data.key_ + "' in '" + data.name_ + "'");
  
   // if the hash has changed, then someone has edited it while we were looking at it
  if (HashThing (*t) != w.postData_ [HASHFIELD])
    throw runtime_error ("That data has been changed by someone else. Please re-locate it and try again.");
  
  if (w.postData_ [CONFIRMDELETE] != "1")
    throw runtime_error ("'Confirm Delete' checkbox was not checked.");
  
  // erase it
  data.erase ();
  
  w << "<p><b>Item deleted.</b></p>";
  
#ifdef COMMIT_CHANGES
    if (data.baseType_ == "rooms")
      WriteRooms ();
    else
      WriteTopThing (CONTROL_FILE, "control", control ());
#endif   
    
  } // end of DeleteRecord

struct fWebShowOneItem
{
  WebClient & w_;
  EditorData & data_;
  const string & keyField_;
  const string & descriptionField_;
  int count_;
  string searchField_;
  char searchFieldType_;

  // we need to pass down the type in the form because of multi-lingual search fields
  //   (eg. fr_short) which won't be in the DTD
  // thus I use fr_short:s  where the s is the field type

  // ctor
  fWebShowOneItem (WebClient & w, EditorData & data, 
                  const string & keyField, const string & descriptionField) 
      : w_ (w), data_ (data), keyField_ (keyField), 
        descriptionField_ (descriptionField), count_ (0), searchFieldType_ ('s')
        {
        pair<string, string> s = GetWord (data_.searchField_, ":");
        searchField_ = s.first;
        if (!s.second.empty ())
          searchFieldType_ =  tolower (s.second [0]);
        }
  
  void operator() (const thing & item)
    {
    // eliminate via regular expression, if wanted
    if (data_.regexp_.OK ())
      {
      string value;
      switch (searchFieldType_)
        {
        case 's': value = item.gets (searchField_);   break;
        case 'n': value = MAKE_STRING (item.geti (searchField_));   break;
        case 'f': value = MAKE_STRING (item.getf (searchField_));   break;
        case 'b': value = MAKE_STRING (item.getb (searchField_));   break;
        default:
          throw runtime_error ("Unknown data type '" + MAKE_STRING (searchFieldType_) + "' in DTD");
        } // end of switch on type
    
      // no match? give up
      if (!data_.regexp_.Execute (value))
        return;
      } // end of having a regexp
    
    if (count_ < MAXRECORDS)
      {
      w_ << "<li>";
      string key;
      if (item.hasi (keyField_))  // rooms may have numeric keys
        key = MAKE_STRING (item.geti (keyField_));
      else
        key = item.gets (keyField_);
      
      string linkArgs = key;
      
      if (!data_.searchField_.empty ())
         linkArgs += "&" + SEARCHFIELD + "=" + data_.searchField_;

      if (!data_.searchValue_.empty ())
         linkArgs += "&" + SEARCHVALUE + "=" + data_.searchValue_;
      
      Hyperlink (w_, data_, linkArgs, key);
      w_ << FixHTML (item.gets (descriptionField_)) << "\n";
      }
    ++count_; 
    }
    
  const int GetCount () { return count_; }

};  // end of fWebShowOneItem

void ListTable (WebClient & w, EditorData & data, 
                const string & key_field, const string & summary_field)
  {
  
  w << "<ul>\n";

  // function object for processing each item
  fWebShowOneItem f (w, data, key_field, summary_field);
  
  // this mucking around is because of the different sorts of data types we 
  // might have
  for (thing * t = data.getNextItem (true);   // first in list/map
      t;                                      // anything?
      t = data.getNextItem ())                // next one
        f (*t);                               // process it

  w << "</ul>\n";

  int count = f.GetCount ();      // function object counted for us
  
  w << "<p>" << count << " item(s)</p>\n";  
  if (count > MAXRECORDS)
    w << "<p>(List limited to " << MAXRECORDS << " items)</p>\n";  
  
  } // end of ListTable

void SearchRecord (WebClient & w, EditorData & data)
  {
  w << "<h2>Search in " << FixHTML (data.name_) << "</h2>\n";
  
  
  if (data.searchValue_.empty ())
    throw runtime_error ("Nothing to search for.");
  
  // compile the search-for regular expression
  data.regexp_.Compile (data.searchValue_);
 
  pair<string, string> s = GetWord (data.searchField_, ":");

  if (s.second.empty ())  
    throw runtime_error ("No data type defined for search field " + data.searchField_);
 
  // now list matching results
  ListTable (w, data, data.keyname_, data.dtd_.gets ("summary_field"));
  
  } // end of SearchRecord

void ListRecords (WebClient & w, EditorData & data)
  {
  
  w << "<h2>List of items in " << data.name_ << "</h2>";

  ListTable (w, data, data.keyname_, data.dtd_.gets ("summary_field"));
  
  } // end of ListRecords

struct editAttribute
{
  WebClient & w_;
  EditorData & data_;
  const string & heading_;
  const char type_;
  int count_;

  // ctor
  editAttribute (WebClient & w, EditorData & data, const string & heading, const char type) 
      : w_ (w), data_ (data), heading_ (heading), type_ (type),  count_ (0) {}
   
  void operator() (const Object & dtditem)
    {
   
    string name = dtditem.gets ("name");    // field name
    string type = dtditem.gets ("type");    // type of data: s, n, f, b
    
    if (type.empty ())
      throw runtime_error ("No data type in DTD for " + name);
    
        // skip wrong types (so we group all numbers together for example)
    if (tolower (type) [0] != type_)
      return;

    // show heading (Eg. "Strings")
    if (++count_ == 1)
      w_ << "<tr bgcolor=\"#99C49B\">" << "<th colspan=\"2\">" 
        << FixHTML (heading_) << "</th>\n";

    // work out how much room to allow
    int size = 20;  // note, defaults are below in the geti calls
    switch (type_)
      {
      case 's': size = dtditem.geti ("max_length", 60); break;  // strings might be long
      case 'n': size = dtditem.geti ("max_length", 10); break;   // numbers, probably only about 10
      case 'f': size = dtditem.geti ("max_length", 10); break;
      } // end of switch on type_
    int maxlength = min (size, 80);   // cut of maximum actually shown at 80 characters
      
    // cannot change key unless we are entering a new record
    bool keyCannotChange = (name == data_.keyname_) &&  
                            !data_.addingAction_;  // field is key
    
    string language;
    string prefix;
    bool multiLanguage = dtditem.getb ("multi_language");
    
    list<pair<string, string> >::const_iterator lang_iter = data_.languages_.begin ();
    
    string fullName = name;   // name with language prefix
    
    // repeat for each supported language - I do it this way because I want to
    // go through the loop at least once
    do
      {
      // get the current language
      if (multiLanguage)
        {
        if (lang_iter != data_.languages_.end ())
          {
          language = lang_iter->first;
          prefix = lang_iter->second;     
          fullName = prefix + name;   // prefix name so success_room becomes fr_success_room
          }// end of another language to go
        }  // end of multi-languages on *this* field
           
      // field label - "fullName" from dtd
      w_ << "<tr><th>" << FixHTML (fullName) << "</th>";
      // field data
      w_ << "<td>";
      string value = w_.postData_ [fullName];    // what they actually typed
      
      // auto-generated keys will be just shown as "(auto)" until they are generated
      if (name == data_.keyname_ &&     // field is key
          data_.addingAction_ &&        // and we are adding a record
          data_.dtd_.getb ("auto_id"))  // and they want auto-generation
          {
          data_.item_.sets (fullName, "(auto)");
          keyCannotChange = true;
          }
     
       /*
      This mucking around here is because I need to display two different things:
      
      a) The incorrect data you actually type, if it is wrong (eg. "abcd" in a numeric field)
      b) The actual correct data from the data record, if it isn't wrong (or on a list or
         such situation where you don't get the chance to make mistakes)
      
      I won't bother with bools, they shouldn't really be wrong, being a checkbox
      */
      
      switch (tolower (type_))
        {
        case 's': if (!data_.fieldHasError (fullName))
                    value = data_.item_.gets (fullName);
                  if (keyCannotChange)
                    {
                    w_ << FixHTML (value);   // primary key can't be edited
                    Hidden (w_, fullName, value);   // have to preserve it however
                    } // end of keyCannotChange
                  else
                    {
                    // combo box, input box, or straight single-line field
                    if (dtditem.getb ("choose_set") && !dtditem.gets ("match_set").empty ())
                      ComboBox (w_, fullName, value, dtditem.gets ("match_set"));
                    else if (dtditem.geti ("lines") > 1)
                      InputBox (w_, fullName, value, dtditem.geti ("lines")); 
                    else
                      InputField (w_, fullName, value, size, maxlength); 
                    } // end of not primary key
                  break;
        case 'n': if (!data_.fieldHasError (fullName))
                    {
                    if (data_.item_.hasi (fullName))   // only if present
                      value = MAKE_STRING (data_.item_.geti (fullName));
                    } // end of not error
                  if (keyCannotChange)
                    {
                    w_ << FixHTML (value);   // primary key can't be edited
                    Hidden (w_, fullName, value);   // have to preserve it however
                    } // end of keyCannotChange
                  else
                    InputField (w_, fullName, value, size, maxlength, true); 
                  break;
        case 'f': if (!data_.fieldHasError (fullName))
                    {
                    if (data_.item_.hasf (fullName))   // only if present
                      value = MAKE_STRING (data_.item_.getf (fullName));
                    } // end of not error
                  if (keyCannotChange)
                    {
                    w_ << FixHTML (value);   // primary key can't be edited
                    Hidden (w_, fullName, value);   // have to preserve it however
                    } // end of keyCannotChange
                  else
                    InputField (w_, fullName, value, size, maxlength, true); 
                  break;
        case 'b': CheckBox   (w_, fullName, data_.item_.getb (fullName)); 
                  break;
        default:
          throw runtime_error ("Unknown data type '" + type + "' in DTD for " + name);
          
        } // end of switch on type_
      
      // show error for this field, if any
      if (data_.fieldHasError (fullName))
        w_ << "<br><b><font color=\"#FF0000\">"
           << data_.errors_ [fullName] << "</font></b>\n";
      
      // put description of field in small letters underneath it
      string describe = dtditem.gets ("describe");
      if (!describe.empty () || (!language.empty () && multiLanguage))
        {
        w_ << "<br><font size=-2>";
        w_ << nl2br (FixHTML (describe));
        if (!language.empty () && multiLanguage )
          w_ << " (Language: " << FixHTML (tocapitals (language)) << ")";
        w_ << "</font>\n";
        }
      
      // show "field required" in dark green
      if (keyCannotChange)
        w_ << " <br><font color=\"#006400\">(Key field - cannot be changed)</font>\n";
      else
        if (dtditem.getb ("required") || name == data_.keyname_)
          w_ << "<br><font color=\"#006400\">(Required field)</font>\n";

      if (dtditem.getb ("regexp"))
        w_ << "<font color=\"#006400\"> (Regular expression)</font>\n";
        
      // link to dtd info for this field (in new browser window)
      w_ << " <font size=-2><a href=\"dtd.php?dtd=" << data_.dtd_.gets ("name") 
         << "&field=" << dtditem.gets ("name") 
         << "\" target=\"_blank\">[ Field info ]</a></font>\n";
      
      w_ << "</td></tr>\n";
      
      // onto next language
     if (lang_iter != data_.languages_.end ())
        ++lang_iter;
    } while (multiLanguage && lang_iter != data_.languages_.end ());
  }   // end of operator() 
};  // end of editAttribute


void ShowRecord (WebClient & w, EditorData & data)
  {
  
  // no action? we are just viewing it
  if (data.action_.empty ())
    w << "<h2>View item '" << FixHTML (data.key_) << "' in " << FixHTML (data.name_) << "</h2>\n";
  
  w << "<p><table>\n";
  for_each_noreturn (ISEQ (data.dtd_.contents_), editAttribute (w, data, "Strings", 's'));  
  for_each_noreturn (ISEQ (data.dtd_.contents_), editAttribute (w, data, "Numbers", 'n'));  
  for_each_noreturn (ISEQ (data.dtd_.contents_), editAttribute (w, data, "Floats", 'f'));  
  for_each_noreturn (ISEQ (data.dtd_.contents_), editAttribute (w, data, "Flags", 'b'));  
  w << "</table></p>\n";

   // if the form had an error, keep the original hash (the one they had when they
  // started editing it)
  if (data.haveError ())
   Hidden (w, HASHFIELD, w.postData_ [HASHFIELD]);  // keep original hash
  else
   Hidden (w, HASHFIELD, HashThing (data.item_));   // hash this version
  
  // we won't find sub-items when doing an add, now will we?
  if (data.addingAction_)
      return;
   
  // while we do owned items, save the itemlist pointer, and the main data name
  ObjectList * savedItemList = data.getItemList ();
  string savedName = data.name_;
  
  if (savedItemList != NULL)
    { // everything except rooms
    
    string subDtdName = data.dtd_.gets ("sub_dtd_name");
    
    // if it has sub-dtd produce a list
    if (!subDtdName.empty ())
      {
      // get the sub dtd (for its description, key and summary fields)
      ObjectList alldtd = control ().items_ ["dtd"];
      // find DTD for this sub-table
      ObjectIter dtditer = FindObject (alldtd, "name", subDtdName);
      if (dtditer == alldtd.end ())
        throw runtime_error ("There is no (sub) DTD for " + subDtdName);
      
      w << "<h3>" <<  FixHTML (dtditer->gets ("description")) << " ...</h3>";
          
       // find the main item
      ObjectIter mainItemIter = FindObject (*savedItemList, "name", data.key_);
      if (mainItemIter == savedItemList->end ())
        throw runtime_error ("Cannot find entry for " + data.key_);
      // make sub tables look like: table:key:sub_dtd_name
      data.name_ = savedName + ":" + data.key_ + ":" + data.dtd_.gets ("sub_dtd_name");   
      data.setItemList (& mainItemIter->contents_);
      ListTable (w, data, dtditer->gets ("key"), dtditer->gets ("summary_field"));
    
     // give them a link to add things, there may not be any there already
      Hyperlink (w, data, 
                "&" + ACTIONFIELD + "=" + NEW_ACTION, 
                "[ Add " + dtditer->gets ("description") + " ]");
      
      } // end of a sub-dtd
     
    } // end of not a room
  else
    {  // room exits??
    // find room
    Room & room = FindRoom (convertString<int> (data.key_, "Bad vnum"));

    w << "<h3>Exits ...</h3>";
      
    data.name_ = savedName + ":" + data.key_ + ":exits";    // make sub tables look like: rooms:1234:exit
    data.setItemList (& room.exits ());
    ListTable (w, data, "short", "depart_self");
    
    // give them a link to add exits, there may not be any there already
    Hyperlink (w, data, 
              "&" + ACTIONFIELD + "=" + NEW_ACTION, 
              "[ Add an exit ]");
    
    w << "<h3>Contents ...</h3>";
  
    data.name_ = savedName + ":" + data.key_ + ":contents";  // make sub tables look like: rooms:1234:contents
    data.setItemList (& room.contents ());
    ListTable (w, data, "id", "short");
  
   // give them a link to add contents
    Hyperlink (w, data, 
              "&" + ACTIONFIELD + "=" + NEW_ACTION, 
              "[ Add contents ]");
     
    w << "<h3>Stock ...</h3>";
    
    data.name_ = savedName + ":" + data.key_ + ":stock";    // make sub tables look like: rooms:1234:stock
    data.setItemList (& room.stock ());
    ListTable (w, data, "id", "short");
    
   // give them a link to add stock
    Hyperlink (w, data, 
              "&" + ACTIONFIELD + "=" + NEW_ACTION, 
              "[ Add stock ]");

    }   // end of sub-fields for rooms
  
  // put item list pointer and name back
  data.setItemList (savedItemList);
  data.name_ = savedName;
 
  } // end of ShowRecord

void NewRecord (WebClient & w, EditorData & data)
  {
  w << "<h2>New item in " << FixHTML (data.name_) << "</h2>\n";
  
  data.key_.clear ();   // clear old key, if any, so we will see blank record
  
  } // end of NewRecord

void DuplicateRecord (WebClient & w, EditorData & data)
  {
  w << "<h2>Duplicate item '" << FixHTML (data.key_) << "' in " << FixHTML (data.name_) << "</h2>\n";
    
  } // end of DuplicateRecord

string getOrPostData (WebClient & w, EditorData & data, const string & item)
  {
  string result;
  
  result = w.postData_ [item];
  // key might be on URI also (eg for hyperlinking on a list)
  if (result.empty () && data.action_.empty ())
    result = w.getData_ [item]; 
  
  return result;
  } // end of getOrPostData

void WebEditor (WebClient & w)
  {
  EditorData data;
    
  // extract out data from form ...
  
  // what to edit
  data.name_ = getOrPostData (w, data, NAMEFIELD);

  // action field might be implied by the name of the button
  if (!w.postData_ ["_add"].empty ())
    data.action_ = ADD_ACTION;
  else if (!w.postData_ ["_search"].empty ())
    data.action_ = SEARCH_ACTION;
  else if (!w.postData_ ["_list"].empty ())
    data.action_ = LIST_ACTION;
  else if (!w.postData_ ["_new"].empty ())
    data.action_ = NEW_ACTION;
  else if (!w.postData_ ["_duplicate"].empty ())
    data.action_ = DUPLICATE_ACTION;
  else if (!w.postData_ ["_delete"].empty ())
    data.action_ = DELETE_ACTION;
  else if (!w.postData_ ["_change"].empty ())
    data.action_ = AMEND_ACTION;    
  else if (!w.postData_ ["_edit"].empty ())
    data.action_ = EDIT_ACTION;    
  
 // action (add, change, delete, search, list)
  if (data.action_.empty ())
    data.action_ = getOrPostData (w, data, ACTIONFIELD);
    
  // record key
  data.key_ = getOrPostData (w, data, KEYFIELD);
  
  // what field to search
  data.searchField_ = getOrPostData (w, data, SEARCHFIELD);

  // what to search for
  data.searchValue_ = getOrPostData (w, data, SEARCHVALUE);
  
  // build list of available languages
  ObjectList & languages = control ().items_ ["languages"];   
  for (ObjectList::const_iterator lang_iter = languages.begin ();
       lang_iter != languages.end ();
        ++lang_iter)
      data.languages_.push_back (
          pair<string, string> (lang_iter->gets ("name"), lang_iter->gets ("prefix")));
  
  // OK, ready to start page up ...
  DoHeader (w, 200, "Edit " + data.name_);
  
//  w << "languages = " << data.languages_.size () << "<br>";
  
#ifdef DEBUG_HTTP  
  WebDebugVars (w);   // show headers, get, post, cookie data
#endif
    
  // the whole thing will be a form ...
  w << "<form METHOD=\"post\" _action=\"" + w.URL_ + "\">";

  // change tables - switch table name, clear action (for simple buttons list)
  if (data.action_ == EDIT_ACTION)
    {
    data.name_ = w.postData_ [NEWNAME];
    data.action_.clear ();    // default to viewing
    data.key_.clear ();       // default to no record
    }
  
  // we need  table name, otherwise we can't do much
  // if no name specified, just let them switch tables
  if (!data.name_.empty ())
    {
    pair<string, string> splitName = GetWord (data.name_, ":");   // split off "rooms:1234:exit"
    
    data.baseType_ = splitName.first;
    
    // get whole dtd
    ObjectList alldtd = control ().items_ ["dtd"];
    // find DTD for this table
    ObjectIter dtditer = FindObject (alldtd, "name", data.baseType_);
    if (dtditer == alldtd.end ())
      throw runtime_error ("There is no DTD for " + data.baseType_);
    // save it for validating, showing, etc.
    data.dtd_ = *dtditer;    // copy dtd
    
    // what the key is
    data.keyname_ = data.dtd_.gets ("key");
    
    // get the appropriate table's list
    ObjectList * itemlist = getObjectList (w, data.baseType_);
    data.setItemList (itemlist); 
    
    if (!(splitName.second).empty ())
      {
      pair<string, string> typeAndKey = GetWord (splitName.second, ":");   // get 1234:exit
      string key = typeAndKey.first;      // eg. (vnum) 1234
      string type = typeAndKey.second;    // exit
      string subDtdName = "!!!";
      
      if (data.baseType_ == "rooms")
        {
        // find room
        Room & room = FindRoom (convertString<int> (key, "Bad vnum"));
        
        if (type == "exits")
          {
          data.setItemList (& room.exits ());
          subDtdName = "exits";
          }
        else if (type == "contents")
          {
          data.setItemList (& room.contents ());
          subDtdName = "objects";
          }
        else if (type == "stock")
          {
          data.setItemList (& room.stock ());
          subDtdName = "objects";
          }
        
        }   // end of sub-fields for rooms
      else  // everything else
        {
        // find base record 
        ObjectIter i = FindObject (*itemlist, data.keyname_, key);
        if (i == itemlist->end ())
          throw runtime_error ("There is no '" + key + "' in '" + data.name_ + "'");
        
        // now switch to DTD for this sub table
        data.setItemList (&(i->contents_));
        subDtdName = type;    // eg. sub_dtd, effects, objects, blah blah
        
        }   // end of sub-fields for other things

      // now switch to DTD for this sub table
      ObjectIter dtditer = FindObject (alldtd, "name", subDtdName);
      if (dtditer == alldtd.end ())
        throw runtime_error ("There is no DTD for sub table " + subDtdName);
      // save it for validating, showing, etc.
      data.dtd_ = *dtditer;    // copy dtd   
 
      // what the key now is
      data.keyname_ = data.dtd_.gets ("key");      
      }   // end of wanting a sub-item (eg. an exit for a room)
          
    // on an amend, delete or search hyperlink, find the record they wanted
    if (data.action_ == AMEND_ACTION ||
        data.action_ == DELETE_ACTION ||
        data.action_ == DUPLICATE_ACTION ||
        (data.action_.empty () && !data.key_.empty ()))    // search results
      FindRecord (w, data);
    
    // validate fields on an add or amend
    if (data.action_ == ADD_ACTION || 
        data.action_ == AMEND_ACTION)
      ValidateInputFields (w, data);
    
    // process various actions
         
    if (data.haveError ())   // skip add/change if validation error
      ShowError (w, tocapitals (data.action_) + " not done");
    else if (data.action_ == ADD_ACTION)
      AddRecord (w, data); 
    else if (data.action_ == AMEND_ACTION)   
      ChangeRecord (w, data); 
    else if (data.action_ == DELETE_ACTION)  
      DeleteRecord (w, data); 
    else if (data.action_ == SEARCH_ACTION)  
      SearchRecord (w, data);  
    else if (data.action_ == LIST_ACTION)    
      ListRecords (w, data);     
    else if (data.action_ == NEW_ACTION)   
      NewRecord (w, data);               
    else if (data.action_ == DUPLICATE_ACTION)   
      DuplicateRecord (w, data); 
    else if (!data.action_.empty ())
      throw runtime_error ("Unknown action: " + data.action_);
    
    // we are adding on a "new" or "duplicate" or failed "add"
    data.addingAction_ = (data.key_.empty () && 
                          (data.action_ == NEW_ACTION ||
                          (data.action_ == ADD_ACTION && data.haveError ()))
                          )
                        ||  
                          ( 
                          !data.key_.empty () &&  
                          data.action_ == DUPLICATE_ACTION 
                          );
                   
    // display the current record
    // when doing a search, or after a delete, don't display the record 
    if ((!data.action_.empty () || !data.key_.empty ())
        && data.action_ != SEARCH_ACTION 
        && data.action_ != LIST_ACTION
        && data.action_ != DELETE_ACTION)  
        ShowRecord (w, data);

    // just seeing a few buttons? tell them what they are currently editing
    if (data.action_.empty () && data.key_.empty ())
      w << "<h2>Editing " << FixHTML (data.name_) << "</h2>\n";
    
    }   // end of table name specified
  
  // show add/change/delete buttons, as appropriate
  
  ShowButtons (w, data);
     
  w << "</form>\n";
    
  } // end of WebEditor

static int LoadWebMap ()
  {
static WebHandlerMap & webmap = getWebHandlerMap ();

  cout << "Loading web handlers in " __FILE__ << endl;
    
  WEBPAGE (WebEditor);        // edit things

  return 0;
  } // end of LoadWebMap

static int loadThemUp = LoadWebMap ();

