/** 
 * This module provides the abstract platform layer for AJAX applications.
 * It's role is to expose browser indepentent services to js applications. All
 * of the XML, HTTP, and XSL objects are encapsulated fully or partially in 
 * their own wrapper objects. 
 * 
 *  Copyright (c) 2007-2009, IHG
 *  All Rights Reserved.
 *  Jonathan McClure
 */

/** alias the root of the at object graph (typically window) */
var dwt_root = this; 

/** 
 * Returns true if object with 'name' is undefined on obj.
 */
function dwt_undef(name, obj){
    if(!obj){ obj = dwt_root; }
    return (typeof obj[name] == "undefined");
}

/** 
 * Create object "a.b.c" namespace. Only creates what's missing.
 */
dwt_mkns = function(ns){
    var names = ns.split(/\./);
    var obj = dwt_root;
    for(var i = 0; i < names.length; ++i) {
      if(dwt_undef(names[i], obj)) {
         obj[names[i]] = {};
      }
      obj = obj[names[i]];
    }
    return obj;
};

/** 
 * create the namespaces in this package
 */
dwt_mkns("dwt.env");
dwt_mkns("dwt.utils");
dwt_mkns("dwt.http");

/** 
 * 
 * @param {String} key is the 
 * @return 
 * @type 
 */
dwt_isIE = function() {
   return !(dwt_undef("ActiveXObject",window))
}

/** 
 * Utility namespace for common services like XML and logging
 */
dwt.utils.Utils = function() {

   /** toggles debug logging */
   var debugLoggingOn = false;

   /** the logger instance */
   var logger;

   this.soapNS = "http://schemas.xmlsoap.org/soap/envelope/";

   /** 
    * Returns a simple formatted date string YYYY-MM-DD.
    * @param {Date} date is the date object
    * @return String holding the format
    */
   this.formatDate = function (date) {
      return date.getYear()  + "-" + 
             date.getMonth() + "-" + 
             date.getDate();
   }

   /** 
    * Returns a simple formatted datetime string YYYY-MM-DD HH:MM:SS.
    * @param {Date} date is the date object
    * @return String holding the format
    */
   this.formatDateTime = function (date) {
      return this.formatDate(date) + " " +
             date.getHour() + ":" + 
             date.getMin() + ":" +
             date.getSeconds(); 
   }

  /** 
   * Returns a new timestamp for the current time in 
   * ISO 8601 format.
   */
   this.new8601DateTime = function() {

     var dt = new Date();
     var m  = dt.getMonth() + 1;  
     var dy = dt.getDate();  
     var h  = dt.getHours();  
     var mn = dt.getMinutes();  
     var s  = dt.getSeconds();  
     return dt.getFullYear() + "-" + 
            (m < 10 ? "0" : "")  + m   + "-" + 
            (dy < 10 ? "0" : "") + dy  + "T" + 
            (h < 10 ? "0" : "")  + h   + ":" +
            (mn < 10 ? "0" : "") + mn  + ":" +
            (s < 10 ? "0" : "")  + s;
   }

   /** 
    * Returns an HTML element for a tag id or null if not found.
    * If the required is specified, then an assert is applied to 
    * the existence of the tag.
    * @param {String} id is the id of the DHTML element 
    * @param {boolean} required is the optional flag to assert 
    * @return an HTML node or null
    */
   this.getHTMLElement = function (id, required) {
      var tag = document.getElementById(id);
      if(required) {
         this.assert(tag,"No such HTML element with id '" + id + "'");
      }
      return tag;
   }

   /** 
    * Returns an HTML form element for a form name and element name. 
    * @param {String} formName is the name of the form 
    * @param {String} elemName is the name of the form element 
    * @return Element from form
    */
   this.getFormElement = function (formName, elemName) {
      var form = document.forms[formName];
      if(form) {
         var elem = form[elemName];
         if (elem) {
            return elem;
         }
      }
      throw Error("No such element '" + elemName + "' for form '" + formName + "'");
   }

   /** 
    * General purpose asserter. A simple alert box is displayed.
    * @param {boolean} condition is condition to assert 
    * @param {String} msg is the assertion message 
    */
   this.assert = function (condition, msg) {
       if (!condition) alert("ASSERT ERROR: " + msg);
   }

   /** 
    * Returns a compiled XSL Template object for a path
    * @param {String} xslFileName is the path name to the XSL file 
    * @return XmlTemplate
    */
   this.getXSLTemplate = function (xslFileName) {
      var xslTemplate = null;
      // load it the IE way
      if(dwt_isIE()) {
         // XSLTemplate requires free threaded document
         var xsl = new ActiveXObject(this.getActiveXName("FreeThreadedDOMDocument"));
         xsl.async = false;
         xsl.load(xslFileName);
         if(xsl.readyState != 4) {
            alert("Document not ready");         
         }
         else if(!xsl.documentElement) {
            alert("Failed to load XML document (" + xslFileName + ")");      
         }
         else {
            xslTemplate = new ActiveXObject(this.getActiveXName("XSLTemplate"));
            xslTemplate.stylesheet = xsl;
         }
       }
       else { // for mozilla, just parse the XSL into a DOM
          xslTemplate = this.XML("url("+xslFileName+")");
       }
       return xslTemplate;
   }

   /** 
    * Performs an XSLT transform on an XML document to return HTML content 
    * @param {Node} xml is the XML node to transform 
    * @param {XmlTemplate} xsl is the compiled XslTemplate 
    * @param {Array} params is an optional map of XSL parameters 
    * @return HTML content as a string
    */
   this.transform = function (xml, xsl, params) {
     var result = "";
     // for IE
     if (dwt_isIE()) {
       //var xml = dwutils.XML(xmlRoot);
       var processor = xsl.createProcessor();
       processor.input = xml;
       if(params) {
         for (var i = 0; i < params.length; i++) {
             processor.addParameter(params[i][0], params[i][1]);
         }
       }
       processor.transform();
       result = processor.output;
     } 
     else { // for mozilla
        try{
            var processor = new XSLTProcessor();
            processor.importStylesheet(xsl);
            if(params) {
               for (var i = 0; i < params.length; i++) {
                   processor.setParameter(null, params[i][0], params[i][1]);
               }
            }
            var newDoc = processor.transformToDocument(xml);
            result = this.xmlToString(newDoc);
            
        }
        catch(e){alert("Error: processing stylesheet "+e.toString());}        
     }
     return result;
   }

   /** 
    * Returns the base portion of the URL that loaded the current document. 
    * @return prefix of the URL
    */
   this.getBaseHttpUrl = function() {
      var url = 
            document.location.protocol + 
           "//" + document.location.host +  
             "/" + this.getWebContext();
      return url;
   }

   /** 
    * Return the web context, just after the host and port for the URL
    * that loaded the current document.
    * @param {String} key is the 
    * @return the name of the web context
    */
   this.getWebContext = function () {
     var webctx = "";
     var url = unescape(document.location.href);
     var pos = url.indexOf("://");
     if ( pos >= 0 ) {
       var urlsplit = url.substring(pos+3).split("/");
       if ( urlsplit.length > 2 )
         webctx = urlsplit[1];
     }
     return webctx;
   }

   /** 
    * Returns the host name of the URL for the current document. 
    * @return the host name
    */
   this.getWebHost = function () {
      document.location.host;
   }

   /** 
    * Enables or disables debug logging 
    * @param {boolean} enabled toggles logging
    */
   this.enableLogDebug = function (enabled) {
      debugLoggingOn = enabled;
   }

   /** 
    * Initializes the logging
    */
   var initLog = function () {
     if(logger === undefined) {
        // Disable logging: avoid infinite recursion due to log statements
        // within requireModule
        debugLoggingOn = false;
        logger = taEngine.requireModule("logConsole/logConsole.js");
        debugLoggingOn = true;
        logger.activate();
     }
   };

   /** 
    * Function for the application to use to log messages 
    * @param {String} msg is the message to log
    * @param {boolean} clear is the optional flag to clear the log output
    */
   this.logDebug = function (msg,clear) {
      if(debugLoggingOn) {
         initLog();
         if(clear) {
            logger.clear();
         }
         logger.log(msg);
      }
   };

   /** 
    * Returns the X and Y coordinates for the current scroll 
    * @return Object with offsetX and offsetY members 
    */
   this.getScrollXY = function() {
      var offsetX = 1; 
      var offsetY = 1;
      if( typeof( window.pageYOffset ) == 'number' ) {
         offsetY = window.pageYOffset;
         offsetX = window.pageXOffset;
      }
      else if( at_isIE() ) {
         offsetY = document.documentElement.scrollTop;
         offsetX = document.documentElement.scrollLeft;
      } 
      return { x : offsetX, y : offsetY };
   }

   /** 
    * Sets the X and Y coordinates for the current scroll 
    * @param {int} x is the X scroll offset 
    * @param {int} y is the Y scroll offset 
    */
   this.setScrollXY = function(x, y) {
      window.scrollTo(x, y);
   }

   /** 
    * Returns an Object for an ActiveX name (for MS IE only) 
    * @param {String} suffix is the last part of the name 
    * @return Object 
    */
   this.getActiveXName = function(suffix) {
     if(dwt_undef("activeXNames",this)) { 
        this.activeXNames = {};
     }
     var name = this.activeXNames[suffix];
     if (!name) {
        var prefixes = ["MSXML4", "MSXML3", "MSXML2", "MSXML", "Microsoft"];
        for (var i = 0; i < prefixes.length && !name; i++) {
          try {
             var tryname = prefixes[i] + "." + suffix;
             var o = new ActiveXObject(tryname);
             name = tryname;
          } catch (ex) { };
        }
     }
     
     if(!name)
        throw Error("Could not find a ActiveX Object for suffix: " + suffix);
     return this.activeXNames[suffix] = name;
   }

   /** 
    * Returns the full text of an element by concatinating
    * all of the child text nodes. There is no portable method
    * on the DOM node for this.
    * @param {Node} node is a DOM node 
    * @return String text of the element contents
    */
   this.xmlNodeText = function (node) {
      var s = "";
      for (var i=0; i < node.childNodes.length; i++) {
         var cnode = node.childNodes[i];
         if (cnode.nodeType == 3) {
            s += cnode.nodeValue;
         }
      }
      return s;
   }

   /** 
    * Serializes an XML node to a simple string
    * @param {Node} node is a DOM node 
    * @return String of the XML
    */
   this.xmlToString = function (node) {
      var s = "<none/>";
      if(node) {
         // for IE
         if(dwt_isIE() && node.xml) {
            s = node.xml;
         }
         else { // for Mozilla
            var ser = new XMLSerializer();
            s = ser.serializeToString(node);                        
         }
      }
      return s;
   }

   /** 
    * Returns a single Node for an xpath expression or null if not found
    * @param {Node} node is the parent node to search 
    * @param {String} xpath is xpath expression 
    * @param {Map} nsMap is the optional set of namespace mappings
    * @return Node or null
    */
   this.select = function (node,xpath,nsMap) {
      var list = new Array();
      if(node) {
         // for IE
         if(dwt_isIE()) {
            if (nsMap) {
               var doc = node.ownerDocument;
               setNSMap(doc, nsMap);
            }
            var nodes = node.selectNodes(xpath);
            if(nodes) {
               for(i=0; i < nodes.length; i++) {
                  list.push(nodes[i]);
               }
            }
         }
         else { // for Mozilla
           var doc = node.ownerDocument;
           var nsr = (nsMap ? createNSResolver(nsMap) : null);
           var nodes = doc.evaluate(xpath, node, nsr, XPathResult.ANY_TYPE, null);
           if(nodes) {
              var node = nodes.iterateNext();
              while(node) {
                 list.push(node);
                 node = nodes.iterateNext();
              }
           }
         }
      }
      return list;
   }

   /** 
    * Returns a list of Nodes for an xpath expression
    * @param {Node} node is the parent node to search 
    * @param {String} xpath is xpath expression 
    * @param {Map} nsMap is the optional set of namespace mappings
    * @return Array holding nodes
    */
   this.selectSingle = function (node, xpath, nsMap) {
      var result = null;
      if(node) {
         // for IE
         if(dwt_isIE() && node.xml) {
            if (nsMap) {
               var doc = node.ownerDocument;
               setNSMap(doc, nsMap);
            }
            result = node.selectSingleNode(xpath);
         }
         else { // for Mozilla
           var doc = node.ownerDocument;
           this.assert(doc,"selectSingle() : No owner doc on node. Probably not an XML node.");
           if (doc) {
              var nsr = (nsMap ? createNSResolver(nsMap) : null);
              var nodes = doc.evaluate(xpath, node, nsr, XPathResult.ANY_TYPE, null);
              if(nodes) {
                 result = nodes.iterateNext();
              }
           }
         }
      }
      return result;
   }

   /** 
    * Return an XML DOM document for an xml string. If the 
    * string is "url(..)", then the XML is downloaded and then
    * parsed.
    * @param {String} str is the XML content, or a URL 
    * @return Node
    */
   this.XML = function (str) {
      if(dwt_isIE()) {
         var island = new ActiveXObject(dwutils.getActiveXName("DOMDocument"));
         island.setProperty("SelectionLanguage", "XPath");
         island.async = false;
         if (str) {
           if (str.substring(0, 4) == "url(" &&  str.substring(str.length-1) == ")") {
              this.url = str.substring(4, str.length-1);
              island.load(this.url);
           } 
           else { 
              island.loadXML(str);
           }
         }
      }
      else { // support for mozilla
         if (str) {           
           if (str.substring(0, 4) == "url(" &&  str.substring(str.length-1) == ")") {
              this.url = str.substring(4, str.length-1);
              var xmlDoc = document.implementation.createDocument("", "xsldoc", null);
              xmlDoc.async = false;
              xmlDoc.load(this.url);
              island = xmlDoc;
           } 
           else { 
              var p = new DOMParser();
              island = p.parseFromString(str,"text/xml");
           }
         }
      }
      if(island.parseError) {
          if (island.parseError != 0) {
            alert("XML island: " + this.url + "\n\n" +
              island.parseError.reason + "\n" +
              "at char: " + island.parseError.linepos + 
               " in line: " + island.parseError.line +
              " near:\n" + island.parseError.srcText + "\n" +"");
          }
      }
      else {
         if(!island.documentElement) {
            alert("no document after parse");
         }
         else {
            var node = island.documentElement;
            if(node.nodeName == "parsererror") {
               alert("XML Parse error occured");
            }
         }
      }
      return island.documentElement;
   }

   /** 
    * Sets the namespace prefix mapping on an MSXML document. This 
    * is used by the xpath select() and selectSingle() functions.
    * @param {Document} doc is the document to set the namespace on
    * @param {Map} nsMap is the optional set of namespace mappings
    */
   function setNSMap(doc, nsMap) {
      var nsList = "";
      for (pfx in nsMap) {
         nsList += "xmlns:" + pfx + "='" + nsMap[pfx] + "' ";
      }
      doc.setProperty("SelectionNamespaces", nsList);
   }

   /** 
    * Construct an NSResolver function for Mozilla. This
    * is used by the xpath select() and selectSingle() functions.
    * @param {nsMap} is the map of prefixes and uris
    */
   function createNSResolver(nsMap) {
     
     // add the function required by Mozilla
     var nsr = function(pfx) {
        var ns = this.namespaces[pfx];
        if (!ns) {
           // mozilla will hide any error, so we provide one with a simple alert
           alert("No namespace mapped for prefix '" + pfx + "'");
           ns = null;
        }
        return ns;
     }
     
     // add the map of namespaces to the function object
     nsr.namespaces = nsMap;

     // return the function
     return nsr;
   }
}

/** singleton alias of the utils package */
var dwutils = new dwt.utils.Utils();

/** 
 * Constructs a new HTTP object using a URL and an optional callback object.
 * @constructor
 * @param {String} baseURL is the url relative to the current document, or a full URL
 * @param {Callback} callback is the optional callback for Async notifications
 */
dwt.http.HTTP = function(baseURL,callback) {

   /** flag to indicate the IO should be logged */
   this.logIO = true;

   /** the base url for the content paths */
   this.baseURL = baseURL;

   /** 
    * Returns the string content for the URL  
    * @param {String} path is the URL relative to this HTTP object's URL 
    * @return String content 
    */
   this.get = function (path) {
      var url = this.baseURL + path;
      try {
         var httpObj = prepareHttp("GET", url);
         // if there is a callback registered, then set up the 
         // handler for the result (even if not async)
         httpObj.send(null);
      }
      catch (e) {
         throw Error("Error sending HTTP request @ " + url)
      }

      // return the result 
      return getResponseData(httpObj);
   }

   /** 
    * Performs an HTTP-XML POST to a server using a Node as the payload 
    * @param {Node} request is the full XML node to send 
    * @return Node as the XML result
    */
   this.postXML = function (request) {
      // make sure this is an XML node
      if(!request.nodeName) {
         throw Error("The dwt.http.HTTP.postXML() was passed a non-xml argument");
      }
      var xml = dwutils.xmlToString(request);
      if(this.logIO) {
         dwutils.logDebug("[http] postXML() request : " + xml);
      }
      // make the request
      try {
         var httpObj = prepareHttp("POST", this.baseURL);
         httpObj.send(xml);
      }
      catch (e) {
         throw Error("Could not connect to server @ " + this.baseURL + " - " + e.toString());
      }

      // validate the response data if this is synchronous, otherwise
      // just return a null;
      var resultNode = null;
      var resultData = getResponseData(httpObj);
      var resultNode = dwutils.XML(resultData);
      if(this.logIO) {
         dwutils.logDebug("[http] postXML() response : " + resultData);
      }
      return resultNode;
   }

   /** 
    * Performs a SOAP POST to a server using a Node as the payload 
    * within the body of the SOAP enevlope. The result node is taken 
    * out of the response and returned.
    * @param {Node} request is the full XML node to send 
    * @return Node as the XML result
    */
   this.callWS = function (request) {

      // create the standard SOAP envelope with header and body
      var env = 
         dwutils.XML(
           "<soap:Envelope xmlns:soap='" + dwutils.soapNS + "'/>");
      var doc = env.ownerDocument;
      var header = doc.createElement("soap:Header");
      env.appendChild(header);
      var body = doc.createElement("soap:Body");
      env.appendChild(body);

      // add the request payload to the body
      body.appendChild(request);

      // send the soap as the request in the normal way
      env = this.postXML(env);

      // get the body
      body = dwutils.selectSingle(env,"./soap:Body", {"soap":dwutils.soapNS});
      if (!body) {
         throw Error('No body on SOAP response');
      }
      
      // check for the SOAP fault! 
      var fault = dwutils.selectSingle(body,"soap:Fault", {"soap":dwutils.soapNS});
      if (fault) {
         var fc = dwutils.selectSingle(fault,"faultcode", {"soap":dwutils.soapNS});
         if (fc) {
            var msg = "SOAP Fault Occured : " + dwutils.xmlNodeText(fc);
            var fs = dwutils.selectSingle(fault,"faultstring", {"soap":dwutils.soapNS});
            if (fs) {
               msg += " - " + dwutils.xmlNodeText(fs);
            }
            throw Error(msg);
         }
      }
               
      // get the payload of the body
      var rs = dwutils.selectSingle(body,"*");
      if (!rs) {
         throw Error('No response in SOAP response');
      }

      return rs;
   }

   /** 
    * Returns an XML document for a path relative to the url for the HTTP object  
    * @param {String} path is the suffix for the base url 
    * @param {String} async is the flag to indicate async 
    * @param {String} callbackId is the id of the callback data for the results 
    * @return Node 
    */
   this.getXML = function (path,async,callbackId) {
      var content = this.get(path,async,callbackId,true/*xml*/);
      if(!async) {
         return dwutils.XML(content)
      }
   }

   /** 
    * Returns the response object and throws an exception is it is not a 200
    * @param {HTTP} httpObj is the HTTP object 
    * @return String content from HTTP response
    */
   function getResponseData(httpObj) {
      var status = httpObj.status;
      if(status != 200 && status != 0) {
         throw Error("HTTP (" + status + ") " + httpObj.statusText);
      }
      return httpObj.responseText;
   }

   /** 
    * Private function to reate and return a fully configured HTTP object equiped
    * with authentication headers, url, method and other options.
    * @param {String} method is GET or POST 
    * @param {String} url is the base url to use 
    * @return HTTP objects
    */
   function prepareHttp(method, url) {
      var loadURL = document.location.toString();
      var isFileSystem = (loadURL.indexOf("file:") == 0);
      // specially setup if we are we running off the file system
      if (isFileSystem) {
         // for mozilla, we have to enable the ability to use non-local urls 
         if (!dwt_isIE()) {
            try {
              netscape.security.PrivilegeManager.enablePrivilege("UniversalBrowserRead");
            } 
            catch (e) {
              throw Error("DWT: Permission UniversalBrowserRead denied.");
            }
         }
      }
      var httpObject = newXmlHttp();
      httpObject.open(method, url, false);
      return httpObject;
   }

   /** 
    * Private factory method to create a new XmlHttpRequest object  
    * @return XMLHttpRequest
    */
   function newXmlHttp() {
      var httpObject = null;
      try {
         if (dwt_isIE()) {
            httpObject = new ActiveXObject(dwutils.getActiveXName("XmlHTTP"));
         }
         else { // Mozilla
            httpObject = new XMLHttpRequest();
         }
      }
      catch (e) {}

      if(!httpObject) {
         alert("We cannot find your browser's XmlHttp object");
      }

      return httpObject;
   }
}

/** 
 * Utility namespace for ESF 
 */
dwt.ESF = function() {

   /** the namespace for ESF types */
   var esfNS = "http://www.ihg.com/esf/v1.0";

   /** the namespace mapping for ESF types */
   var esfNSMap = {"esf":esfNS};

   /** 
    * Adds the standard ESF headers to a given request
    * @param {XML} request node
    */
   this.initRequest = function(rq) {

      var h = dwutils.XML("<esf:header xmlns:esf='" + esfNS + "'/>");
      var doc = h.ownerDocument;
      
      var n = doc.createElement("esf:language");
      n.setAttribute("isoCountryCode", "USA");
      n.setAttribute("isoLanguageCode", "eng");
      h.appendChild(n);
      
      n = doc.createElement("esf:channel");
      n.setAttribute("name", "USA");
      h.appendChild(n);

      n = doc.createElement("esf:headerVersion");
      n.setAttribute("build", "1");
      n.setAttribute("major", "1");
      n.setAttribute("minor", "0");
      h.appendChild(n);

      n = doc.createElement("esf:environment");
      n.setAttribute("name", "Prod");
      h.appendChild(n);

      n = doc.createElement("esf:transactionId");
      n.setAttribute("id", "123");
      h.appendChild(n);

      n = doc.createElement("esf:transactionLogging");
      n.setAttribute("level", "info");
      h.appendChild(n);

      n = doc.createElement("esf:sendTimeStamp");
      var tm = doc.createTextNode("2001-12-17T09:30:47.0Z");
      n.appendChild(tm);
      h.appendChild(n);
      
      // the header must always be the first element
      var f = rq.firstChild;
      if (f) {
         rq.insertBefore(h, f);
      }
      else {
         rq.appendChild(h);
      }

      return rq;
   }

   /** 
    * Inspects the ESF headers of a given response and throws an 
    * Error if there was a failure detected. 
    * @param {XML} request node
    */
   this.checkResponse = function(rs) {
      var tranStatus = dwutils.selectSingle(rs,"esf:header/esf:transactionStatus",esfNSMap);
      if (!tranStatus) {
         throw Error("No esf:transactionStatus node found on response");
      }
      if ("true" != tranStatus.getAttribute("transactionSuccessful")) {
         var msg = tranStatus.getAttribute("overallStatusDescription");
         var errorStatus = dwutils.selectSingle(tranStatus,"esf:errors/esf:status",esfNSMap);
         if (errorStatus) {
            msg += " - " + errorStatus.getAttribute("statusDescription");
         }         
         throw Error(msg);
      }
      return rs;
   }

   /** 
    * Utility method to send an ESF based request to a webservice
    * and return the response. If there is an error in the response
    * headers, an Error is thrown.     
    * @param {String} url the service url
    * @param {XML} rq is the request node
    * @return {XML} response node
    */
   this.sendRQ = function(url, rq) {
       var http = new dwt.http.HTTP(url);
       return this.checkResponse( http.callWS( this.initRequest(rq) ) );
   }
}

/** singleton alias of the esf utils package */
var esfutils = new dwt.ESF();

