text: // Copyright (c) 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.
  
  'use strict';
  
  ////////////////////////////////////////////////////////////////////////////////
  // DirectoryTreeUtil
  
  /**
   * Utility methods. They are intended for use only in this file.
   */
  var DirectoryTreeUtil = {};
  
  /**
   * Generate a list of the directory entries for the top level on the tree.
   * @return {Array.<DirectoryEntry>} Entries for the top level on the tree.
   */
  DirectoryTreeUtil.generateTopLevelEntries = function() {
    var entries = [
      DirectoryModel.fakeDriveEntry_,
      DirectoryModel.fakeDriveOfflineEntry_,
      DirectoryModel.fakeDriveSharedWithMeEntry_,
      DirectoryModel.fakeDriveRecentEntry_,
    ];
  
    for (var i = 0; i < entries.length; i++) {
      entries[i]['label'] = PathUtil.getRootLabel(entries[i].fullPath);
    }
  
    return entries;
  };
  
  /**
   * Checks if the given directory can be on the tree or not.
   *
   * @param {string} path Path to be checked.
   * @return {boolean} True if the path is eligible for the directory tree.
   *     Otherwise, false.
   */
  DirectoryTreeUtil.isEligiblePathForDirectoryTree = function(path) {
    return PathUtil.isDriveBasedPath(path);
  };
  
  Object.freeze(DirectoryTreeUtil);
  
  ////////////////////////////////////////////////////////////////////////////////
  // DirectoryTreeBase
  
  /**
   * Implementation of methods for DirectoryTree and DirectoryItem. These classes
   * inherits cr.ui.Tree/TreeItem so we can't make them inherit this class.
   * Instead, we separate their implementations to this separate object and call
   * it with setting 'this' from DirectoryTree/Item.
   */
  var DirectoryItemTreeBaseMethods = {};
  
  /**
   * Updates sub-elements of {@code this} reading {@code DirectoryEntry}.
   * The list of {@code DirectoryEntry} are not updated by this method.
   *
   * @param {boolean} recursive True if the all visible sub-directories are
   *     updated recursively including left arrows. If false, the update walks
   *     only immediate child directories without arrows.
   */
  DirectoryItemTreeBaseMethods.updateSubElementsFromList = function(recursive) {
    var index = 0;
    var tree = this.parentTree_ || this;  // If no parent, 'this' itself is tree.
    while (this.entries_[index]) {
      var currentEntry = this.entries_[index];
      var currentElement = this.items[index];
  
      if (index >= this.items.length) {
        var item = new DirectoryItem(currentEntry, this, tree);
        this.add(item);
        index++;
      } else if (currentEntry.fullPath == currentElement.fullPath) {
        if (recursive && this.expanded)
          currentElement.updateSubDirectories(true /* recursive */);
  
        index++;
      } else if (currentEntry.fullPath < currentElement.fullPath) {
        var item = new DirectoryItem(currentEntry, this, tree);
        this.addAt(item, index);
        index++;
      } else if (currentEntry.fullPath > currentElement.fullPath) {
        this.remove(currentElement);
      }
    }
  
    var removedChild;
    while (removedChild = this.items[index]) {
      this.remove(removedChild);
    }
  
    if (index == 0) {
      this.hasChildren = false;
      this.expanded = false;
    } else {
      this.hasChildren = true;
    }
  };
  
  /**
   * Finds a parent directory of the {@code entry} in {@code this}, and
   * invokes the DirectoryItem.selectByEntry() of the found directory.
   *
   * @param {DirectoryEntry|Object} entry The entry to be searched for. Can be
   *     a fake.
   * @return {boolean} True if the parent item is found.
   */
  DirectoryItemTreeBaseMethods.searchAndSelectByEntry = function(entry) {
    for (var i = 0; i < this.items.length; i++) {
      var item = this.items[i];
      if (util.isParentEntry(item.entry, entry)) {
        item.selectByEntry(entry);
        return true;
      }
    }
    return false;
  };
  
  Object.freeze(DirectoryItemTreeBaseMethods);
  
  ////////////////////////////////////////////////////////////////////////////////
  // DirectoryItem
  
  /**
   * A directory in the tree. Each element represents one directory.
   *
   * @param {DirectoryEntry} dirEntry DirectoryEntry of this item.
   * @param {DirectoryItem|DirectoryTree} parentDirItem Parent of this item.
   * @param {DirectoryTree} tree Current tree, which contains this item.
   * @extends {cr.ui.TreeItem}
   * @constructor
   */
  function DirectoryItem(dirEntry, parentDirItem, tree) {
    var item = cr.doc.createElement('div');
    DirectoryItem.decorate(item, dirEntry, parentDirItem, tree);
    return item;
  }
  
  /**
   * @param {HTMLElement} el Element to be DirectoryItem.
   * @param {DirectoryEntry} dirEntry DirectoryEntry of this item.
   * @param {DirectoryItem|DirectoryTree} parentDirItem Parent of this item.
   * @param {DirectoryTree} tree Current tree, which contains this item.
   */
  DirectoryItem.decorate =
      function(el, dirEntry, parentDirItem, tree) {
    el.__proto__ = DirectoryItem.prototype;
    (/** @type {DirectoryItem} */ el).decorate(
        dirEntry, parentDirItem, tree);
  };
  
  DirectoryItem.prototype = {
    __proto__: cr.ui.TreeItem.prototype,
  
    /**
     * The DirectoryEntry corresponding to this DirectoryItem. This may be
     * a dummy DirectoryEntry.
     * @type {DirectoryEntry|Object}
     */
    get entry() {
      return this.dirEntry_;
    },
  
    /**
     * The element containing the label text and the icon.
     * @type {!HTMLElement}
     * @override
     */
    get labelElement() {
      return this.firstElementChild.querySelector('.label');
    }
  };
  
  /**
   * Calls DirectoryItemTreeBaseMethods.updateSubElementsFromList().
   *
   * @param {boolean} recursive True if the all visible sub-directories are
   *     updated recursively including left arrows. If false, the update walks
   *     only immediate child directories without arrows.
   */
  DirectoryItem.prototype.updateSubElementsFromList = function(recursive) {
    DirectoryItemTreeBaseMethods.updateSubElementsFromList.call(this, recursive);
  };
  
  /**
   * Calls DirectoryItemTreeBaseMethods.updateSubElementsFromList().
   * @param {DirectoryEntry|Object} entry The entry to be searched for. Can be
   *     a fake.
   * @return {boolean} True if the parent item is found.
   */
  DirectoryItem.prototype.searchAndSelectByEntry = function(entry) {
    return DirectoryItemTreeBaseMethods.searchAndSelectByEntry.call(this, entry);
  };
  
  /**
   * @param {DirectoryEntry} dirEntry DirectoryEntry of this item.
   * @param {DirectoryItem|DirectoryTree} parentDirItem Parent of this item.
   * @param {DirectoryTree} tree Current tree, which contains this item.
   */
  DirectoryItem.prototype.decorate = function(
      dirEntry, parentDirItem, tree) {
    var path = dirEntry.fullPath;
    var label;
    label = dirEntry.label ? dirEntry.label : dirEntry.name;
  
    this.className = 'tree-item';
    this.innerHTML =
        '<div class="tree-row">' +
        ' <span class="expand-icon"></span>' +
        ' <span class="icon"></span>' +
        ' <span class="label"></span>' +
        '</div>' +
        '<div class="tree-children"></div>';
    this.setAttribute('role', 'treeitem');
  
    this.parentTree_ = tree;
    this.directoryModel_ = tree.directoryModel;
    this.parent_ = parentDirItem;
    this.label = label;
    this.fullPath = path;
    this.dirEntry_ = dirEntry;
    this.fileFilter_ = this.directoryModel_.getFileFilter();
  
    // Sets hasChildren=false tentatively. This will be overridden after
    // scanning sub-directories in DirectoryTreeUtil.updateSubElementsFromList.
    this.hasChildren = false;
  
    this.addEventListener('expand', this.onExpand_.bind(this), false);
    var icon = this.querySelector('.icon');
    icon.classList.add('volume-icon');
    var iconType = PathUtil.getRootType(path);
    if (iconType && PathUtil.isRootPath(path))
      icon.setAttribute('volume-type-icon', iconType);
    else
      icon.setAttribute('file-type-icon', 'folder');
  
    if (this.parentTree_.contextMenuForSubitems)
      this.setContextMenu(this.parentTree_.contextMenuForSubitems);
    // Adds handler for future change.
    this.parentTree_.addEventListener(
        'contextMenuForSubitemsChange',
        function(e) { this.setContextMenu(e.newValue); }.bind(this));
  
    if (parentDirItem.expanded)
      this.updateSubDirectories(false /* recursive */);
  };
  
  /**
   * Overrides WebKit's scrollIntoViewIfNeeded, which doesn't work well with
   * a complex layout. This call is not necessary, so we are ignoring it.
   *
   * @param {boolean} unused Unused.
   * @override
   */
  DirectoryItem.prototype.scrollIntoViewIfNeeded = function(unused) {
  };
  
  /**
   * Removes the child node, but without selecting the parent item, to avoid
   * unintended changing of directories. Removing is done externally, and other
   * code will navigate to another directory.
   *
   * @param {!cr.ui.TreeItem} child The tree item child to remove.
   * @override
   */
  DirectoryItem.prototype.remove = function(child) {
    this.lastElementChild.removeChild(child);
    if (this.items.length == 0)
      this.hasChildren = false;
  };
  
  /**
   * Invoked when the item is being expanded.
   * @param {!UIEvent} e Event.
   * @private
   **/
  DirectoryItem.prototype.onExpand_ = function(e) {
    this.updateSubDirectories(
        true /* recursive */,
        function() {},
        function() {
          this.expanded = false;
        }.bind(this));
  
    e.stopPropagation();
  };
  
  /**
   * Retrieves the latest subdirectories and update them on the tree.
   * @param {boolean} recursive True if the update is recursively.
   * @param {function()=} opt_successCallback Callback called on success.
   * @param {function()=} opt_errorCallback Callback called on error.
   */
  DirectoryItem.prototype.updateSubDirectories = function(
      recursive, opt_successCallback, opt_errorCallback) {
    if (util.isFakeEntry(this.entry)) {
      if (opt_errorCallback)
        opt_errorCallback();
      return;
    }
  
    var sortEntries = function(fileFilter, entries) {
      entries.sort(function(a, b) {
        return (a.name.toLowerCase() > b.name.toLowerCase()) ? 1 : -1;
      });
      return entries.filter(fileFilter.filter.bind(fileFilter));
    };
  
    var onSuccess = function(entries) {
      this.entries_ = entries;
      this.redrawSubDirectoryList_(recursive);
      opt_successCallback && opt_successCallback();
    }.bind(this);
  
    var reader = this.entry.createReader();
    var entries = [];
    var readEntry = function() {
      reader.readEntries(function(results) {
        if (!results.length) {
          onSuccess(sortEntries(this.fileFilter_, entries));
          return;
        }
  
        for (var i = 0; i < results.length; i++) {
          var entry = results[i];
          if (entry.isDirectory)
            entries.push(entry);
        }
        readEntry();
      }.bind(this));
    }.bind(this);
    readEntry();
  };
  
  /**
   * Updates sub-elements of {@code parentElement} reading {@code DirectoryEntry}
   * with calling {@code iterator}.
   *
   * @param {string} changedDirectryPath The path of the changed directory.
   */
  DirectoryItem.prototype.updateItemByPath = function(changedDirectryPath) {
    if (changedDirectryPath === this.entry.fullPath) {
      this.updateSubDirectories(false /* recursive */);
      return;
    }
  
    for (var i = 0; i < this.items.length; i++) {
      var item = this.items[i];
      if (PathUtil.isParentPath(item.entry.fullPath, changedDirectryPath)) {
        item.updateItemByPath(changedDirectryPath);
        break;
      }
    }
  };
  
  /**
   * Redraw subitems with the latest information. The items are sorted in
   * alphabetical order, case insensitive.
   * @param {boolean} recursive True if the update is recursively.
   * @private
   */
  DirectoryItem.prototype.redrawSubDirectoryList_ = function(recursive) {
    this.updateSubElementsFromList(recursive);
  };
  
  /**
   * Select the item corresponding to the given {@code entry}.
   * @param {DirectoryEntry|Object} entry The entry to be selected. Can be a fake.
   */
  DirectoryItem.prototype.selectByEntry = function(entry) {
    if (util.isSameEntry(entry, this.entry)) {
      this.selected = true;
      return;
    }
  
    if (this.searchAndSelectByEntry(entry))
      return;
  
    // If the path doesn't exist, updates sub directories and tryes again.
    this.updateSubDirectories(
        false /* recursive */,
        this.searchAndSelectByEntry.bind(this, entry));
  };
  
  /**
   * Executes the assigned action as a drop target.
   */
  DirectoryItem.prototype.doDropTargetAction = function() {
    this.expanded = true;
  };
  
  /**
   * Executes the assigned action. DirectoryItem performs changeDirectory.
   */
  DirectoryItem.prototype.doAction = function() {
    if (this.fullPath != this.directoryModel_.getCurrentDirPath())
      this.directoryModel_.changeDirectory(this.fullPath);
  };
  
  /**
   * Sets the context menu for directory tree.
   * @param {cr.ui.Menu} menu Menu to be set.
   */
  DirectoryItem.prototype.setContextMenu = function(menu) {
    if (this.entry && PathUtil.isEligibleForFolderShortcut(this.entry.fullPath))
      cr.ui.contextMenuHandler.setContextMenu(this, menu);
  };
  
  ////////////////////////////////////////////////////////////////////////////////
  // DirectoryTree
  
  /**
   * Tree of directories on the middle bar. This element is also the root of
   * items, in other words, this is the parent of the top-level items.
   *
   * @constructor
   * @extends {cr.ui.Tree}
   */
  function DirectoryTree() {}
  
  /**
   * Decorates an element.
   * @param {HTMLElement} el Element to be DirectoryTree.
   * @param {DirectoryModel} directoryModel Current DirectoryModel.
   * @param {VolumeManagerWrapper} volumeManager VolumeManager of the system.
   */
  DirectoryTree.decorate = function(el, directoryModel, volumeManager) {
    el.__proto__ = DirectoryTree.prototype;
    (/** @type {DirectoryTree} */ el).decorate(directoryModel, volumeManager);
  };
  
  DirectoryTree.prototype = {
    __proto__: cr.ui.Tree.prototype,
  
    // DirectoryTree is always expanded.
    get expanded() { return true; },
    /**
     * @param {boolean} value Not used.
     */
    set expanded(value) {},
  
    /**
     * The DirectoryEntry corresponding to this DirectoryItem. This may be
     * a dummy DirectoryEntry.
     * @type {DirectoryEntry|Object}
     * @override
     **/
    get entry() {
      return this.dirEntry_;
    },
  
    /**
     * The DirectoryModel this tree corresponds to.
     * @type {DirectoryModel}
     */
    get directoryModel() {
      return this.directoryModel_;
    },
  
    /**
     * The VolumeManager instance of the system.
     * @type {VolumeManager}
     */
    get volumeManager() {
      return this.volumeManager_;
    },
  };
  
  cr.defineProperty(DirectoryTree, 'contextMenuForSubitems', cr.PropertyKind.JS);
  
  /**
   * Calls DirectoryItemTreeBaseMethods.updateSubElementsFromList().
   *
   * @param {boolean} recursive True if the all visible sub-directories are
   *     updated recursively including left arrows. If false, the update walks
   *     only immediate child directories without arrows.
   */
  DirectoryTree.prototype.updateSubElementsFromList = function(recursive) {
    DirectoryItemTreeBaseMethods.updateSubElementsFromList.call(this, recursive);
  };
  
  /**
   * Calls DirectoryItemTreeBaseMethods.updateSubElementsFromList().
   * @param {DirectoryEntry|Object} entry The entry to be searched for. Can be
   *     a fake.
   * @return {boolean} True if the parent item is found.
   */
  DirectoryTree.prototype.searchAndSelectByEntry = function(entry) {
    return DirectoryItemTreeBaseMethods.searchAndSelectByEntry.call(this, entry);
  };
  
  /**
   * Decorates an element.
   * @param {DirectoryModel} directoryModel Current DirectoryModel.
   * @param {VolumeManagerWrapper} volumeManager VolumeManager of the system.
   */
  DirectoryTree.prototype.decorate = function(directoryModel, volumeManager) {
    cr.ui.Tree.prototype.decorate.call(this);
  
    this.directoryModel_ = directoryModel;
    this.volumeManager_ = volumeManager;
    this.entries_ = DirectoryTreeUtil.generateTopLevelEntries();
  
    this.fileFilter_ = this.directoryModel_.getFileFilter();
    this.fileFilter_.addEventListener('changed',
                                      this.onFilterChanged_.bind(this));
  
    this.directoryModel_.addEventListener('directory-changed',
        this.onCurrentDirectoryChanged_.bind(this));
  
    // Add a handler for directory change.
    this.addEventListener('change', function() {
      if (this.selectedItem &&
          (!this.currentEntry_ ||
           !util.isSameEntry(this.currentEntry_, this.selectedItem.entry))) {
        this.currentEntry_ = this.selectedItem.entry;
        this.selectedItem.doAction();
        return;
      }
    }.bind(this));
  
    this.privateOnDirectoryChangedBound_ =
        this.onDirectoryContentChanged_.bind(this);
    chrome.fileBrowserPrivate.onDirectoryChanged.addListener(
        this.privateOnDirectoryChangedBound_);
  
    this.scrollBar_ = MainPanelScrollBar();
    this.scrollBar_.initialize(this.parentNode, this);
  
    // Once, draws the list with the fake '/drive/' entry.
    this.redraw(false /* recursive */);
    // Resolves 'My Drive' entry and replaces the fake with the true one.
    this.maybeResolveMyDriveRoot_(function() {
      // After the true entry is resolved, draws the list again.
      this.redraw(true /* recursive */);
    }.bind(this));
  };
  
  /**
   * Select the item corresponding to the given entry.
   * @param {DirectoryEntry|Object} entry The directory entry to be selected. Can
   *     be a fake.
   */
  DirectoryTree.prototype.selectByEntry = function(entry) {
    // If the target directory is not in the tree, do nothing.
    if (!DirectoryTreeUtil.isEligiblePathForDirectoryTree(entry.fullPath))
      return;
  
    this.maybeResolveMyDriveRoot_(function() {
      if (this.selectedItem && util.isSameEntry(entry, this.selectedItem.entry))
        return;
  
      if (this.searchAndSelectByEntry(entry))
        return;
  
      this.selectedItem = null;
      this.updateSubDirectories(
          false /* recursive */,
          // Success callback, failure is not handled.
          function() {
            if (!this.searchAndSelectByEntry(entry))
              this.selectedItem = null;
          }.bind(this));
    }.bind(this));
  };
  
  /**
   * Resolves the My Drive root's entry, if it is a fake. If the entry is already
   * resolved to a DirectoryEntry, completionCallback() will be called
   * immediately.
   * @param {function()} completionCallback Called when the resolving is
   *     done (or the entry is already resolved), regardless if it is
   *     successfully done or not.
   * @private
   */
  DirectoryTree.prototype.maybeResolveMyDriveRoot_ = function(
      completionCallback) {
    var myDriveItem = this.items[0];
    if (!util.isFakeEntry(myDriveItem.entry)) {
      // The entry is already resolved. Don't need to try again.
      completionCallback();
      return;
    }
  
    // The entry is a fake.
    this.directoryModel_.resolveDirectory(
        myDriveItem.fullPath,
        function(entry) {
          if (!util.isFakeEntry(entry))
            myDriveItem.dirEntry_ = entry;
  
          completionCallback();
        },
        completionCallback);
  };
  
  /**
   * Retrieves the latest subdirectories and update them on the tree.
   * @param {boolean} recursive True if the update is recursively.
   * @param {function()=} opt_successCallback Callback called on success.
   * @param {function()=} opt_errorCallback Callback called on error.
   */
  DirectoryTree.prototype.updateSubDirectories = function(
      recursive, opt_successCallback, opt_errorCallback) {
    this.entries_ = DirectoryTreeUtil.generateTopLevelEntries();
    this.redraw(recursive);
    if (opt_successCallback)
      opt_successCallback();
  };
  
  /**
   * Redraw the list.
   * @param {boolean} recursive True if the update is recursively. False if the
   *     only root items are updated.
   */
  DirectoryTree.prototype.redraw = function(recursive) {
    this.updateSubElementsFromList(recursive);
  };
  
  /**
   * Invoked when the filter is changed.
   * @private
   */
  DirectoryTree.prototype.onFilterChanged_ = function() {
    // Returns immediately, if the tree is hidden.
    if (this.hidden)
      return;
  
    this.redraw(true /* recursive */);
  };
  
  /**
   * Invoked when a directory is changed.
   * @param {!UIEvent} event Event.
   * @private
   */
  DirectoryTree.prototype.onDirectoryContentChanged_ = function(event) {
    if (event.eventType == 'changed') {
      // TODO: Use Entry instead of urls. This will stop working once migrating
      // to separate file systems. See: crbug.com/325052.
      if (!DirectoryTreeUtil.isEligiblePathForDirectoryTree(event.entry.fullPath))
        return;
  
      var myDriveItem = this.items[0];
      myDriveItem.updateItemByPath(event.entry.fullPath);
    }
  };
  
  /**
   * Invoked when the current directory is changed.
   * @param {!UIEvent} event Event.
   * @private
   */
  DirectoryTree.prototype.onCurrentDirectoryChanged_ = function(event) {
    this.selectByEntry(event.newDirEntry);
  };
  
  /**
   * Sets the margin height for the transparent preview panel at the bottom.
   * @param {number} margin Margin to be set in px.
   */
  DirectoryTree.prototype.setBottomMarginForPanel = function(margin) {
    this.style.paddingBottom = margin + 'px';
    this.scrollBar_.setBottomMarginForPanel(margin);
  };
  
  /**
   * Updates the UI after the layout has changed.
   */
  DirectoryTree.prototype.relayout = function() {
    cr.dispatchSimpleEvent(this, 'relayout');
  };
  