text: // Copyright 2013 The Chromium Authors. All rights reserved.
  // Use of this source code is governed by a BSD-style license that can be
  // found in the LICENSE file.
  
  var ClientRenderer = (function() {
    var ClientRenderer = function() {
      this.playerListElement = document.getElementById('player-list');
      this.audioPropertiesTable =
          document.getElementById('audio-property-table').querySelector('tbody');
      this.playerPropertiesTable =
          document.getElementById('player-property-table').querySelector('tbody');
      this.logTable = document.getElementById('log').querySelector('tbody');
      this.graphElement = document.getElementById('graphs');
      this.audioPropertyName = document.getElementById('audio-property-name');
  
      this.players = null;
      this.selectedPlayer = null;
      this.selectedAudioComponentType = null;
      this.selectedAudioComponentId = null;
      this.selectedAudioCompontentData = null;
  
      this.selectedPlayerLogIndex = 0;
  
      this.filterFunction = function() { return true; };
      this.filterText = document.getElementById('filter-text');
      this.filterText.onkeyup = this.onTextChange_.bind(this);
  
      this.bufferCanvas = document.createElement('canvas');
      this.bufferCanvas.width = media.BAR_WIDTH;
      this.bufferCanvas.height = media.BAR_HEIGHT;
  
      this.clipboardTextarea = document.getElementById('clipboard-textarea');
      var clipboardButtons = document.getElementsByClassName('copy-button');
      for (var i = 0; i < clipboardButtons.length; i++) {
        clipboardButtons[i].onclick = this.copyToClipboard_.bind(this);
      }
  
      this.saveLogButton = document.getElementById('save-log-button');
      this.saveLogButton.onclick = this.saveLog_.bind(this);
  
      this.hiddenKeys = ['component_id', 'component_type', 'owner_id'];
  
      // Tell CSS to hide certain content prior to making selections.
      document.body.classList.add(ClientRenderer.Css_.NO_PLAYERS_SELECTED);
      document.body.classList.add(ClientRenderer.Css_.NO_COMPONENTS_SELECTED);
    };
  
    /**
     * CSS classes added / removed in JS to trigger styling changes.
     * @private @enum {string}
     */
    ClientRenderer.Css_ = {
      NO_PLAYERS_SELECTED: 'no-players-selected',
      NO_COMPONENTS_SELECTED: 'no-components-selected',
      SELECTABLE_BUTTON: 'selectable-button'
    };
  
    function removeChildren(element) {
      while (element.hasChildNodes()) {
        element.removeChild(element.lastChild);
      }
    };
  
    function createSelectableButton(id, groupName, text, select_cb) {
      // For CSS styling.
      var radioButton = document.createElement('input');
      radioButton.classList.add(ClientRenderer.Css_.SELECTABLE_BUTTON);
      radioButton.type = 'radio';
      radioButton.id = id;
      radioButton.name = groupName;
  
      var buttonLabel = document.createElement('label');
      buttonLabel.classList.add(ClientRenderer.Css_.SELECTABLE_BUTTON);
      buttonLabel.setAttribute('for', radioButton.id);
      buttonLabel.appendChild(document.createTextNode(text));
  
      var fragment = document.createDocumentFragment();
      fragment.appendChild(radioButton);
      fragment.appendChild(buttonLabel);
  
      // Listen to 'change' rather than 'click' to keep styling in sync with
      // button behavior.
      radioButton.addEventListener('change', function() {
        select_cb();
      });
  
      return fragment;
    };
  
    function selectSelectableButton(id) {
      var element = document.getElementById(id);
      if (!element) {
        console.error('failed to select button with id: ' + id);
        return;
      }
  
      element.checked = true;
    }
  
    function downloadLog(text) {
      var file = new Blob([text], {type: 'text/plain'});
      var a = document.createElement('a');
      a.href = URL.createObjectURL(file);
      a.download = "media-internals.txt";
      a.click();
    }
  
    ClientRenderer.prototype = {
      /**
       * Called when an audio component is added to the collection.
       * @param componentType Integer AudioComponent enum value; must match values
       * from the AudioLogFactory::AudioComponent enum.
       * @param components The entire map of components (name -> dict).
       */
      audioComponentAdded: function(componentType, components) {
        this.redrawAudioComponentList_(componentType, components);
  
        // Redraw the component if it's currently selected.
        if (this.selectedAudioComponentType == componentType &&
            this.selectedAudioComponentId &&
            this.selectedAudioComponentId in components) {
          // TODO(chcunningham): This path is used both for adding and updating
          // the components. Split this up to have a separate update method.
          // At present, this selectAudioComponent call is key to *updating* the
          // the property table for existing audio components.
          this.selectAudioComponent_(
              componentType, this.selectedAudioComponentId,
              components[this.selectedAudioComponentId]);
        }
      },
  
      /**
       * Called when an audio component is removed from the collection.
       * @param componentType Integer AudioComponent enum value; must match values
       * from the AudioLogFactory::AudioComponent enum.
       * @param components The entire map of components (name -> dict).
       */
      audioComponentRemoved: function(componentType, components) {
        this.redrawAudioComponentList_(componentType, components);
      },
  
      /**
       * Called when a player is added to the collection.
       * @param players The entire map of id -> player.
       * @param player_added The player that is added.
       */
      playerAdded: function(players, playerAdded) {
        this.redrawPlayerList_(players);
      },
  
      /**
       * Called when a player is removed from the collection.
       * @param players The entire map of id -> player.
       * @param playerRemoved The player that was removed.
       */
      playerRemoved: function(players, playerRemoved) {
        if (playerRemoved === this.selectedPlayer) {
          removeChildren(this.playerPropertiesTable);
          removeChildren(this.logTable);
          removeChildren(this.graphElement);
          document.body.classList.add(ClientRenderer.Css_.NO_PLAYERS_SELECTED);
        }
        this.redrawPlayerList_(players);
      },
  
      /**
       * Called when a property on a player is changed.
       * @param players The entire map of id -> player.
       * @param player The player that had its property changed.
       * @param key The name of the property that was changed.
       * @param value The new value of the property.
       */
      playerUpdated: function(players, player, key, value) {
        if (player === this.selectedPlayer) {
          this.drawProperties_(player.properties, this.playerPropertiesTable);
          this.drawLog_();
          this.drawGraphs_();
        }
        if (key === 'name' || key === 'url') {
          this.redrawPlayerList_(players);
        }
      },
  
      createVideoCaptureFormatTable: function(formats) {
        if (!formats || formats.length == 0)
          return document.createTextNode('No formats');
  
        var table = document.createElement('table');
        var thead = document.createElement('thead');
        var theadRow = document.createElement('tr');
        for (var key in formats[0]) {
          var th = document.createElement('th');
          th.appendChild(document.createTextNode(key));
          theadRow.appendChild(th);
        }
        thead.appendChild(theadRow);
        table.appendChild(thead);
        var tbody = document.createElement('tbody');
        for (var i=0; i < formats.length; ++i) {
          var tr = document.createElement('tr')
          for (var key in formats[i]) {
            var td = document.createElement('td');
            td.appendChild(document.createTextNode(formats[i][key]));
            tr.appendChild(td);
          }
          tbody.appendChild(tr);
        }
        table.appendChild(tbody);
        table.classList.add('video-capture-formats-table');
        return table;
      },
  
      redrawVideoCaptureCapabilities: function(videoCaptureCapabilities, keys) {
        var copyButtonElement =
            document.getElementById('video-capture-capabilities-copy-button');
        copyButtonElement.onclick = function() {
          window.prompt('Copy to clipboard: Ctrl+C, Enter',
                        JSON.stringify(videoCaptureCapabilities))
        }
  
        var videoTableBodyElement  =
            document.getElementById('video-capture-capabilities-tbody');
        removeChildren(videoTableBodyElement);
  
        for (var component in videoCaptureCapabilities) {
          var tableRow =  document.createElement('tr');
          var device = videoCaptureCapabilities[ component ];
          for (var i in keys) {
            var value = device[keys[i]];
            var tableCell = document.createElement('td');
            var cellElement;
            if ((typeof value) == (typeof [])) {
              cellElement = this.createVideoCaptureFormatTable(value);
            } else {
              cellElement = document.createTextNode(
                  ((typeof value) == 'undefined') ? 'n/a' : value);
            }
            tableCell.appendChild(cellElement)
            tableRow.appendChild(tableCell);
          }
          videoTableBodyElement.appendChild(tableRow);
        }
      },
  
      getAudioComponentName_: function(componentType, id) {
        var baseName;
        switch (componentType) {
          case 0:
          case 1:
            baseName = 'Controller';
            break;
          case 2:
            baseName = 'Stream';
            break;
          default:
            baseName = 'UnknownType'
            console.error('Unrecognized component type: ' + componentType);
            break;
        }
        return baseName + ' ' + id;
      },
  
      getListElementForAudioComponent_: function(componentType) {
        var listElement;
        switch (componentType) {
          case 0:
            listElement = document.getElementById(
                'audio-input-controller-list');
            break;
          case 1:
            listElement = document.getElementById(
                'audio-output-controller-list');
            break;
          case 2:
            listElement = document.getElementById(
                'audio-output-stream-list');
            break;
          default:
            console.error('Unrecognized component type: ' + componentType);
            listElement = null;
            break;
        }
        return listElement;
      },
  
      redrawAudioComponentList_: function(componentType, components) {
        // Group name imposes rule that only one component can be selected
        // (and have its properties displayed) at a time.
        var buttonGroupName = 'audio-components';
  
        var listElement = this.getListElementForAudioComponent_(componentType);
        if (!listElement) {
          console.error('Failed to find list element for component type: ' +
              componentType);
          return;
        }
  
        var fragment = document.createDocumentFragment();
        for (id in components) {
          var li = document.createElement('li');
          var button_cb = this.selectAudioComponent_.bind(
                  this, componentType, id, components[id]);
          var friendlyName = this.getAudioComponentName_(componentType, id);
          li.appendChild(createSelectableButton(
              id, buttonGroupName, friendlyName, button_cb));
          fragment.appendChild(li);
        }
        removeChildren(listElement);
        listElement.appendChild(fragment);
  
        if (this.selectedAudioComponentType &&
            this.selectedAudioComponentType == componentType &&
            this.selectedAudioComponentId in components) {
          // Re-select the selected component since the button was just recreated.
          selectSelectableButton(this.selectedAudioComponentId);
        }
      },
  
      selectAudioComponent_: function(componentType, componentId, componentData) {
        document.body.classList.remove(
           ClientRenderer.Css_.NO_COMPONENTS_SELECTED);
  
        this.selectedAudioComponentType = componentType;
        this.selectedAudioComponentId = componentId;
        this.selectedAudioCompontentData = componentData;
        this.drawProperties_(componentData, this.audioPropertiesTable);
  
        removeChildren(this.audioPropertyName);
        this.audioPropertyName.appendChild(document.createTextNode(
            this.getAudioComponentName_(componentType, componentId)));
      },
  
      redrawPlayerList_: function(players) {
        this.players = players;
  
        // Group name imposes rule that only one component can be selected
        // (and have its properties displayed) at a time.
        var buttonGroupName = 'player-buttons';
  
        var hasPlayers = false;
        var fragment = document.createDocumentFragment();
        for (id in players) {
          hasPlayers = true;
          var player = players[id];
          var usableName = player.properties.name ||
              player.properties.url ||
              'Player ' + player.id;
  
          var li = document.createElement('li');
          var button_cb = this.selectPlayer_.bind(this, player);
          li.appendChild(createSelectableButton(
              id, buttonGroupName, usableName, button_cb));
          fragment.appendChild(li);
        }
        removeChildren(this.playerListElement);
        this.playerListElement.appendChild(fragment);
  
        if (this.selectedPlayer && this.selectedPlayer.id in players) {
          // Re-select the selected player since the button was just recreated.
          selectSelectableButton(this.selectedPlayer.id);
        }
  
        this.saveLogButton.style.display = hasPlayers ? "inline-block" : "none";
      },
  
      selectPlayer_: function(player) {
        document.body.classList.remove(ClientRenderer.Css_.NO_PLAYERS_SELECTED);
  
        this.selectedPlayer = player;
        this.selectedPlayerLogIndex = 0;
        this.selectedAudioComponentType = null;
        this.selectedAudioComponentId = null;
        this.selectedAudioCompontentData = null;
        this.drawProperties_(player.properties, this.playerPropertiesTable);
  
        removeChildren(this.logTable);
        removeChildren(this.graphElement);
        this.drawLog_();
        this.drawGraphs_();
      },
  
      drawProperties_: function(propertyMap, propertiesTable) {
        removeChildren(propertiesTable);
        var sortedKeys = Object.keys(propertyMap).sort();
        for (var i = 0; i < sortedKeys.length; ++i) {
          var key = sortedKeys[i];
          if (this.hiddenKeys.indexOf(key) >= 0)
            continue;
  
          var value = propertyMap[key];
          var row = propertiesTable.insertRow(-1);
          var keyCell = row.insertCell(-1);
          var valueCell = row.insertCell(-1);
  
          keyCell.appendChild(document.createTextNode(key));
          valueCell.appendChild(document.createTextNode(value));
        }
      },
  
      appendEventToLog_: function(event) {
        if (this.filterFunction(event.key)) {
          var row = this.logTable.insertRow(-1);
  
          var timestampCell = row.insertCell(-1);
          timestampCell.classList.add('timestamp');
          timestampCell.appendChild(document.createTextNode(
              util.millisecondsToString(event.time)));
          row.insertCell(-1).appendChild(document.createTextNode(event.key));
          row.insertCell(-1).appendChild(document.createTextNode(event.value));
        }
      },
  
      drawLog_: function() {
        var toDraw = this.selectedPlayer.allEvents.slice(
            this.selectedPlayerLogIndex);
        toDraw.forEach(this.appendEventToLog_.bind(this));
        this.selectedPlayerLogIndex = this.selectedPlayer.allEvents.length;
      },
  
      saveLog_: function() {
        var strippedPlayers = []
        for (id in this.players) {
          var p = this.players[id];
          strippedPlayers.push({properties: p.properties, events: p.allEvents});
        }
        downloadLog(JSON.stringify(strippedPlayers, null, 2));
      },
  
      drawGraphs_: function() {
        function addToGraphs(name, graph, graphElement) {
          var li = document.createElement('li');
          li.appendChild(graph);
          li.appendChild(document.createTextNode(name));
          graphElement.appendChild(li);
        }
  
        var url = this.selectedPlayer.properties.url;
        if (!url) {
          return;
        }
  
        var cache = media.cacheForUrl(url);
  
        var player = this.selectedPlayer;
        var props = player.properties;
  
        var cacheExists = false;
        var bufferExists = false;
  
        if (props['buffer_start'] !== undefined &&
            props['buffer_current'] !== undefined &&
            props['buffer_end'] !== undefined &&
            props['total_bytes'] !== undefined) {
          this.drawBufferGraph_(props['buffer_start'],
                                props['buffer_current'],
                                props['buffer_end'],
                                props['total_bytes']);
          bufferExists = true;
        }
  
        if (cache) {
          if (player.properties['total_bytes']) {
            cache.size = Number(player.properties['total_bytes']);
          }
          cache.generateDetails();
          cacheExists = true;
  
        }
  
        if (!this.graphElement.hasChildNodes()) {
          if (bufferExists) {
            addToGraphs('buffer', this.bufferCanvas, this.graphElement);
          }
          if (cacheExists) {
            addToGraphs('cache read', cache.readCanvas, this.graphElement);
            addToGraphs('cache write', cache.writeCanvas, this.graphElement);
          }
        }
      },
  
      drawBufferGraph_: function(start, current, end, size) {
        var ctx = this.bufferCanvas.getContext('2d');
        var width = this.bufferCanvas.width;
        var height = this.bufferCanvas.height;
        ctx.fillStyle = '#aaa';
        ctx.fillRect(0, 0, width, height);
  
        var scale_factor = width / size;
        var left = start * scale_factor;
        var middle = current * scale_factor;
        var right = end * scale_factor;
  
        ctx.fillStyle = '#a0a';
        ctx.fillRect(left, 0, middle - left, height);
        ctx.fillStyle = '#aa0';
        ctx.fillRect(middle, 0, right - middle, height);
      },
  
      copyToClipboard_: function() {
        if (!this.selectedPlayer && !this.selectedAudioCompontentData) {
          return;
        }
        var properties = this.selectedAudioCompontentData ||
            this.selectedPlayer.properties;
        var stringBuffer = [];
  
        for (var key in properties) {
          var value = properties[key];
          stringBuffer.push(key.toString());
          stringBuffer.push(': ');
          stringBuffer.push(value.toString());
          stringBuffer.push('\n');
        }
  
        this.clipboardTextarea.value = stringBuffer.join('');
        this.clipboardTextarea.classList.remove('hiddenClipboard');
        this.clipboardTextarea.focus();
        this.clipboardTextarea.select();
  
        // Hide the clipboard element when it loses focus.
        this.clipboardTextarea.onblur = function(event) {
          setTimeout(function(element) {
            event.target.classList.add('hiddenClipboard');
          }, 0);
        };
      },
  
      onTextChange_: function(event) {
        var text = this.filterText.value.toLowerCase();
        var parts = text.split(',').map(function(part) {
          return part.trim();
        }).filter(function(part) {
          return part.trim().length > 0;
        });
  
        this.filterFunction = function(text) {
          text = text.toLowerCase();
          return parts.length === 0 || parts.some(function(part) {
            return text.indexOf(part) != -1;
          });
        };
  
        if (this.selectedPlayer) {
          removeChildren(this.logTable);
          this.selectedPlayerLogIndex = 0;
          this.drawLog_();
        }
      },
    };
  
    return ClientRenderer;
  })();
  