// Copyright 2014 Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

google.charts.load('current', {packages: ['corechart', 'gauge', 'default', 'format', 'ui', 'table']});

function humanize(num, size, units) {
  var unit;
  for (unit = units.pop(); units.length && num >= size; unit = units.pop()) {
    num /= size;
  }
  return [num, unit];
}

// Following the IEC naming convention
function humanizeIEC(num) {
  var ret = humanize(num, 1024, ['TiB', 'GiB', 'MiB', 'KiB', 'B']);
  return ret[0].toFixed(2) + ' ' + ret[1];
}
// Following the Metric naming convention

function humanizeMetric(num) {
  var ret = humanize(num, 1000, ['TB', 'GB', 'MB', 'KB', 'Bytes']);
  return ret[0].toFixed(2) + ' ' + ret[1];
}

// Draw a table.
function drawTable(
    seriesTitles, titleTypes, data, elementId, numPages, sortIndex) {
  var dataTable = new google.visualization.DataTable();
  for (var i = 0; i < seriesTitles.length; i++) {
    dataTable.addColumn(titleTypes[i], seriesTitles[i]);
  }
  dataTable.addRows(data);
  if (!(elementId in window.charts)) {
    window.charts[elementId] =
        new google.visualization.Table(document.getElementById(elementId));
  }

  var cssClassNames = {
    'headerRow': '',
    'tableRow': 'table-row',
    'oddTableRow': 'table-row'
  };
  var opts = {
    alternatingRowStyle: true,
    page: 'enable',
    pageSize: numPages,
    allowHtml: true,
    sortColumn: sortIndex,
    sortAscending: false,
    cssClassNames: cssClassNames
  };
  window.charts[elementId].draw(dataTable, opts);
}

// Draw a line chart.
function drawLineChart(seriesTitles, data, elementId, unit) {
  var min = Infinity;
  var max = -Infinity;
  for (var i = 0; i < data.length; i++) {
    // Convert the first column to a Date.
    if (data[i] != null) {
      data[i][0] = new Date(data[i][0]);
    }

    // Find min, max.
    for (var j = 1; j < data[i].length; j++) {
      var val = data[i][j];
      if (val < min) {
        min = val;
      }
      if (val > max) {
        max = val;
      }
    }
  }

  // We don't want to show any values less than 0 so cap the min value at that.
  // At the same time, show 10% of the graph below the min value if we can.
  var minWindow = min - (max - min) / 10;
  if (minWindow < 0) {
    minWindow = 0;
  }

  // Add the definition of each column and the necessary data.
  var dataTable = new google.visualization.DataTable();
  dataTable.addColumn('datetime', seriesTitles[0]);
  for (var i = 1; i < seriesTitles.length; i++) {
    dataTable.addColumn('number', seriesTitles[i]);
  }
  dataTable.addRows(data);

  // Create and draw the visualization.
  if (!(elementId in window.charts)) {
    window.charts[elementId] =
        new google.visualization.LineChart(document.getElementById(elementId));
  }

  // TODO(vmarmol): Look into changing the view window to get a smoother
  // animation.
  var opts = {
    curveType: 'function',
    height: 300,
    legend: {position: 'none'},
    focusTarget: 'category',
    vAxis: {
      title: unit,
      viewWindow: {
        min: minWindow,
      }
    },
    legend: {
      position: 'bottom'
    }
  };
  // If the whole data series has the same value, try to center it in the chart.
  if (min == max) {
    opts.vAxis.viewWindow.max = 1.1 * max;
    opts.vAxis.viewWindow.min = 0.9 * max;
  }

  window.charts[elementId].draw(dataTable, opts);
}

// Gets the length of the interval in nanoseconds.
function getInterval(current, previous) {
  var cur = new Date(current);
  var prev = new Date(previous);

  // ms -> ns.
  return (cur.getTime() - prev.getTime()) * 1000000;
}

// Checks if the specified stats include the specified resource.
function hasResource(stats, resource) {
  return stats.stats.length > 0 && stats.stats[0][resource];
}

// Checks if all containers in provided list include the specified resource.
function hasResourceForAll(containerInfos, resource) {
  if (containerInfos.length === 0) {
    return false;
  }
  for (var i = 0; i < containerInfos.length; i++) {
    if (!hasResource(containerInfos[i], resource)) {
      return false;
    }
  }
  return true;
}

// Draw a set of gauges. Data is comprised of an array of arrays with two
// elements:
// a string label and a numeric value for the gauge.
function drawGauges(elementId, gauges) {
  gauges.unshift(['Label', 'Value']);

  // Create and populate the data table.
  var data = google.visualization.arrayToDataTable(gauges);

  // Create and draw the visualization.
  var options = {
    height: 100,
    redFrom: 90,
    redTo: 100,
    yellowFrom: 75,
    yellowTo: 90,
    minorTicks: 5,
    animation: {duration: 900, easing: 'linear'}
  };
  var chart =
      new google.visualization.Gauge(document.getElementById(elementId));
  chart.draw(data, options);
}

// Get the machine info.
function getMachineInfo(rootDir, callback) {
  $.getJSON(rootDir + 'api/v1.0/machine', function(data) { callback(data); });
}

// Get ps info.
function getProcessInfo(rootDir, containerName, callback) {
  $.getJSON(rootDir + 'api/v2.0/ps' + containerName)
      .done(function(data) { callback(data); })
      .fail(function(jqhxr, textStatus, error) { callback([]); });
}

// Get the container stats for the specified container.
function getStats(rootDir, containerName, callback) {
  // Request 60s of container history and no samples.
  var request = JSON.stringify({
    // Update main.statsRequestedByUI while updating "num_stats" here.
    'num_stats': 60,
    'num_samples': 0
  });

  $.when(
       $.post(rootDir + 'api/v1.0/containers' + containerName, request),
       $.post(rootDir + 'api/v1.1/subcontainers' + containerName, request))
      .done(function(containersResp, subcontainersResp) {
        callback(containersResp[0], subcontainersResp[0]);
      });
}

// Draw the graph for CPU usage.
function drawCpuTotalUsage(elementId, machineInfo, stats) {
  if (stats.spec.has_cpu && !hasResource(stats, 'cpu')) {
    return;
  }

  var titles = ['Time', 'Total'];
  var data = [];
  for (var i = 1; i < stats.stats.length; i++) {
    var cur = stats.stats[i];
    var prev = stats.stats[i - 1];
    var intervalNs = getInterval(cur.timestamp, prev.timestamp);

    var elements = [];
    elements.push(cur.timestamp);
    elements.push((cur.cpu.usage.total - prev.cpu.usage.total) / intervalNs);
    data.push(elements);
  }
  drawLineChart(titles, data, elementId, 'Cores');
}

// Draw the graph for CPU load.
function drawCpuLoad(elementId, machineInfo, stats) {
  var titles = ['Time', 'Average'];
  var data = [];
  for (var i = 1; i < stats.stats.length; i++) {
    var cur = stats.stats[i];

    var elements = [];
    elements.push(cur.timestamp);
    elements.push(cur.cpu.load_average / 1000);
    data.push(elements);
  }
  drawLineChart(titles, data, elementId, 'Runnable threads');
}

// Draw the graph for per-core CPU usage.
function drawCpuPerCoreUsage(elementId, machineInfo, stats) {
  if (stats.spec.has_cpu && !hasResource(stats, 'cpu')) {
    return;
  }

  // Add a title for each core.
  var titles = ['Time'];
  for (var i = 0; i < machineInfo.num_cores; i++) {
    titles.push('Core ' + i);
  }
  var data = [];
  for (var i = 1; i < stats.stats.length; i++) {
    var cur = stats.stats[i];
    var prev = stats.stats[i - 1];
    var intervalNs = getInterval(cur.timestamp, prev.timestamp);

    if (cur.cpu.usage.per_cpu_usage == undefined) {
        return;
    }

    var elements = [];
    elements.push(cur.timestamp);
    for (var j = 0; j < machineInfo.num_cores; j++) {
      elements.push(
          (cur.cpu.usage.per_cpu_usage[j] - prev.cpu.usage.per_cpu_usage[j]) /
          intervalNs);
    }
    data.push(elements);
  }
  drawLineChart(titles, data, elementId, 'Cores');
}

// Draw the graph for CPU usage breakdown.
function drawCpuUsageBreakdown(elementId, machineInfo, containerInfo) {
  if (containerInfo.spec.has_cpu && !hasResource(containerInfo, 'cpu')) {
    return;
  }

  var titles = ['Time', 'User', 'Kernel'];
  var data = [];
  for (var i = 1; i < containerInfo.stats.length; i++) {
    var cur = containerInfo.stats[i];
    var prev = containerInfo.stats[i - 1];
    var intervalNs = getInterval(cur.timestamp, prev.timestamp);

    var elements = [];
    elements.push(cur.timestamp);
    elements.push((cur.cpu.usage.user - prev.cpu.usage.user) / intervalNs);
    elements.push(
        (cur.cpu.usage.system - prev.cpu.usage.system) / intervalNs);
    data.push(elements);
  }
  drawLineChart(titles, data, elementId, 'Cores');
}

// Return chart titles and data from an array of subcontainerInfos, using the
// passed dataFn to return the individual data points.
function getSubcontainerChartData(subcontainerInfos, dataFn) {
  var titles = ['Time'];
  var data = [];
  for (var i = 0; i < subcontainerInfos.length; i++) {
    titles.push(subcontainerInfos[i].name);
    for (var j = 1; j < subcontainerInfos[i].stats.length; j++) {
      var cur = subcontainerInfos[i].stats[j];
      var prev = subcontainerInfos[i].stats[j - 1];

      // Generate a sparse array with timestamp at index zero and data point at
      // index i+1, to be used as a DataTable row.
      var elements = [cur.timestamp];
      subcontainerInfos.forEach(function() {
        elements.push(null);
      });
      elements[i+1] = dataFn(cur, prev);
      data.push(elements);
    }
  }
  return {
    titles: titles,
    data: data,
  }
}

// Draw the graph for per-subcontainer CPU usage.
function drawCpuPerSubcontainerUsage(elementId, subcontainerInfos) {
  if (!hasResourceForAll(subcontainerInfos, 'cpu')) {
    return;
  }

  var chartData = getSubcontainerChartData(subcontainerInfos,
      function(cur, prev) {
    var intervalNs = getInterval(cur.timestamp, prev.timestamp);
    return (cur.cpu.usage.total - prev.cpu.usage.total) / intervalNs;
  });
  drawLineChart(chartData.titles, chartData.data, elementId, 'Cores');
}

// Draw the graph for per-subcontainer memory usage.
function drawMemoryPerSubcontainerUsage(elementId, subcontainerInfos) {
  if (!hasResourceForAll(subcontainerInfos, 'memory')) {
    return;
  }

  var chartData = getSubcontainerChartData(subcontainerInfos,
      function(cur, prev) {
    return cur.memory.usage / oneMegabyte;
  });
  drawLineChart(chartData.titles, chartData.data, elementId, 'Megabytes');
}


// Draw the gauges for overall resource usage.
function drawOverallUsage(elementId, machineInfo, containerInfo) {
  var cur = containerInfo.stats[containerInfo.stats.length - 1];
  var gauges = [];

  var cpuUsage = 0;
  if (containerInfo.spec.has_cpu && containerInfo.stats.length >= 2) {
    var prev = containerInfo.stats[containerInfo.stats.length - 2];
    var rawUsage = cur.cpu.usage.total - prev.cpu.usage.total;
    var intervalNs = getInterval(cur.timestamp, prev.timestamp);

    // Convert to millicores and take the percentage
    cpuUsage =
        Math.round(((rawUsage / intervalNs) / machineInfo.num_cores) * 100);
    if (cpuUsage > 100) {
      cpuUsage = 100;
    }
    gauges.push(['CPU', cpuUsage]);
  }

  var memoryUsage = 0;
  if (containerInfo.spec.has_memory) {
    // Saturate to the machine size.
    var limit = containerInfo.spec.memory.limit;
    if (limit > machineInfo.memory_capacity) {
      limit = machineInfo.memory_capacity;
    }

    memoryUsage = Math.round((cur.memory.usage / limit) * 100);
    gauges.push(['Memory', memoryUsage]);
  }

  var numGauges = gauges.length;
  if (cur.filesystem) {
    for (var i = 0; i < cur.filesystem.length; i++) {
      var data = cur.filesystem[i];
      var totalUsage = Math.floor((data.usage * 100.0) / data.capacity);
      var els = window.cadvisor.fsUsage.elements[data.device];

      // Update the gauges in the right order.
      gauges[numGauges + els.index] = ['FS #' + (els.index + 1), totalUsage];
    }

    // Limit the number of filesystem gauges displayed to 5.
    // 'Filesystem details' section still shows information for all filesystems.
    var max_gauges = numGauges + 5;
    if (gauges.length > max_gauges) {
      gauges = gauges.slice(0, max_gauges);
    }
  }

  drawGauges(elementId, gauges);
}

var oneMegabyte = 1024 * 1024;
var oneGigabyte = 1024 * oneMegabyte;

function drawMemoryUsage(elementId, machineInfo, containerInfo) {
  if (containerInfo.spec.has_memory && !hasResource(containerInfo, 'memory')) {
    return;
  }

  var titles = ['Time', 'Total', 'Hot'];
  var data = [];
  for (var i = 0; i < containerInfo.stats.length; i++) {
    var cur = containerInfo.stats[i];

    var elements = [];
    elements.push(cur.timestamp);
    elements.push(cur.memory.usage / oneMegabyte);
    elements.push(cur.memory.working_set / oneMegabyte);
    data.push(elements);
  }

  // Get the memory limit, saturate to the machine size.
  var memory_limit = machineInfo.memory_capacity;
  if (containerInfo.spec.memory.limit &&
      (containerInfo.spec.memory.limit < memory_limit)) {
    memory_limit = containerInfo.spec.memory.limit;
  }

  // Updating the progress bar.
  var cur = containerInfo.stats[containerInfo.stats.length - 1];
  var hotMemory = Math.floor((cur.memory.working_set * 100.0) / memory_limit);
  var totalMemory = Math.floor((cur.memory.usage * 100.0) / memory_limit);
  var coldMemory = totalMemory - hotMemory;
  $('#progress-hot-memory').width(hotMemory + '%');
  $('#progress-cold-memory').width(coldMemory + '%');
  $('#memory-text')
      .text(
          humanizeIEC(cur.memory.usage) + ' / ' + humanizeIEC(memory_limit) +
          ' (' + totalMemory + '%)');

  drawLineChart(titles, data, elementId, 'Megabytes');
}

// Get the index of the interface with the specified name.
function getNetworkInterfaceIndex(interfaceName, interfaces) {
  for (var i = 0; i < interfaces.length; i++) {
    if (interfaces[i].name == interfaceName) {
      return i;
    }
  }
  return -1;
}

// Draw the graph for network tx/rx bytes.
function drawNetworkBytes(elementId, machineInfo, stats) {
  if (stats.spec.has_network && !hasResource(stats, 'network')) {
    return;
  }

  // Get interface index.
  var interfaceIndex = -1;
  if (stats.stats.length > 0) {
    interfaceIndex = getNetworkInterfaceIndex(
        window.cadvisor.network.interface, stats.stats[0].network.interfaces);
  }
  if (interfaceIndex < 0) {
    console.log(
        'Unable to find interface"', interfaceName, '" in ',
        stats.stats.network);
    return;
  }

  var titles = ['Time', 'Tx bytes', 'Rx bytes'];
  var data = [];
  for (var i = 1; i < stats.stats.length; i++) {
    var cur = stats.stats[i];
    var prev = stats.stats[i - 1];
    var intervalInSec = getInterval(cur.timestamp, prev.timestamp) / 1000000000;

    var elements = [];
    elements.push(cur.timestamp);
    elements.push(
        (cur.network.interfaces[interfaceIndex].tx_bytes -
         prev.network.interfaces[interfaceIndex].tx_bytes) /
        intervalInSec);
    elements.push(
        (cur.network.interfaces[interfaceIndex].rx_bytes -
         prev.network.interfaces[interfaceIndex].rx_bytes) /
        intervalInSec);
    data.push(elements);
  }
  drawLineChart(titles, data, elementId, 'Bytes per second');
}

// Draw the graph for network errors
function drawNetworkErrors(elementId, machineInfo, stats) {
  if (stats.spec.has_network && !hasResource(stats, 'network')) {
    return;
  }

  // Get interface index.
  var interfaceIndex = -1;
  if (stats.stats.length > 0) {
    interfaceIndex = getNetworkInterfaceIndex(
        window.cadvisor.network.interface, stats.stats[0].network.interfaces);
  }
  if (interfaceIndex < 0) {
    console.log(
        'Unable to find interface"', interfaceName, '" in ',
        stats.stats.network);
    return;
  }

  var titles = ['Time', 'Tx', 'Rx'];
  var data = [];
  for (var i = 1; i < stats.stats.length; i++) {
    var cur = stats.stats[i];
    var prev = stats.stats[i - 1];
    var intervalInSec = getInterval(cur.timestamp, prev.timestamp) / 1000000000;

    var elements = [];
    elements.push(cur.timestamp);
    elements.push(
        (cur.network.interfaces[interfaceIndex].tx_errors -
         prev.network.interfaces[interfaceIndex].tx_errors) /
        intervalInSec);
    elements.push(
        (cur.network.interfaces[interfaceIndex].rx_errors -
         prev.network.interfaces[interfaceIndex].rx_errors) /
        intervalInSec);
    data.push(elements);
  }
  drawLineChart(titles, data, elementId, 'Errors per second');
}

// Update the filesystem usage values.
function drawFileSystemUsage(machineInfo, stats) {
  var cur = stats.stats[stats.stats.length - 1];
  if (!cur.filesystem) {
    return;
  }

  var el = $('<div>');
  for (var i = 0; i < cur.filesystem.length; i++) {
    var data = cur.filesystem[i];
    var totalUsage = Math.floor((data.usage * 100.0) / data.capacity);

    // Update DOM elements.
    var els = window.cadvisor.fsUsage.elements[data.device];
    els.progressElement.width(totalUsage + '%');
    els.textElement.text(
        humanizeMetric(data.usage) + ' / ' + humanizeMetric(data.capacity) +
        ' (' + totalUsage + '%)');
  }
}

function drawImages(images) {
  if (images == null || images.length == 0) {
    return;
  }
  window.charts = {};
  var titles = ['Repository', 'Tags', 'ID', 'Virtual Size', 'Creation Time'];
  var titleTypes = ['string', 'string', 'string', 'number', 'number'];
  var sortIndex = 0;
  var data = [];
  for (var i = 0; i < images.length; i++) {
    var elements = [];
    var tags = [];
    var repos = images[i].repo_tags[0].split(':');
    repos.splice(-1, 1);
    for (var j = 0; j < images[i].repo_tags.length; j++) {
      var splits = images[i].repo_tags[j].split(':');
      if (splits.length > 1) {
        tags.push(splits[splits.length - 1]);
      }
    }
    elements.push(repos.join(':'));
    elements.push(tags.join(', '));
    elements.push(images[i].id.substr(0, 24));
    elements.push(
        {v: images[i].virtual_size, f: humanizeIEC(images[i].virtual_size)});
    var d = new Date(images[i].created * 1000);
    elements.push({v: images[i].created, f: d.toLocaleString()});
    data.push(elements);
  }
  drawTable(titles, titleTypes, data, 'docker-images', 30, sortIndex);
}

function drawProcesses(isRoot, rootDir, processInfo) {
  if (processInfo.length == 0) {
    $('#processes-top').text('No processes found');
    return;
  }
  var titles = [
    'User', 'PID', 'PPID', 'Start Time', 'CPU %', 'MEM %', 'RSS',
    'Virtual Size', 'Status', 'Running Time', 'Command', 'PSR'
  ];
  var titleTypes = [
    'string', 'number', 'number', 'string', 'number', 'number', 'number',
    'number', 'string', 'string', 'string', 'number'
  ];
  var sortIndex = 4;
  if (isRoot) {
    titles.push('Container');
    titleTypes.push('string');
  }
  var data = [];
  for (var i = 0; i < processInfo.length; i++) {
    var elements = [];
    elements.push(processInfo[i].user);
    elements.push(processInfo[i].pid);
    elements.push(processInfo[i].parent_pid);
    elements.push(processInfo[i].start_time);
    elements.push({
      v: processInfo[i].percent_cpu,
      f: processInfo[i].percent_cpu.toFixed(2)
    });
    elements.push({
      v: processInfo[i].percent_mem,
      f: processInfo[i].percent_mem.toFixed(2)
    });
    elements.push({v: processInfo[i].rss, f: humanizeIEC(processInfo[i].rss)});
    elements.push({
      v: processInfo[i].virtual_size,
      f: humanizeIEC(processInfo[i].virtual_size)
    });
    elements.push(processInfo[i].status);
    elements.push(processInfo[i].running_time);
    elements.push(processInfo[i].cmd);
    elements.push(processInfo[i].psr);
    if (isRoot) {
      var cgroup = processInfo[i].cgroup_path;
      // Use the raw cgroup link as it works for all containers.
      var cgroupLink = '<a href="' + rootDir + 'containers/' + cgroup + '">' +
          cgroup.substr(0, 30) + ' </a>';
      elements.push({v: cgroup, f: cgroupLink});
    }
    data.push(elements);
  }
  drawTable(titles, titleTypes, data, 'processes-top', 25, sortIndex);
}

// Draw the filesystem usage nodes.
function startFileSystemUsage(elementId, machineInfo, stats) {
  window.cadvisor.fsUsage = {};

  // A map of device name to DOM elements.
  window.cadvisor.fsUsage.elements = {};

  var cur = stats.stats[stats.stats.length - 1];
  var el = $('<div>');
  if (!cur.filesystem) {
    return;
  }
  for (var i = 0; i < cur.filesystem.length; i++) {
    var data = cur.filesystem[i];
    el.append(
        $('<div>')
            .addClass('row col-sm-12')
            .append($('<h4>').text('FS #' + (i + 1) + ': ' + data.device)));

    var progressElement =
        $('<div>').addClass('progress-bar progress-bar-danger');
    el.append(
        $('<div>')
            .addClass('col-sm-9')
            .append($('<div>').addClass('progress').append(progressElement)));

    var textElement = $('<div>').addClass('col-sm-3');
    el.append(textElement);

    window.cadvisor.fsUsage.elements[data.device] = {
      'progressElement': progressElement,
      'textElement': textElement,
      'index': i,
    };
  }
  $('#' + elementId).empty().append(el);

  drawFileSystemUsage(machineInfo, stats);
}

// Expects an array of closures to call. After each execution the JS runtime is
// given control back before continuing.
// This function returns asynchronously
function stepExecute(steps) {
  // No steps, stop.
  if (steps.length == 0) {
    return;
  }

  // Get a step and execute it.
  var step = steps.shift();
  step();

  // Schedule the next step.
  setTimeout(function() { stepExecute(steps); }, 0);
}

// Draw all the charts on the page.
function drawCharts(machineInfo, containerInfo, subcontainers) {
  var steps = [];

  if (containerInfo.spec.has_cpu || containerInfo.spec.has_memory) {
    steps.push(function() {
      drawOverallUsage('usage-gauge', machineInfo, containerInfo);
    });
  }

  // CPU.
  if (containerInfo.spec.has_cpu) {
    steps.push(function() {
      drawCpuTotalUsage('cpu-total-usage-chart', machineInfo, containerInfo);
    });
    // TODO(rjnagal): Re-enable CPU Load after understanding resource usage.
    // steps.push(function() {
    // 	drawCpuLoad("cpu-load-chart", machineInfo, containerInfo);
    // });
    steps.push(function() {
      drawCpuPerCoreUsage(
          'cpu-per-core-usage-chart', machineInfo, containerInfo);
    });
    steps.push(function() {
      drawCpuUsageBreakdown(
          'cpu-usage-breakdown-chart', machineInfo, containerInfo);
    });
  }

  // Memory.
  if (containerInfo.spec.has_memory) {
    steps.push(function() {
      drawMemoryUsage('memory-usage-chart', machineInfo, containerInfo);
    });
  }

  // Network.
  if (containerInfo.spec.has_network) {
    steps.push(function() {
      drawNetworkBytes('network-bytes-chart', machineInfo, containerInfo);
    });
    steps.push(function() {
      drawNetworkErrors('network-errors-chart', machineInfo, containerInfo);
    });
  }

  // Filesystem.
  if (containerInfo.spec.has_filesystem) {
    steps.push(function() { drawFileSystemUsage(machineInfo, containerInfo); });
  }

  // Custom Metrics
  if (containerInfo.spec.has_custom_metrics) {
    steps.push(function() {
      getCustomMetrics(
          window.cadvisor.rootDir, window.cadvisor.containerName,
          function(metricsInfo) {
            drawCustomMetrics(
              'custom-metrics-chart', containerInfo, metricsInfo);
          });
    });
  }

  // Subcontainers.
  var subcontainerInfos = filterSubcontainers(containerInfo, subcontainers);
  if (subcontainerInfos.length > 0) {
    if (hasResourceForAll(subcontainerInfos, 'cpu')) {
      steps.push(function() {
        var displayCount = $('#cpu-per-subcontainer-display-count').val();
        drawCpuPerSubcontainerUsage(
            'cpu-per-subcontainer-usage-chart',
            sliceByCpu(subcontainerInfos, displayCount));
      });
    }

    if (hasResourceForAll(subcontainerInfos, 'memory')) {
      steps.push(function() {
        var displayCount = $('#memory-per-subcontainer-display-count').val();
        drawMemoryPerSubcontainerUsage(
            'memory-per-subcontainer-usage-chart',
            sliceByMemory(subcontainerInfos, displayCount));
      });
    }
  }

  stepExecute(steps);
}

// Return an slice of subcontainers sorted by CPU, with at most 'count' entries.
function sliceByCpu(subcontainerInfos, count) {
  subcontainerInfos.sort(function(a, b) {
    if (a.averages.cpu > b.averages.cpu) {
      return -1;
    } else if (a.averages.cpu < b.averages.cpu) {
      return 1;
    } else {
      return compareByName(a, b);
    }
  });
  return subcontainerInfos.slice(0, Math.min(subcontainerInfos.length, count))
      .sort(compareByName);
}

// Return an slice of subcontainers sorted by memory, with at most 'count'
// entries.
function sliceByMemory(subcontainerInfos, count) {
  subcontainerInfos.sort(function(a, b) {
    if (a.averages.memory > b.averages.memory) {
      return -1;
    } else if (a.averages.memory < b.averages.memory) {
      return 1;
    } else {
      return compareByName(a, b);
    }
  });
  return subcontainerInfos.slice(0, Math.min(subcontainerInfos.length, count))
      .sort(compareByName);
}

// Return sort comparitor based on subcontroller name.
function compareByName(subA, subB) {
  if (subA.name > subB.name) {
    return 1;
  } else if (subA.name < subB.name) {
    return -1;
  } else {
    return 0;
  }
}

// Return a map of the averages of the subcontainer stats.
function getSubcontainerAverages(subcontainer) {
  var cpuSum = 0;
  var memorySum = 0;
  subcontainer.stats.forEach(function(stat) {
    cpuSum += stat.cpu.usage.total;
    memorySum += stat.memory.usage / oneMegabyte;
  });
  return {
    cpu: cpuSum / subcontainer.stats.length,
    memory: memorySum / subcontainer.stats.length,
  }
}

// Return a list of immediate subcontainers, including metric averages.
function filterSubcontainers(containerInfo, subcontainers) {
  if (!containerInfo.subcontainers ||
      containerInfo.subcontainers.length === 0 ||
      !subcontainers) {
    return [];
  }

  var subcontainerNames = {};
  containerInfo.subcontainers.forEach(function(subcontainer) {
    subcontainerNames[subcontainer.name] = subcontainer.name;
  });

  var subcontainerInfos = [];
  subcontainers.forEach(function(subcontainer) {
    if (subcontainerNames[subcontainer.name] !== undefined) {
      subcontainer.averages = getSubcontainerAverages(subcontainer);
      subcontainerInfos.push(subcontainer);
    }
  });

  return subcontainerInfos;
}

function setNetwork(interfaceName) {
  $('#network-selection-text')
      .empty()
      .append($('<span>').text('Interface: '))
      .append($('<b>').text(interfaceName));
  window.cadvisor.network.interface = interfaceName;

  // Draw the new stats.
  refreshStats();
}

// Creates the network selection dropdown.
function startNetwork(selectionElement, containerInfo) {
  if (!hasResource(containerInfo, 'network') ||
      containerInfo.stats.length == 0 ||
      !containerInfo.stats[0].network.interfaces ||
      containerInfo.stats[0].network.interfaces.length == 0) {
    return;
  }

  window.cadvisor.network = {};
  window.cadvisor.network.interface = '';

  // Add all interfaces to the dropdown.
  var el = $('#' + selectionElement);
  for (var i = 0; i < containerInfo.stats[0].network.interfaces.length; i++) {
    var interfaceName = containerInfo.stats[0].network.interfaces[i].name;
    el.append($('<li>')
                  .attr('role', 'presentation')
                  .append($('<a>')
                              .attr('role', 'menuitem')
                              .attr('tabindex', -1)
                              .click(setNetwork.bind(null, interfaceName))
                              .text(interfaceName)));
  }
  setNetwork(containerInfo.stats[0].network.interfaces[0].name);
}

// Refresh the stats on the page.
function refreshStats() {
  var machineInfo = window.cadvisor.machineInfo;
  getStats(
      window.cadvisor.rootDir, window.cadvisor.containerName,
      function(containerInfo, subcontainers) {
        if (window.cadvisor.firstRun) {
          window.cadvisor.firstRun = false;

          if (containerInfo.spec.has_filesystem) {
            startFileSystemUsage(
                'filesystem-usage', machineInfo, containerInfo);
          }
          if (containerInfo.spec.has_network) {
            startNetwork('network-selection', containerInfo);
          }
          if (containerInfo.spec.has_custom_metrics) {
            startCustomMetrics('custom-metrics-chart', containerInfo);
          }
        }
        drawCharts(machineInfo, containerInfo, subcontainers);
      });
}

function addAllLabels(containerInfo, metricsInfo) {
  if (metricsInfo.length == 0) {
    return;
  }
  var metricSpec = containerInfo.spec.custom_metrics;
  for (var containerName in metricsInfo) {
    var container = metricsInfo[containerName];
    for (i = 0; i < metricSpec.length; i++) {
      metricName = metricSpec[i].name;
      metricLabelVal = container[metricName];
      firstLabel = true;
      for (var label in metricLabelVal) {
        if (label == '') {
          $('#button-' + metricName).hide();
        }

        $('#' + metricName + '_labels')
            .append(
                $('<li>')
                    .attr('role', 'presentation')
                    .append($('<a>')
                                .attr('role', 'menuitem')
                                .click(setLabel.bind(null, metricName, label))
                                .text(label)));
        if (firstLabel) {
          firstLabel = false;
          setLabel(metricName, label);
        }
      }
    }
  }
}

function getMetricIndex(metricName) {
  for (i = 0; i < window.cadvisor.metricLabelPair.length; ++i) {
    if (window.cadvisor.metricLabelPair[i][0] == metricName) { return i; }
  }
  return -1;
}

function setLabel(metric, label) {
  $('#' + metric + '-selection-text')
      .empty()
      .append($('<span>').text('Label: '))
      .append($('<b>').text(label));

  index = getMetricIndex(metric);
  if (index == -1) {
    window.cadvisor.metricLabelPair.push([metric, label]);
  } else {
    window.cadvisor.metricLabelPair[index][1] = label;
  }

  refreshStats();
}

function getSelectedLabel(metricName) {
  index = getMetricIndex(metricName);
  if (index == -1) { return ''; }
  return window.cadvisor.metricLabelPair[index][1];
}

function startCustomMetrics(elementId, containerInfo) {
  var metricSpec = containerInfo.spec.custom_metrics;
  var metricStats = containerInfo.stats.custom_metrics;
  var el = $('<div>');

  if (metricSpec.length < window.cadvisor.maxCustomMetrics) {
    window.cadvisor.maxCustomMetrics = metricSpec.length;
    for (i = 0; i < window.cadvisor.maxCustomMetrics; i++) {
      metricName = metricSpec[i].name;
      var divText =
          '<div class=\'dropdown\'> <button class=\'btn btn-default' +
          ' dropdown-toggle\' type=\'button\' id=\'button-' + metricName;
      divText +=
          '\' data-toggle=\'dropdown\' aria-haspopup=\'true\'' +
          ' aria-expanded=\'false\'>';
      divText += '<span id=\'' + metricName +
          '-selection-text\'></span> <span class=\'caret\'></span> </button>';
      divText += '<ul id=\'' + metricName +
          '_labels\' class=\'dropdown-menu\' role=\'menu\'' +
          ' aria-labelledby=\'button-' + metricName + '\'> </ul> </div>';
      divText += '<div id=\'' + elementId + '-' + metricName + '\'> </div>';
      el.append($(divText));
    }
  }
  el.append($('</div>'));

  $('#' + elementId).append(el);
}

function getCustomMetrics(rootDir, containerName, callback) {
  $.getJSON(rootDir + 'api/v2.0/appmetrics/' + containerName)
      .done(function(data) { callback(data); })
      .fail(function(jqhxr, textStatus, error) { callback([]); });
}

function drawCustomMetrics(elementId, containerInfo, metricsInfo) {
  if (metricsInfo.length == 0) {
    return;
  }
  var metricSpec = containerInfo.spec.custom_metrics;
  for (var containerName in metricsInfo) {
    var container = metricsInfo[containerName];
    for (i = 0; i < window.cadvisor.maxCustomMetrics; i++) {
      metricName = metricSpec[i].name;
      metricUnits = metricSpec[i].units;
      var titles = ['Time', metricName];
      metricLabelVal = container[metricName];
      if (window.cadvisor.firstCustomCollection) {
        window.cadvisor.firstCustomCollection = false;
        addAllLabels(containerInfo, metricsInfo);
      }
      var data = [];
      selectedLabel = getSelectedLabel(metricName);
      metricVal = metricLabelVal[selectedLabel];
      for (var index in metricVal) {
        metric = metricVal[index];
        var elements = [];
        for (var attribute in metric) {
          value = metric[attribute];
          elements.push(value);
        }
        if (elements.length < 2) {
          elements.push(0);
        }
        data.push(elements);
      }
      drawLineChart(titles, data, elementId + '-' + metricName, metricUnits);
    }
  }
}

// Executed when the page finishes loading.
function startPage(containerName, hasCpu, hasMemory, rootDir, isRoot) {
  // Don't fetch data if we don't have any resource.
  if (!hasCpu && !hasMemory) {
    return;
  }

  window.charts = {};
  window.cadvisor = {};
  window.cadvisor.firstRun = true;
  window.cadvisor.rootDir = rootDir;
  window.cadvisor.containerName = containerName;

  window.cadvisor.firstCustomCollection = true;
  window.cadvisor.metricLabelPair = [];
  window.cadvisor.maxCustomMetrics = 10;

  // Draw process information at start and refresh every 60s.
  getProcessInfo(rootDir, containerName, function(processInfo) {
    drawProcesses(isRoot, rootDir, processInfo);
  });
  setInterval(function() {
    getProcessInfo(rootDir, containerName, function(processInfo) {
      drawProcesses(isRoot, rootDir, processInfo);
    });
  }, 60000);

  // Get machine info, then get the stats every 1s.
  getMachineInfo(rootDir, function(machineInfo) {
    window.cadvisor.machineInfo = machineInfo;
    setInterval(function() { refreshStats(); }, 1000);
  });
}
