/***************************************************************
This program is free software; you can use it for any purpose
subject to the following condition.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
****************************************************************/
var __XML = /(<\?xml +version *= *"1.\d" *\?>\s*)/;
var __WS = /(\s+)/;
var __TWS = /\s+$/;
var __TAG = /(<\/?[A-Za-z_][A-Za-z0-9_]*[^>]*>)/;
var __END = /(<\/[A-Za-z_][A-Za-z0-9_]*\s*>)/;
var __AS = /(=["'])/;
var __IGNORE_FIELD = "__";

function xml_om(src)
{
   // private methods
   function __findnode(obj, a)
   {
      var n = a.length;
      var node = obj.top;
      var start = 0;
      for (var i = start; i < n; i++) {
         var joint = a[i];
         var p = joint.indexOf("[");
         var found = false;
         if (p == -1) {
            for (var j = 0; j < node.nodelist.length; j++) {
               if (joint == node.nodelist[j].name) {
					   node = node.nodelist[j];
                  found = true;
                  break;
         	   }
            }
            if (!found)
               return null;
         } else {
            var index = parseInt(joint.substr(p+1), 10);
            var list = node.nodelist;
            var l = list.length;
            if (l <= index)
				   return null;
            var name = list[0].name;
            for (var j = 1; j < l; j++) {
				   if (list[j].name != name)
					   return null;
            }
            obj.findex = index;
            node = list[index];
         }
      }
      return node;
   }

   function __clone_node(node)
   {
      var newnode = new __node(null, node.parent);
      newnode.name = node.name;
      if (node.etext != null)
         newnode.etext = node.etext;
      for (var i = 0; i < node.nodelist.length; i++)
         newnode.nodelist[i] = __clone_node(node.nodelist[i]);
      if (node.attributes != null)
         newnode.attributes = __clone_attributes(node.attributes);
      return newnode;
   }

   function __clone_attributes(at)
   {
      var attribs = new Object();
      for (member in at) {
         attribs[member] = at[member];
      }
      return attribs;
   }
   function __format_node(node, s, indent)
   {
      var sp = "                                        ".substr(0, 3*indent);
      s += sp+"<"+node.name;
      if (node.attributes != null) {
         for (member in node.attributes)
            s += " "+member+"=\""+__encode_entities(node.attributes[member])+"\"";
      }
      if (node.etext != null) {
         s += ">"+__encode_entities(node.etext)+"</"+node.name+">\r\n"
         return s;
      }
      var a = node.nodelist;
	   if (a && a.length) {
         s += ">\r\n";
         for (var i = 0; i < a.length; i++)
		      s = __format_node(a[i], s, indent+1);
         s += sp+"</"+node.name+">";
      } else
         s += "/>"
      s += "\r\n";
      return s;
   }

   function __encode_entities(s)
   {
      s = s.replace(/&/g, "&amp;");
      return s.replace(/</g, "&lt;");
   }

   function __preamble(s)
   {
      return (s.search(__XML) == -1)? null: s.substr(RegExp.$1.length);
   }
   
   function __addnode(parent, name, after)
   {
      var nn = new __node(null, parent);
      nn.name = name;
      var n = parent.nodelist.length;
      parent.nodelist.length++;
      for (var i = n; i > 0; i--) {
         if (parent.nodelist[i-1].name == after) {
            parent.nodelist[i] = nn;
            return;
         } 
         parent.nodelist[i] = parent.nodelist[i-1];
      }
      parent.nodelist[0] = nn;
   }

   function __addattrib(node, name, value)
   {
      if (node.attributes == null)
         node.attributes = new Object();
      node.attributes[name] = value;
   }

   // This is essentially the parser for the source XML
   function __node(so, parent)
   {
      function __prune(s)
      {
         return s.search(__WS)? s: s.substr(RegExp.$1.length);
      }

      function __rprune(s)
      {
         var pos = s.search(__TWS);
         return (pos >= 0)? s.substring(0, pos): s;
      }

      function __decode_entities(s)
      {
         s = s.replace(/&lt;/g, "<");
         return s.replace(/&amp;/g, "&");
      }

      function __parse_attribs(as)
      {
         var n, a = new Object();
         var pos = as.search(__AS);
         if (pos == -1) return null;
         var resid;
         for (var k = 0; pos != -1; k++) {
            var quot = RegExp.$1.charAt(1);
            var q1 = pos+1
            var q2 = as.indexOf(quot, q1+1);
            if (q2 == -1)
               return null;
            n = as.substring(0, pos);
            if (n.search(__WS) != -1)
               return null;
            t = as.substring(q1+1, q2);
            a[n] = __decode_entities(t);
            resid = as.substr(q2+1);
            as = __prune(resid);
            pos = as.search(__AS);
         }

         if (resid.length) {
            resid.search(__WS);
            if (resid.length != RegExp.$1.length)
               return null;
         }

         return a;
      }

      this.valid = false;
      this.parent = parent;
      this.nodelist = new Array();
      this.attributes = null;
      if (so == null) {
         // Nothing to parse - just return bare object
         this.valid = true;
         return;
      }

      so.src = __prune(so.src);

      var x = so.src.search(__TAG);
      var taglen = RegExp.$1.length;
      //if (x != 0) {
      //   return;
      //}

      var tagend = taglen-1;

      var mt = (so.src.charAt(tagend-1) == '/');
      if (so.atTop && mt)
         return;
      so.atTop = false;

      var ps = so.src.search(__WS);
      var as = null;
      if (ps == -1 || ps > tagend)
         this.name = so.src.substring(1, (mt? tagend-1: tagend));
      else {
         var n = so.src.substring(1, ps);
         this.name = __prune(n);
         as = __prune(so.src.substring(ps+1, (mt? tagend-1: tagend)));
      }

      if (as != null && as.length) {
         if ((this.attributes = __parse_attribs(as)) == null) {
            return;
         }
      }
      so.src = __prune(so.src.substr(tagend+1));
      if (so.src.charAt(0) != "<") {
         // Element contains text
         var t = so.src.search(__END);
         if (t == -1) 
            return;
         var el = RegExp.$1.length;
         var en = __rprune(RegExp.$1.substr(2, el-3));
         this.etext = __decode_entities(__rprune(so.src.substr(0, t)));
         if (en != this.name)
            return;
         // In this case, no child elements
         if (this.etext.search(__TAG) != -1)
            return;
         so.src = __prune(so.src.substr(t+el));
         this.valid = true;
         return;
      }
      if (!mt) {
         for (var n = 0;; n++) {
            var t = so.src.search(__END);
            if (t == -1)
               return;
            if (t == 0) {
               var tl = RegExp.$1.length;
               if (__rprune(RegExp.$1.substr(2, tl-3)) != this.name)
                  return;
               so.src = __prune(so.src.substr(tl));
               break;
            }
            var nn = new __node(so, this);
            if (!nn.valid) {
               return;
            }
            this.nodelist[n] = nn;
         }
      }

      this.valid = true;
   }

   // public methods
   this.xml = function()
   {
      return "<?xml version=\"1.0\"?>\n"+__format_node(this.top, "", 0);
   }

   this.getValue = function(s)
   {
      if (!s)
         return false;
      var a = s.split(".");
      if (a.length == 1)
         return this.top.attributes[s];
      var atn = a[a.length-1];
      a.length--;
      var node = __findnode(this, a);
      if (node == null)
         return null;
      return (atn.length != 0)? node.attributes[atn]: node.etext;
   }

   this.getAttributes = function(s)
   {
      if (!s)
         return null;
      var a = s.split(".");
      var node = __findnode(this, a);
      if (node == null)
         return null;
      var a = new Array();
      var i = 0;
      for (member in node.attributes)
         a[i++] = node.attributes[member];
      return a;
   }

   this.setValue = function (s, v)
   {
      if (!s)
         return false;
      var a = s.split(".");
      if (a.length == 1) {
         this.top.attributes[s] = ""+v;
         return true;
      }
      var atn = a[a.length-1];
      a.length--;
      var node = __findnode(this, a);
   Screen.writeln(node.name+", "+atn.length);
      if (node == null)
         return false;
      if (atn.length)
         node.attributes[atn] = ""+v;
      else
         node.etext = ""+v;
      return true;
   }

   this.setAttributes = function (s, a)
   {
      if (!s)
         return false;
      var aj = s.split(".");
      var node = __findnode(this, aj);
      if (node == null)
         return false;
      var i = 0;
      for (member in node.attributes)
         node.attributes[member] = ""+a[i++];
      return true;
   }

   this.cloneArrayElement = function(s)
   {
      if (!s)
         return false;
      var a = s.split(".");
      var node = __findnode(this, a);
      if (node == null)
         return false;
      var newnode = __clone_node(node);
      var plist = node.parent.nodelist;
      if (this.findex == plist.length-1)
         plist[plist.length] = newnode;
      else {
         for (var i = plist.length; i > this.findex; i--)
            plist[i] = plist[i-1];
         plist[this.findex] = newnode;
      }
      return true;
   }

   this.deleteArrayElement = function (s)
   {
      if (!s)
         return false;
      var a = s.split(".");
      var node = __findnode(this, a);
      if (node == null)
         return false;
      var plist = node.parent.nodelist;
      for (var i = this.findex; i < plist.length-1; i++)
         plist[i] = plist[i+1];
      plist.length--;
      return true;
   }

   this.listLength = function(s)
   {
      if (!s)
		   return -1;
      var a = s.split(".");
      var node = __findnode(this, a);
      if (node == null)
         return -1;
      return node.nodelist.length;
   }
   
   this.addElement = function(s, n, after)
   {
      if (!s)
         return false;
      if (s == ".")
         return __addnode(this.top, n, after);
      var a = s.split(".");
      if (a.length == 1)
         return null;
      a.length--;
      var node = __findnode(this, a);
      if (node == null)
         return false;
      return __addnode(node, n, after);
   }
   
   this.addAttribute = function(s, n, v)
   {
      if (!s)
         return false;
      if (s == ".")
         return __addattrib(this.top, n, v);
      var a = s.split(".");
      if (a.length == 1)
         return null;
      a.length--;
      var node = __findnode(this, a);
      if (node == null)
         return false;
      return __addattrib(node, n, v);
   }

   this.xmlName = function()
   {
	   return this.top.name;
   }

   this.ok = function()
   {
      return this.top.valid;
   }

   this.toform = function(f)
   {
      var ec = f.length;
      var rbdone = "";
      for (var i = 0; i < ec; i++) {
         var e = f.elements[i];
         var name = e.name;
         if (!name || name.indexOf(__IGNORE_FIELD) == 0)
            continue
         var val = this.getValue(name);
         if (val == null)
            // attribute not found
            continue;
         var type = e.type;
         if (type == "radio") {
            if (rbdone.indexOf(name) != -1)
               continue;
            var group = f.elements[name];
            for (var b = 0; b < group.length; b++) {
               if (group[b].value == val) {
                  group[b].checked = true;
                  break;
               }
            }
            rbdone += name+" ";
         } else if (type == "checkbox") {
            var tv = val.charAt(0);
            tv = tv.toLowerCase();
   		 if (tv == 'y' || tv == "1" || tv == 't')
               e.checked = true;
            else
               e.checked = false;
         } else if (type == "textarea") {
            e.defaultValue = val;
         } else
            e.value = val;
      }
   }

   this.fromform(f)
   {
      var ec = f.length;
      var rbdone = "";
      for (var i = 0; i < ec; i++) {
         var e = f.elements[i];
         var name = e.name;
         if (!name || name.indexOf(__IGNORE_FIELD) == 0)
            continue;
         var type = e.type;
         var val = "";
         if (type == "radio") {
            if (rbdone.indexOf(name) != -1)
               continue;
            var group = f.elements[name];
            for (var b = 0; b < group.length; b++) {
               if (group[b].checked) {
                  this.setValue(name, group[b].value);
                  break;
               }
            }
            rbdone += name+" ";
         } else if (type == "checkbox") {
            this.setValue(name, (e.checked? "y": "n"));
         } else
            this.setValue(name, e.value);
      }
   }

   this.src = __preamble(src);
   this.atTop = true;
	this.top = new __node(this, null);
}

// Here is an example of usage.
var src = 
'<?xml  version = "1.0"?>\n'+
'<individual name="John Doe" age="&lt; 30 &amp; handsome">\n'+
'      <street>22 Washington Ave.        </street>\n'+
'      <city>      Somewhere   </city>\n'+
'      <state>                  NJ                            </state >\n'+
'      <zip>07000</zip>\n'+
'</individual >';

var om = new xml_om(src);
if (!om.ok()) {
   Screen.writeln("bad: "+om.src);
   Clib.exit();
} else
   Screen.writeln(om.xml());
om.setValue("city.", "Newark");
Screen.write(om.xml());
om.addElement(".", "whatever");
om.addElement("whatever.", "thingie");
om.setValue("whatever.thingie.", "Cabbages and Kings");
om.addAttribute("whatever.", "flannel", "shirt");
om.addAttribute("whatever.", "tweed", "jacket");
Screen.write(om.xml());
Screen.writeln(om.getValue("whatever.thingie."));
Screen.writeln(om.getValue("whatever.tweed"));
om.cloneArrayElement("whatever.thingie[0]");
om.cloneArrayElement("whatever.thingie[0]");
om.setValue("whatever.thingie[1].", "Ships & Sealing Wax");
Screen.write(om.xml());
Clib.getch();



