/*****************************************************************************
 * $Header$
 * $Author$
 * $Revision$
 * $Date$
 *
 * Helper class to handle XML.
 *
 * Copyright: Neolane 2001-2007
 *****************************************************************************/

/** constant definitions */
var XML = { XPATH_AXE_NONE: 0, XPATH_AXE_CHILD: 1, XPATH_AXE_DESCENDANT: 2, 
  XPATH_AXE_DESCENDANT_OR_SELF: 3, XPATH_AXE_PARENT:4,
  ELEMENT_NODE:1, ATTRIBUTE_NODE:2, TEXT_NODE:3, CDATA_SECTION_NODE:4, 
  ENTITY_REFERENCE_NODE:5, ENTITY_NODE:6, PROCESSING_INSTRUCTION_NODE:7, 
  COMMENT_NODE:8, DOCUMENT_NODE:9, DOCUMENT_TYPE_NODE:10, 
  DOCUMENT_FRAGMENT_NODE:11, NOTATION_NODE:12 };
 
/** Create a DOMDocument from a qualified name
  *
  * @strQualifiedName */
function newDOMDocument(strQualifiedName)
{
  var xmlDoc = null;
  if (window.ActiveXObject)
  { // ActiveX method
    xmlDoc = new ActiveXObject("Microsoft.XMLDOM");
    if (strQualifiedName != null && strQualifiedName.length > 0)
    { // Add the root element
      elemRoot = xmlDoc.createElement(strQualifiedName);
      xmlDoc.documentElement = elemRoot;
    }
  }
  else if (document.implementation)
  {
    if (document.implementation.createDocument)
      xmlDoc = document.implementation.createDocument("", strQualifiedName, null);
  }
  if (xmlDoc == null)
    throw Error(xtk_core.xml_dom_implementation_not_found());
  return xmlDoc;
}

/** Find the first child element of the given node.
  *
  * @nd an xml node.
  * @return the first child elment of the given node. null if the given node
  *         does not have a child element. */
function firstChildElement(nd)
{
  var ndChild = nd.firstChild;
  while ( ndChild != null )
  {
    if ( ndChild.nodeType == 1 /* ELEMENT_NODE */ )
      break;
      
    ndChild = ndChild.nextSibling;  
  }
  
  return ndChild;
}

/** Find next sibling element of the given node.
  *
  * @nd an xml node.
  * @return the next sibling element of the given node. null if the given node
  *         does not have a sibling element. */
function nextSiblingElement(nd)
{
  var ndSibling = nd.nextSibling;
  while ( ndSibling != null )
  {
    if ( ndSibling.nodeType == 1 )
      break;
      
    ndSibling = ndSibling.nextSibling;  
  }
  
  return ndSibling;
}

/** Find XML elements from the given XPath
  *
  * @ndElement a xml element.
  * @strPath   a XPATH relative to ndElement.
  * @return a XtkVector of elements. */
function findNodes(ndElement, strXPath)
{
  function internalFindNodes(strParentXPath, ndElement, strXPath, nodesArray)
  {
    if ( ndElement == null )
      return;

    var strXPathElement;
    var ndChild = firstChildElement(ndElement);
    while ( ndChild != null )
    {
      strXPathElement = strParentXPath + ndChild.nodeName;
      if ( strXPathElement == strXPath )
        // element found
        nodesArray.push(ndChild);
      else
        // recurse on children
        internalFindNodes(strXPathElement + "/", ndChild, strXPath, nodesArray)

      ndChild = nextSiblingElement(ndChild);      
    }
  }

  var nodesArray = new Array();
  if ( strXPath == "." )
    // special case of the node itself
    nodesArray.push(ndElement);
  else 
    internalFindNodes("", ndElement, strXPath, nodesArray);
    
  return nodesArray;
}

/** Find XML elements from the given XPath
  *
  * @ndElement a xml element.
  * @strPath   a XPATH relative to ndElement.
  * @return the first element which match with the given XPATH. */
function findElement(ndElement, strXPath)
{
  function internalFindElement(strParentXPath, ndElement, strXPath, iSearchCollectionIndex)
  {
    if ( ndElement == null )
      return null;
    
    var strXPathElement;
    var ndFound, ndChild = firstChildElement(ndElement);    
    var iCollectionIndex = 0
    while ( ndChild != null )
    {
      strXPathElement = strParentXPath + ndChild.nodeName;
      if ( strXPathElement == strXPath )
      {
        if( ++iCollectionIndex == iSearchCollectionIndex )
          // element found
          return ndChild;
      }
      else
      { // recurse on children
        ndFound = internalFindElement(strXPathElement + "/", ndChild, strXPath, iSearchCollectionIndex)
        if ( ndFound != null )
          return ndFound;
      }
      
      ndChild = nextSiblingElement(ndChild);      
    }
  }
  
  var strParentXPath = "";

  if ( strXPath == "." )
    // special case of the node itself
    return ndElement;
  else if ( strXPath.charAt(0) == '/' )
  {
    if ( ndElement.ownerDocument != null )
      // the given element is not located at the root of the document
      ndElement = ndElement.ownerDocument;
      
    strParentXPath = '/';
  }
  var iSearchCollectionIndex = 1 // index in the collection if applicable
  var iBracketStart = strXPath.indexOf('[')
  if ( iBracketStart != -1 )
  { // this is a collection
    iSearchCollectionIndex = parseInt(strXPath.substring(iBracketStart+1, strXPath.indexOf(']')))
    strXPath = strXPath.substring(0, iBracketStart)
  }
    

  return internalFindElement(strParentXPath, ndElement, strXPath, iSearchCollectionIndex);
}

/** Find the first element that have the name required.
  *
  * @node the node to search in
  * @strName the name of the element we're searching for
  * @return the found element or null. */
function findChildElement(node, strName)
{
  child = node.firstChild;
  while (child != null) 
  {
    if (child.nodeType == 1 && child.nodeName == strName)
      return child;
    child = child.nextSibling;
  }
  return null;
}

/** Return the value contained in an element node like this :
  * <node>toto</node>
  *
  * @elemCurrent the DOM element containing the value
  * @return the value */
function elementValue(elemCurrent)
{
  strText = "";
  child = elemCurrent.firstChild;

  while( child != null )
  {
    if( child.nodeType == 3
      || child.nodeType == 4)
      strText += child.nodeValue;

    child = child.nextSibling;
  }

  return strText;
}

/** Get a value of the node from a XPath
  *
  * Supported XPATH are : 
  * 
  *   node                  # a child element
  *   @attribute            # an attribute of the current element
  *   node/@attribute       # an attribute of a child node
  *   //node/@attribute     # '//' for descendant or self
  *
  * @node the node containing the requested value.
  * @strXPath the path of requested value.
  * @return the value as a string. */
function getXPathValue(node, strXPath)
{
  function PrivateXPathValue(node, strXPath, axe)
  {
    if ( strXPath.length == 0 )
    { // case of the value of an element
      var textNode, value = ""
      for (var i=0; i < node.childNodes.length; i++)
      {
        textNode = node.childNodes.item(i)
        if ( textNode.nodeType == XML.TEXT_NODE 
          || textNode.nodeType == XML.CDATA_SECTION_NODE)
          value += textNode.nodeValue
      }
      
      return value
    }
      
    if ( strXPath.charAt(0) == '@' )
    {
      if ( node.nodeType == XML.DOCUMENT_NODE )
        node = node.documentElement
      return node.getAttribute(strXPath.substring(1));
    }
      
    var iSepIndex = strXPath.indexOf('/');
    if ( iSepIndex != 0 )
    {
      var strLeftPart, strRightPart = "";
      var iCollectionIndex = 0, iSearchCollectionIndex = 1 // index in the collection if applicable
      if ( iSepIndex == -1 )
        // end of XPath reached
        strLeftPart = strXPath;
      else
      {
        strLeftPart   = strXPath.substring(0, iSepIndex);
        strRightPart  = strXPath.substring(iSepIndex+1);
      }
      
      var bracketStart = strLeftPart.indexOf('[')
      if ( bracketStart != -1 )
      { // this is a collection
        iSearchCollectionIndex = parseInt(strLeftPart.substring(bracketStart+1, strLeftPart.indexOf(']')))
        strLeftPart = strLeftPart.substring(0, bracketStart)
      }
      
      var nChild = node.childNodes.length; 
      for (var i=0; i < nChild; i++)
        if ( node.childNodes[i].nodeType == 1 
          && (axe == XML.XPATH_AXE_DESCENDANT_OR_SELF 
            || (node.childNodes[i].nodeName == strLeftPart && ++iCollectionIndex == iSearchCollectionIndex)) )
          return PrivateXPathValue(node.childNodes[i], strRightPart, axe);
    }
  
    return ""; // not found
  }
  
  var axe = XML.XPATH_AXE_NONE;  
  if ( strXPath.charAt(0) == '.' )
    if ( strXPath.charAt(1) == '.' )
      axe = XML.XPATH_AXE_PARENT;
    else
      axe = XML.XPATH_AXE_SELF;
  else if ( strXPath.charAt(0) == '/' && strXPath.charAt(1) == '/' )
  {
    axe = XML.XPATH_AXE_DESCENDANT_OR_SELF;
    strXPath = strXPath.substring(2);
  }
  else if ( strXPath.charAt(0) == '/' )
  { // starting at the root of the document
    if ( node.ownerDocument != null )
      node = node.ownerDocument;
    strXPath = strXPath.substring(1);
  }
  
  return PrivateXPathValue(node, strXPath, axe);
}

function setXPathValue(node, strXPath, strValue, asCDATA)
{
  if ( strXPath.charAt(0) == '/' )
  { // starting at the root of the document
    if ( node.ownerDocument != null )
      node = node.ownerDocument;
      
    strXPath = strXPath.substring(1);
  }
  else if ( strXPath.length == 0 )
  { // case of the value of an element
    while ( node.firstChild != null )
      node.removeChild(node.firstChild)
    if ( typeof asCDATA != undefined && asCDATA )
      node.appendChild(node.ownerDocument.createCDATASection(strValue));
    else        
      node.appendChild(node.ownerDocument.createTextNode(strValue));
    return;
  }
    
  if ( strXPath.charAt(0) == '@' )
  {
    if ( (strValue == undefined || strValue.length == 0) 
      && node.attributes.getNamedItem(strXPath.substring(1)) == undefined )
      // nothing to do => the value is empty and the attribute does not exist
      return;

    return node.setAttribute(strXPath.substring(1), strValue);
  }
    
  var iSepIndex = strXPath.indexOf('/');
  if ( iSepIndex != 0 )
  {
    var strLeftPart, strRightPart = "";
    var iCollectionIndex = 0, iSearchCollectionIndex = 1 // index in the collection if applicable
    if ( iSepIndex == -1 )
      // end of XPath reached
      strLeftPart = strXPath;
    else
    {
      strLeftPart   = strXPath.substring(0, iSepIndex);
      strRightPart  = strXPath.substring(iSepIndex+1);
    }
    
    var bracketStart = strLeftPart.indexOf('[')
    if ( bracketStart != -1 )
    { // this is a collection
      iSearchCollectionIndex = parseInt(strLeftPart.substring(bracketStart+1, strLeftPart.indexOf(']')))
      strLeftPart = strLeftPart.substring(0, bracketStart)
    }
    
    var nChild = node.childNodes.length; 
    for (var i=0; i < nChild; i++)
      if ( node.childNodes[i].nodeType == 1 
        && node.childNodes[i].nodeName == strLeftPart )
      {
        if ( ++iCollectionIndex == iSearchCollectionIndex )
          // found 
          return setXPathValue(node.childNodes[i], strRightPart, strValue, asCDATA);
      }
        
    if ( strLeftPart.length > 0 )
    { // element not exists
      if ( strValue == undefined || strValue.length == 0 )
        // the value to set is empty => we can stop here
        return
      
      // creating missing elements
      var newNode
      do
      {
        newNode = node.appendChild(node.ownerDocument.createElement(strLeftPart))
      } while ( ++iCollectionIndex < iSearchCollectionIndex )
      setXPathValue(newNode, strRightPart, strValue, asCDATA);
    }
  }
}

/** copies attributes from a ref node to a new one
  *
  * @ndNewNode node that will hold the newly added attributes
  * @ndRefNode node that contains attribute values
  * @applyAttributes function to apply attributes 
  **/
function internalCopyAttributes(ndNewNode, ndRefNode, applyAttribute)
{
  for(var i = 0; i < ndRefNode.attributes.length; i++)
  {
    if( applyAttribute == null )
      ndNewNode.setAttribute(ndRefNode.attributes[i].name, ndRefNode.attributes[i].value)
    else
      applyAttribute(ndNewNode, ndRefNode.attributes[i].name, ndRefNode.attributes[i].value)
  }
}

/** creates another node compatible with document threading model
  *
  * @ndMainDocument destination document
  * @ndNode to copy
  * @bDeep should we use deepCopy or not
  * @bUseNativeAPI should we use the internal API or not
  * @applyAttribute function to override default setAttributeMethod
  * @bEvalScriptNodes in case a node with nodeName SCRIPT is found 
  *  the nodeValue of its content is evaluated
  **/
function internalImportNode(ndMainDocument, ndNode, bDeep, bUseNativeAPI, 
                            applyAttribute, bEvalScriptNodes)
{
  if( !ndMainDocument.importNode || !bUseNativeAPI )
  {
    var ndNew;
    if( ndNode.nodeType == 1 )
    {
      // ## IE kludge
      if( ndNode.nodeName.toLowerCase() == "input" &&
          ndNode.getAttribute("type").toLowerCase() == "radio" && document.all )
        ndNew = ndMainDocument.createElement('<input type="radio" name="' + ndNode.getAttribute("name") + '" value="' + ndNode.getAttribute("value") + '">')
      else 
        ndNew = ndMainDocument.createElement(ndNode.nodeName)
      internalCopyAttributes(ndNew, ndNode, applyAttribute)
    }
    else if( ndNode.nodeType == 3 )
      ndNew = ndMainDocument.createTextNode(ndNode.nodeValue)

    if( bDeep && ndNode.hasChildNodes() )
    {
      for(var ndChild = ndNode.firstChild; ndChild != null; ndChild = ndChild.nextSibling)
      {
        if( bEvalScriptNodes && ndChild.nodeName.toLowerCase()=="script" )
        { 
          if( ndChild.childNodes.length >= 2 )
          // FF kludge: Firefox doesn't allow nodeValue with size > 4096. 
          // Thus, it creates a  node with size 4096 and new nodes containing the rest of the data.
          // textContent attribute retrieves full data. ONLY FIREFOX.
            eval(ndChild.textContent);
          else
            eval(ndChild.firstChild.nodeValue)
          continue;
        }
        if( !bUseNativeAPI && ndNew.nodeName.toLowerCase()=="a" && ndChild.nodeName.toLowerCase()=="p" )
        {
          // a tag <P> below a <A> is not XHTML valid - inline P content in A
          // add attributes 
          internalCopyAttributes(ndNew, ndChild, applyAttribute)
          // Add the content
          ndNewChild = internalImportNode(ndMainDocument, ndChild, bDeep, bUseNativeAPI, applyAttribute, bEvalScriptNodes)
          for(var ndTextChild = ndNewChild.firstChild; ndTextChild != null; ndTextChild = ndTextChild.nextSibling)
            ndNew.appendChild(ndTextChild)
          continue;
        }
        ndNewChild = internalImportNode(ndMainDocument, ndChild, bDeep, bUseNativeAPI, applyAttribute, bEvalScriptNodes)
        if( ndNewChild != undefined && ndNewChild != null )
          ndNew.appendChild(ndNewChild)
      }
    }
    return ndNew;
  }
  else
    return ndMainDocument.importNode(ndNode, bDeep)
}

/** creates another node compatible with document threading model
  *
  * @ndMainDocument destination document
  * @ndNode to copy
  * @bDeep should we use deepCopy or not
  **/
function importNode(ndMainDocument, ndNode, bDeep)
{
  return internalImportNode(ndMainDocument, ndNode, bDeep, true, null, false)
}

/** creates another node compatible with document HTML
  * @ndNode to copy
  * @bDeep should we use deepCopy or not
  **/
function importNodeAsHTML(ndNode, bDeep)
{
  return internalImportNode(document, ndNode, bDeep, false, applyHTMLAttribute, true)
}

/** Basic copy of attributes except kludges depending on the browser
  * IE kludge for style attribute and more to come....
  * @ndNode to copy
  * @bDeep should we use deepCopy or not
  **/
function applyHTMLAttribute(ndHTMLElement, sAttribute, sAttributeValue)
{
  // IE and FF kludges
  if( sAttribute=="style" && document.all )
    ndHTMLElement.style.setAttribute("cssText", sAttributeValue)
  else if( sAttribute=="class" )
    ndHTMLElement.className = sAttributeValue
  else if( sAttribute=="dataBind" )
  {
    if( ndHTMLElement.getAttribute("type").toLowerCase() == "checkbox" ||
        ndHTMLElement.getAttribute("type").toLowerCase() == "radio" )
    {
      ndHTMLElement.onValueChange = new Function("xpath", "newValue", "if( xpath == '" + sAttributeValue + "' ) this.checked = Format.parseBoolean(newValue);")
      ndHTMLElement.onSubmit = new Function("document.controller.setValue('"+sAttributeValue+"', this.checked);")
    }
    else
    {
      ndHTMLElement.onValueChange = new Function("xpath", "newValue", "if( xpath == '" + sAttributeValue + "' ) this.value = newValue;")
      ndHTMLElement.onSubmit = new Function("document.controller.setValue('"+sAttributeValue+"', this.value);")
    }
    document.controller.registerObserver(sAttributeValue, ndHTMLElement, ndHTMLElement.onValueChange, "valueChanged", document.controller.OBSERVE_XPATH)
  }
  else
    ndHTMLElement.setAttribute(sAttribute, sAttributeValue)
}

/** Replaces the content of an DOMNode with a new Content 
  *
  * @ndOrigin Dom node representing an sub element in a enclosing document
  * @ndNewContent new DOMNode to replace original one
  **/
function replaceContent(ndMainDocument, sXPath, ndNewContent)
{
  var ndNewValue = null
  if( ndNewContent != null )
    ndNewValue = importNode(ndMainDocument, ndNewContent, true)
  var ndOldValue = findElement(ndMainDocument, sXPath)
  // Kludge to auto create element 
  if( typeof ndOldValue == 'undefined' || ndOldValue == null )
  {
    setXPathValue(ndMainDocument, sXPath, 1)
    ndOldValue = findElement(ndMainDocument, sXPath)
  }
  ndParent = ndOldValue.parentNode
  if( ndParent != null )
  {
    if( ndNewValue != null )
      ndParent.insertBefore(ndNewValue, ndOldValue)
    ndParent.removeChild(ndOldValue)
  }
}

/** Convert XML reserved characters (& < > ") to their associated entities
  *
  * @strText the string to escape
  * @return the string escaped */
function escapeXmlString(strText)
{
  if( strText == null )
    return null;
  // Use reg exps to replace reserved characters
  strText = strText.replace(/&/g, "&amp;");
  strText = strText.replace(/"/g, "&quot;");
  strText = strText.replace(/</g, "&lt;");
  strText = strText.replace(/>/g, "&gt;");
  return strText;
}

/** Parse an XML document
  *
  *
  */
function parseXMLString(strXML)
{
  if ( document.implementation.createDocument == undefined )
  { // IE case => required a FreeThreadedDOMDocument
    var xml = new ActiveXObject("Msxml2.FreeThreadedDOMDocument.3.0");
    xml.loadXML(strXML);
    return xml;
  }
  
  var domParser = new DOMParser();
  if ( domParser.parseFromString == undefined )
  { // Safari case ##PF: doesn't work at all => permission denied
    var xmlhttp = new XMLHttpRequest();
    xmlhttp.open("GET", "data:text/xml;charset=utf-8," + encodeURIComponent('<?xml version="1.0"?>' + strXML), false);
    xmlhttp.send(null);
    return xmlhttp.responseXML;
  }  
  return domParser.parseFromString(strXML, "text/xml");
}

/** Serialise an XML document.
  * 
  * Know limitation: The XMLSerializer works only in Safari if you want to get 
  * the serialization of the root object. */
function toXMLString(xmlDocument)
{
  try 
  {
    //serialization to string DOM Browser
    var serializer = new XMLSerializer();
    return serializer.serializeToString(xmlDocument);
  } 
  catch(e) 
  {
    if ( typeof xmlDocument == "undefined" || xmlDocument == null )
      throw "Unable to serialize xml document.";
       
    if ( typeof xmlDocument.xml != "undefined" )
      // (IE only)
      return xmlDocument.xml;
  }
    
  throw "XMLSerializer::serializeToString(): not implemented on that browser.";
}

/** Creatse a Plain Old JavaScript Object 
  * 
  * From an XML definition. */
function createPOJsO(elXMLContent, eXMLDesc)
{
  if( firstChildElement(eXMLDesc)==null && 
      (elXMLContent == null || 
       (elXMLContent.attributes.length==0 && firstChildElement(elXMLContent)==null)) )
    return elXMLContent == null ? "" : elementValue(elXMLContent);

  var oResult = new Object()
  var strPropertyName = ""
  
  // default construction from meta-data
  var elFirstChild = firstChildElement(eXMLDesc)
  while( elFirstChild != null )
  {
    strPropertyName = elFirstChild.nodeName.replace(/-/g, '_')
    if( Format.parseBoolean(elFirstChild.getAttribute("attribute"))==true )
    {
      if( elXMLContent != null && elXMLContent.getAttribute(elFirstChild.nodeName) != null )
        oResult[strPropertyName] = elXMLContent.getAttribute(elFirstChild.nodeName)
      else
        oResult[strPropertyName] = ""
    }
    elFirstChild = nextSiblingElement(elFirstChild)
  }
 
  elFirstChild = firstChildElement(eXMLDesc)
  while( elFirstChild != null )
  {
    if( Format.parseBoolean(elFirstChild.getAttribute("element"))==true )
    {
      strPropertyName = elFirstChild.nodeName.replace(/-/g, '_')
      var elChildContent = null;
      if( elXMLContent != null )
        elChildContent = findChildElement(elXMLContent, elFirstChild.nodeName);
      if( Format.parseBoolean(elFirstChild.getAttribute("unbound"))==false )
        oResult[strPropertyName] = createPOJsO(elChildContent, elFirstChild)
      else
      {
        if( oResult[strPropertyName] == null )
          oResult[strPropertyName] = new Array()
        if( elChildContent != null )
        {
          do
          {
            if( elChildContent.nodeName == elFirstChild.nodeName )
              oResult[strPropertyName].push(createPOJsO(elChildContent ,elFirstChild))
          }
          while( (elChildContent = nextSiblingElement(elChildContent)) != null )
        }
      }
    }
    elFirstChild = nextSiblingElement(elFirstChild)
  }
  return oResult
}

/** Creatse a Plain Old JavaScript Object 
  * 
  * From an XML definition. */
function convertToPOJsO(elXMLContent)
{
  var oResult = new Object();
  for(var iAttr = 0; iAttr < elXMLContent.attributes.length; iAttr++)
    oResult[elXMLContent.attributes[iAttr].name] = elXMLContent.attributes[iAttr].value
    
  elChild  = firstChildElement(elXMLContent)
  while( elChild )
  {
    oResult[elChild.nodeName] = convertToPOJsO(elChild)
    nextSiblingElement(elChild);
  }
  return oResult;
}