text: var _ = require('underscore'),
  	async = require('async'),
  	keystone = require('../'),
  	utils = require('keystone-utils');
  
  
  /**
   * UpdateHandler Class
   *
   * @param {Object} item to update
   * @api public
   */
  
  function UpdateHandler(list, item, req, res, options) {
  
  	if (!(this instanceof UpdateHandler))
  		return new UpdateHandler(list, item);
  
  	this.list = list;
  	this.item = item;
  	this.req = req;
  	this.res = res;
  	this.user = req.user;
  	this.options = options || {};
  
  	if (!this.options.errorMessage) {
  		this.options.errorMessage = 'There was a problem saving your changes:';
  	}
  
  	if (this.options.user) {
  		this.user = this.options.user;
  	}
  
  	this.validationMethods = {};
  	this.validationErrors = {};
  
  }
  
  
  /**
   * Adds a custom validation method for a given path
   *
   * @param {string} path to call method for
   * @param {function} method to call
   * @api public
   */
  
  UpdateHandler.prototype.validate = function(path, fn) {
  	this.validationMethods[path] = fn;
  	return this;
  };
  
  
  /**
   * Adds a validationError to the updateHandler; can be used before
   * `.process()` is called to handle errors generated by custom pre-
   * processing.
   *
   * @param {string} path that failed validation
   * @param {string} message to display
   * @param {string} error type (defaults to 'required')
   * @api public
   */
  
  UpdateHandler.prototype.addValidationError = function(path, msg, type) {
  	this.validationErrors[path] = {
  		name: 'ValidatorError',
  		path: path,
  		message: msg,
  		type: type || 'required'
  	};
  	return this;
  };
  
  
  /**
   * Processes data from req.body, req.query, or any data source.
   *
   * Options:
   * - fields (comma-delimited list or array of field paths)
   * - flashErrors (boolean, default false; whether to push validation errors to req.flash)
   * - ignoreNoedit (boolean, default false; whether to ignore noedit settings on fields)
   * - validationErrors (object; validation errors from previous form handling that should be included)
   *
   * @param {Object} data
   * @param {Object} options (can be comma-delimited list of fields) (optional)
   * @param {Function} callback (optional)
   * @api public
   */
  
  UpdateHandler.prototype.process = function(data, options, callback) {
  
  	var usingDefaultFields = false;
  
  	if ('function' == typeof options) {
  		callback = options;
  		options = null;
  	}
  
  	// Initialise options
  
  	if (!options) {
  		options = {};
  	} else if ('string' == typeof options) {
  		options = { fields: options };
  	}
  
  	if (!options.fields) {
  		options.fields = _.keys(this.list.fields);
  		usingDefaultFields = true;
  	} else if ('string' == typeof options.fields) {
  		options.fields = options.fields.split(',').map(function(i) { return i.trim(); });
  	}
  
  	options.required = options.required || {};
  	options.errorMessage = options.errorMessage || this.options.errorMessage;
  	options.invalidMessages = options.invalidMessages || {};
  	options.requiredMessages = options.requiredMessages || {};
  
  	// Parse a string of required fields into field paths
  	if ('string' == typeof options.required) {
  		var requiredFields = options.required.split(',').map(function(i) { return i.trim(); });
  		options.required = {};
  		requiredFields.forEach(function(path) {
  			options.required[path] = true;
  		});
  	}
  
  	// Make sure fields with the required option set are included in the required paths
  	options.fields.forEach(function(path) {
  		var field = (path instanceof keystone.Field) ? path : this.list.field(path);
  		if (field && field.required) {
  			options.required[path] = true;
  		}
  	}, this);
  
  	// TODO: The whole progress queue management code could be a lot neater...
  
  	var actionQueue = [],
  		addValidationError = this.addValidationError.bind(this),
  		validationErrors = this.validationErrors;
  
  	var progress = (function(err) {
  		if (err) {
  			if (options.logErrors) {
  				console.log('Error saving changes to ' + this.item.list.singular + ' ' + this.item.id + ':');
  				console.log(err);
  			}
  			callback(err, this);
  		} else if (_.size(validationErrors)) {
  			if (options.flashErrors) {
  				this.req.flash('error', {
  					type: 'ValidationError',
  					title: options.errorMessage,
  					list: _.pluck(validationErrors, 'message')
  				});
  			}
  			callback({
  				message: 'Validation failed',
  				name: 'ValidationError',
  				errors: validationErrors
  			}, this);
  		} else if (actionQueue.length) {
  			// TODO: parallel queue handling for cloudinary uploads?
  			actionQueue.pop()();
  		} else {
  			saveItem();
  		}
  	}).bind(this);
  
  	var saveItem = (function() {
  		this.item.save((function(err) {
  			if (err) {
  				if (err.name == 'ValidationError') {
  					// don't log simple validation errors
  					if (options.flashErrors) {
  						this.req.flash('error', {
  							type: 'ValidationError',
  							title: options.errorMessage,
  							list: _.pluck(err.errors, 'message')
  						});
  					}
  				} else {
  					if (options.logErrors) {
  						console.log('Error saving changes to ' + this.item.list.singular + ' ' + this.item.id + ':');
  						console.log(err);
  					}
  					if (options.flashErrors) {
  						this.req.flash('error', 'There was an error saving your changes: ' + err.message + ' (' + err.name + (err.type ? ': ' + err.type : '') + ')');
  					}
  				}
  			}
  			return callback(err, this);
  		}).bind(this));
  	}).bind(this);
  
  	options.fields.forEach(function(path) {
  
  		// console.log('Processing field ' + path);
  		var message;
  
  		var field = (path instanceof keystone.Field) ? path : this.list.field(path),
  			invalidated = false;
  
  		if (!field) {
  			throw new Error('UpdateHandler.process called with invalid field path: ' + path);
  		}
  
  		// skip uneditable fields
  		if (usingDefaultFields && field.noedit && !options.ignoreNoedit) {
  			// console.log('Skipping field ' + path + ' (noedit: true)');
  			return;
  		}
  
  		// Some field types have custom behaviours for queueing or validation
  		switch (field.type) {
  
  			case 'localfile':
  			case 'localfiles':
  			case 'cloudinaryimage':
  			case 'cloudinaryimages':
  			case 'azurefile':
  			case 's3file':
  				actionQueue.push(field.getRequestHandler(this.item, this.req, options.paths, (function(err) {
  				    if (err && options.flashErrors) {
  				        this.req.flash('error', field.label + ' upload failed - ' + err.message);
  				    }
  				    progress(err);
  				}).bind(this)));
  			break;
  
  			case 'location':
  				actionQueue.push(field.getRequestHandler(this.item, this.req, options.paths, (function(err) {
  					if (err && options.flashErrors) {
  						this.req.flash('error', field.label + ' improve failed - ' + (err.status_text || err.status));
  					}
  					progress(err);
  				}).bind(this)));
  			break;
  
  			case 'password':
  				// passwords should only be set if a value is provided.
  				// if no value is provided, as long as the field isn't required or empty, bail.
  				if (!data[field.path] && (!options.required[field.path] || this.item.get(field.path))) {
  					return;
  				}
  				// validate the password fields match, with a custom error message.
  				if (data[field.path] != data[field.paths.confirm]) {
  					message = options.invalidMessages[field.path + '_match'] || 'Passwords must match';
  					addValidationError(field.path, message);
  					invalidated = true;
  				}
  			break;
  
  		}
  
  		// validate field input, unless it's already been invalidated by field-specific behaviour
  		if (!invalidated && !field.validateInput(data)) {
  			// console.log('Field ' + field.path + ' is invalid');
  			message = options.invalidMessages[field.path] || field.options.invalidMessage || 'Please enter a valid ' + field.typeDescription + ' in the ' + field.label + ' field';
  			addValidationError(field.path, message);
  			invalidated = true;
  		}
  
  		// validate required fields, unless they've already been invalidated by field-specific behaviour
  		// console.log('Field ' + path + ' required: ' + options.required[field.path]);
  		if (!invalidated && options.required[field.path] && !field.validateInput(data, true, this.item)) {
  			// console.log('Field ' + field.path + ' is required, but not provided.');
  			message = options.requiredMessages[field.path] || field.options.requiredMessage || field.label + ' is required';
  			addValidationError(field.path, message);
  			invalidated = true;
  		}
  
  		// check for a custom validation rule at the path, and run it (unless the field is already invalid)
  		if (!invalidated && this.validationMethods[field.path]) {
  			message = this.validationMethods[field.path](data);
  			if (message) {
  				addValidationError(field.path, message);
  			}
  		}
  
  		// console.log('Setting field value: ' + path);
  
  		field.updateItem(this.item, data);
  
  	}, this);
  
  	progress();
  
  };
  
  
  /*!
   * Export class
   */
  
  exports = module.exports = UpdateHandler;
  