/*
 * Global variables
 */
var gGraphs = {};
var gCSVs   = [];
var gFiles  = [];

var brush = d3.svg.brush()
  .on("brushend", brushed);

var margin = {top: 10, right: 10, bottom: 100, left: 40},
    height = 70 - margin.top,
    width  = 600 - margin.right - margin.left;

var x = d3.time.scale().range([0, width]),
    y = d3.scale.linear().range([height, 0]);


/*
 * DOM functions
 */
$(document).on('dragenter', function (e) {
    e.stopPropagation();
    e.preventDefault();
    $('#drop').css('border', '4px solid #fff')
  });
$(document).on('dragover', function (e) {
  e.stopPropagation();
  e.preventDefault();
  $('#drop').css('border', '4px solid red')
});
$(document).on('drop', function (e) {
  e.stopPropagation();
  e.preventDefault();
  processFiles(e.originalEvent.target.files || e.originalEvent.dataTransfer.files);
  $('#drop-background').hide();
});


/*
 * Init functions
 */
$(document).ready(function() {
    // initialize graphs contained in gCSVs
    if (gCSVs !== undefined && gCSVs.length > 0) {
      $('#drop-background').hide();
      for (i in gCSVs) {
        processCSV(gCSVs[i], "csv " + i)
      }
    }
  }
)


/*
 * Settings functions
 */
var settings = {
  "interface": "standard",
  "xaxis": 'time',
  "granularity": undefined, // set while browsing the first file
  "agg_function": "avg"
}


/*
 * CSV Processing functions
 */
function processFiles(files) {
  if ((files.length + gFiles.length) > 1) { // switch to sequential if multiple files are present
    settings.xaxis = 'sequential';
  }
  for (i = 0; i < files.length; i++) {
    gFiles.push(files[i]);
    processFile(files[i]);
  }
}

function processFile(file) {
  var reader = new FileReader();
  var name   = file.name;
  reader.onload = function(e) {
    text = e.target.result;
    processCSV(text, name);
  }

  reader.readAsText(file);
}

/*
 * return dstat header end line
 * TODO: Improve the detection mechanism
 */
function findHeaderEnd(lines) {
  host   = "";
  dataIn = -1

  // Let's browse only 20 lines, if there is more, it could mean it's not a valid file
  for (var i = 0; i < 20; i++) {
    line = lines[i].replace(/"/g, '').split(',');
    if (line[0] == "Host:") {
      host = line[1]
    }
    if (lines[i].length > 0 && lines[i][0] != '"') {
      dataIn = i;
      break;
    }
  }

  // Not a valid file
  if (dataIn == -1) {
    return null;
  }

  return { "host"    : host,
           "groups"  : lines[dataIn - 2].replace(/"/g, '').split(','),
           "headers" : lines[dataIn - 1].replace(/"/g, '').split(','),
           "nlines"  : lines.length - dataIn,
           "dataIn"  : dataIn
         };
}

/*
 * Entry point for parsing CSV
 * CSV file is read and dispatch line by line in a 2D array
 * Then we browse these 2D array to create the required d3 structure
 *
 */
function processCSV(csv, filename) {
  lines   = csv.split('\n');
  l_env   = findHeaderEnd(lines);

  if (l_env == null) {
    alert('Non valid CSV file: ' + filename + '. Failed at parsing header')
    return;
  }

  host    = l_env.host;
  groups  = l_env.groups;
  headers = l_env.headers;
  nlines  = l_env.nlines;
  graphs  = [];
  map     = [];
  gindex  = -1;

  /* Browse headers */
  for (var i = 0, j = 0; i < headers.length; i++, j++) {
    if (groups[i] != "") {
      last_group         = groups[i];
      j                  = 0;
      graphs.push({name: last_group, d: []});
      gindex++;
    }

    graphs[gindex].d.push({key: headers[i], values: []});
    map[i] = {group: gindex, index: j, name: headers[i]};
  }

  /* try to increase granularity to reduce memory consumption if required */
  // TODO be smarter here, save different configuration per file?

  if (settings.granularity === undefined) {
    if (lines.length > 7200) {
      settings.granularity = 15
    }  else if (lines.length > 3600) {
      settings.granularity = 5
    } else {
      settings.granularity = 1
    }
  }

  // First, let's merge headers and groups in a single object
  xValues = getValues(graphs, 'system', 'time');
  /* Use time for XAxis */
  if (xValues !== null && settings.xaxis == "time") {
    graphs.xAxis = function (xa) {
      xa.axisLabel('Time').tickFormat(function(d) {
          if (typeof d === 'string') {
            return d;
          }
          return d3.time.format('%Hh %Mm %Ss')(new Date(d));
        })
    };
    for (var lindex = l_env.dataIn, iindex = 0; lindex < lines.length; lindex++, iindex++) {
      line = lines[lindex].replace(/"/g, '').split(',');
      for (var cindex = 0; cindex < line.length; cindex++) {
        lmap = map[cindex];
        if (lmap != null && lmap.name === 'time') {
          xValues.push(Date.parse(line[cindex].replace(/(\d+)-(\d+)\s+(\d+):(\d+):(\d+)/, '1942/$2/$1 $3:$4:$5')));
          break;
        }
      }
    } /* Use sequence for xAxis */
  } else {
      xValues = d3.range(nlines);
      graphs.xAxis = function (xa) {
        xa.axisLabel('').tickFormat(function(d) {
            return d3.format('d')(d);
          })
      };
  }

  /* Then, populate the graphs object with the CSV values, aggregate data while browsing */
  tmp_values = {}
  for (var lindex = l_env.dataIn, iindex = 0; lindex < lines.length; lindex++, iindex++) {
    line = lines[lindex].replace(/"/g, '').split(',');
    for (var cindex = 0; cindex < line.length; cindex++) {
      lmap = map[cindex];
      if (lmap != null && lmap.name != 'time') {
        nVal = parseFloat(line[cindex]);
        /* handle non numerical line */
        if (isNaN(nVal)) {
          val = line[cindex]
          graphs[lmap.group].yformat = function(_) {return _};
        }
        else {
          val = nVal
          graphs[lmap.group].yformat = d3.format('<-,02f');
        }

        if (tmp_values[lmap.group] == undefined) {
          tmp_values[lmap.group] = []
        }
        if (tmp_values[lmap.group][lmap.index] == undefined) {
          tmp_values[lmap.group][lmap.index] = []
        }
        tmp_values[lmap.group][lmap.index].push(val)

        if (!((iindex + 1) % settings.granularity)) { // add a new entry in the graph
          if (settings.agg_function == "avg") {
            graphs[lmap.group].d[lmap.index].values.push({y: tmp_values[lmap.group][lmap.index].average(), x: xValues[iindex]});
          } if (settings.agg_function == "max") {
            graphs[lmap.group].d[lmap.index].values.push({y: Math.max(...tmp_values[lmap.group][lmap.index]), x: xValues[iindex]});
          }
          tmp_values = {}
        }
      }
    }
  }

  /* Apply specificities for each data headers */
  for (var i in graphs) {
    name       = graphs[i].name;
    name       = name.replace(/[&\/\\#,+()$~%.'":*?<>{}\s]/g,'_');
    sfname     = name + "_data";
    ofname     = name + "_options";
    gdfunction = undefined;
    options    = {};

    if (typeof window[sfname] == "function") {
      gdfunction = window[sfname];
    }
    if (typeof window[ofname] == "function") {
      options = window[ofname]();
    }
    for (var j in graphs[i].d) {
      if (gdfunction !== undefined) {
        graphs[i].d[j].values = gdfunction(graphs[i].d[j].values);
      }
      if (options !== undefined) {
        if (options.area === true) {
          graphs[i].d[j].area = true;
        }
      }
    }
  }

  /* Create the brush */
  dmin = graphs[1].d[1].values[0].x;
  dmax = graphs[1].d[0].values[graphs[1].d[0].values.length -1].x;
  // If there is many point, let's start by focusing on the last ones to reduce loading time
  if ((lines.length / settings.granularity) > 500) {
    dmin = graphs[1].d[0].values[graphs[1].d[0].values.length - 500].x;
  }

  displayFocusGraph(graphs, dmin, dmax);

  /* create & display the graphs */
  for (var gindex = 0; gindex <  graphs.length; gindex++) {
    if (graphs[gindex].name != "system" && graphs[gindex].d[0].key != "time") {
      graphName   = graphs[gindex].name;
      graphData   = graphs[gindex].d;
      graphFormat = graphs[gindex].yformat;
      panel = createPanel(graphName, graphData, filename)
      displayGraph(graphName, graphData, graphFormat, panel, dmin, dmax, filename);
    }
  }
}


/*
 * Create or use an already existing panel to display the graph
 */
function createPanel(graphName, graphData, filename) {
  id  = graphName.replace(/[&\/\\#,+()$~%.'":*?<>{}\s]/g,'_');
  data = reduceData(graphData)
  div = d3.select('#' + id);

  if (! settings.compact) {
    if (div.empty()) {
      div = d3.select('#dashboard').append('div').attr('class', ' list-group-item').attr('id', id);
      header = div.append('div').attr('class', 'panel-heading').append('h3').attr('class', 'panel-title');
      header.append('span').text(graphName);
      header.append('span').attr('class', 'glyphicon glyphicon-chevron-right pull-right clickable');
    }
    elt = div.append('div').attr('class', 'row list-body');
    elt.append('p').text(filename)
    elt.append('svg').datum(reduceData(graphData));
  } else if (settings.compact) {
    if (div.empty()) {
      div    = d3.select('#dashboard').append('div').attr('class', 'list-group-item-compact').attr('id', id);
      header = d3.select('#dashboard').append('div').attr('class', 'panel-left')
      info   = d3.select('#dashboard').append('div').attr('class', 'panel-left')

      d3.select('#dashboard').append('div').attr('class', 'panel-clear')
      header.append('p').attr('class', 'panel-left').text(graphName)
      colors = nv.utils.getColor()
      for (var i in graphData) {
        header.append('p').attr('class', 'panel-left').text(graphData[i].key).style({color: colors(i), 'margin-left': '15px'});
      }
    }
    elt = div.append('div').attr('class', 'row list-body');
    elt.append('svg').datum(data);
  }

  return div;
}

/*
 * Create generic graph options
 */
function createInitialOptions(graphData) {
  options = {}

  return options;
}

/*
 * Create the graph d3 object
 */
function displayGraph(graphName, graphData, graphFormat, panel, dmin, dmax, filename) {
  panel.selectAll('svg').each(function() {
      var elt = d3.select(this);

      nv.addGraph(function() {
          var graphId    = graphName.replace(/[&\/\\#,+()$~%.'":*?<>{}\s]/g,'_');
          var graphFu    = graphId + '_graph';
          var graphFuPre = graphId + '_options';
          var options    = createInitialOptions(graphData);
          var chart = null;

          if (typeof window[graphFuPre] == "function") {
            options = window[graphFuPre]();
          }

          if (options.type == 'stacked') {
            chart = nv.models.stackedAreaChart()
              .margin({left: 100})
              .useInteractiveGuideline(! settings.compact)
              .showLegend(! settings.compact)
              .style('expand')
              .interpolate("basis")
              .showControls(false)
              .showXAxis(! settings.compact)
              .showYAxis(true)
              ;
          } else if (options.type == 'pie') {
            chart = nv.models.bulletChart()
              .margin({left: 100})
              ;
          } else {
            chart = nv.models.lineChart()
              .margin({left: 100})
              .useInteractiveGuideline(! settings.compact)
              .interpolate("basis")
              .showLegend(! settings.compact)
              .showXAxis(! settings.compact)
              .showYAxis(true)
            ;
          }


          graphs.xAxis(chart.xAxis);

          if (typeof window[graphFu] == "function") {
            window[graphFu](chart);
          }

          var pb = d3.select(elt[0][0].parentNode.parentNode).select('.clickable').on("click", function() {
            pb = d3.select(this.parentNode.parentNode.parentNode).selectAll('.list-body');
            isHidden = pb.style('display') == 'none';
            pb.style('display', isHidden ? 'inherit' : 'none');
            chart.update()
          });

          elt.call(chart);
          nv.utils.windowResize(chart.update);

          return chart;
        }, function(chart) {
          if (gGraphs[graphName] == undefined) gGraphs[graphName] = {};
          gGraphs[graphName][filename] = {elt: elt, chart: chart, data: graphData, filename: filename};
        });
    });
}

function getValues(graphs, group, header) {
  for (var i in graphs) {
    if (graphs[i].name == group) {
      for (var j in graphs[i].d) {
        if (graphs[i].d[j].key == header) {
          return graphs[i].d[j].values;
        }
      }
    }
  }

  return null;
}

function getExists(graphs, group, header) {
  for (var i in graphs) {
    if (graphs[i].name == group) {
      for (var j in graphs[i].d) {
        if (graphs[i].d[j].key == header) {
          return true;
        }
      }
    }
  }

  return false;
}

/*
 * Create the focus graph
 * By default, use the oposite of idle cpu time
 * If not found in the CSV, take the first element
 */
function displayFocusGraph(graphs, dmin, dmax) {
  if ($('#focus').children().size() > 0) {
    return;
  }
  data = getValues(graphs, "total cpu usage", "idl")
  if (data) { // Rollback to the first element if not found
    data = data.map(function(idl) { return {x: idl.x, y: (100 - parseFloat(idl.y)) };});
  }
  else {
    data = graphs[0].d[0].values;
    data = data.map(function(idl) { return {x: idl.x, y: idl.y };});
  }

  x.domain(d3.extent(data.map(function(val) { return val.x })));
  y.domain([0, d3.max(data.map(function(val) { return val.y }))]);

  brush.x(x).extent([dmin, dmax]);

  var area = d3.svg.area()
    .interpolate("basis")
    .x(function(d) { return x(d.x) })
    .y1(function(d) { return y(d.y) })
    .y0(height);

  var svg = d3.select('#focus').append('svg')
    .attr("width", width + margin.left + margin.right)
    .attr("height", height + margin.top + margin.bottom);

  var xAxis = d3.svg.axis().scale(x).orient("bottom");

  svg.append("defs").append("clipPath")
    .attr("id", "clip")
    .append("rect")
    .attr("width", width)
    .attr("height", height);

  context = svg.append("g")
    .attr("class", "context")
    .attr("transform", "translate(" + margin.left + "," + margin.top + ")");

  context.append("path")
    .datum(data)
    .attr("class", "area")
    .attr("d", area);

  context.append("g")
    .attr("class", "x axis")
    .attr("transform", "translate(0," + height + ")")
    .call(xAxis.ticks(5));

  context.append("g")
    .attr("class", "x brush")
    .call(brush)
    .selectAll("rect")
    .attr("y", -6)
    .attr("height", height + 7);

  brushed();
}

function reduceData(data) {
  var ext  = (brush.empty() || brush.extent() == null ? x.domain() : brush.extent());
  var extD = [ext[0] instanceof String ? Date.parse(ext[0]) : ext[0],
             ext[1] instanceof String ? Date.parse(ext[1]) : ext[1]];

  var ndata = data.map(function(d, i) {
    return {
      key: d.key,
      area: d.area,
      values: d.values.filter(function(d, i) {
        return d.x > extD[0] && d.x < extD[1];
      })
  }});
  return ndata;
}


function brushed() {
  var ext  = (brush.empty() || brush.extent() == null ? x.domain() : brush.extent());

  for (var name in gGraphs) {
    if (gGraphs.hasOwnProperty(name)) {
      for (gIndex in gGraphs[name]) {
        var graph = gGraphs[name][gIndex];
        var chart = graph.chart;
        var elt   = graph.elt;
        var data  = graph.data;
        var ndata = reduceData(data);

        chart.xDomain(ext);
        elt.call(chart.xAxis);
        elt.datum(ndata);

        elt.call(chart.update);
      }
    }
  }
}

function change_xaxis(type) {
  settings.xaxis = type;
  refresh();
}

function change_interface(type) {
  settings.compact = (type == 'compact');
  refresh();
}

function change_granularity(granularity, aggr_function) {
  settings.granularity = granularity
  settings.agg_function = aggr_function
  refresh();
}

/*
 * Destroy all graphs & the focus bars
 * and recreate them using the content of
 * gFiles (all uploaded files)
 */
function refresh() {
  d3.select('#dashboard').html("");
  d3.select('#focus').html("");
  for (i in gFiles) {
    processFile(gFiles[i]);
  }
}


/*
 * Toggle header menu
 */
function toggle_menu() {
  if ($('#menu').is(":hidden")) {
    $('#menu').slideDown('slow');
    $('#dashboard').animate({ "padding-top": "+=50px" }, 'slow');
  } else {
    $('#menu').slideUp('slow');
    $('#dashboard').animate({ "padding-top": "-=50px" }, 'slow');
  }
}

/*
 * Type extension
 */
Array.prototype.sum = Array.prototype.sum || function() {
  return this.reduce(function(sum, a) { return sum + Number(a) }, 0);
}

Array.prototype.average = Array.prototype.average || function() {
  return this.sum() / (this.length || 1);
}