// 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.

'use strict';

/**
 * Watches for changes in the tracked directory, including local metadata
 * changes.
 *
 * @param {MetadataCache} metadataCache Instance of MetadataCache.
 * @extends {cr.EventTarget}
 * @constructor
 */
function FileWatcher(metadataCache) {
  this.queue_ = new AsyncUtil.Queue();
  this.metadataCache_ = metadataCache;
  this.watchedDirectoryEntry_ = null;

  this.onDirectoryChangedBound_ = this.onDirectoryChanged_.bind(this);
  chrome.fileBrowserPrivate.onDirectoryChanged.addListener(
      this.onDirectoryChangedBound_);

  this.filesystemMetadataObserverId_ = null;
  this.thumbnailMetadataObserverId_ = null;
  this.driveMetadataObserverId_ = null;
}

/**
 * FileWatcher extends cr.EventTarget.
 */
FileWatcher.prototype.__proto__ = cr.EventTarget.prototype;

/**
 * Stops watching (must be called before page unload).
 */
FileWatcher.prototype.dispose = function() {
  chrome.fileBrowserPrivate.onDirectoryChanged.removeListener(
      this.onDirectoryChangedBound_);
  if (this.watchedDirectoryEntry_)
    this.resetWatchedEntry_(function() {}, function() {});
};

/**
 * Called when a file in the watched directory is changed.
 * @param {Event} event Change event.
 * @private
 */
FileWatcher.prototype.onDirectoryChanged_ = function(event) {
  if (this.watchedDirectoryEntry_ &&
      event.entry.toURL() === this.watchedDirectoryEntry_.toURL()) {
    var e = new Event('watcher-directory-changed');
    this.dispatchEvent(e);
  }
};

/**
 * Called when general metadata in the watched directory has been changed.
 *
 * @param {Array.<Entry>} entries Array of entries.
 * @param {Object.<string, Object>} properties Map from entry URLs to metadata
 *     properties.
 * @private
 */
FileWatcher.prototype.onFilesystemMetadataChanged_ = function(
    entries, properties) {
  this.dispatchMetadataEvent_('filesystem', entries, properties);
};

/**
 * Called when thumbnail metadata in the watched directory has been changed.
 *
 * @param {Array.<Entry>} entries Arrray of entries.
 * @param {Object.<string, Object>} properties Map from entry URLs to metadata
 *     properties.
 * @private
 */
FileWatcher.prototype.onThumbnailMetadataChanged_ = function(
    entries, properties) {
  this.dispatchMetadataEvent_('thumbnail', entries, properties);
};

/**
 * Called when drive metadata in the watched directory has been changed.
 *
 * @param {Array.<Entry>} entries Array of entries.
 * @param {Object.<string, Object>} properties Map from entry URLs to metadata
 *     properties.
 * @private
 */
FileWatcher.prototype.onDriveMetadataChanged_ = function(
    entries, properties) {
  this.dispatchMetadataEvent_('drive', entries, properties);
};

/**
 * Dispatches an event about detected change in metadata within the tracked
 * directory.
 *
 * @param {string} type Type of the metadata change.
 * @param {Array.<Entry>} entries Array of entries.
 * @param {Object.<string, Object>} properties Map from entry URLs to metadata
 *     properties.
 * @private
 */
FileWatcher.prototype.dispatchMetadataEvent_ = function(
    type, entries, properties) {
  var e = new Event('watcher-metadata-changed');
  e.metadataType = type;
  e.entries = entries;
  e.properties = properties;
  this.dispatchEvent(e);
};

/**
 * Changes the watched directory. In case of a fake entry, the watch is
 * just released, since there is no reason to track a fake directory.
 *
 * @param {!DirectoryEntry|!Object} entry Directory entry to be tracked, or the
 *     fake entry.
 * @param {function()} callback Completion callback.
 */
FileWatcher.prototype.changeWatchedDirectory = function(entry, callback) {
  if (!util.isFakeEntry(entry)) {
    this.changeWatchedEntry_(
        entry,
        callback,
        function() {
          console.error(
             'Unable to change the watched directory to: ' + entry.toURL());
          callback();
        });
  } else {
    this.resetWatchedEntry_(
        callback,
        function() {
          console.error('Unable to reset the watched directory.');
          callback();
        });
  }
};

/**
 * Resets the watched entry to the passed directory.
 *
 * @param {function()} onSuccess Success callback.
 * @param {function()} onError Error callback.
 * @private
 */
FileWatcher.prototype.resetWatchedEntry_ = function(onSuccess, onError) {
  // Run the tasks in the queue to avoid races.
  this.queue_.run(function(callback) {
    // Release the watched directory.
    if (this.watchedDirectoryEntry_) {
      chrome.fileBrowserPrivate.removeFileWatch(
          this.watchedDirectoryEntry_.toURL(),
          function(result) {
            this.watchedDirectoryEntry_ = null;
            if (result)
              onSuccess();
            else
              onError();
            callback();
          }.bind(this));
      this.metadataCache_.removeObserver(this.filesystemMetadataObserverId_);
      this.metadataCache_.removeObserver(this.thumbnailMetadataObserverId_);
      this.metadataCache_.removeObserver(this.driveMetadataObserverId_);
    } else {
      onSuccess();
      callback();
    }
  }.bind(this));
};

/**
 * Sets the watched entry to the passed directory.
 *
 * @param {!DirectoryEntry} entry Directory to be watched.
 * @param {function()} onSuccess Success callback.
 * @param {function()} onError Error callback.
 * @private
 */
FileWatcher.prototype.changeWatchedEntry_ = function(
    entry, onSuccess, onError) {
  var setEntryClosure = function() {
    // Run the tasks in the queue to avoid races.
    this.queue_.run(function(callback) {
      chrome.fileBrowserPrivate.addFileWatch(
          entry.toURL(),
          function(result) {
            if (!result) {
              this.watchedDirectoryEntry_ = null;
              onError();
            } else {
              this.watchedDirectoryEntry_ = entry;
              onSuccess();
            }
            callback();
          }.bind(this));
      this.filesystemMetadataObserverId_ = this.metadataCache_.addObserver(
        entry,
        MetadataCache.CHILDREN,
        'filesystem',
        this.onFilesystemMetadataChanged_.bind(this));
      this.thumbnailMetadataObserverId_ = this.metadataCache_.addObserver(
        entry,
        MetadataCache.CHILDREN,
        'thumbnail',
        this.onThumbnailMetadataChanged_.bind(this));
      this.driveMetadataObserverId_ = this.metadataCache_.addObserver(
        entry,
        MetadataCache.CHILDREN,
        'drive',
        this.onDriveMetadataChanged_.bind(this));
    }.bind(this));
  }.bind(this);

  // Reset the watched directory first, then set the new watched directory.
  this.resetWatchedEntry_(setEntryClosure, onError);
};

/**
 * @return {DirectoryEntry} Current watched directory entry.
 */
FileWatcher.prototype.getWatchedDirectoryEntry = function() {
  return this.watchedDirectoryEntry_;
};
