text: 'use strict';
  
  var crypto = require('crypto');
  
  var objectAssign = require('object-assign');
  var objectOmit = require('object.omit');
  var Bluebird = require('bluebird');
  var tryJsonParse = require('try-json-parse');
  
  var TaskProxy = function(opts) {
    objectAssign(this, {
      task: opts.task,
      file: opts.file,
      opts: opts.opts,
      originalPath: opts.file.path
    });
  };
  
  function makeHash(key) {
    return crypto.createHash('md5').update(key).digest('hex');
  }
  
  objectAssign(TaskProxy.prototype, {
    processFile: function() {
      var self = this;
  
      return this._checkForCachedValue().then(function(cached) {
        // If we found a cached value
        // The path of the cache key should also be identical to the original one when the file path changed inside the task
        if (cached.value && (!cached.value.filePathChangedInsideTask || cached.value.originalPath === self.file.path)) {
          // Extend the cached value onto the file, but don't overwrite original path info
          var file = objectAssign(
            self.file,
            objectOmit(cached.value, ['cwd', 'path', 'base', 'stat', 'history'])
          );
          // Restore the file path if it was set
          if (cached.value.path && cached.value.filePathChangedInsideTask) {
            file.path = cached.value.path;
          }
          return file;
        }
  
        // Otherwise, run the proxied task
        return self._runProxiedTaskAndCache(cached.key);
      });
    },
  
    removeCachedResult: function() {
      var self = this;
  
      return this._getFileKey().then(function(cachedKey) {
        var removeCached = Bluebird.promisify(self.opts.fileCache.removeCached, {
          context: self.opts.fileCache
        });
  
        return removeCached(self.opts.name, cachedKey);
      });
    },
  
    _getFileKey: function() {
      var getKey = this.opts.key;
  
      if (typeof getKey === 'function' && getKey.length === 2) {
        getKey = Bluebird.promisify(getKey.bind(this.opts));
      }
  
      return Bluebird.resolve(getKey(this.file)).then(function(key) {
        if (!key) {
          return key;
        }
  
        return makeHash(key);
      });
    },
  
    _checkForCachedValue: function() {
      var self = this;
  
      return this._getFileKey().then(function(key) {
        // If no key returned, bug out early
        if (!key) {
          return {
            key: key,
            value: null
          };
        }
  
        var getCached = Bluebird.promisify(self.opts.fileCache.getCached.bind(self.opts.fileCache));
  
        return getCached(self.opts.name, key).then(function(cached) {
          if (!cached) {
            return {
              key: key,
              value: null
            };
          }
  
          var parsedContents = tryJsonParse(cached.contents);
          if (parsedContents === undefined) {
            parsedContents = {cached: cached.contents};
          }
  
          if (self.opts.restore) {
            parsedContents = self.opts.restore(parsedContents);
          }
  
          return {
            key: key,
            value: parsedContents
          };
        });
      });
    },
  
    _runProxiedTaskAndCache: function(cachedKey) {
      var self = this;
  
      return self._runProxiedTask().then(function(result) {
        // If this wasn't a success, continue to next task
        // TODO: Should this also offer an async option?
        if (self.opts.success !== true && !self.opts.success(result)) {
          return result;
        }
  
        return self._storeCachedResult(cachedKey, result).then(function() {
          return result;
        });
      });
    },
  
    _runProxiedTask: function() {
      var self = this;
  
      return new Bluebird(function(resolve, reject) {
        function handleError(err) {
          // TODO: Errors will step on each other here
  
          // Reduce the maxListeners back down
          self.task.setMaxListeners(self.task._maxListeners - 1);
  
          reject(err);
        }
  
        function handleData(datum) {
          // Wait for data (can be out of order, so check for matching file we wrote)
          if (self.file !== datum) {
            return;
          }
  
          // Be good citizens and remove our listeners
          self.task.removeListener('error', handleError);
          self.task.removeListener('data', handleData);
  
          // Reduce the maxListeners back down
          self.task.setMaxListeners(self.task._maxListeners - 2);
  
          resolve(datum);
        }
  
        // Bump up max listeners to prevent memory leak warnings
        var currMaxListeners = self.task._maxListeners || 0;
        self.task.setMaxListeners(currMaxListeners + 2);
  
        self.task.on('data', handleData);
        self.task.once('error', handleError);
  
        // Run through the other task and grab output (or error)
        // Not sure if a _.defer is necessary here
        self.task.write(self.file);
      });
    },
  
    _getValueFromResult: function(result) {
      var getValue;
  
      if (typeof this.opts.value !== 'function') {
        if (typeof this.opts.value === 'string') {
          getValue = {};
          getValue[this.opts.value] = result[this.opts.value];
        }
  
        return Bluebird.resolve(getValue);
      } else if (this.opts.value.length === 2) {
        // Promisify if passed a node style function
        getValue = Bluebird.promisify(this.opts.value.bind(this.opts));
      } else {
        getValue = this.opts.value;
      }
  
      return Bluebird.resolve(getValue(result));
    },
  
    _storeCachedResult: function(key, result) {
      var self = this;
  
      // If we didn't have a cachedKey, skip caching result
      if (!key) {
        return Bluebird.resolve(result);
      }
  
      return this._getValueFromResult(result).then(function(value) {
        var val;
        var addCached = Bluebird.promisify(self.opts.fileCache.addCached.bind(self.opts.fileCache));
  
        if (typeof value !== 'string') {
          if (value && typeof value === 'object' && Buffer.isBuffer(value.contents)) {
            // Shallow copy so "contents" can be safely modified
            val = objectAssign({}, value);
            val.contents = val.contents.toString('utf8');
          }
  
          // Check if the task changed the file path
          if (value.path !== self.originalPath) {
            value.filePathChangedInsideTask = true;
          }
  
          // Keep track of the original path
          value.originalPath = self.originalPath;
  
          val = JSON.stringify(value, null, 2);
        } else {
          val = value;
        }
  
        return addCached(self.opts.name, key, val);
      });
    }
  });
  
  module.exports = TaskProxy;
  