/**
 * @preserve TeeChart(tm) for JavaScript(tm)
 * @fileOverview TeeChart for JavaScript(tm)
 * v1.4 December 2012
 * Copyright(c) 2012 by Steema Software SL. All Rights Reserved.
 * http://www.steema.com
 *
 * Licensed with commercial and non-commercial attributes,
 * specifically: http://www.steema.com/licensing/html5
 *
 * JavaScript is a trademark of Oracle Corporation.
 */

/**
 * @author <a href="mailto:david@steema.com">Steema Software</a>
 * @version 1.4
 */

/*global window, exports, XMLHttpRequest, parser, Engine, self, document, xmlDoc, DOMParser, ActiveXObject */

/**
 * @namespace Tee.Data namespace, Multi-Dimensional Charting.
 */
var Tee=Tee || {};

(function() {
 "use strict";

if (typeof exports !== 'undefined') exports.Tee=Tee;

/**
 * @constructor
 * @class Main Tee.Data class to perform multi-dimensional queries and charts.
 */
Tee.Data=function() {

this.datasets=[];

//var engine=this;

 /**
  * @param {String} title The string identifier for this dataset.
  * @param {object} object The custom data for this dataset.
  * @param {String} [field=""] The object data optional field that contains an array.
  * @param {String} [id=""] The optional field in the array that uniquely identifies each array row.
  * @returns {Tee.Data.Dimension} Returns a top-level dimension.
  */
this.addDataSet=function(title, object, field, id) {
  if (object && object.implementation) // IE ?? instanceof XMLDocument)
      object=xmlToJson(object);

  var d=new Tee.Data.Dimension(title, field, id);

  d.object=object;
  d.dataset=d;
  d.dimensions=[];
  d.engine=this;

  this.datasets.push(d);
  return d;
}

this.addJSON=function(title, json, field, id) {
  return this.addDataSet(title, JSON.parse(json), field, id);
}

this.applyStyle=function(chart, index) {
  switch (index) {
    case 0:  {
                chart.panel.transparent=false;
                chart.panel.format.round.x=0;
                chart.panel.format.round.y=0;
                chart.panel.format.stroke.size=2;
                chart.panel.format.stroke.fill="darkgray";
                break;
             }
  }
}

/**
  Groups small values into a single "other" value.
  Requires sorted data.values in DESCENDING order.
  If there are more than "max" number of values, the rest are grouped and
  removed.
*/
this.groupOther=function(data, max, label) {
  var t, l=data.values.length, l2, r;

  if (l>max) {
    r=0, l2=l-max;

    for (t=max; t<l; t++)
      r+=data.values[t];

    data.values[max-1]=r;
    data.labels[max-1]=label;

    data.values.splice(max, l2);
    data.labels.splice(max, l2);
    data.code.splice(max, l2);

    return l2;
  }
  else
    return -1;
}


this.slider=function(chart, items, x,y,width,height) {
  var s=new Tee.Slider(chart);
  s.min=0;
  s.max=items.length-1;
  s.step=1;
  s.position=s.min;
  s.useRange=false;
  s.thumbSize=12;
  s.horizontal=true;

  s.bounds.x=x;
  s.bounds.y=y;
  s.bounds.width=width;
  s.bounds.height=height;

  chart.tools.add(s);

  return s;
}

// XML to JSON, adapted from:
// http://stackoverflow.com/questions/7769829/tool-javascript-to-convert-a-xml-string-to-json

var xmlToJson = function(xml) {
    var obj = {}, s;
    
    if (xml.nodeType == 1) {
        if (xml.attributes.length > 0) {
            //obj["@attributes"] = {};
            for (var j = 0; j < xml.attributes.length; j++) {
                var attribute = xml.attributes.item(j);
                //obj["@attributes"][attribute.nodeName] = attribute.nodeValue;
                obj[attribute.nodeName] = attribute.value;
            }
        }
    } else if (xml.nodeType == 3) {
        obj = xml.nodeValue;
    }
    if (xml.hasChildNodes()) {
        for (var i = 0; i < xml.childNodes.length; i++) {
            var item = xml.childNodes.item(i);
            var nodeName = item.nodeName;
            if (typeof (obj[nodeName]) == "undefined") {
               if (nodeName === "#text")
                 if (item.hasChildNodes())
                   obj[nodeName] = xmlToJson(item);
                 else
                 {
                   s=item.nodeValue.trim();
                   if (s!=="")
                      obj=s;
                 }
               else
                 if (nodeName=="xml")
                    obj =xmlToJson(item);
                 else
                    obj[nodeName] = xmlToJson(item);
            } else {
                if (typeof (obj[nodeName].push) == "undefined") {
                    var old = obj[nodeName];
                    obj[nodeName] = [];
                    obj[nodeName].push(old);
                }
                obj[nodeName].push(xmlToJson(item));
            }
        }
    }
    return obj;
}

this.loadXMLDoc=function(url)
{
  var xhttp= window.XMLHttpRequest ? new XMLHttpRequest() : new ActiveXObject("Microsoft.XMLHTTP");
  xhttp.open("GET",url,false);
  xhttp.send();
  return xhttp.responseXML;
}

this.loadXMLString=function(text)
{
  if (window.DOMParser)
  {
    parser=new DOMParser();
    xmlDoc=parser.parseFromString(text,"text/xml");
  }
  else // Internet Explorer
  {
    xmlDoc=new ActiveXObject("Microsoft.XMLDOM");
    xmlDoc.async=false;
    xmlDoc.loadXML(text);
  }
  return xmlDoc;
}

  function accumulate(measure, value, total) {
    total.count++;

    switch (measure) {
      case "sum":
      case "average": total.value+=value; break;
      case "high" : if ((total.count==1) || (value>total.value))
                        total.value=value; break;
      case "low":   if ((total.count==1) || (value<total.value))
                        total.value=value; break;
    }
  }

  this.queryDim=function(m, dim, total) { //, masterdim) {

    var lin=m.dimension.getLinksTo(dim),
        //f, f2,
        t, tot2;

    if (lin) {
      var lin0=lin[0];

      m.initDims();

      var mfunc = typeof m.name === 'function' ? m.name : null;

      m.dimension.traverse( function(o,index) {
        var f=null;

        if ((!m.name) || m.consider(o)) {

          if (dim) {
            if ((lin0.field===null) && (dim==lin0.dimension))
               if (dim.hasID)
                  f=dim.parent ? dim.data[dim.id] : o[dim.id];
               else
                 if (dim==m.dimension)
                   f=index || total.length;
                 else
                   f=dim.field ? o[dim.field] : index;
            else
            {
               f=dim.searchAcross(lin, o);
               if (f && dim.hasID)
                 f=f[dim.id];
            }
          }
          else
          {
            f=m.dimension.title;
            if (!f) {
              f=m.dimension.dataset.object;
              if (m.dimension.field) f=f[m.dimension.field];
            }
          }

          if ((f !== null) && ( (!dim) || dim.nulls || (f !== "") )) {

            var i=-1, ti, valo= mfunc ? mfunc(o) : m.name ? o[m.name] : o;

            if (typeof valo === 'undefined')
                valo=null;
            else
            {
              if (typeof valo !== 'number')
                  valo=parseFloat(valo);

              if (valo !== valo) // NaN
                  valo=null;
            }

            //document.write(f[dim.id]+" "+valo+"<br/>");

            if (valo !== null) {

              if (dim && dim.datetime)
                 f=dim.datePart(f);

              for (ti=0; ti<total.length; ti++)
                if (total[ti].id==f) {
                  i=ti;

                  if (total[i].count===0)
                  {
                    total[i].count=1;
                    total[i].value=valo;
                  }
                  else
                    accumulate(m.measure, valo, total[i]);

                  break;
                }

              if (i==-1)
                 total.push({ id:f, value:valo, count:1 });
            }
          }
        }

        return true;
      });

      if (m.measure=="count")
      for (t=0; t<total.length; t++) {
        tot2=total[t];
        tot2.value=tot2.count;
      }
      else
      if (m.measure=="average")
      for (t=0; t<total.length; t++) {
        tot2=total[t];
        if (tot2.count>0)
            tot2.value=tot2.value/tot2.count;
      }

      return true;
    }
    else
      return false;
  }

  function sumOfCount(total) {
    var res=0, t;
    for (t=0; t<total.length; t++)
       res += total[t].count;
    return res;
  }

  function initTotal(total, Ids) {
    if (Ids) {
      var ttt=0, l=Ids.length;

      for (ttt=0; ttt<l; ttt++)
        total.push({ id:Ids[ttt], value:0, count:0 });
    }
  }

  this.query=function(dimension, metric) {
    var m,
        metrics,
        dim,
        dimensions,
        total,
        totals=[],
        mt,
        dimt,
        //ttt,
        oldIds,
        dl,
        dim0;

    metrics = (metric instanceof Array) ? metric : [metric];
    dimensions = (dimension instanceof Array) ? dimension : [dimension];

    dl=dimensions.length;

    if (dl>0) {
      dim0=dimensions[0];

      var dim0sel=dim0 ? dim0.selected : null;

      oldIds=[];

      if (dim0sel)
         oldIds = (dim0sel instanceof Array ? dim0sel : [dim0sel]);
      else
      if (dim0 && (dim0.id || dim0.field))
         oldIds = dim0.getIds();
    }

    for (mt=0; mt<metrics.length; mt++) {

      m=metrics[mt];
      if (!m) continue;

      for (dimt=dl-1; dimt>=0; dimt--) {

        dim=dimensions[dimt];

        if (dimt>0) {

          if (!dim) continue;

          var old=dim.selected, dimValues;

          if (old)
          {
            if (!(old instanceof Array)) old=[old];

            if (dim.selectedInclude)
               dimValues = old;
            else
            {
              dimValues = dim.getIds();
              for (var oldt=0; oldt<old.length; oldt++)
                  dimValues.splice(dimValues.indexOf(old[oldt]),1);
            }
          }
          else
            dimValues = dim.getIds();

          for (var valt=0; valt<dimValues.length; valt++) {
             dim.selected=dimValues[valt];

             if ((!dim.nulls) && (dim.selected==="")) continue;

             total=[];
             initTotal(total, oldIds);

             if (this.queryDim(m, dimensions[dimt-1], total, dim)) {

                if ( dim.nulls || (sumOfCount(total)>0) )
                  totals.push({ metric:m, dimension:dim, values:total, master:dim.selected, masterdim: dimensions[dimt-1] });
             }
          }

          dim.selected=old;
          break;
        }
        else
        {
          total=[];
          initTotal(total, oldIds);

          if (this.queryDim(m, dim, total))
             totals.push({ metric:m, dimension:dim, values:total, master:null });
        }
      }

      // Eliminate nulls:

      if ((dl>0) && dim0 && (!dim0.nulls)) {

        var dd, ddd=0, res;

        while (ddd<totals[0].values.length)
        {
          res=0;
          for (dd=0; dd<totals.length; dd++)
             res += totals[dd].values[ddd].count;

          if (res===0) {
            for (dd=0; dd<totals.length; dd++)
                 totals[dd].values.splice(ddd,1);
          }
          else
            ddd++;
        }
      }
    }

    return totals;
  }

this.Chart=function(canvas, title) {
  var chart=new Tee.Chart(canvas);
  chart.engine=this;
  chart.panel.margins.left=0;

  var tip=new Tee.ToolTip(chart);
  tip.render="dom";
  chart.tools.add(tip);

  tip.ongettext=function(tip, text, series, index) {
    var s="", la=series.data.labels;

    if (series.chart.series.count()>1)
        s="<br/>"+series.title;

    if (series.data.x) {
      s+="<br/>"+series.data.x[index];
    }

    return text+" "+ ((la.length>0) ? la[index] : "" ) +s;
  }

  //chart.axes.each(function() { this.labels.valueFormat="0"; });

  var clickAnimation=new Tee.Animation();

  clickAnimation.onstart=function() { this.s.fill="yellow"; }
  clickAnimation.onstop=function() { this.s.fill=this.old; }
  clickAnimation.duration=100;
  clickAnimation.animateHover=function(chart) {
    this.s=chart.series.items[0].hover.stroke;
    this.old=this.s.fill;
    this.animate(chart);
  }

  chart.title.text=title;

  chart.guessStyle=function(total) {
    if (this.defaultStyle) return this.defaultStyle;
    else
    {
      return total.length > 30 ? Tee.Line : Tee.Bar;
    }
  }

  chart.newSeries=function(metric, tot) {

    var total=tot.values, dimension=tot.dimension, masterTitle=tot.master;

    if (!tot.masterdim) {
      if (masterTitle && dimension && dimension.id)
          masterTitle=masterTitle[dimension.id];
      else
          masterTitle=tot.master || tot.metric.title;
    }

    // Add to series

    var SeriesStyle=chart.guessStyle(total),
        b=chart.addSeries(new SeriesStyle()), data=b.data;

    b.title="" + (masterTitle || '(none)');
    b.marks.style="value";
    b.cursor="pointer";

    //b.valueFormat="0";

    data.values=[];
    data.labels=[];
    data.code=[];

    var tot2,
        lc=chart.series.items.length,
        res=[], f, label,
        t;

    if (dimension && (dimension.titles)) {
       findLinkInDimension(dimension.titles, dimension, res);
    }

    if (lc>1) {
      var bprev=chart.series.items[lc-2], i, bdata=bprev.data;

      for (t=0; t<bdata.values.length; t++) {
        data.values.push(0);
        data.labels.push(bdata.labels[t]);
        data.code.push(bdata.code[t]);
      }

      for (t=0; t<total.length; t++) {
        tot2=total[t];
        i=bdata.code.indexOf(tot2.id);

        if (i==-1) {
          data.values.push(tot2.value);

          if (dimension && (dimension.titles)) {
            f=dimension.searchAcross(res, tot2.id);
            if (f)
              label=f[dimension.titles.id];
            else
              label="";
          }
          else
            label=tot2.id[dimension.id];

          data.labels.push(label);
          data.code.push(tot2.id);
        }
        else
          data.values[i]=tot2.value;
      }
    }
    else
    {
      //var labeldim=dimension;
      //hasDim=(!tot.masterdim) && labeldim && labeldim.hasID;

      for (t=0; t<total.length; t++) {
        tot2=total[t];
        data.values.push(tot2.value);

        if (dimension && (dimension.titles)) {
          f=dimension.searchAcross(res, tot2.id);
          if (f)
            label=f[dimension.titles.id];
          else
            label="";
        }
        else
          label= "" + tot2.id; // (hasDim ? tot2.id[labeldim.id] : tot2.id);

        data.labels.push(label);
        data.code.push(tot2.id);
      }
    }

    if (SeriesStyle==Tee.CircularGauge)
        b.setValue(b.data.values[0]);

    if (this.onnewseries)
        this.onnewseries(this,b);

    return b;
  }

  chart.defaultStyle=null;
  chart.animateChanges=false;


  // Changes all series to style.
  // style can be a string of function from: Tee.Bar, Tee.Line, Tee.Pie, etc.

  chart.setSeriesStyle=function(style) {

    if ((!style) || (style==='') || (style==='auto')) {
      this.defaultStyle=null;
      return;
    }

    var s=this.series, newS, data, St;

    if (typeof style === "string") {
      St=eval(style);
    }
    else
      St=style;

    this.defaultStyle=St;

    for (var t=0; t<s.items.length; t++) {
       data=s.items[t].data;
       newS=new St();

       newS.setChart(newS,this);  // addSeries ?
       newS.format.fill=s.items[t].format.fill;

       newS.data=data;
       s.items[t]=newS;
    }

    this.draw();
  }

  // Creates a query based on dimension and metric, and adds the resulting
  // data to chart.
  // When dontRedraw = true, the chart will not be repainted.

  chart.fill=function(dimension, metric, dontRedraw) {
     this.fillQuery(this.engine.query(dimension, metric), dimension, metric, dontRedraw);
  }

  chart.fillQuery=function(totals, dimension, metric, dontRedraw) {
    var t, tot, m,
        old=chart.series.items;

    chart.series.items=[];

    if (totals) {
      for (t=0; t<totals.length; t++) {
        tot=totals[t];
        m=tot.metric;

        this.newSeries(m, tot).onclick=chart.engine._onclickseries;

        if ((chart.title.text==="") || (typeof chart.title.text === 'undefined'))
            chart.title.text=m.dimension.title;

        if (!(metric instanceof Array)) {
          chart.legend.title.text=m.title+"\n"+m.measure;
          chart.legend.title.format.font.textAlign="right";
        }

        var dim2=(dimension instanceof Array) ? dimension[0] : dimension;

        if (dim2 && (dim2!=m.dataset)) {
          chart.axes.bottom.title.text=dim2.datetime ? dim2.title+" "+dim2.dateKeys[dim2.datetime.selected] : dim2.title;
        }
      }

      if (totals.length>0)
        chart.axes.left.title.text=totals[0].metric.title;
    }

    //chart.axes.bottom.title.text=dimension.title;

    var sort=this.sort;

    if (sort.sortBy !== "")
      this.sortData(sort.sortBy, sort.order==="ascending");

    if (sort.series !== "")
       this.sortSeries(sort.series=='ascending');

    if (chart.animateChanges)
    if (old.length==chart.series.items.length) {
      var ss=chart.series.items[0], ssvalues=ss.data.values;

      if (old[0].prototype==ss.prototype) {
        if (old[0].data.values.length==ssvalues.length) {

          var newValues=ssvalues.slice(0), val;

          var a=new Tee.Animation(chart, function(step) {
            for (var t=0; t<ssvalues.length; t++) {
              val=old[0].data.values[t];
              ssvalues[t]=val+((newValues[t]-val)*step);
            }
          });

          var oldauto=chart.axes.left.automatic;
          chart.axes.left.automatic=false;

          a.duration=150;
          a.animate();

          chart.axes.left.automatic=oldauto;
        }
      }
    }

    if (!dontRedraw)
       chart.draw();
  }

  this.applyStyle(chart,0);

  chart.animateClick=function() {
    clickAnimation.animateHover(chart);
  }

  this.onclickseries=null;

  this._onclickseries=function(series,index) {
    var c=series.chart;

    if (c.onclickseries) {
       c.animateClick();
       c.onclickseries(series,index);
    }
  }

  // Changes series items colors, setting "silver" to disabled ones.

  chart.setSeriesPalette=function(series, dimension) {
    var p=series.palette, l=series.count(), c=series.data.code, o;
    p.colors=new Array(l);

    for(var t=0; t<l; t++) {
      o=c[t];

      if (!dimension.isSelected(o))
        p.colors[t]="silver";
    }
  }


    function totalOf(items, index) {
      var res=0, tt;
      for (tt=0; tt<items.length; tt++)
        res+=items[tt].data.values[index];
      return res;
    }

  // Reorders all series points, using output order from the first series.
  // sortBy can be "values" or "labels"

  chart.sortData=function(sortBy, ascending) {
    var sorted, l=this.series.count(), s0=this.series.items[0];

    if (l>0) {

      if ((l>1) && (sortBy=='values')) {
        var d=s0.data.values, len=d.length, t, items=this.series.items;

        sorted=new Array(len);

        for(t=0; t<len; t++) sorted[t]=t;

        sorted.sort( function(a,b) {
            var ta=totalOf(items, a), tb=totalOf(items, b);
            return ascending ? ta-tb : tb-ta;
          });
      }
      else
        sorted=this.series.items[0].doSort(sortBy, ascending);

      this.series.each(function(s) {
        var data2={ values:[], labels:[], code:[] },
            data=s.data, tt;

        for(var t=0; t<data.values.length; t++) {
          tt=sorted[t];
          data2.values.push(data.values[tt]);
          data2.labels.push(data.labels[tt]);
          data2.code.push(data.code[tt]);
        }

        s.data=data2;

      });
    }
  }

  // Re-orders all series in chart, based on series "title" text:

  chart.sortSeries=function(ascending) {
    var i=this.series.items, len=i.length;

    if (len<2) return;

    var sorted=new Array(len),
        A,B, before=ascending ? -1 : 1, after=ascending ? 1 : -1,
        t;

    for(t=0; t<len; t++) sorted[t]=t;

    sorted.sort( function(a,b) {
                       A=i[a].title.toLowerCase();
                       B=i[b].title.toLowerCase();
                       return A<B ? before : A==B ? 0 : after
                   });

    var newList=new Array(len);
    for(t=0; t<len; t++) newList[t]=i[sorted[t]];

    this.series.items=newList;
  }

  // For all series in chart, groups small values into an "Other" item.

  chart.groupOther=function(maxValues) {

    this.series.each(function(series) {
      var l=Engine.groupOther(series.data, maxValues, "Other");
      if (l > -1) {
        series.data.code[l]=null;
        series.data.code.splice(0,l);
      }
    });

  }

  chart.totalPoints=function() {
    var res=0;
    this.series.each(function(s) { res+=s.data.values.length; });
    return res;
  }

  // "sortBy" can be: 'values' or 'labels' or '' (empty)
  
  chart.sort={ sortBy:'', order:'descending', series:'ascending' }

  chart.setPositionPercent=function(left,top,width,height) {

    function getParentWidth() {
      if (self.innerHeight)
         return self.innerWidth;
      else
      if (document.documentElement && document.documentElement.clientHeight)
         return document.documentElement.clientWidth;
      else
      if (document.body)
         return document.body.clientWidth;
      else
         return 0;
    }

    function getParentHeight() {
      if (self.innerHeight)
         return self.innerHeight;
      else
      if (document.documentElement && document.documentElement.clientHeight)
         return document.documentElement.clientHeight;
      else
      if (document.body)
         return document.body.clientHeight;
      else
         return 0;
    }

    var h=getParentHeight(), w=getParentWidth(), c=this.canvas,
        x=left*w*0.01,
        y=top*h*0.01;

    width *=w*0.01;
    height *=h*0.01;

    width=width|0;
    height=height|0;

    c.style.position='absolute';
    c.style.left=x+"px";
    c.style.top=y+"px";
    c.style.width=width+"px";
    c.style.height=height+"px";

    c.setAttribute('width',width);
    c.setAttribute('height',height);

    this.bounds.width=width;
    this.bounds.height=height;
    this.draw();
  }

  if (Tee.SeriesAnimation) {
    var anim =chart.animation = new Tee.SeriesAnimation(chart);
    anim.kind="zoomin";
    anim.duration=300;

    var fade=new Tee.FadeAnimation(chart);
    fade.fade.series=true;
    anim.items.push(fade);
  }

  return chart;
}

}

/**
 * @constructor
 * @class Class to represent dimension data suitable to measure.
 * @param {Tee.Data.Dimension} dimension The parent dimension that owns this metric.
 * @param {String} title The string that identifies this metric.
 * @param {String} [name=""] The optional field string to obtain values to measure from dimension items.
 */
Tee.Data.Metric=function(dimension, title, name) {
  this.name=name;
  this.title=title || name;
  this.dimension=dimension;

  var dataset=this.dataset=dimension.dataset,
      engine=dataset.engine;

  this.measure="sum"; // average, high, low, count

  this.initDims=function() {
    var t, l=engine.datasets.length, tt, d, ld, dd, dim=this.dimension;

    this.allDims=[];

    for (t=0; t<l; t++) {
      d=engine.datasets[t].dimensions;
      ld=d.length;

      for (tt=0; tt<ld; tt++) {
        dd=d[tt];

        if (dim != dd)
        if (! dim.hasParent(dd)) {
          if (dd.anySelected()) {

             if (dd.dataset == dim.dataset)
                dd._link=null;
             else
                dd._link=dim.getLinksTo(dd);

             this.allDims.push(dd);
          }
        }
      }
    }
  }

  this.consider=function(data) {
    var t, li=this.allDims.length, d, f;  //, d2;

    for (t=0; t<li; t++) {
      d=this.allDims[t];

      if (d._link) {

        f=data; //d.id ? data[id] : data;

        for (var links=0; links<d._link.length; links++) {
          f=d.search(d._link[links], f);
        }

        if (f && d.id) f=f[d.id];
      }
      else
        f=data[d.field];

      //if (!f) return false;

      if ( (typeof f  === 'undefined') || (f === null) )
         return false;

      if (d.datetime) f=d.datePart(f);

      if (!d.inSelected(f))
        return false;
    }

    return true;
  }

}

/**
 * @constructor
 * @class Class to represent a Dimension of data.
 * @param {String} title The string identifier for this dimension.
 * @param {String} field The field string to obtain dimension child items.
 * @param {String} [id=""] The optional field string to obtain unique identifiers for dimension child items.
 */
Tee.Data.Dimension=function(title, field, id) {

  this.dataset=null;
  this.parent=null;

  this.engine=null;

  this.subDimensions=[];

  this.field=field;
  this.id=id;
  this.title=title || field;

  this.nulls=true;

  this.hasID=((typeof id !== 'undefined') && (id!==null) && (id!==""));

  this.metrics=[];
  this.links=[];

  this.selected=null;
  this.select=null;
  this.selectedInclude=true;

  this.dateKeys={ c:'Century', x:'Decade', y:'Year', m:'Month', w:'Weekday', d:'Day' };

 /**
  * @returns {Tee.Data.Dimension} Creates and returns a new child dimension.
  * @param {String} title The string identifier for this dimension.
  * @param {String} field The field string to obtain dimension child items.
  * @param {String} [id=""] The optional field string to obtain unique identifiers for dimension child items.
  */
  this.addDimension=function(title, field, id) {
     return this.addSubDimension(this, title, field, id);
  }

  this.addSubDimension=function(parent, title, field, id) {
     var d=new Tee.Data.Dimension(title, field, id);

     d.engine=this.engine;
     d.parent=parent;

     if (parent) {
        d.dataset=parent.dataset;

        if (d.dataset)
            d.index=d.dataset.dimensions.push(d);

        parent.subDimensions.push(d);
     }

     return d;
  }

 /**
  * @returns {Tee.Data.Metric} Creates and returns a new metric for this dimension.
  * @param {String} title The string identifier for this metric.
  * @param {String} name The field string to obtain metric values from parent dimension items.
  * @param {String} [measure="sum"] The optional measure style for this metric.
  */
  this.addMetric=function(title, name, measure) {
     var r=new Tee.Data.Metric(this, title, name);

     if (measure)
         r.measure=measure;

     this.metrics.push(r);
     return r;
  }

 /**
  * @returns {object} Creates and returns a new link object that knows how to get
    from this origin dimension to destination {dimension} parameter.
  * @param {String|Array} field The string field(s) identifier(s) for this origin dimension.
  * @param {Tee.Data.Dimension} dimension The destination dimension.
  * @param {String|Array} datasetField The string field(s) identifier(s) for the destination dimension.
  */
  this.addLink=function(field, dimension, datasetField) {
    var l={ field: field, dimension: dimension, datasetField: datasetField, parent: this }
    this.links.push(l);
    return l;
  }

 /**
  * @returns {boolean} Returns true when this dimension has {dimension} parameter as parent in the hierarchy.
  * @param {Tee.Data.Dimension} dimension The dimension to test as parent.
  */
  this.hasParent=function(dimension) {
    var d=this.parent;
    while (d)
      if (d==dimension) return true;
      else
        d=d.parent;

    return false;
  }

 /**
  * @returns {boolean} Returns true when data is a value of the current selected filter.
  * @param {object} data The value to check for.
  */
  this.inSelected=function(data) {
    //if (this.datetime) data=this.datePart(data);

    if (this.select)
       return this.select(data);
    else
    if (this.selected instanceof Array)
      //for (var tt=0; tt<this.selected.length; tt++)
      //  if (this.selected[tt]==data)
      //      return true;
      return this.selectedInclude ? this.selected.indexOf(data)!=-1 : this.selected.indexOf(data)==-1;
    else
      return this.selectedInclude ? this.selected==data : this.selected != data;
  }

 /**
  * @returns {object} Search for an item with {id} as identifier and return it.
  * @param {object} id The identifier value to search for.
  */
  this.get=function(id) {
    var t=this.id, data, date=this.datetime, _this=this;

    this.traverse(function(o) {
      if (date) o= _this.datePart(o);

      if (id== (t ? o[t] : o) ) {
        data=o;
        return false;
      }
      else
        return true;
    });

    return data;
  }

  function trunc(value) { return value | 0; }

 /**
  * @returns {Number} Returns the currently selected part of {date} parameter.
  * @param {Date} date The Date value.
  */
  this.datePart=function(date) {
    var da=this.datetime, s=da.selected;

    if (typeof date == 'string') {
      var parts=date.match(/(\d+)/g);

      if (s=='y') return parts[da.yearField || 2]; else
      if (s=='m') return parts[da.monthField || 0]; else
      if (s=='x') return 10*trunc(parseInt(parts[2],10) / 10); else
      if (s=='d') return parts[da.dayField || 1]; else
      if (s=='w') return (new Date(date).getDay());
    }
    else
    if (date instanceof Date)
    {
      if (s=='y') return date.getFullYear(); else
      if (s=='m') return date.getMonth()+1; else
      if (s=='x') return 10*trunc(date.getFullYear() / 10); else
      if (s=='d') return date.getDate(); else
      if (s=='w') return date.getDay();
    }

    return date;
  }

  // Traverse dataset and return all object values for this Dimension:
  this.getValues=function() {
    var r=[], f=this.field, id=this.id, _this=this;

    this.traverse(function(value) {
       if (id)
          value =  f ? value[f] : value[id];

       if (_this.datetime) value=_this.datePart(value);

       if (r.indexOf(value)==-1) r.push(value);
       return true;
    });

    return r;
  }

  // Traverse dataset and return all unique "id" values for this Dimension:
  this.getIds=function() {

    var r=[], f,

        //d = this.dataset.object,
        //i=d.length, t,

        id=this.id, field=this.field, _this=this,
        p=this.parent ? this.parent : this;

    p.traverse(function(value) {
       f=id ? value[id] : (field ? value[field] : value);
       if (_this.datetime) f=_this.datePart(f);

       if (_this.nulls || f)
          if (r.indexOf(f)==-1) r.push(f);

       return true;
    });

    return r;
  }

 /**
  * @returns {boolean} Returns true when {data} parameter is in the currently selected list.
  * @param {object} data The value to search.
  */
  this.isSelected=function(data) {
    if (this.select)
       return this.select(data);
    else
    if (this.selected)
      return this.inSelected(data);
    else
      return true;
  }

 /**
  * @returns {boolean} Returns true when selected filter is not empty.
  */
  this.anySelected=function() {

    if (this.select)
       return true;
    else
    {
      var s=this.selected;

      if ((s!==null) && (typeof s != 'undefined'))
        if (s instanceof Array)
          for (var t=0; t<s.length; t++) {
            if (s[t]) return true;
          }
        else
          return true;

      return false;
    }
  }

 /**
  * Selects or unselects a given series index point value.
  * @param {Tee.Series} series The series to toggle its index point format.
  */
  this.toggleSelected=function(series, index) {
    if (!this.selected) this.selected=[];

    var code=series.data.code[index];

    //if (this.id) code=code[this.id];

    var i=this.selected.indexOf(code);

    if (i===-1)
      this.selected.push(code);
    else {
      this.selected.splice(i,1);
      if (this.selected.length===0) this.selected=null;
    }

    series.chart.setSeriesPalette(series,this);
  }

 /**
  * Traverses all items belonging to this dimension and calls the process function for each item.
  * @param {function} process The function that will get called for each item in the dimension.
  */
  this.traverse=function(process) {

    function traverseLevel(dataset,levelIndex) {

      function doProcess(da2, t2) {
        var tt, o3, kk;

        if (da2 && (typeof da2 === 'object')) {

          // Traverse all "da2" object properties:
          // { A:123, B:456, C:789, D:....  }

          var k=Object.keys(da2), kl=k.length,
              lev2=levels[0],
              hasSelected2=lev2.anySelected();

          for (tt=0; tt<kl; tt++) {

            kk=k[tt];

            if ((!hasSelected2) || lev2.isSelected(kk)) {

              o3=da2[kk];
              lev2.data=o3;

              if (!process(o3,kk))
                 return false;
            }
          }

          return true;
        }
        else
          return process(da2,t2);
      }

      var t, ml,
          lev=levels[levelIndex],
          hasSelected=lev.anySelected(),
          da=lev.field ? dataset[lev.field] : dataset,
          daSel = lev.parent ? dataset : da;

      if ((!hasSelected) || lev.isSelected(lev.id ? daSel[lev.id] : daSel)) {
        lev.data=daSel;

        if (da instanceof Array) {
            ml=da.length;

            if (levelIndex>0)
              for (t=0; t<ml; t++) {
                  if (!traverseLevel(da[t],levelIndex-1))
                     return false;
              }
            else
              for (t=0; t<ml; t++) {
                    if (!process(da[t],t))
                       return false;
              }
        }
        else
            if (levelIndex>0) {
              if (!doProcess(da))
                   return false;
            }
            else
            if (!process(daSel))
                return false;
      }

      return true;
    }

    var levels=[], di=this;

    do  {
      levels.push(di);
    } while (di = di.parent);

    var o=this.dataset.object;
    if (o)
       traverseLevel(o,levels.length-1);
  }

  this.searchAcross=function(lin, o) {
     var f=o, tmp=this;

     for (var links=0; links<lin.length; links++) {
       f=tmp.search(lin[links],f, links>0);

       if (links<lin.length-1) {
         tmp=lin[links+1].parent;
         lin[links+1].searchDimension=tmp;
       }
     }

     if (f && this.engine.cache) {
       if (!o.cache) o.cache=[];

       if (!o.cache[this.index])
         o.cache[this.index]=f;
     }

     return f;
  }

  this.search=function(link, data, inverted) {

    if (data.cache && data.cache[this.index])
       return data.cache[this.index];

    var f=null, // _this=this,
        values,
        ld,
        t,
        linkSearch=link.search,
        linkField=inverted ? link.field : link.datasetField,
        linkValues=inverted ? link.datasetField : link.field,
        isArray=(linkValues instanceof Array);

    if (isArray) {
      values=[];

      for(t=0; t<linkValues.length; t++)
         values.push(data[linkValues[t]]);
    }
    else
      values=data[linkValues];

    ld=link.searchDimension || link.dimension;

    do {
      ld.traverse( function(o) {

        if (linkSearch) {
          if (linkSearch(o,data)) {
            f=o;
            return false;
          }
        }
        else
        if (isArray) {

           var result=true;

           for(t=0; t<values.length; t++) {
              if (o[linkField[t]] !== values[t]) {
                result=false;
                break;
              }
           }

           if (result) { f=o; return false; }
        }
        else
        if (o[linkField]==values) {
           f=o;
           return false;
        }

        return true;

      });

      if (!f) break;
      else
      if (ld.dataset !== this.dataset)
         return f;
      else
      if (ld !== this) {

         if (ld.parent) {
           ld=ld.parent;
           f=ld.data;
         }
         else
           break;
      }

    } while( ld !== this);

    return f ? (this.id ? f : f[this.field]) : null;
  }

  this.getLinksTo=function(dimension) {

    if ((!dimension) || (dimension==this.dataset) || (dimension.dataset==this.dataset) )
      return [{ field: null, dimension: dimension, datasetField: null, parent: this }];

    else
    {
      var res=[],
          d=findLinkInDimension(dimension, this, res);

      if (!d) d=findLinkInDimension(this, dimension, res);
      
      return d ? res : null;
    }
  }

}

function findLinkInDimension(needle, haystack, res) {
  var d, t, _this=haystack, li, lil, need, r;

  do {
    li=_this.links;

    if (li) {
      lil=li.length;
      for (t=0; t<lil; t++) {

        d=li[t].dimension;
        do {
          need=needle;

          do {
            if (d==need) {
              li[t].searchDimension=li[t].dimension;
              res.push(li[t]);
              return li[t];
            }

          } while (need=need.parent);

          r=findLinkInDimension(needle, d, res);

          if (r) {
            res.unshift(li[t]);
            return r;
          }
          else {
            r=findLinkInDimension(d, needle, res);
            if (r) {
              res.unshift(li[t]);
              return r;
            }
          }

        } while (d = d.parent);

      }
    }

  } while (_this=_this.parent);

  li=haystack.subDimensions;

  for (t=0; t<li.length; t++) {
     r=findLinkInDimension(needle, li[t], res);
     if (r)
        return r;
  }

  return null;
}


}).call(this);
