// Comparison functions
function naturalComp (x, y) { 
  return (x < y) ? -1 : ((x == y) ? 0 : 1); 
}

function dateComp(x, y) { 
  return x.getTime() - y.getTime(); 
}

// Transformations
function identity(x) {
  return x;
}



function map(fn, seq1) {
  var res = new Array();

  if (!seq1)
    seq1 = [];

  if (arguments.length == 2) {
    // fast path
    for (var i = 0; i < seq1.length; i++)
      res[i] = fn(seq1[i]);
  } else {
    var args = new Array();
    for (var i = 0; i < seq1.length; i++) {
      for (var j = 1; j < arguments.length; j++)
	args[j - 1] = arguments[j][i];
      res[i] = fn.apply(null, args);
    }
  }

  return res;
}

function mapObject(fn, obj, toArray) {
  var res; 

  if (toArray) {
    res = new Array();

    for (var key in obj)
      res.push(fn(key, obj[key]));
  } else {
    res = new Object();

    for (var key in obj)
      res[key] = fn(key, obj[key]);
  }

  return res;
}

// builds a tree of DOM nodes from a specification array,
// a triple of [tagName, attrObject, childArray]
function dom(spec, doc) {
  var doc = (doc) ? doc : document;

  switch (typeof spec) {
  case "string":
    return doc.createTextNode(spec);
  case "object":
    if (spec.constructor == [].constructor) {
      var node = doc.createElement(spec[0]);
      var attrs = spec[1];
      var children = spec[2];

      for (var key in attrs) {
        if (key == "id")
          node.id = attrs[key];
        else if (key == "class")
          node.className = attrs[key];

        node.setAttribute(key, attrs[key]);
      }
      for (var i = 0; i < children.length; i++)
        node.appendChild(dom(children[i], doc));
      
      return node;
    } else if (spec.tagName) {
      return spec;
    } else {
      return doc.createTextNode(spec.toString());
    }
  default:
    return doc.createTextNode(String(spec));
  }
}

// for cross-browser compatibility
function addListener(elt, evType, fn, capture) {
  if (elt.addEventListener)
    elt.addEventListener(evType, fn, capture);
  else
    elt.attachEvent("on" + evType, fn);
}

function xmlRequest(uri, method, params, handler) {
  var req = (window.ActiveXObject) 
    ? new ActiveXObject("Microsoft.XMLHTTP") : new XMLHttpRequest();
  req.onreadystatechange = function() {
    handler(req);
  };
  req.open("post", uri, true);
  var message = new XMessage(method, params);
  req.setRequestHeader("Content-Type", "text/xml; charset=UTF-8");
  req.send(message.doc);
  return req;
}


var catastrophicErrorHandler = null;

function makeHandler(fn) {
  return function(req) {
    switch(req.readyState) {
    case 4:
      if (req.status == 200) {
	if (req.responseXML) {
	  var result = (new XMessage()).parse(req.responseXML);
	  if (result.methodResponse !== undefined) {
	    fn(result.methodResponse);
	    return;
	  }
	}
      }
      // If we get here, we have some kind of error condition.
      if (catastrophicErrorHandler)
	catastrophicErrorHandler(req);
      else
	alert("We're sorry.  This application has experienced an unrecoverable " +
	      "error.  The following information is for debugging purposes only. " +
	      "If you do not understand it, you may safely disregard it.\n\n" +
	      "HTTP Response: " + req.status + " -- " + req.statusText + "\n\n" +
	      req.responseText);
 
    }
  };
}

// XMessage class for (un)marshalling data from/to XML.
// By the way, Dave Winer is a moron.  
// The XML-RPC spec is chock full of gross stupidity.
function XMessage(method, args) {
  var doc = null;

  if (document.implementation && document.implementation.createDocument)
    doc = document.implementation.createDocument("", "", null);
  else if (window.ActiveXObject)
    doc = new ActiveXObject("Microsoft.XMLDOM");
  else
    alert("This browser is not capable of running this application.");

  if (!doc)
    return;

  var message = this;
  var spec = ["methodCall", {}, 
	      [["methodName", {}, [method]],
	       ["params", {},
		map(function(arg) {
		  return ["param", {}, [["value", {}, [message.getValue(doc, arg)]]]];
		}, args)]]];
  doc.appendChild(dom(spec, doc));
  this.doc = doc;
}

XMessage.prototype.toXML = function() {
  if (this.doc.xml)
    return this.doc.xml;

  var ser = new XMLSerializer();
  return ser.serializeToString(this.doc);
};

// Fucking Safari.  So, it has XMLSerializer, but it's broken; it doesn't
// automatically create entity references for '<',  '>', and '&'.
// So, we create the entity references "by hand."  Oh wait:  Mozilla doesn't 
// properly implement Document.createEntityReference()?  Wow.  This is fun.
XMessage.prototype.addString = function(doc, parent, str) {
  var map = {"<": "lt", ">": "gt", "&": "amp"};
  var last = 0;

  for (var i = 0; i < str.length; i++) {
    var c = str.charAt(i);
    switch (c) {
    case "<": case ">": case "&":
      if (i > last)
	parent.appendChild(doc.createTextNode(str.substring(last, i)));
      var ref = doc.createEntityReference(map[c]);
      if (ref)
	parent.appendChild(ref);
      else
	parent.appendChild(doc.createTextNode(c));
      last = i + 1;
    }
  }
  if (last < str.length)
    parent.appendChild(doc.createTextNode(str.substring(last)));

  parent.normalize();
};

XMessage.prototype.getValue = function(doc, arg) {
  var datum = null;

  switch(typeof arg) {
  case "string":
    datum = doc.createElement("string");
    this.addString(doc, datum, arg);
    break;
  case "number":
    datum = doc.createElement(((parseInt(arg) == parseFloat(arg))
			       ? "int" : "double"));
    datum.appendChild(doc.createTextNode(arg));
    break;
  case "boolean":
    datum = doc.createElement("boolean");
    datum.appendChild(doc.createTextNode(((arg) ? "1" : "0")));
    break;
  case "object":
    if (arg == null) {
      datum = doc.createElement("nil");
    } else if (arg.constructor == [].constructor) {
      datum = doc.createElement("array");
      var contents = doc.createElement("data");
      datum.appendChild(contents);

      for (var i = 0; i < arg.length; i++) {
	var item = arg[i];
	var val = doc.createElement("value");
	contents.appendChild(val);
	val.appendChild(this.getValue(doc, item));
      }
    } else if (arg.toUTCString) {
      // we'll assume it's a Date
      datum = doc.createElement("dateTime.iso8601");
      var str = arg.getFullYear();
      var mon = String(arg.getMonth() + 1);
      var date = String(arg.getDate());
      var hours = arg.getHours();
      var minutes = arg.getMinutes();
      var seconds = arg.getSeconds();
      str += ((mon.length == 1) ? "0" + mon : mon) +
	((date.length == 1) ? "0" + date : date) +
	"T" +
	((hours.length == 1) ? "0" + hours : hours) +
	((minutes.length == 1) ? "0" + minutes : minutes) +
	((seconds.length == 1) ? "0" + seconds : seconds);
      datum.appendChild(doc.createTextNode(str));
    } else {
      // arbitrary object
      datum = doc.createElement("struct");
      for (var name in arg) {
	var member = doc.createElement("member");
	datum.appendChild(member);
	var nameElt = doc.createElement("name");
	member.appendChild(nameElt);
	this.addString(doc, nameElt, name);
	var val = doc.createElement("value");
	val.appendChild(this.getValue(doc, arg[name]));
	member.appendChild(val);
      }
    }
    break;
  default:
    alert("XML serialization failed: attempt to serialize a value of type " + (typeof arg));
  }

  return datum;
};

// unmarshall the damned stuff
XMessage.prototype.parse = function(doc) {
  var root = doc.documentElement;
  if (root.tagName == "methodResponse") {
    // No, this format isn't too baroque, or anything...
    // Did I mention what an moron Dave Winer is?
    var val = this.parseValue(root.firstChild.firstChild.firstChild.firstChild);
    return {methodResponse: val};
  } else {
    // must be a fault
    var val = this.parseValue(root.firstChild.firstChild.firstChild);
    return {fault: val};
  }
};

XMessage.prototype.parseValue = function(element) {
  switch(element.tagName) {
  case "nil":
    return null;
  case "string":
    return (element.firstChild) ? element.firstChild.nodeValue : "";
  case "int": case "i4":
    return parseInt(element.firstChild.nodeValue);
  case "double":
    return parseFloat(element.firstChild.nodeValue);
  case "boolean":
    return (element.firstChild.nodeValue == "1");
  case "dateTime.iso8601":
    // of course ISO 8601 allows so many different formats, it's completely fucking useless
    // we use YYYYMMDD'T'HH:MM:SS
    var str = element.firstChild.nodeValue;
    return new Date(parseInt(str.substring(0, 4), 10),
		    parseInt(str.substring(4, 6), 10) - 1,
		    parseInt(str.substring(6, 8), 10),
		    parseInt(str.substring(9, 11), 10),
		    parseInt(str.substring(12, 14), 10),
		    parseInt(str.substring(15, 17), 10));
  case "array":
    var data = element.firstChild;
    var arr = new Array();
    for (var i = 0; i < data.childNodes.length; i++)
      arr.push(this.parseValue(data.childNodes[i].firstChild));
    return arr;
  case "struct":
    var obj = new Object();
    for (var i = 0; i < element.childNodes.length; i++) {
      var member = element.childNodes[i];
      var name = member.getElementsByTagName("name")[0];
      var value = member.getElementsByTagName("value")[0];
      obj[name.firstChild.nodeValue] = this.parseValue(value.firstChild);
    }
    return obj;
  default:
    alert("XML deserialization failed:  attempt to deserialize element with name: " + 
	  element.tagName);
    return null;
  }
};
