function XmlElement(xml) {
  this.xml = xml;
  
  this.nativeImpl = true;
  try {
    this.nativeImpl = (xml.getElementsByTagNameNS != undefined);
  } catch (e) {}
}

XmlElement.prototype.stripNameSpace = function(nodeName) {
  var idx = nodeName.lastIndexOf(":");
  if (idx == -1) {
    return nodeName;
  }
  
  return nodeName.substring(idx+1, nodeName.length);
}

XmlElement.prototype.getElementsByTagNameNS = function(namespaceURI, localName) {
  var elems = new Array();
  
  if (this.nativeImpl) {
    var nodeList = this.xml.getElementsByTagNameNS(namespaceURI, localName);
    for (var i = 0; i < nodeList.length; ++i) {
      elems.push(nodeList.item(i));
    }
  } else {
    this.getElementsByTagNameNSImpl(elems, this.xml, namespaceURI, localName);
  }
  
  return elems;
}

XmlElement.prototype.getElementsByTagNameNSImpl = function(result, xml, namespaceURI, localName) {
  var nodeList = xml.childNodes;
  for (var i = 0; i < nodeList.length; ++i) {
    var elem = nodeList.item(i);
    if (elem.namespaceURI == namespaceURI) {
      var nodeName = elem.nodeName;
      if ((nodeName) && (this.stripNameSpace(elem.nodeName) == localName)) {
        result.push(elem);
      }
    }
    this.getElementsByTagNameNSImpl(result, elem, namespaceURI, localName);
  }
}

// Namespaces
XmlElement.xmlSchemaNS = "http://www.w3.org/2001/XMLSchema";
XmlElement.soapSchemaNS = "http://schemas.xmlsoap.org/soap/envelope/";

///////////////////////////////////////////////////////////////////////////////

function SoapSerializer() {}

SoapSerializer.prototype.object2SoapXml = function(name, obj) {
  var objType = typeof(obj);
  
  if ((objType == "function") || (obj instanceof Function)) {
    return ""; // ignore functions
  }
  
  if ((objType == "string") || (obj instanceof String)) {
    return this.string2SoapXml(name, obj);  
  }
  
  if ((objType == "boolean") || (obj instanceof Boolean)) {
    return this.boolean2SoapXml(name, obj);  
  }
  
  if ((objType == "number") || (obj instanceof Number)) {
    return this.number2SoapXml(name, obj);  
  }
  
  if (obj instanceof Array) {
    return this.array2SoapXml(name, obj);
  }
  
  if (obj instanceof Date) {
    return this.date2SoapXml(name, obj);  
  }
    
  return this.complexType2SoapXml(name, obj);  
}

SoapSerializer.prototype.array2SoapXml = function(name, obj) {
  var soapXml = "";
  for (var i = 0; i < obj.length; ++i) {
    soapXml += this.object2SoapXml(name, obj[i]);
  }
  return soapXml;
}  

SoapSerializer.prototype.complexType2SoapXml = function(name, obj) {
  var soapXml = "";
  for (attrib in obj) {
    soapXml += this.object2SoapXml(attrib, obj[attrib]);
  }
  return this.xmlTag(name, soapXml);
}  

SoapSerializer.prototype.string2SoapXml = function(name, obj) {
  var escapedStr = obj.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
  return this.xmlTag(name, escapedStr);
}

SoapSerializer.prototype.number2SoapXml = function(name, obj) {
  return this.xmlTag(name, obj);
}

SoapSerializer.prototype.boolean2SoapXml = function(name, obj) {
  return this.xmlTag(name, ((obj) ? ("true") : ("false")));
}

SoapSerializer.prototype.date2SoapXml = function(name, obj) {
  var tzOffset = -obj.getTimezoneOffset();
  var tzMinutesPart = Math.abs(tzOffset) % 60;
  var tzHoursPart = (Math.abs(tzOffset) - 60*tzMinutesPart) / 60;
  var tzSign = ((tzOffset >= 0) ? "+" : "-");

  var soapDate =
    dojo.string.pad(obj.getFullYear(), 4, "0", false) + "-" +
    dojo.string.pad(obj.getMonth()+1, 2, "0", false) + "-" +
    dojo.string.pad(obj.getDate(), 2, "0", false) + "T" +
    dojo.string.pad(obj.getHours(), 2, "0", false) + ":" +
    dojo.string.pad(obj.getMinutes(), 2, "0", false) + ":" +
    dojo.string.pad(obj.getSeconds(), 2, "0", false) + tzSign +
    dojo.string.pad(tzHoursPart, 2, "0", false) + ":" +
    dojo.string.pad(tzMinutesPart, 2, "0", false);

  return this.xmlTag(name, soapDate);
}

SoapSerializer.prototype.xmlTag = function(tagName, tagContent) {
  return "<" + tagName + ">" + tagContent + "</" + tagName + ">";
}

///////////////////////////////////////////////////////////////////////////////

function SoapDeserializer() {
  this.types = new Object();
}

SoapDeserializer.prototype.addTypes = function(soapXml) {
  var xml = new XmlElement(soapXml);
  var typeNodes = xml.getElementsByTagNameNS(
    XmlElement.xmlSchemaNS, "complexType");
  for (var typeIdx = 0; typeIdx < typeNodes.length; ++typeIdx) {
    var typeNode = typeNodes[typeIdx];
    var typeName = typeNode.attributes.getNamedItem("name").nodeValue;
    
    var type = new Object();
    
    var memberNodes = new XmlElement(typeNode).getElementsByTagNameNS(
      XmlElement.xmlSchemaNS, "element");
    for (var memberIdx = 0; memberIdx < memberNodes.length; ++memberIdx) {
      var memberNode = memberNodes[memberIdx];
      var memberName = memberNode.attributes.getNamedItem("name").nodeValue;
      var memberType = memberNode.attributes.getNamedItem("type").nodeValue;
      memberType = this.stripNameSpace(memberType); // TODO: support namespaces
      var memberMaxOccurs = memberNode.attributes.getNamedItem("maxOccurs");
      
      type[memberName] = {
        type: memberType,
        isArray: ((memberMaxOccurs != null) && (memberMaxOccurs.nodeValue == "unbounded"))
      };
    }
    
    this.types[typeName] = type;
  }
  
  /*
  var str = "";
  for (var typeName in this.types) {
    str += typeName + ":\n";
    var type = this.types[typeName];
    for (var memberName in type) {
      var memberType = type[memberName];
      str += " " + memberName + " " + memberType.type + " " + memberType.isArray + "\n";
    }
  }
  alert(str);
  */
}

SoapDeserializer.prototype.stripNameSpace = function(typeName) {
  var idx = typeName.lastIndexOf(":");
  if (idx == -1) {
    return typeName;
  }
  
  return typeName.substring(idx+1, typeName.length);
}
  
SoapDeserializer.prototype.soapXml2Object = function(soapXml) {
  var objType = this.stripNameSpace(soapXml.nodeName);
  return this.xml2NonArrayObject(soapXml.childNodes, objType);
}

SoapDeserializer.prototype.indexOfThrows = function(str, searchValue, fromIndex) {
  var index = str.indexOf(searchValue, fromIndex);
  if (index == -1) {
    throw new Error("Invalid string format.");   
  }
  return index;
}

SoapDeserializer.prototype.strToDate = function(str) {
  var tzSign = 1;

  var idxHyphen1 = this.indexOfThrows(str, "-");
  var idxHyphen2 = this.indexOfThrows(str, "-", idxHyphen1+1);
  var idxT = this.indexOfThrows(str, "T", idxHyphen2+1);
  var idxColon1 = this.indexOfThrows(str, ":", idxT+1);
  var idxColon2 = this.indexOfThrows(str, ":", idxColon1+1);
  var idxPlusOrMinus = str.indexOf("+", idxColon2+1);
  if (idxPlusOrMinus == -1) {
    idxPlusOrMinus = this.indexOfThrows(str, "-", idxColon2+1);
    tzSign = -1;
  }
  var idxColon3 = this.indexOfThrows(str, ":", idxPlusOrMinus+1);

  var year = parseInt(str.substring(0, idxHyphen1), 10);
  var month = parseInt(str.substring(idxHyphen1+1, idxHyphen2), 10);
  var day = parseInt(str.substring(idxHyphen2+1, idxT), 10);
  var hour = parseInt(str.substring(idxT+1, idxColon1), 10);
  var minute = parseInt(str.substring(idxColon1+1, idxColon2), 10);
  var second = parseInt(str.substring(idxColon2+1, idxPlusOrMinus), 10);
  var tzHour = parseInt(str.substring(idxPlusOrMinus+1, idxColon3), 10);
  var tzMinute = parseInt(str.substring(idxColon3+1), 10);

  var tzMs = tzSign*1000*60*(60*tzHour + tzMinute);
  var dateMs = Date.UTC(year, month-1, day, hour, minute, second) - tzMs;

  var date = new Date();
  date.setTime(dateMs);
  return date;
}

SoapDeserializer.prototype.xml2NonArrayObject = function(soapXmlNodeList, type) {
  if (type == "string") {
    // TODO: unescape String
    return new String(soapXmlNodeList.item(0).nodeValue);
  }
  
  if (type == "boolean") {
    return new Boolean(soapXmlNodeList.item(0).nodeValue == "true");  
  }
  
  if ((type == "int") || (type == "long") || (type == "float") || (type == "double")) {
    return new Number(soapXmlNodeList.item(0).nodeValue);
  }
  
  if (type == "dateTime") {
    return this.strToDate(soapXmlNodeList.item(0).nodeValue);
  }
  
  // known complex type?
  var typeRecord = this.types[type];
  if (typeRecord) {
    var obj = new Object();
    
    for (var memberIdx = 0; memberIdx < soapXmlNodeList.length; ++memberIdx) {
      var memberNode = soapXmlNodeList.item(memberIdx);
      var memberName = memberNode.nodeName;
      var memberTypeInfo = typeRecord[memberName];

      var memberObj = this.xml2NonArrayObject(
        memberNode.childNodes, memberTypeInfo.type);
        
      if (!memberTypeInfo.isArray) {
        obj[memberName] = memberObj;
      } else {
        if (!obj[memberName]) {
          obj[memberName] = new Array();                
        }
        obj[memberName].push(memberObj);
      }
    }
    
    return obj;
  }
  
  // unknown type
  return null;
}

///////////////////////////////////////////////////////////////////////////////

function SoapWebServiceProxy(url, timeout) {
  this.url = url;
  this.timeout = timeout; // in ms
  this.wsdl = null;
  this.namespace = null;
  this.xsd = null;
  this.soapSerializer = new SoapSerializer();
  this.soapDeserializer = new SoapDeserializer();
}

SoapWebServiceProxy.prototype.init = function(callbackAndContext) {
  dojo.xhrGet( {    
    url: this.url + "?wsdl",
    handleAs: "xml",
    timeout: this.timeout,
    load: this.onWsdl,
    error: function(response, ioArgs) {
      //alert("wsdl error code: " + ioArgs.xhr.status);

      var thisObj = ioArgs.args.thisObj;
      thisObj.callCallback(ioArgs.args.callbackAndContext, {error: true});

      return response;
    },
    sync: false,
    thisObj: this,                          // not a Dojo parameter!
    callbackAndContext: callbackAndContext  // not a Dojo parameter!
  });
}

SoapWebServiceProxy.prototype.callCallback = function(callbackAndContext, response) {
  if (callbackAndContext) {
    var callback = callbackAndContext.callback;
    var context = callbackAndContext.context;
    
    if (callback) {
      callback(response, context);
    }
  }
}

SoapWebServiceProxy.prototype.onWsdl = function(response, ioArgs) {
  var thisObj = ioArgs.args.thisObj;
  var callbackAndContext = ioArgs.args.callbackAndContext;  
  
  thisObj.wsdl = response;
  
  // verify binding (Document is supported, RPC isn't).
  // @see http://www.ibm.com/developerworks/webservices/library/ws-whichwsdl/
  var xml = new XmlElement(response);
  var bindingNode = xml.getElementsByTagNameNS(
    "http://schemas.xmlsoap.org/wsdl/soap/", "binding")[0];
  var bindingStyle = bindingNode.attributes.getNamedItem("style").nodeValue;
  if (bindingStyle != "document") {
    throw new Error("Unsupported binding style");  
  }
  
  // extract namespace
  var definitionsNode = response.getElementsByTagName("definitions").item(0);
  thisObj.namespace = definitionsNode.attributes.getNamedItem("targetNamespace").nodeValue;

  // get types xsd
  var xsdImport = xml.getElementsByTagNameNS(
    XmlElement.xmlSchemaNS, "import")[0];
  var schemaLocation = xsdImport.attributes.getNamedItem("schemaLocation").nodeValue;

  dojo.xhrGet( {    
    url: schemaLocation, 
    handleAs: "xml",
    timeout: thisObj.timeout,
    load: thisObj.onXsd,
    error: function(response, ioArgs) {
      //alert("xsd error code: " + ioArgs.xhr.status);
      
      var thisObj = ioArgs.args.thisObj;
      thisObj.callCallback(ioArgs.args.callbackAndContext, {error: true});
      
      return response;
    },
    sync: false,
    thisObj: thisObj,                       // not a Dojo parameter!
    callbackAndContext: callbackAndContext  // not a Dojo parameter!
  });
  
  return response;
}

SoapWebServiceProxy.prototype.onXsd = function(response, ioArgs) {
  var thisObj = ioArgs.args.thisObj;
  
  thisObj.xsd = response;
  thisObj.soapDeserializer.addTypes(response);
  
  thisObj.callCallback(ioArgs.args.callbackAndContext, {error: false});
  
  return response;
}

SoapWebServiceProxy.prototype.getWebMethodInfo = function(webMethod) {
  // TODO: get from wsdl
  return { input: "ns1:"+webMethod, output: "ns1:"+webMethod+"Response" };
}

SoapWebServiceProxy.prototype.callWebMethod = function(webMethod, params, callbackAndContext) {
  var soapXmlHeader = "<?xml version=\"1.0\" ?>\n<soapenv:Envelope xmlns:soapenv=\"" + XmlElement.soapSchemaNS + "\" xmlns:ns1=\"" + this.namespace + "\"><soapenv:Body>";
  var soapXmlFooter = "</soapenv:Body>";
  
  var webMethodInfo = this.getWebMethodInfo(webMethod);
  var soapXmlBody = this.soapSerializer.object2SoapXml(webMethodInfo.input, params);
  
  var soapXml = soapXmlHeader + soapXmlBody + soapXmlFooter;

  dojo.rawXhrPost( {
    url: this.url,
    handleAs: "xml",
    timeout: this.timeout,
    contentType: "text/xml; charset=utf-8",
    postData: soapXml,
    load: function(response, ioArgs) {
      var thisObj = ioArgs.args.thisObj;
    
      var xml = new XmlElement(response);
      var responseBody = xml.getElementsByTagNameNS(
        XmlElement.soapSchemaNS, "Body")[0];
      var responseMessage = responseBody.childNodes.item(0);
      var responseObject = thisObj.soapDeserializer.soapXml2Object(responseMessage);
      
      thisObj.callCallback(callbackAndContext, {error: false, obj: responseObject});
      
      return response;
    },
    error: function(response, ioArgs) {
      //alert("callWebMethod error code: " + ioArgs.xhr.status);

      var thisObj = ioArgs.args.thisObj;
      thisObj.callCallback(callbackAndContext, {error: true});
      
      return response;
    },
    sync: false,
    thisObj: this // not a Dojo parameter!
  });        
}