/*******************************************************************************
 * reportlistcl.js
 *
 * HTML Rendering of the ReportList
 *
 * Copyright:    Copyright (c) 2008
 * Company:      Neolane
 *
 * $Author$
 * $Revision$
 ******************************************************************************/ 

/** Constructor 
  *
  * @htmlParent HTML element in the document where to render the list. 
  * @data       Data to display in an XML format. 
  * @style      Cell style definitions. */ 
function ReportList(htmlParent, data, styles, defaultFontSize)
{
  this.data       = data
  this.htmlTable  = htmlParent
  this.styles     = new Array()
  this.rowNum     = 0 // used by css generation (even odd classes)

  // compile styles
  var childNode = styles.documentElement.firstChild
  while ( childNode != null )
  {
    if ( childNode.nodeType == XML.ELEMENT_NODE )
      this.compileStyle(childNode, defaultFontSize)

    childNode = childNode.nextSibling
  }
}

ReportList.prototype.COLORS  = new Array("#3B87A2", "#FF8012", "#8CA23B", "#A23B5E", "#004A62",
                                "#FFCCA0", "#C75400", "#CAD4A5", "#A5C8D4", "#6D0026",
                                "#D4A5B4", "#4F6400")

ReportList.prototype.IMG_PLUS  = "/xtk/img/plus.png"
ReportList.prototype.IMG_MINUS = "/xtk/img/minus.png"

/** Compile a style to create a template of the <td>.
  *
  * @style the XML definition of the style. The schema of 
  *        this element is the one defined for the flat report
  *        format:
  *
  *   <style format="percentage" alignement="center">
  *     <font bold="true"/>
  *   </style>
  *
  * @see https://wiki.neolane.net/wiki/Reporting_5.00#srcSchema_like_xml_structure. */
ReportList.prototype.compileStyle = function(style, defaultFontSize)
{
  var td = document.createElement("td")
  var compiledStyle = compileStyle(style, td, defaultFontSize)
  compiledStyle.td = td
  this.styles.push(compiledStyle)
}

/** Render the list as an HTML table. */
ReportList.prototype.render = function()
{
  // clear the previous rendering of the table
  while ( this.htmlTable.firstChild != null )
    this.htmlTable.removeChild(this.htmlTable.firstChild)
    
  var htmlBody = document.createElement("tbody")
  this.renderNode(this.data, htmlBody)
  this.htmlTable.appendChild(htmlBody)
}

/** Click on a cell */
ReportList.prototype.onCellClick = function(ctx, action, target, transition, option, param, e)
{
  PerformClickAction(ctx, action, target, transition, option, param, e)
}

/** Generate a function for a clickable cell
  *
  * @object     the list.
  * @ctx        an array of value to set in the context before executing the action.
  * @action     action to perform (url, next, previous, refresh, transition)
  * @target     window target.
  * @transition name of transition if action parameter is 'transition'. 
  * @href       URL field. 
  * @param      URL parameters. */
ReportList.prototype.makeOnClickEvent = function(object, ctx, action, target, transition, href, param)
{
  return function(e)
  {
    return ReportList.prototype.onCellClick.call(object, ctx, action, target, transition, href, param, e)
  }
}

/** Render a node the of the list.
  *
  * @node       XML definition of the node.
  * @htmlParent HTML element in the document where to render the node. */
ReportList.prototype.renderNode = function(aData, htmlParent)
{
  // array for headers whose rows should be collapsed by default
  var groupsToCollapse = new Array()

  for( var iRowIndex = 0 ; iRowIndex < aData.length ; iRowIndex++ )
  {
    var row = aData[iRowIndex]
    trNode = document.createElement("tr")
    trNode.className = row.t //type of the row (header, footer, row) + level
    trNode.className += ++this.rowNum % 2 == 0 ? " even" : " odd"
    htmlParent.appendChild(trNode)
    
    for( var iCellIndex = 0 ; iCellIndex < row.d.length ; iCellIndex++ )
    {
      var cell = row.d[iCellIndex]
      var value = null
      
      // cell representation in HTML (TD) with its colspan and etc.:
      var htmlNode
      // TD which contains the cell's elements
      var htmlNodeTxt
      
      if ( typeof cell != "undefined" && typeof cell.s != "undefined" )
      {
        var rawValue = cell.v
        var style = this.styles[cell.s]
        
        // Set htmlNode with this cell style
        htmlNode = style.td.cloneNode(true)
        // htmlNodeTxt is htmlNode by default
        htmlNodeTxt = htmlNode
        
        // Add an image for the collapsing of inner rows
        var nextRow = aData[iRowIndex+1]
        if( trNode.className.indexOf("header") != -1    // only for header rows
         && iCellIndex == 0                             // on their first cell
         && this.getLevel(trNode.className) > 0         // if their level is > 0
         && this.canBeCollapsed(nextRow != undefined ? nextRow.t : null,
                                this.getLevel(trNode.className)) )  // no rows to collapse ?
        {
          var img = document.createElement("img")
          img.src = this.IMG_MINUS
          img.onclick = function()
          {
            ReportList.prototype.toggleCollapsing(this);
          }
          img.className = "collapse"
          
          // For vertical alignment, we create a new table which will contain 
          // the image and the text in two separate TDs.
          var table = document.createElement("table")
          var tbody = document.createElement("tbody")
          var tr = document.createElement("tr")
          // Avoid considering this tr as the parent TR of our img when
          // recursing to find the parent table rows.
          tr.tryAgain = true
          var tdImg = document.createElement("td")
          tdImg.className = "collapse"
          
          // We set htmlNodeTxt to be our new TD so that the text will be put into it
          htmlNodeTxt = style.td.cloneNode(true)  // inherit the right style
          
          // Redefine inline style for the following cases :
          htmlNodeTxt.style.width = "100%"
          htmlNodeTxt.className = "collapse"
          htmlNodeTxt.style.border = "none"
          
          // construct the table hierarchy
          tdImg.appendChild(img)
          tr.appendChild(tdImg)
          tr.appendChild(htmlNodeTxt)
          tbody.appendChild(tr)
          table.appendChild(tbody)
          htmlNode.appendChild(table)
          
          // Register this image for collapsing if the collapsing is set 
          // to default for this header (cf row.c).
          if( row.c )
            groupsToCollapse.push(img)
        }
        
        if ( style.hideValue != true )
          value = FormatHelper.format[style.format].call(this, rawValue, style)
        
        // Clickable cells
        if ( style.onclick == true )
        {
          var aContext = new Array()
          var aParam = new Array()
          var bNotClickable = false
          
          if( typeof cell.l != "undefined" )
            for( var i = 0 ; i < cell.l.length ; i++ )
            {
              if( cell.l[i].t == "set" )
                aContext.push({"xpath" : cell.l[i].xpath, "value" : cell.l[i].v})
              else if( cell.l[i].t == "param" )
                aParam.push({"name" : cell.l[i].name, "value" : cell.l[i].v})
              else if( cell.l[i].t == "notClickableIf" )
                bNotClickable = cell.l[i].v
            }

          if( bNotClickable == "false" || bNotClickable == false )
            htmlNodeTxt.onclick = this.makeOnClickEvent(this, aContext, style.onclickAction, 
              style.onclickTarget, style.onclickTransition, style.onclickOption, aParam)
          else
          {
            if( htmlNodeTxt.firstChild != null )
              // Remove "A" element 
              htmlNodeTxt.removeChild(htmlNodeTxt.firstChild)
          }
        }
        
        // showing extra visuals
        if ( style.bargraph != undefined )
        {
          var bargraph = style.bargraph.cloneNode(true)
          var bargraphValue = Math.max(parseFloat(rawValue) * 100, 0)
          if( isNaN(bargraphValue) )
            bargraphValue = 0
          bargraph.firstChild.firstChild.style.width = bargraphValue.toString() + "%"
          htmlNodeTxt.appendChild(bargraph)
        }
        else if ( style.rating == true )
        {
          var fValue = parseFloat(rawValue)
          for (var i=1; i <= style.ratingMax; i++)
          {
            var div = document.createElement("div")
            div.className = "rating" + (fValue >= i ? "On" : "Off")
            htmlNodeTxt.appendChild(div)
          }
        }
        else if ( style.color == true && rawValue != "" && rawValue != "none" )
        {
          // Either our raw value is an int, and as such an index into the 
          // color table, or a CSS color (#A78B34, red, none, ...), in 
          // which case we set it directly.
          var color = rawValue
          if( !isNaN(color) )
          {
            var colorIndex = parseInt(color)
            color = this.COLORS[colorIndex % this.COLORS.length]
          }
          
          var div = document.createElement("div")
          div.className = "color"
          
          // The user could have given us a bad color, in which case we 
          // use a color cell with a transparent background (for IE).
          try
          {
            div.style.backgroundColor = color
          }
          catch(e)
          {
            div.style.backgroundColor = "transparent"
          }
          
          htmlNodeTxt.appendChild(div)
        }
      }
      else
        // no style defined => empty cell
        htmlNode = document.createElement("td")

      if ( value != null )
      {
        var textNode = document.createTextNode(value)
        var parentNode = htmlNodeTxt || htmlNode
        
        if ( style.onclick == true )
        {
          var childNode = parentNode.firstChild
          while ( childNode != null && childNode.nodeName != "A" )
            childNode = childNode.nextSibling
          
          if( childNode == null ) 
            // "A" element was removed previously, add text element
            parentNode.appendChild(textNode)
          else
            childNode.appendChild(textNode)
        }
        else
          parentNode.appendChild(textNode)
      }
      
      if ( typeof cell.cs != "undefined" )
        htmlNode.colSpan = cell.cs

      trNode.appendChild(htmlNode)
    }
  }

  // Post process rows :
  // collapse all the rows below headers registered in groupsToCollapse
  for( var iRowIndex = 0 ; iRowIndex < groupsToCollapse.length ; iRowIndex++ )
    this.toggleCollapsing(groupsToCollapse[iRowIndex])
}

// Retrieve the level of a row from its class
// example of a tr with a level : <tr class="header level0 odd">
ReportList.prototype.getLevel = function(className)
{
  return className.match(/level(\d+)/)[1]
}

// Return true if the row can be collapsed or not for the given level
// of collapsing.
ReportList.prototype.canBeCollapsed = function(className, level)
{
  var canBeCollapsed = true
  if( className != undefined )
  {
    if( className.indexOf("header") != -1
      && this.getLevel(className) <= level )
      canBeCollapsed = false
    else if( className.indexOf("footer") != -1
      && this.getLevel(className) < level )
      canBeCollapsed = false
  }
  return canBeCollapsed
}

// ToggleVisibility takes an img DOM element as its argument
// and then loop other its parent's following tr elements which
// levels are superior to its parent's.
ReportList.prototype.toggleCollapsing = function(image)
{
  var displayModifier
  if( image.src.indexOf(ReportList.prototype.IMG_PLUS) == -1 )
  {
    displayModifier = function(row)
    {
      // Function used to hide the children rows
      row.style.display = "none"
    }
    // Set the plus image for decollapsing
    image.src = ReportList.prototype.IMG_PLUS
  }
  else
  {
    displayModifier = function(row)
    {
      // Function used to show the children rows
      row.style.display = ""
      
      // And change the image src back to its decollapsed state as we extend the
      // rows regardless of their previous state.
      var img = row.getElementsByTagName("img")[0]
      if( img != undefined && img.src.indexOf(ReportList.prototype.IMG_PLUS) != -1 )
        img.src = ReportList.prototype.IMG_MINUS
    }
    // Set the minus image for collapsing
    image.src = ReportList.prototype.IMG_MINUS
  }
  
  var row = image.parentNode;
  // Inner table => avoid TR with tryAgain attribute.
  while( row != undefined && ( row.nodeName != "TR" || row.tryAgain == true ) )
    row = row.parentNode;
  var level = this.getLevel(row.className)

  row = row.nextSibling
  // We continue only if the current row is not a header or a footer unless 
  // their level is superior to the original one.
  while( row != undefined && this.canBeCollapsed(row.className, level) )
  {
    displayModifier(row)
    
    row = row.nextSibling
  }
}

function FormatHelper()
{}

/** Formating values functions */
FormatHelper.format = {
 
  "number": function(text, style)
  {
    var value  = parseFloat(text)
    var options = {
      "pattern": style.pattern
    }
    
    return value.format(options)
  },

  "percentage": function(text, style)
  {
    var value  = parseFloat(text) * 100
    var options = {
      "pattern":  style.pattern,
      "symbol":   style.symbol
    }
    
    return value.format(options)
  },

  "currency": function(text, style)
  {
    var value  = parseFloat(text) 
    var options = {
      "pattern":  style.pattern,
      "symbol":   '$'
    }
    
    return value.format(options)
  },
  
  "date": function(text, style)
  {
    var d = Format.parseDateTimeInter(text)
    var fmt = Format.formatDate(d, false)
    if ( style.useTime )
      fmt += " " + Format.formatTime(d, !style.useSec)
      
    return fmt
  },
  "time": function(text, style)
  {
    var d 
    if( text.indexOf('/') > -1 )
      d = Format.parseDateTimeInter(text)
    else
      d = Format.parseTime(text)
    var fmt = Format.formatTime(d, !style.useSec)
      
    return fmt
  },
  "timespan": function(text, style)
  {
    var value = parseFloat(text)
    var fmt   = Format.formatTimeSpan(value)
    return fmt
  },
  
  "string": function(text)
  {
    return text
  }
}

/** Compile a style to create a template of the <td>.
  *
  * @style the XML definition of the style. The schema of 
  *        this element is the one defined for the flat report
  *        format:
  *
  *   <style format="percentage" alignement="center">
  *     <font bold="true"/>
  *   </style>
  *
  * @see https://wiki.neolane.net/wiki/Reporting_5.00#srcSchema_like_xml_structure. */

function compileStyle(style, htmlElement, defaultFontSize)
{
  var format = style.getAttribute("format")
  
  if ( format == null )
    format = "string"

  var compiledStyle = {
    "format":             format, 
    "negativeNumberRed":  style.getAttribute("negativeNumberRed") == "true" ? true : false
  }
  
  // In IE, td inherits text-align property from their parents
  htmlElement.style.textAlign = "left"

  if ( format == "number" || format == "currency" || format == "percentage" )
  { // 
    // numeric formats
    //   
    
    // create the formating pattern
    var pattern
    if ( style.getAttribute("use1000Sep") == "true" )
      pattern = "#,###"
    else
      pattern = "#"
      
    var decimalPlaces = style.getAttribute("decimalPlaces")
    decimalPlaces = decimalPlaces == null ? 2 : decimalPlaces
    if ( decimalPlaces > 0 )
    {
      pattern += "."
      for (var i=0; i < decimalPlaces; i++)
        pattern += "0"
    }
    
    if ( format == "percentage" )
    {
      pattern += "¤"
      if( Format.settings.percentSeparator )
        compiledStyle.symbol = Format.settings.percentSeparator + "%"
      else
        compiledStyle.symbol = "%"
    }
    
    compiledStyle.pattern = pattern
  
    // default alignment for numeric values is right 
    htmlElement.style.textAlign = "right"
  }
  else if ( format == "date" || format == "time" )
  {
    compiledStyle.useTime = style.getAttribute("useTime") == "true"
    compiledStyle.useSec  = style.getAttribute("useSec") == "true"
  }
    
  if ( style.getAttribute("alignment") != null 
    && style.getAttribute("alignment") != "auto" )
    htmlElement.style.textAlign = style.getAttribute("alignment")
    
  if( style.getAttribute("verticalAlignment") != null && style.getAttribute("verticalAlignment") != "default" )
    htmlElement.style.verticalAlign = style.getAttribute("verticalAlignment")
  
  var widthMode = style.getAttribute("widthMode")
  if ( widthMode == 'max' )
    htmlElement.style.width = style.getAttribute("width") + "%"
  else if ( widthMode == 'manual' )
    htmlElement.style.width = style.getAttribute("width") + "em"
  // automatic widthMode has no width, it uses content to size itself
  
  if( style.getAttribute("wrapText") != null && 
      style.getAttribute("wrapText") == "true" )
    htmlElement.style.whiteSpace = 'normal'
  else
    // By default, no wrap
    htmlElement.style.whiteSpace = 'nowrap'
  
  var childNode = style.firstChild
  while ( childNode != null )
  {
    if ( childNode.nodeType == XML.ELEMENT_NODE )
    {
      if ( childNode.nodeName == "font" )
      {
        var fontStyle = childNode.getAttribute("style")
        if ( fontStyle == "bold" || fontStyle == "boldItalic" )
          htmlElement.style.fontWeight = "bold"
        if ( fontStyle == "italic" || fontStyle == "boldItalic" )
          htmlElement.style.fontStyle = "italic"
        if( fontStyle == "normal" )
        {
          htmlElement.style.fontWeight = "normal"
          htmlElement.style.fontStyle = "normal"
        }
          
        var fontSize = childNode.getAttribute("size")
        if ( fontSize != undefined && fontSize != "0" && fontSize != "" )
        {
          if( isNaN(defaultFontSize) )
            htmlElement.style.fontSize = "1em"
          else
            htmlElement.style.fontSize = (fontSize / defaultFontSize) + "em"
        }
        var color = childNode.getAttribute("color")
        if ( color != undefined )
          htmlElement.style.color = color
      }
      else if ( childNode.nodeName == "extra" )
      {
        var visual = childNode.getAttribute("visual")
        if ( visual == "bargraph" )
        {
          var bargraph = document.createElement("div")
          bargraph.className = "bargraph"
          var bargraphContainer = document.createElement("div")
          bargraphContainer.className = "container"
          bargraph.appendChild(bargraphContainer)          
          var bargraphValue = document.createElement("div")
          bargraphValue.className = "blue"
          bargraphContainer.appendChild(bargraphValue)
          compiledStyle.bargraph = bargraph
        }
        else if ( visual == "rating" )
        {
          compiledStyle.rating = true
          compiledStyle.ratingMax = parseInt(childNode.getAttribute("ratingMax"))
        }
        else if ( visual == "color" )
        {
          compiledStyle.color = true
          compiledStyle.hideValue = true
        }
        
        if ( childNode.getAttribute("hideValue") == "true" )
          // do not display the value in cell
          compiledStyle.hideValue = true
      }
      else if ( childNode.nodeName == "onclick" 
        && childNode.getAttribute("action") != "none" )
      {
        var a = document.createElement("a")
        a.href = "#"
        
        // Need to get URL href
        compiledStyle.onclick = true        
        compiledStyle.onclickAction     = childNode.getAttribute("action")
        compiledStyle.onclickTarget     = childNode.getAttribute("target")
        compiledStyle.onclickTransition = childNode.getAttribute("transition")
        if( compiledStyle.onclickAction == "url" )
          compiledStyle.onclickOption = childNode.getAttribute("href")
        else if( compiledStyle.onclickAction == "javascript" )
          compiledStyle.onclickOption = getXPathValue(childNode, "javascript");
        else if( compiledStyle.onclickAction == "refreshData" )
        {
          compiledStyle.onclickOption = new Array()      
          var styleChildNode = childNode.firstChild
          while ( styleChildNode != null )
          {
            if( styleChildNode.nodeName == "objectTarget" )
              compiledStyle.onclickOption.push(styleChildNode.getAttribute("identifier"))              
            styleChildNode = styleChildNode.nextSibling
          }
        }

        htmlElement.appendChild(a)
        
        if ( compiledStyle.onclickAction == "transition" )
          compiledStyle.onclickAction = "next"
      }
      else if ( childNode.nodeName == "border" )
      {
        var ndBorder = childNode.firstChild
        var strToEvalBorderProperties = ""
        while( ndBorder != null )
        {
          if( ndBorder.nodeType == XML.ELEMENT_NODE )
            // As setProperty is not supported under IE, htmlElement.style.setProperty(ndBorder.nodeName, ndBorder.getAttribute("value"),"") is not possible
            // Note that in this case, ndBorder.nodeName must be formated like "border-top" and not "borderTop"
            // Workaround: generate a string that contains affectations and evaluate it
            strToEvalBorderProperties += "htmlElement.style." + ndBorder.nodeName + " = \"" + ndBorder.attributes[0].nodeValue + "\";"
          ndBorder = ndBorder.nextSibling
        }
        eval(strToEvalBorderProperties)
      }
    }
    childNode = childNode.nextSibling
  }

  return compiledStyle
}
