//////////////////////////////////////////
//           ajaximrpg 5.00             //
//       AJAX Instant Messenger         //
//  Copyright (c) 2006-2008, 2010-2012  //
//      Do not remove this notice       //
//////////////////////////////////////////

/**
 * A class to manage all character sheets.
 **/
var BasicSheet = {
   sheets: {},   // character sheets
   windows: {},  // character sheet windows
   position: 1,  // position to track cascading windows

   /**
    * Install by creating database tables.
    *
    * @author Daniel Howard
    **/
   install: function(nextHandler) {
      // use ajaximrpg's database setup dialog
      BasicSheet.nextHandler = nextHandler;
      AdminWindows.database('bsheet', 'BasicSheet.database();');
   },

   /**
    * Show database connection box in database installation dialog.
    *
    * @author Daniel Howard
    **/
   database: function() {
      var sqlhost = $('admin_db_host').value;
      var sqluser = $('admin_db_user').value;
      var sqlpass = $('admin_db_password').value;
      $('admin_db_table_box').innerHTML = '';
      var xhConn = new XHConn();
      xhConn.connect(pingTo, "POST", "call=showdatabases&password="+Admin.installPassword+"&sqlhost="+sqlhost+"&sqluser="+sqluser+"&sqlpass="+sqlpass+"&db=off", function(xh) {
         var response = xh.outputText.evalJSON(true);
         if (response.error == '') {
            var options = '';
            for (var d=0; d < response.databases.length; ++d) {
              options += '<option value="' + response.databases[d] + '">' + response.databases[d] + '</option>';
            }
            $('admin_db_table_box').innerHTML = 
               '<form>' + Languages.get('database') + ': <select id="admin_db_name">' + options + '</select>&nbsp;&nbsp;&nbsp;&nbsp;' +
               ButtonCtl.create(Languages.get('create'), 'AdminWindows.createDatabase();', 'admin_db_create') +
               ButtonCtl.create(Languages.get('drop'), 'AdminWindows.dropDatabase();', 'admin_db_drop') +
               ButtonCtl.create(Languages.get('view'), 'AdminWindows.viewDatabase();', 'admin_db_view') +
               '</form>' +
               '<div id="admin_db_prefix_box">' +
               'Table Prefix (e.g. bsheet_sheets)' + ': <input type="text" id="admin_db_prefix" name="admin_db_prefix" value="bsheet_" style="width:70px;" onkeypress="handleInput(event, function() { BasicSheet.installIt(); })" />' +
               '</div>' +
               '<div id="admin_db_configure_box">' +
               ButtonCtl.create(Languages.get('install'), 'BasicSheet.configure();') +
               '</div>';

            $('admin_db_prefix_box').setStyle({position: 'absolute', top: '35px', left: '155px'});
            $('admin_db_configure_box').setStyle({position: 'absolute', top: '95px', left: '225px'});
            $('admin_db_error_msg').innerHTML = '';
         } else {
            $('admin_db_error_msg').innerHTML = response.error;
            new Effect.Shake('admin_db');
         }
      });
   },

   /**
    * Tell the server to perform installation with certain values.
    *
    * @author Daniel Howard
    */
   configure: function() {
      var createdb = true;
      var writephp = true;
      var sqlhost = $('admin_db_host').value;
      var sqluser = $('admin_db_user').value;
      var sqlpass = $('admin_db_password').value;
      var sqldb = $('admin_db_name').value;
      var sqlprefix = $('admin_db_prefix').value;
      var maxBuddyIconSize = '10';

      var xhConn = new XHConn();
      xhConn.connect(pingTo, "POST", "plugin=BasicSheet&call=install&password="+Admin.installPassword+"&newpassword="+Admin.adminPassword+"&createdb="+createdb+"&writephp="+writephp+"&sqlhost="+sqlhost+"&sqluser="+sqluser+"&sqlpass="+sqlpass+"&sqldb="+sqldb+"&sqlprefix="+sqlprefix+"&db=off", function(xh) {
         var response = xh.outputText.evalJSON(true);
         if (response.error == '') {
            Windows.close('admin_db');
            BasicSheet.nextHandler();
         } else {
            var errmsg = response.info[response.info.length - 1];
            errmsg = errmsg.substring(errmsg.indexOf('('));
            $('admin_db_error_msg').innerHTML = errmsg;
            $('admin_db_error_msg').show();
            new Effect.Shake('admin_db');
         }
      });
   },

   /**
    * Create a character sheet in a window.
    *
    * @arguments
    *   key - the index into the sheets array (eg viewkey)
    *   url - bsheet character view or edit link
    *
    * @author Daniel Howard
    **/
   create: function(key, url) {
      // create the window with the given sheet
      var load = function(sheet) {
         var win = null;
         if (BasicSheet.windows.hasOwnProperty(sheet.viewkey)) {
            win = BasicSheet.windows[sheet.viewkey];
         }
         if (win == null) {
            // find a good location for the window
            var winLeft = (2 + (BasicSheet.position * 25)) + 'px';
            var winTop  = (2 + (BasicSheet.position * 25)) + 'px';
            var winWidth = 450;
            var winHeight = 300;

            if (BasicSheet.position++ == 2) {
               BasicSheet.position = 0;
            }

            // create the actual window
            var winId = randomString(32)+'_bsheet';
            var winTitle = '<span id="bsheet_title_'+sheet.viewkey+'">'+sheet.name+'</span>';

            win = new BasicSheetWindow({id: winId, className: "dialog", title: winTitle, width: winWidth, height: winHeight, top: winTop, left: winLeft, resizable: true, draggable: true, detachable: false, minWidth: 450, minHeight: 150, showEffectOptions: {duration: 0}, hideEffectOptions: {duration: 0}});
            win.setConstraint(true, {left: 0, right: 0, top: 0, bottom: 0});
            win.sheetKey = sheet.viewkey;
            win.url = url;
            win.template = null;
            win.optionsDialog = false;

            // get the template
            win.applyTemplate(sheet.template, function() {
               // show the window
               win.show();
               win.toFront();
               Windows.focusedWindow = win;
               BasicSheet.windows[win.sheetKey] = win;
            });
         } else {
            // bring the existing window to the front
            win.show();
            win.toFront();
            Windows.focusedWindow = win;
         }
      };

      if (key == null) {
         // load the bsheet from the bsheet server
         var urlparts = url.split('?', 2);
         BasicSheet.connect(urlparts[0], 'POST', urlparts[1]+'&type=json', [], function(xh) {
            if (xh != null) {
               if (xh.responseText.isJSON()) {
                  var sheet = xh.responseText.evalJSON(true);
                  BasicSheet.sheets[sheet.viewkey] = sheet;
                  load(sheet);
                  return true;
               }
            } else {
               Dialog.alert('Could not load character sheet');
            }
         });
      } else {
         load(BasicSheet.sheets[key]);
      }
   },

   /**
    * Connect to bsheet server using XHConn; try direct access
    * first, then try curl-forwarding.
    *
    * @arguments
    *   url - the URL to connect to
    *   method - GET or POST
    *   data - the data to send in sprintf format
    *   args - the sprintf arguments for sprintf format
    *   fnDone - the connection function
    *
    * @author Daniel Howard
    **/
   connect: function(url, method, data, args, fnDone) {
      var a, arg, encodedData;
      encodedData = data;
      for (a=0; a < args.length; ++a) {
         arg = encodeURIComponent(args[a]);
         encodedData = encodedData.replace(new RegExp('\\{'+a+'\\}', 'gi'), arg);
      }
      var xhConn = new XHConn();
      xhConn.connect(url, method, encodedData, function(xh) {
         if (!fnDone(xh)) {
            encodedData = data;
            for (a=0; a < args.length; ++a) {
               arg = encodeURIComponent(args[a]);
               arg = arg.replace(new RegExp('%', 'g'), '%25');
               encodedData = encodedData.replace(new RegExp('\\{'+a+'\\}', 'gi'), arg);
            }
            encodedData = 'url='+encodeURIComponent(url+'?'+encodedData);
            var xhConn = new XHConn();
            xhConn.connect(hrefPath+'plugins/ajaximrpg/bsheet/forward.php', method, encodedData, function(xh) {
               if (!fnDone(xh)) {
                  fnDone(null);
               }
            });
         }
      });
   },

   /**
    * Show the bsheet server dialog in a chat room.
    *
    * @arguments
    *   room - the room to add the dialog to
    *   html - the HTML to put in the dialog
    *
    * @author Daniel Howard
    **/
   openDialog: function(room, html) {
      var win = Chatroom.windows[room];
      if (!win.plugins.ajaximrpg.bsheet.animating) {
         win.plugins.ajaximrpg.bsheet.animating = true;
         // user might resize chatroom while dialog is open
         win.plugins.ajaximrpg.bsheet.handleResize = win.handleResize;
         win.handleResize = function(eventName, detached) {
            var winId = this.getId();
            $(winId+'_userlist').setStyle({height: ((this.getSize().height/2)-11)+'px'});
            $(winId+'_doclist').setStyle({top: ((this.getSize().height/2)+26)+'px', height: ((this.getSize().height/2)-10)+'px'});
            $(winId+'_rcvd').setStyle({width: (this.getSize().width-170)+'px', height: (this.getSize().height-233)+'px'});
            $(winId+'_bsheet_dialog').setStyle({top: (this.getSize().height-232)+'px', width: (this.getSize().width-170)+'px'});
            $(winId+'_insertEmoticon').setStyle({top: (this.getSize().height-65)+'px'});
            $(winId+'_sendBox').setStyle({top: (this.getSize().height-45)+'px', width: (this.getSize().width-175)+'px'});
         };
         var winId = win.getId();
         var pos = $(winId+'_rcvd').positionedOffset();
         var left = pos[0];
         var top = pos[1];
         var height = $(winId+'_rcvd').getHeight() - 4;
         var node = document.createElement('div');
         node = $(node);
         node.id = winId+'_bsheet_dialog';
         node.name = winId+'_bsheet_dialog';
         node.className = 'rcvdMessages';
         node.setStyle({overflow: 'hidden', display: 'none', left: (left+5)+'px', top: (top+$(winId+'_rcvd').getHeight()-28)+'px', height: '100px', width: ($(winId+'_rcvd').getWidth()-4)+'px'});
         html = '<div id="'+winId+'_bsheet_contents" name="'+winId+'_bsheet_contents">'+html+'</div>';
         node.innerHTML = html;
         $(winId+'_insertEmoticon').parentNode.insertBefore(node, $(winId+'_insertEmoticon'));
         new Effect.Tween($(winId+'_rcvd'), 0, 130, { duration: 0.5, afterFinish: function() {
            win.plugins.ajaximrpg.bsheet.animating = false;
            if ($(winId+'_bsheet_url')) {
               $(winId+'_bsheet_url').select();
               $(winId+'_bsheet_url').focus();
            }
         } }, function(p) {
            $(winId+'_rcvd').setStyle({height: (height-p)+'px'});
            if (p > 10) {
               node.setStyle({display: '', top: (top+$(winId+'_rcvd').getHeight()-31)+'px', height: (p-10)+'px'});
            }
         });
         if ($(winId+'_bsheet_url')) {
            setTimeout("$('"+winId+"_bsheet_url').select();$('"+winId+"_bsheet_url').focus();", 250);
         }
      }
   },

   /**
    * Hide the bsheet server dialog in a chat room.
    *
    * @arguments
    *   room - the room that is showing the dialog
    *   fade - fade before rolling up
    *
    * @author Daniel Howard
    **/
   closeDialog: function(room, fade) {
      var win = Chatroom.windows[room];
      if (!win.plugins.ajaximrpg.bsheet.animating) {
         win.plugins.ajaximrpg.bsheet.animating = true;
         var winId = win.getId();
         var top = $(winId+'_rcvd').positionedOffset()[1];
         var height = $(winId+'_rcvd').getHeight() - 4;
         var rollup = function() {
            new Effect.Tween($(winId+'_rcvd'), 0, 130, { duration: 0.5, afterFinish: function() {
               win.plugins.ajaximrpg.bsheet.animating = false;
            } }, function(p) {
               $(winId+'_rcvd').setStyle({height: (height+p)+'px'});
               if (p < 120) {
                  $(winId+'_bsheet_dialog').setStyle({display: '', top: (top+$(winId+'_rcvd').getHeight()-31)+'px', height: (120-p)+'px'});
               } else if ($(winId+'_bsheet_dialog')) {
                  $(winId+'_bsheet_dialog').remove();
                  win.handleResize = win.plugins.ajaximrpg.bsheet.handleResize;
               }
            });
         };
         if ($(winId+'_bsheet_dialog')) {
            if (fade) {
               new Effect.Fade($(winId+'_bsheet_contents'), {
                  duration: 0.5,
                  afterFinish: function() {
                     rollup();
                  }
               });
            } else {
               rollup();
            }
         }
      }
   },

   /**
    * Toggle (show/hide) bsheet server login dialog.
    *
    * @arguments
    *   room - the room that will use the login session
    *
    * @author Daniel Howard
    **/
   login: function(room) {
      var win = Chatroom.windows[room];
      var winId = win.getId();
      if ($(winId+'_bsheet_dialog')) {
         BasicSheet.closeDialog(room, false);
         return;
      }
      var html = '';
      html += '<div style="float: right;"><input class="bsheet_close" style="background: url(themes/'+theme+'/window/close.png) no-repeat left; height:17px; width:17px;" type="button" value="" onclick="BasicSheet.closeDialog(\''+room+'\', false);"></input></div>\r\n';
      html += '<div style="margin: 0 auto;"><div style="text-align: center;">Login to bsheet server</div></div>\r\n';
      html += '<table width="99%">\r\n';
      html += '<tr>\r\n';
      html += '<td style="width: 1px;" align="left"><span style="white-space: nowrap;">Server URL</span></td>\r\n';
      html += '<td align="left" style="width: 99%; text-align: center;"><input style="width: 99%;" type="text" id="'+winId+'_bsheet_url" name="'+winId+'_bsheet_url" value="'+win.plugins.ajaximrpg.bsheet.url+'" onkeypress="handleInput(event, function() { BasicSheet.loggedIn(\''+room+'\'); })" /></td>\r\n';
      html += '</tr>\r\n';
      html += '<tr>\r\n';
      html += '<td style="width: 1px; white-space: nowrap;" align="left">'+Languages.get('username')+'</td>\r\n';
      html += '<td align="left" style="width: 99%; text-align: center;"><input style="width: 99%;" type="text" id="'+winId+'_bsheet_player" name="'+winId+'_bsheet_player" value="'+win.plugins.ajaximrpg.bsheet.player+'" onkeypress="handleInput(event, function() { BasicSheet.loggedIn(\''+room+'\'); })" /></td>\r\n';
      html += '</tr>\r\n';
      html += '<tr>\r\n';
      html += '<td style="width: 1px; white-space: nowrap;" align="left">'+Languages.get('password')+'</td>\r\n';
      html += '<td align="left" style="width: 99%; text-align: center;"><input style="width: 99%;" type="password" id="'+winId+'_bsheet_password" name="'+winId+'_bsheet_password" value="'+win.plugins.ajaximrpg.bsheet.password+'" onkeypress="handleInput(event, function() { BasicSheet.loggedIn(\''+room+'\'); })" /></td>\r\n';
      html += '</tr>\r\n';
      html += '<tr>\r\n';
      html += '<td valign="middle" colspan="2" align="right" style="width: 99%;">';
      html += '<span id="'+winId+'_bsheet_error_msg" style="color: #ff0000;">&nbsp;</span>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;';
      html += '<input type="checkbox" id="'+winId+'_bsheet_share" name="'+winId+'_bsheet_share" value="share" checked="yes" />Share now&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;';
      html += '<input type="button" id="'+winId+'_bsheet_login" name="'+winId+'_bsheet_login" value="'+Languages.get('signOn')+'" onclick="BasicSheet.loggedIn(\''+room+'\'); return false;" onkeypress="handleInput(event, function() { BasicSheet.loggedIn(\''+room+'\'); })" />';
      html += '<input type="button" id="'+winId+'_bsheet_register" name="'+winId+'_bsheet_register" value="Register" onclick="BasicSheet.register(\''+room+'\'); return false;" onkeypress="handleInput(event, function() { BasicSheet.register(\''+room+'\'); })" />';
      html += '</td>\r\n';
      html += '</tr>\r\n';
      html += '</table>\r\n';
      BasicSheet.openDialog(room, html);
   },
   
   /**
    * Show bsheet server registration web page.
    *
    * @arguments
    *   room - the room used for the login session
    *
    * @author Daniel Howard
    **/
   register: function(room) {
      var win = Chatroom.windows[room];
      var winId = win.getId();
      win.plugins.ajaximrpg.bsheet.url = $(winId+'_bsheet_url').value;
      win.plugins.ajaximrpg.bsheet.player = $(winId+'_bsheet_player').value;
      win.plugins.ajaximrpg.bsheet.password = $(winId+'_bsheet_password').value;
      window.open(win.plugins.ajaximrpg.bsheet.url);
   },
   
   /**
    * Log in and save the bsheet server login session.
    *
    * @arguments
    *   room - the room used for the login session
    *
    * @author Daniel Howard
    **/
   loggedIn: function(room) {
      var win = Chatroom.windows[room];
      var winId = win.getId();
      $(winId+'_bsheet_error_msg').innerHTML = '&nbsp;';
      win.plugins.ajaximrpg.bsheet.url = $(winId+'_bsheet_url').value;
      win.plugins.ajaximrpg.bsheet.player = $(winId+'_bsheet_player').value;
      win.plugins.ajaximrpg.bsheet.password = $(winId+'_bsheet_password').value;
      var url = win.plugins.ajaximrpg.bsheet.url;
      var player = win.plugins.ajaximrpg.bsheet.player;
      var password = win.plugins.ajaximrpg.bsheet.password;
      BasicSheet.connect(url, 'POST', 'player={0}&password={1}&action=User&type=json', [player, password], function(xh) {
         if (xh != null) {
            if (xh.responseText.isJSON()) {
               var sheets = xh.responseText.evalJSON(true);
               if (sheets.error == '') {
                  var tree = $(Chatroom.windows[room].getId() + '_tree').tree;
                  tree.tree[tree.tree.length-1].remove(0);
                  tree.tree[tree.tree.length-1].insert(-1, '<a href="#" onclick="BasicSheet.share(\''+room+'\', true); return false;">Share</a>');
                  tree.tree[tree.tree.length-1].insert(-1, '<a href="#" onclick="BasicSheet.logout(\''+room+'\'); return false;">Logout</a>');
                  if ($(winId+'_bsheet_share').checked) {
                     BasicSheet.share(room, false);
                  } else {
                     BasicSheet.closeDialog(room, true);
                  }
               } else {
                  $(winId+'_bsheet_error_msg').innerHTML = Languages.get(sheets.error);
               }
               return true;
            }
         } else {
            $(winId+'_bsheet_error_msg').innerHTML = Languages.get('couldNotConnect');
         }
      });
   },
   
   /**
    * Toggle (show/hide) bsheet server share dialog.
    *
    * @arguments
    *   room - the room that will use the login session
    *   closeIfOpen - close or replace if dialog already open
    *
    * @author Daniel Howard
    **/
   share: function(room, closeIfOpen) {
      var func = function(win, afterFinish) {
         var winId = win.getId();
         var url = win.plugins.ajaximrpg.bsheet.url;
         var player = win.plugins.ajaximrpg.bsheet.player;
         var password = win.plugins.ajaximrpg.bsheet.password;
         BasicSheet.connect(url, 'POST', 'player={0}&password={1}&action=List&type=json', [player, password], function(xh) {
            if (xh != null) {
               if (xh.responseText.isJSON()) {
                  var sheets = xh.responseText.evalJSON(true);
                  var html = '';
                  html += '<div style="float: right; "><input class="bsheet_close" style="background: url(themes/'+theme+'/window/close.png) no-repeat left; height:17px; width:17px;" type="button" value="" onclick="BasicSheet.closeDialog(\''+room+'\', false);"></input></div>\r\n';
                  html += '<div style="margin: 0 auto;"><div style="text-align: center;">Share a character sheet</div></div>\r\n';
                  html += '<div style="height: 99px; overflow: auto; clear: right;">';
                  html += '<table width="98%" style="text-align:center;" class="listNotSelected">\r\n';
                  html += '<thead><tr style="cursor:pointer;">';
                  html += '<th>Character Name</th>';
                  html += '<th>Campaign</th>';
                  html += '<th>Race</th>';
                  html += '<th>Class</th>';
                  html += '<th>Level</th>';
                  html += '<th>AC</th>';
                  html += '<th>HP</th>';
                  html += '<th>Key</th>';
                  html += '</tr></thead>';
                  html += '<tbody>';
                  for (var s=0; s < sheets.length; ++s) {
                     html += '<tr style="cursor:pointer;" onmouseover="BasicSheet.shareHover(this);" onmouseout="BasicSheet.shareMouseOut(this);" onclick="BasicSheet.message(user, \''+room+'\', \'/share bsheet '+url+'?p=\'+this.getElementsByTagName(\'td\')[7].innerHTML, function(){});">';
                     html += '<td>'+sheets[s].name+'</td>';
                     html += '<td>'+sheets[s].campaign+'</td>';
                     html += '<td>'+sheets[s].race+'</td>';
                     html += '<td>'+sheets[s].clazz+'</td>';
                     html += '<td>'+sheets[s].level+'</td>';
                     html += '<td>'+sheets[s].ac+'</td>';
                     html += '<td>'+sheets[s].hp+'&#47;'+sheets[s].hptotal+'</td>';
                     html += '<td>'+sheets[s].editkey+'</td>';
                     html += '</tr>';
                  }
                  html += '</tbody>';
                  html += '</table>';
                  html += '</div>';
                  $(winId+'_bsheet_contents').innerHTML = html;
                  afterFinish();
                  return true;
               }
            }
         });
      };
      var win = Chatroom.windows[room];
      var winId = win.getId();
      if ($(winId+'_bsheet_dialog')) {
         if (closeIfOpen) {
            BasicSheet.closeDialog(room, false);
         } else {
            new Effect.Fade($(winId+'_bsheet_contents'), {
               duration: 0.5,
               afterFinish: function() {
                  var html = '';
                  html = '<center>Loading character sheets...</center>';
                  $(winId+'_bsheet_contents').innerHTML = html;
                  $(winId+'_bsheet_contents').style.display = '';
                  func(win, function() {
                     new Effect.Appear($(winId+'_bsheet_contents'), {
                        duration: 0.5
                     });
                  });
               }
            });
         }
      } else {
         var html = '';
         html += '<center>Loading character sheets...</center><br />\r\n';
         BasicSheet.openDialog(room, html);
         func(win, function() {
         });
      }
   },

   /**
    * Highlight a row in the share list.
    *
    * @arguments
    *   el - the table row element
    *
    * @author Daniel Howard
    **/
   shareHover: function(el) {
      Element.addClassName(el, 'listHover').removeClassName('listSelected').removeClassName('listNotSelected');
   },
   
   /**
    * Remove the highlight from a row in the share list.
    *
    * @arguments
    *   el - the table row element
    *
    * @author Daniel Howard
    **/
   shareMouseOut: function(el) {
      Element.addClassName(el, 'listNotSelected').removeClassName('listSelected').removeClassName('listHover');
   },
   
   /**
    * Log off the bsheet server.
    *
    * @arguments
    *   room - the room used for the login session
    *
    * @author Daniel Howard
    **/
   logout: function(room) {
      var winId = Chatroom.windows[room].getId();
      if ($(winId+'_bsheet_dialog')) {
         BasicSheet.closeDialog(room, false);
      }
      var tree = $(winId+'_tree').tree;
      tree = tree.tree[tree.tree.length-1];
      tree.remove(0);
      tree.remove(0);
      tree.insert(-1, '<a href="#" title="Login to bsheet server" onclick="BasicSheet.login(\''+room+'\'); return false;">Login</a>');
   },
   
   /**
    * Handle any 'bsheet' messages that enter the message stream.
    * They might come from this user or from a different user.
    *
    * @arguments
    *   sender - the user that sent this message
    *   room - the room that the message was sent to
    *   message - the message/command that was sent
    *   nextHandler - the next plugin that might handle the message
    *
    * @author Daniel Howard
    **/
   message: function(sender, room, message, nextHandler) {
//      if (message.indexOf('/printsheets') == 0) {
//         var sheets = makeArray(BasicSheet.sheets);
//         for (var s=0; s < sheets.length; s++) {
//            alert(Object.toJSON(sheets[s]));
//         }
//         return true;
//      }
      if (user == sender) {
         // don't insist on '/share bsheet'; let URL only work
         var url = message;
         if (url.indexOf('/share bsheet ') == 0) {
            url = url.substring(14);
         }
         if ((url.indexOf('http://') == 0) || (url.indexOf('https://') == 0)) {
            var urlparts = url.split('?', 2);
            if (urlparts.length == 2) {
               // load the bsheet from the bsheet server
               BasicSheet.connect(urlparts[0], 'POST', urlparts[1]+'&type=json', [], function(xh) {
                  if (xh != null) {
                     if (xh.responseText.isJSON()) {
                        var sheet = xh.responseText.evalJSON(true);
                        if (sheet.hasOwnProperty('viewkey')) {
                           var existing = meetsCondition(makeArray(BasicSheet.sheets), function(sheets, i) {
                              return (sheets[i].viewkey == sheet.viewkey);
                           });
                           if (existing.length == 0) {
                              BasicSheet.sheets[sheet.viewkey] = sheet;
                              if (sheet.hasOwnProperty('editkey')) {
                                 url = url.replace('p='+sheet.editkey, 'p='+sheet.viewkey);
                              }
      
                              var encoded = '/share bsheet '+url;
                              encoded = encoded.replace('+', '%2B');
                              encoded = encodeURIComponent(encoded);
      
                              // share the bsheet to other users
                              var xhConn = new XHConn();
                              xhConn.connect(pingTo, 'POST', "call=share&to="+encodeURIComponent(room)+"&message="+encoded, function(xh) {
                                 if (xh.outputText.substring(0, 7) == 'shared:') {
                                    var link = xh.outputText.substring(21);
                                    if (sheet.hasOwnProperty('editkey')) {
                                       link = link.replace('p='+sheet.viewkey, 'p='+sheet.editkey);
                                    }
                                    var html = '<span id="bsheet_treeitem_'+sheet.viewkey+'"><a href="#" title="'+sheet.name+' character sheet (bsheet plugin)" onclick="BasicSheet.create(\''+sheet.viewkey+'\', \''+link+'\'); return false;">'+sheet.name+'</a></span>';
                                    $(Chatroom.windows[room].getId() + '_tree').tree.tree[0].insert(-1, html);
                                 } else {
                                    Dialog.alert('Could not share bsheet');
                                 }
                              });
                           } else {
                              Dialog.alert('This bsheet is already in the tree.');
                           }
                        } else {
                           nextHandler();
                        }
                        return true;
                     }
                  } else {
                     nextHandler();
                  }
               });
            } else {
               nextHandler();
            }
         } else {
            nextHandler();
         }
      } else {
         if (message.indexOf('/share bsheet ') == 0) {
            // load the bsheet from the bsheet server
            var parts = message.split(' ');
            var url = parts[2];
            var urlparts = url.split('?', 2);
            BasicSheet.connect(urlparts[0], 'POST', urlparts[1]+'&type=json', [], function(xh) {
               if (xh != null) {
                  if (xh.responseText.isJSON()) {
               		// add the bsheet to the user's shared documents
                     var sheet = xh.responseText.evalJSON(true);
                     if (sheet.hasOwnProperty('viewkey')) {
                        var html = '<span id="bsheet_treeitem_'+sheet.viewkey+'"><a href="#" title="'+sheet.name+' character sheet (bsheet plugin)" onclick="BasicSheet.create(\''+sheet.viewkey+'\', \''+url+'\'); return false;">'+sheet.name+'</a></span>';
                        BasicSheet.sheets[sheet.viewkey] = sheet;
                        $(Chatroom.windows[room].getId() + '_tree').tree.tree[0].insert(-1, html);
                     } else {
                        nextHandler();
                     }
                     return true;
                  }
               } else {
                  nextHandler();
               }
            });
         } else {
            nextHandler();
         }
      }
   },
   
   /**
    * Add the 'bsheet' help to the help message.
    *
    * @arguments
    *   alias - the user alias (usually to test for GM)
    *
    * @author Daniel Howard
    **/
   getHelpMessage: function(alias) {
      return '/share bsheet <i>editurl</i> ... share a character sheet<br />';
   },
   
   /**
    * Receive a 'sync' event and update the 'sheets' array.
    *
    * @arguments
    *   syncEvent - an event describing what was changed
    *
    * @author Daniel Howard
    **/
   handleSync: function(syncEvent) {
      if (syncEvent.target.type == 'bsheet') {
         if ((typeof syncEvent.input) != 'undefined') {
            BasicSheet.sheets[syncEvent.target.sheet][syncEvent.input.name] = syncEvent.input.value;
            if (syncEvent.input.name == 'name') {
               var sheet = BasicSheet.sheets[syncEvent.target.sheet];
               $('bsheet_treeitem_'+sheet.viewkey).innerHTML = modifyHtmlTag($('bsheet_treeitem_'+sheet.viewkey).innerHTML, new RegExp('<a[^>]*>', 'ig'), function(tag) {
                  var name = sheet.name;
                  name = name.replace(new RegExp('\'', 'g'), '&#39;');
                  name = name.replace(new RegExp('"', 'g'), '&#34;');
                  tag.a.title = name+' character sheet (bsheet plugin)';
                  tag.body = name;
                  return tag;
               });
               if ($('bsheet_title_'+sheet.viewkey)) {
                  $('bsheet_title_'+sheet.viewkey).innerHTML = sheet.name;
               }
            }
         } else if ((typeof syncEvent.table) != 'undefined') {
            if (syncEvent.table.type == 'change') {
               BasicSheet.sheets[syncEvent.target.sheet][syncEvent.table.name].items[syncEvent.table.row][syncEvent.table.col] = syncEvent.table.value;
            } else if (syncEvent.table.type == 'insert') {
               BasicSheet.sheets[syncEvent.target.sheet][syncEvent.table.name].items.splice(syncEvent.table.row, 0, syncEvent.table.values);
            } else if (syncEvent.table.type == 'delete') {
               BasicSheet.sheets[syncEvent.target.sheet][syncEvent.table.name].items.splice(syncEvent.table.row, 1);
            }
         }
         if ((typeof BasicSheet.windows[syncEvent.target.sheet]) != 'undefined') {
            BasicSheet.windows[syncEvent.target.sheet].handleSync(syncEvent);
         }
      }
   },
   
   /**
    * Create extra tree nodes in all chatrooms.
    *
    * @arguments
    *   room - the chat room that was just created
    *
    * @author Daniel Howard
    **/
   onCreateChatroom: function(room) {
      // attach bsheet default variables to the room window
      var win = Chatroom.windows[room];
      if ((typeof win.plugins) == 'undefined') {
         win.plugins = {};
      }
      if ((typeof win.plugins.ajaximrpg) == 'undefined') {
         win.plugins.ajaximrpg = {};
      }
      if ((typeof win.plugins.ajaximrpg.bsheet) == 'undefined') {
         win.plugins.ajaximrpg.bsheet = {
            'url': hrefPath+'plugins/ajaximrpg/bsheet/index.php',
            'player': 'nobody@ajaximrpg.com',
            'password': '',
            'animating': false,
            'handleResize': function(){}
         };
      }
      // add bsheet tree items to the document tree
      var winId = win.getId();
      var tree = $(winId+'_tree').tree;
      tree.insert(-1, 'bsheet');
      tree.tree[tree.tree.length-1].insert(-1, '<a href="#" title="Login to bsheet server" onclick="BasicSheet.login(\''+room+'\'); return false;">Login</a>');
   },

   /**
    * Handle resize events.
    *
    * @arguments
    *   eventName - the event causing the resize
    *   win - the window that is being resized
    *   detached - if the window is detached
    *
    * @author Daniel Howard
    **/
   onResize: function(eventName, win, detached) {
      for (var name in BasicSheet.windows) {
         if (((typeof BasicSheet.windows[name].getId) != 'undefined') && ((typeof $(BasicSheet.windows[name].getId())) != 'undefined')) {
            if (BasicSheet.windows[name].getId() == win.getId()) {
               BasicSheet.windows[name].onResize(eventName, detached);
            }
         }
      }
   },

   /**
    * Handle close events.
    *
    * @arguments
    *   eventName - the event causing the resize
    *   win - the window that is being resized
    *
    * @author Daniel Howard
    **/
   onClose: function(eventName, win) {
      for (var name in BasicSheet.windows) {
         if (((typeof BasicSheet.windows[name].getId) != 'undefined') && ((typeof $(BasicSheet.windows[name].getId())) != 'undefined')) {
            if (BasicSheet.windows[name].getId() == win.getId()) {
               BasicSheet.windows[name].onClose(eventName);
            }
         }
      }
   }
   
};

/**
 * A single character sheet window.
 **/
var BasicSheetWindow = Class.create();
Object.extend(BasicSheetWindow.prototype, Window.prototype);
Object.extend(BasicSheetWindow.prototype, {
   /**
    * Return true if this window is editable.
    *
    * A 25-digit 'edit key' allows reads and edits; a 20-digit
    * 'view key' only allows reads.
    *
    * @return True if editable; false if read only.
    *
    * @author Daniel Howard
    **/
   canEdit: function() {
      var p = '';
      var urlparts = this.url.split('?', 2);
      var queryparts = urlparts[1].split('&');
      for (var q=0; q < queryparts.length; ++q) {
         var valueparts = queryparts[q].split('=');
         if (valueparts[0] == 'p') {
            p = valueparts[1];
            break;
         }
      }
      return p.length == 25;
   },

   /**
    * Layout the character sheet window according to the
    * given template.
    *
    * @arguments
    *   template - the template (server folder) to use
    *   func - the function to call when done
    *
    * @author Daniel Howard
    **/
   applyTemplate: function(template, func) {
      var win = this;
      var winId = this.getId();
      var remove = (win.template == null)? '': '&remove='+win.template;

      // get template info
      var xhConn = new XHConn();
      xhConn.connect('plugins/ajaximrpg/bsheet/template.php', 'GET', 'template='+template+remove, function(xh) {
         var contents = xh.responseText;
         var htmlfile = null;
         var cssid = null;
         var files = contents.split('\n');
         for (var f=0; f < files.length; ++f) {
            if (endsWith(files[f], win.canEdit()? '/edit.html': '/view.html')) {
               htmlfile = files[f];
            }
            if (endsWith(files[f], '.css')) {
               var css = document.createElement('link');
               css.setAttribute('rel', 'stylesheet');
               css.setAttribute('type', 'text/css');
               css.setAttribute('href', files[f]+'?rnd='+parseInt(Math.random()*999999));
               document.getElementsByTagName('head')[0].appendChild(css);
               cssid = files[++f];
            }
         }
         // load the template HTML
         var noCache = 'rnd='+parseInt(Math.random()*999999);
         var xhConn = new XHConn();
         xhConn.connect(htmlfile, 'GET', noCache, function(xh) {
            var html = xh.responseText;
            win.template = template;
            win.cssid = cssid;
            win.applyTemplateHtml(html, func);
         });
      });
   },

   /**
    * Layout the character sheet window according to the
    * given HTML.
    *
    * @arguments
    *   html - the HTML to use
    *   func - the function to call when done
    *
    * @author Daniel Howard
    **/
   applyTemplateHtml: function(html, func) {
      var win = this;
      var winId = this.getId();
      var sheet = BasicSheet.sheets[win.sheetKey];
      var path = 'plugins/ajaximrpg/bsheet/templates/'+win.template;

      // convert to Windows CRLF for download
      html = html.replace(new RegExp('\\r\\n', 'g'), '\n');
      html = html.replace(new RegExp('\\n', 'g'), '\r\n');
      win.templateHtml = html;

      // get contents of <body> tag
      modifyHtmlTag(html, new RegExp('<body[^>]*>', 'ig'), function(tag) {
         if (tag.body != null) {
            html = tag.body;
         }
      });

      var ie6style = '';
      if (Prototype.Browser.IE && parseInt(navigator.userAgent.substring(navigator.userAgent.indexOf("MSIE")+5)) == 6) {
    	 ie6style = ' bsheet_ie6vscroll';
      }

      html = '<div id="'+winId+'_contents" class="bsheet_container '+win.cssid+ie6style+'">'+html+'</div>';
      html += '<div style="text-align: center;" id="'+winId+'_options">';
      html += '<a href="#" onclick="BasicSheet.windows[\''+win.sheetKey+'\'].showOptions(); return false;">Options</a>';
      var uphtml = '';
      uphtml += '<input type="file" name="file" /><input type="submit" value="Apply" />';
      uphtml += 'Download: ';
      uphtml += '<input type="button" value=".tar" onclick="BasicSheet.windows[\''+win.sheetKey+'\'].downloadArchive();" />';
      uphtml += '<input type="button" value=".html" onclick="BasicSheet.windows[\''+win.sheetKey+'\'].downloadHtml();" />';
      html += getUploadHtml('plugins/ajaximrpg/bsheet/template.php', uphtml, function(contents) {
         if (contents == null) {
            Dialog.alert('No file uploaded!');
         } else {
            contents = contents.replace(new RegExp('\\r\\n', 'g'), '\n');
            var sep = contents.indexOf('\n');
            var type = (sep == -1)? 'unknown': contents.substring(0, sep);
            contents = (sep == -1)? '': contents.substring(sep+1);
            contents = contents.replace(new RegExp('\\n', 'g'), '\r\n');
            if ((type == 'archive') && (contents != '')) {
               win.applyTemplate(contents);
            } else if ((type == 'html') && (contents != '')) {
               win.applyTemplateHtml(contents);
            } else if (type == 'zip') {
               Dialog.alert('.zip files not supported!');
            } else {
               Dialog.alert('File does not contain a template!');
            }
         }
      });
      html += '</div>';

      var idMap = {};
      var table_parents = {};

      // replace double underscore variables with their values
      html = html.replace(new RegExp('__id', 'g'), winId);
      html = html.replace(new RegExp('__name', 'g'), win.sheetKey);

      // fix "input" tags
      html = modifyHtmlTag(html, new RegExp('<input\\s*[^>]*data\\s*=\\s*"\\w+"[^>]*>', 'ig'), function(tag) {
         if (tag.a.hasOwnProperty('id')) {
            if (tag.a.id.indexOf(winId) == -1) {
               tag.a.id = winId+'_'+tag.a.id;
            }
         } else {
            tag.a.id = winId+'_'+tag.a.data+'_'+randomString(4);
         }
         idMap[tag.a.data] = tag.a.id;
         if (!tag.a.hasOwnProperty('onchange')) {
            tag.a['onchange'] = 'BasicSheet.windows[\''+win.sheetKey+'\'].onValueChange(\''+tag.a.id+'\', \''+tag.a.data+'\')';
         }
         tag.a.value = sheet[tag.a.data];
         delete tag.a.data;
         return tag;
      });

      // fix "textarea" tags
      html = modifyHtmlTag(html, new RegExp('<textarea\\s*[^>]*data\\s*=\\s*"\\w+"[^>]*>', 'ig'), function(tag) {
         if (tag.a.hasOwnProperty('id')) {
            if (tag.a.id.indexOf(winId) == -1) {
               tag.a.id = winId+'_'+tag.a.id;
            }
         } else {
            tag.a.id = winId+'_'+tag.a.data+'_'+randomString(4);
         }
         idMap[tag.a.data] = tag.a.id;
         if (!tag.a.hasOwnProperty('onchange')) {
            tag.a['onchange'] = 'BasicSheet.windows[\''+win.sheetKey+'\'].onValueChange(\''+tag.a.id+'\', \''+tag.a.data+'\')';
         }
         tag.body = sheet[tag.a.data];
         delete tag.a.data;
         return tag;
      });

      // create table items
      html = replaceHtmlTag(html, new RegExp('<(\\w+)\\s*[^>]*data\\s*=\\s*"([^"]*)/item"[^>]*>', 'ig'), function(html, regexp, match) {
         // create function that creates HTML for a table item
         var get_item_html = function(template) {
            return function(values) {
               var col = 0;
               return modifyHtmlTag(template, new RegExp('<input\\s*[^>]*data\\s*=\\s*"(\\w+)/item/(\\w+)"\s*[^>]*>', 'ig'), function(tag) {
                  if (tag.match[2] == 'delete') {
                     if (!tag.a.hasOwnProperty('onclick')) {
                        tag.a.onclick = 'BasicSheet.windows[\''+win.sheetKey+'\'].deleteRow(\''+tag.match[1]+'\', this)';
                     }
                  } else {
                     tag.a.value = values[col++];
                     if (tag.a.hasOwnProperty('class')) {
                        tag.a['class'] += ' bsheet_column_'+tag.match[2];
                     } else {
                        tag.a['class'] = 'bsheet_column_'+tag.match[2];
                     }
                     if (!tag.a.hasOwnProperty('onchange')) {
                        tag.a.onchange = 'BasicSheet.windows[\''+win.sheetKey+'\'].onTableChange(\''+tag.match[1]+'\', \''+tag.match[2]+'\', this)';
                     }
                  }
                  delete tag.a.data;
                  return tag;
               });
            };
         }(html);
         table_parents[match[2]] = get_item_html;
         html = modifyHtmlTag(html, new RegExp('<\\w+[\s]*[^>]*data="([^"]*)/item"[^>]*>', 'ig'), function(tag) {
            delete tag.a.data;
            return tag;
         });
         html = '';
         html += '<'+match[1]+' id="'+winId+'_'+match[2]+'_marker"></'+match[1]+'>';
         var values = [];
         var table = match[2];
         for (var row=0; row < sheet[table].items.length; ++row) {
            values = [];
            for (var col in sheet[table].items[row]) {
               var type = typeof sheet[table].items[row][col];
               if ((type == 'boolean') || (type == 'number') || (type == 'string')) {
                  values.push(sheet[table].items[row][col]);
               }
            }
            html += get_item_html(values);
         }
         return html;
      });
      

      // fix "add item" tags
      html = modifyHtmlTag(html, new RegExp('<input\\s*[^>]*data\\s*=\\s*"(\\w+)/item/add"[^>]*>', 'ig'), function(tag) {
         if (tag.a.hasOwnProperty('id')) {
            if (tag.a.id.indexOf(winId) == -1) {
               tag.a.id = winId+'_'+tag.a.id;
            }
         } else {
            tag.a.id = winId+'_'+tag.match[1]+'_'+randomString(4);
         }
         if (!tag.a.hasOwnProperty('onclick')) {
            tag.a.onclick = 'BasicSheet.windows[\''+win.sheetKey+'\'].insertRow(\''+tag.match[1]+'\', this)';
         }
         delete tag.a.data;
         return tag;
      });

      if (path != null) {
         // fix "src" style URLs
         html = replaceGroup(html, new RegExp('src="[\\s]*([^"]*)"', 'g'), function(url) {
            if ((url.indexOf('://') == -1) && (url.indexOf('/') != 0)) {
               return path+'/'+url;
            }
            return url;
         });

         // fix url() style URLs
         html = replaceGroup(html, new RegExp('url[\\s]*\\([\\s]*([^\)]*)\\)', 'g'), function(url) {
            if ((url.indexOf('://') == -1) && (url.indexOf('/') != 0)) {
               return path+'/'+url;
            }
            return url;
         });
      }

      // show 'html' variable in new window; user can do "Page Source"
//      var debughtml = html;
////      debughtml = debughtml.replace(new RegExp(winId, 'g'), 'bsheet');
////      debughtml = debughtml.replace(new RegExp('plugins/ajaximrpg/bsheet/', 'g'), '');
//      var source = '';
////      source += '<html>\r\n';
////      source += '<head>\r\n';
////      source += '<link href="bsheet.css" rel="stylesheet" type="text/css"></link>\r\n';
////      source += '</head>\r\n';
////      source += '<body bgcolor="black">\r\n';
//      source += debughtml;
////      source += '</body>\r\n';
////      source += '</html>\r\n';
//      var debug = window.open();
//      debug.document.write(source);
//      debug.document.close();
      
      win.idMap = idMap;

      win.getContent().innerHTML = html;

      win.onResize('', false);

      // add table manipulation methods to the parent tag of the table items
      $H(table_parents).each(function(pair) {
         // find the number of tags before the table rows
         var child = $(winId+'_'+pair.key+'_marker');
         var parent = $(child.parentNode);
         var children = parent.childElements();
         var top = 0;
         while (top < children.length) {
            if (children[top] == child) {
               break;
            }
            ++top;
         }
         // function: get the row that an element is in
         parent.getRowOfElement = function(top) {
            return function(el) {
               var row = 0;
               var children = this.childElements();
               while (row < children.length) {
                  if ($(el).descendantOf(children[top+row])) {
                     break;
                  }
                  ++row;
               }
               return row;
            };
         }(top);
         // function: get the given row element
         parent.getRowElement = function(top) {
            return function(row) {
               return this.childElements()[top+row];
            };
         }(top);
         // function: get the column element in a given row
         parent.getColumnElement = function(top) {
            return function(row, column) {
               var el = null;
               var descendants = this.childElements()[top+row].descendants();
               for (var d=0; d < descendants.length; ++d) {
                  if (descendants[d].hasClassName('bsheet_column_'+column)) {
                     el = descendants[d];
                     break;
                  }
               }
               return el;
            };
         }(top);
         // function: insert a node from arbitrary HTML
         parent.insertElementFromHtml = function(before, html) {
            var first = null;
            modifyHtmlTag(html, new RegExp('<(\\w+)[^>]*>', 'ig'), function(tag) {
               if (first == null) {
                  first = tag;
               }
            });
            if (first.name.toLowerCase() == 'tr') {
               var node = null;
               try {
                  node = parent.insertRow(before);
               } catch (e) {
                  node = parent.insertRow(-1);
               }
               node = $(node);
               for (var a in first.a) {
                  if ((typeof first.a[a]) == 'string') {
                     node.setAttribute(a, first.a[a]);
                     if (a == 'class') {
                        node.setAttribute('className', first.a[a]);
                     }
                  }
               }
               node.update(first.body);
            } else {
               var node = document.createElement(first.name);
               for (var a in first.a) {
                  if ((typeof first.a[a]) == 'string') {
                     node.setAttribute(a, first.a[a]);
                     if (a == 'class') {
                        node.setAttribute('className', first.a[a]);
                     }
                  }
               }
               node.innerHTML = first.body;
               node.innerHTMLRaw = first.body; // for debugging
               var children = this.childElements();
               if (before < children.length) {
                  this.insertBefore(node, children[before]);
               } else {
                  this.appendChild(node);
               }
            }
         };
         // function: insert a row in a table
         parent.insertTableRow = function(top, getItemHtml) {
            return function(syncEvent, table, el) {
               var parent = this;
               if (syncEvent != null) {
                  var row = BasicSheet.sheets[win.sheetKey][syncEvent.table.name].items.length;
                  var values = [];
                  for (var col in syncEvent.table.values) {
                     var type = typeof syncEvent.table.values[col];
                     if ((type == 'boolean') || (type == 'number') || (type == 'string')) {
                        values.push(syncEvent.table.values[col]);
                     }
                  }
                  var html = getItemHtml(values);
                  parent.insertElementFromHtml(top + row, html);
               } else {
                  var urlparts = win.url.split('?', 2);
                  BasicSheet.connect(urlparts[0], 'POST', urlparts[1]+'&{0}=&type=json', [ table ], function(xh) {
                     if (xh != null) {
                        if (xh.responseText.isJSON()) {
                           var sheet = xh.responseText.evalJSON(true);
                           if (sheet[table].items.length == (BasicSheet.sheets[win.sheetKey][table].items.length + 1)) {
                              var values = [];
                              var row = sheet[table].items.length-1;
                              for (var col in sheet[table].items[row]) {
                                 var type = typeof sheet[table].items[row][col];
                                 if ((type == 'boolean') || (type == 'number') || (type == 'string')) {
                                    values.push(sheet[table].items[row][col]);
                                 }
                              }
                              var html = getItemHtml(values);
                              parent.insertElementFromHtml(top + row, html);
                              var syncEvent = { 'table': { 'name': table, 'type': 'insert', 'row': row, 'values': sheet[table].items[row] } };
                              win.sync(syncEvent);
                           } else {
                              Dialog.alert('BasicSheetWindow.insertRow(el): insert rejected; sent: '+BasicSheet.sheets[win.sheetKey][table].items.length+'; received:'+sheet[table].items.length);
                           }
                           return true;
                        }
                     }
                  });
               }
            };
         }(top, pair.value);
         // function: delete a row in a table
         parent.deleteTableRow = function(top) {
            return function(syncEvent, table, el) {
               var parent = this;
               var row = 0;
               if (syncEvent != null) {
                  parent.removeChild(parent.getRowElement(syncEvent.table.row));
               } else {
                  row = this.getRowOfElement(el);
                  var urlparts = win.url.split('?', 2);
                  BasicSheet.connect(urlparts[0], 'POST', urlparts[1]+'&{0}.delete='+row+'&type=json', [ table ], function(xh) {
                     if (xh != null) {
                        if (xh.responseText.isJSON()) {
                           var sheet = xh.responseText.evalJSON(true);
                           if (sheet[table].items.length == (BasicSheet.sheets[win.sheetKey][table].items.length - 1)) {
                              new Effect.Fade(parent.getRowElement(row), {
                                 duration: 0.5,
                                 afterFinish: function() {
                                    var syncEvent = { 'table': { 'name': table, 'type': 'delete', 'row': row } };
                                    win.sync(syncEvent);
                                    parent.removeChild(parent.getRowElement(row));
                                 }
                              });
                           } else {
                              Dialog.alert('BasicSheetWindow.deleteRow("'+table+'", '+row+'): delete rejected, '+BasicSheet.sheets[win.sheetKey][table].items.length+' client rows, '+sheet[table].items.length+' server rows');
                           }
                           return true;
                        }
                     }
                  });
               }
            };
         }(top);
         // function: change data in a table
         parent.changeTableValue = function(top) {
            return function(syncEvent, table, col, el, value) {
               var parent = this;
               var row = 0;
               if (syncEvent != null) {
                  el = parent.getColumnElement(syncEvent.table.row, syncEvent.table.col);
                  if (el != null) {
                     el.value = syncEvent.table.value;
                  }
               } else {
                  row = parent.getRowOfElement(el);
                  value = el.value;
                  var urlparts = win.url.split('?', 2);
                  BasicSheet.connect(urlparts[0], 'POST', urlparts[1]+'&{0}.update=u1&u1.id.get='+row+'&u1.'+col+'.set={1}&type=json', [ table, value ], function(xh) {
                     if (xh != null) {
                        if (xh.responseText.isJSON()) {
                           var sheet = xh.responseText.evalJSON(true);
                           if (value == sheet[table].items[row][col]) {
                              var syncEvent = { 'table': { 'name': table, 'type': 'change', 'row': row, 'col': col, 'value': value } };
                              win.sync(syncEvent);
                           } else {
                              Dialog.alert('BasicSheetWindow.onTableChange("'+row+', '+col+'"): update rejected, sent:"'+value+'", received:"'+sheet[table].items[row][col]+'"');
                              el.value = sheet[table].items[row][col];
                           }
                           return true;
                        }
                     }
                  });
               }
            };
         }(top);
         parent.removeChild(child);
         win.idMap[pair.key] = parent;
      });

      if ((typeof func) == 'function') {
         func();
      }
   },

   /**
    * Toggle (show/hide) template options dialog.
    *
    * @author Daniel Howard
    **/
   showOptions: function() {
      var win = this;
      var winId = this.getId();
      var start = win.optionsDialog? 36: 14;
      var end = win.optionsDialog? 14: 36;
      new Effect.Tween($(winId+'_rcvd'), start, end, { duration: 0.25, afterFinish: function() {
         win.optionsDialog = !win.optionsDialog;
      } }, function(p) {
         $(winId+'_contents').setStyle({height: (win.getSize().height-p)+'px'});
      });
   },

   /**
    * Download an archive of the current template.
    *
    * @author Daniel Howard
    **/
   downloadArchive: function() {
      $('hidden_upload').src = 'plugins/ajaximrpg/bsheet/template.php?archive='+this.template;
   },

   /**
    * Download the HTML for the current template.
    *
    * @author Daniel Howard
    **/
   downloadHtml: function() {
      var win = this;
      var xhConn = new XHConn();
      xhConn.connect('ajax_upload.php', 'POST', this.templateHtml, function(xh) {
         $('hidden_upload').src = 'ajax_upload.php?file='+(win.canEdit()? 'edit.html': 'view.html');
      }, false);
   },

   /**
    * Send a 'sync' event and update the 'sheets' array.
    *
    * @arguments
    *   syncEvent - an event describing what was changed
    *
    * @author Daniel Howard
    **/
   sync: function(syncEvent) {
      syncEvent.target = {};
      syncEvent.target.type = 'bsheet';
      syncEvent.target.sheet = this.sheetKey;
      
      if ((typeof syncEvent.input) != 'undefined') {
         BasicSheet.sheets[syncEvent.target.sheet][syncEvent.input.name] = syncEvent.input.value;
         if (syncEvent.input.name == 'name') {
         var sheet = BasicSheet.sheets[syncEvent.target.sheet];
            var name = sheet.name;
            name = name.replace(new RegExp('\'', 'g'), '&#39;');
            name = name.replace(new RegExp('"', 'g'), '&#34;');
            $('bsheet_treeitem_'+sheet.viewkey).innerHTML = modifyHtmlTag($('bsheet_treeitem_'+sheet.viewkey).innerHTML, new RegExp('<a[^>]*>', 'ig'), function(tag) {
               tag.a.title = name+' character sheet (bsheet plugin)';
               tag.body = name;
               return tag;
            });
            $('bsheet_title_'+sheet.viewkey).innerHTML = name;
         }
      } else if ((typeof syncEvent.table) != 'undefined') {
         if (syncEvent.table.type == 'change') {
            BasicSheet.sheets[syncEvent.target.sheet][syncEvent.table.name].items[syncEvent.table.row][syncEvent.table.col] = syncEvent.table.value;
         } else if (syncEvent.table.type == 'insert') {
            BasicSheet.sheets[syncEvent.target.sheet][syncEvent.table.name].items.splice(syncEvent.table.row, 0, syncEvent.table.values);
         } else if (syncEvent.table.type == 'delete') {
            BasicSheet.sheets[syncEvent.target.sheet][syncEvent.table.name].items.splice(syncEvent.table.row, 1);
         }
      }

      var encodedData = Object.toJSON(syncEvent);
      encodedData = encodedData.replace('+', '%2B');
      encodedData = encodeURIComponent(encodedData);
      
      var xhConn = new XHConn();
      xhConn.connect(pingTo, 'POST', "call=sync&to=lobby&data="+encodedData, function(xh) {
      });
   },
   
   /**
    * Receive a 'sync' event and update this window when another user
    * changed a value.
    *
    * @arguments
    *   syncEvent - an event describing what was changed
    *
    * @author Daniel Howard
    **/
   handleSync: function(syncEvent) {
      var winId = this.getId();
      var id = '';
      if ((typeof syncEvent.input) != 'undefined') {
         id = this.idMap[syncEvent.input.name];
         if ($(id)) {
            $(id).value = syncEvent.input.value;
         } else {
            Dialog.alert('BasicSheet.handleSync(): "'+syncEvent.input.name+'" element not found');
         }
      } else if ((typeof syncEvent.table) != 'undefined') {
         if (syncEvent.table.type == 'insert') {
            this.idMap[syncEvent.table.name].insertTableRow(syncEvent);
         } else if (syncEvent.table.type == 'delete') {
            this.idMap[syncEvent.table.name].deleteTableRow(syncEvent);
         } else if (syncEvent.table.type == 'change') {
            this.idMap[syncEvent.table.name].changeTableValue(syncEvent);
         } else {
            Dialog.alert('BasicSheet.handleSync(): unknown operation on "'+syncEvent.table.name+'"');
         }
      } else {
         Dialog.alert('BasicSheet.handleSync(): unknown event');
      }
   },

   /**
    * Insert a row at the end of a table on the character sheet,
    * update the bsheet server and send 'sync' events to other
    * users.
    *
    * @arguments
    *   table - the key/table (eg weapons) to update
    *   el - an HTML element in the table
    *
    * @author Daniel Howard
    **/
   insertRow: function(table, el) {
      this.idMap[table].insertTableRow(null, table, el);
   },
   
   /**
    * Delete a row from a table on the character sheet, update
    * the bsheet server and send 'sync' events to other users.
    *
    * @arguments
    *   table - the key/table (eg weapons) to update
    *   el - an HTML element in the row that was deleted
    *
    * @author Daniel Howard
    **/
   deleteRow: function(table, el) {
      this.idMap[table].deleteTableRow(null, table, el);
   },

   /**
    * Update the bsheet server and sync with other users for
    * a table value that changed.
    *
    * @arguments
    *   table - the key/table (eg weapons) to update
    *   el - the HTML element that was changed
    *
    * @author Daniel Howard
    **/
   onTableChange: function(table, col, el) {
      this.idMap[table].changeTableValue(null, table, col, el);
   },
   
   /**
    * Update the bsheet server and sync with other users for
    * a non-table value that changed.
    *
    * @arguments
    *   id - the 'id' attribute of the input tag
    *   key - the key/column (eg charisma) that changed
    *
    * @author Daniel Howard
    **/
   onValueChange: function(id, key) {
      var win = this;
      var urlparts = this.url.split('?', 2);
      if ($(id)) {
         var value = $(id).value;
         BasicSheet.connect(urlparts[0], 'POST', urlparts[1]+'&{0}={1}&type=json', [ key, value ], function(xh) {
            if (xh != null) {
               if (xh.responseText.isJSON()) {
                  var sheet = xh.responseText.evalJSON(true);
                  if (sheet[key] == value) {
                     var syncEvent = { 'input': { 'name': key, 'value': value } };
                     win.sync(syncEvent);
                  } else {
                     Dialog.alert('BasicSheetWindow.onValueChange("'+key+'"): update rejected, sent:"'+value+'", received:"'+sheet[key]+'"');
                     $(id).value = sheet[key];
                  }
                  return true;
               }
            }
         });
      } else {
         Dialog.alert('BasicSheetWindow.onValueChange("'+key+'"): unknown field');
      }
   },

   /**
    * Handle resize events.
    *
    * @arguments
    *   eventName - the event causing the resize
    *   detached - if the window is detached
    *
    * @author Daniel Howard
    **/
   onResize: function(eventName, detached) {
      var win = this;
      var winId = this.getId();
      var height = this.getSize().height - (win.optionsDialog? 36: 14);
      $(winId+'_contents').setStyle({height: height+'px'});
   },

   /**
    * Handle resize events.
    *
    * @arguments
    *   eventName - the event causing the resize
    *   detached - if the window is detached
    *
    * @author Daniel Howard
    **/
   onClose: function(eventName) {
      var win = this;
      if (win.template != null) {
         // erase the unused temporary template
         var xhConn = new XHConn();
         xhConn.connect('plugins/ajaximrpg/bsheet/template.php', 'GET', 'remove='+win.template, function(xh) {
            var contents = xh.responseText;
         });
      }
      delete BasicSheet.windows[this.sheetKey];
   }
});

/**
 * Replace the capture groups for unambiguous RegExp
 * searches and strings.  This is not general purpose.
 *
 * @arguments
 *   s - The source string to search.
 *   regexp - RegExp object with a capture group.
 *   r - Capture group handling function.
 * @return The new string.
 *
 * @author Daniel Howard
 **/
function replaceGroup(s, regexp, r) {
   var match = regexp.exec(s);
   while (match != null) {
      regexp.lastIndex += match[0].length;
      var start = 0;
      for (var g=1; g < match.length; ++g) {
         if ((typeof r) == 'function') {
            var rep = r(match[g], g);
            if ((typeof rep) == 'string') {
               if (g > 0) {
                  start += match[0].substring(start).indexOf(match[g]);
                  s = s.substring(0, match.index+start)+rep+s.substring(match.index+start+match[g].length);
                  start += rep.length;
                  regexp.lastIndex += rep.length - match[g].length;
               }
            }
         }
      }
      match = regexp.exec(s);
   }
   return s;
}

// hook in this plugin so it can catch events
plugins['BasicSheet'] = BasicSheet;

