/*
 * provider.js: Abstraction providing an interface into pluggable configuration storage.
 *
 * (C) 2011, Nodejitsu Inc.
 *
 */

var async = require('async'),
    common = require('./common');

//
// ### function Provider (options)
// #### @options {Object} Options for this instance.
// Constructor function for the Provider object responsible
// for exposing the pluggable storage features of `nconf`.
//
var Provider = exports.Provider = function (options) {
  //
  // Setup default options for working with `stores`,
  // `overrides`, `process.env` and `process.argv`.
  //
  options       = options || {};
  this.stores  = {};
  this.sources = [];
  this.init(options);
};

//
// Define wrapper functions for using basic stores 
// in this instance
//
['argv', 'env', 'file'].forEach(function (type) {
  Provider.prototype[type] = function (options) {
    return this.add(type, options);
  };
});

//
// Define wrapper functions for using 
// overrides and defaults
//
['defaults', 'overrides'].forEach(function (type) {
  Provider.prototype[type] = function (options) {
    return this.add('literal', options);
  };
});

//
// ### function use (name, options)
// #### @type {string} Type of the nconf store to use.
// #### @options {Object} Options for the store instance.
// Adds (or replaces) a new store with the specified `name` 
// and `options`. If `options.type` is not set, then `name` 
// will be used instead:
//
//    provider.use('file');
//    provider.use('file', { type: 'file', filename: '/path/to/userconf' })
//
Provider.prototype.use = function (name, options) {
  options  = options      || {};
  var type = options.type || name;

  function sameOptions (store) {
    return Object.keys(options).every(function (key) {
      return options[key] === store[key];
    });
  }
  
  var store = this.stores[name],
      update = store && !sameOptions(store);
  
  if (!store || update) {
    if (update) {
      this.remove(name);
    }
    
    this.add(name, options);
  }
  
  return this;
};

//
// ### function add (name, options)
// #### @name {string} Name of the store to add to this instance
// #### @options {Object} Options for the store to create
// Adds a new store with the specified `name` and `options`. If `options.type`
// is not set, then `name` will be used instead:
//
//    provider.add('memory');
//    provider.add('userconf', { type: 'file', filename: '/path/to/userconf' })
//
Provider.prototype.add = function (name, options) {
  options  = options      || {};
  var type = options.type || name;
  
  if (!require('../nconf')[common.capitalize(type)]) {
    throw new Error('Cannot add store with unknown type: ' + type);
  }
  
  this.stores[name] = this.create(type, options);
  
  if (this.stores[name].loadSync) {
    this.stores[name].loadSync();
  }
  
  return this;
};

//
// ### function remove (name) 
// #### @name {string} Name of the store to remove from this instance
// Removes a store with the specified `name` from this instance. Users
// are allowed to pass in a type argument (e.g. `memory`) as name if
// this was used in the call to `.add()`.
//
Provider.prototype.remove = function (name) {
  delete this.stores[name];
  return this;
};

//
// ### function create (type, options)
// #### @type {string} Type of the nconf store to use.
// #### @options {Object} Options for the store instance.
// Creates a store of the specified `type` using the 
// specified `options`.
//
Provider.prototype.create = function (type, options) {
  return new (require('../nconf')[common.capitalize(type.toLowerCase())])(options);
};

//
// ### function init (options)
// #### @options {Object} Options to initialize this instance with.
// Initializes this instance with additional `stores` or `sources` in the
// `options` supplied. 
//
Provider.prototype.init = function (options) {
  var self = this;
  
  //
  // Add any stores passed in through the options
  // to this instance.
  //
  if (options.type) {
    this.add(options.type, options);
  }
  else if (options.store) {
    this.add(options.store.name || options.store.type, options.store);
  }
  else if (options.stores) {
    Object.keys(options.stores).forEach(function (name) {
      var store = options.stores[name];
      self.add(store.name || name || store.type, store);
    });
  }
  
  //
  // Add any read-only sources to this instance
  //
  if (options.source) {
    this.sources.push(this.create(options.source.type || options.source.name, options.source));
  }
  else if (options.sources) {
    Object.keys(options.sources).forEach(function (name) {
      var source = options.sources[name];
      self.sources.push(self.create(source.type || source.name || name, source));
    });
  }
};

//
// ### function get (key, callback)
// #### @key {string} Key to retrieve for this instance.
// #### @callback {function} **Optional** Continuation to respond to when complete.
// Retrieves the value for the specified key (if any).
//
Provider.prototype.get = function (key, callback) {
  //
  // If there is no callback we can short-circuit into the default
  // logic for traversing stores.
  //
  if (!callback) {
    return this._execute('get', 1, key, callback);
  }
  
  //
  // Otherwise the asynchronous, hierarchical `get` is 
  // slightly more complicated because we do not need to traverse
  // the entire set of stores, but up until there is a defined value.
  //
  var current = 0,
      names = Object.keys(this.stores),
      self = this,
      response;
      
  async.whilst(function () {
    return typeof response === 'undefined' && current < names.length;
  }, function (next) {
    var store = self.stores[names[current]];
    current++;
    
    if (store.get.length >= 2) {
      return store.get(key, function (err, value) {
        if (err) {
          return next(err);
        }
        
        response = value;
        next();
      });
    }
    
    response = store.get(key);
    next();
  }, function (err) {
    return err ? callback(err) : callback(null, response);
  });
};

//
// ### function set (key, value, callback)
// #### @key {string} Key to set in this instance
// #### @value {literal|Object} Value for the specified key
// #### @callback {function} **Optional** Continuation to respond to when complete.
// Sets the `value` for the specified `key` in this instance.
//
Provider.prototype.set = function (key, value, callback) {
  return this._execute('set', 2, key, value, callback);
};

//
// ### function reset (callback)
// #### @callback {function} **Optional** Continuation to respond to when complete.
// Clears all keys associated with this instance.
//
Provider.prototype.reset = function (callback) {
  return this._execute('reset', 0, callback);  
};

//
// ### function clear (key, callback)
// #### @key {string} Key to remove from this instance
// #### @callback {function} **Optional** Continuation to respond to when complete.
// Removes the value for the specified `key` from this instance.
//
Provider.prototype.clear = function (key, callback) {
  return this._execute('clear', 1, key, callback);
};

//
// ### function merge ([key,] value [, callback])
// #### @key {string} Key to merge the value into
// #### @value {literal|Object} Value to merge into the key
// #### @callback {function} **Optional** Continuation to respond to when complete.
// Merges the properties in `value` into the existing object value at `key`. 
//
// 1. If the existing value `key` is not an Object, it will be completely overwritten.
// 2. If `key` is not supplied, then the `value` will be merged into the root.
//
Provider.prototype.merge = function () {
  var self = this,
      args = Array.prototype.slice.call(arguments),
      callback = typeof args[args.length - 1] === 'function' && args.pop(),
      value = args.pop(),
      key = args.pop();
      
  function mergeProperty (prop, next) {
    return self._execute('merge', 2, prop, value[prop], next);
  }
      
  if (!key) {
    if (Array.isArray(value) || typeof value !== 'object') {
      return onError(new Error('Cannot merge non-Object into top-level.'), callback);
    }
    
    return async.forEach(Object.keys(value), mergeProperty, callback || function () { })
  }
  
  return this._execute('merge', 2, key, value, callback);
};

//
// ### function load (callback)
// #### @callback {function} Continuation to respond to when complete.
// Responds with an Object representing all keys associated in this instance.
//
Provider.prototype.load = function (callback) {
  var self = this;
  
  function getStores () {
    return Object.keys(self.stores).map(function (name) { 
      return self.stores[name];
    });
  }
  
  function loadStoreSync(store) {
    if (!store.loadSync) {
      throw new Error('nconf store ' + store.type + ' has no loadSync() method');
    }
    
    return store.loadSync();
  }
  
  function loadStore(store, next) {
    if (!store.load && !store.loadSync) {
      return next(new Error('nconf store ' + store.type + ' has no load() method'));
    }
    
    return store.loadSync
      ? next(null, store.loadSync())
      : store.load(next);
  }
  
  function loadBatch (targets, done) {
    if (!done) {
      return common.merge(targets.map(loadStoreSync));
    }

    async.map(targets, loadStore, function (err, objs) {
      return err ? done(err) : done(null, common.merge(objs));
    });
  }
  
  function mergeSources (data) {
    //
    // If `data` was returned then merge it into 
    // the system store.
    //
    if (data && typeof data === 'object') {
      self.use('sources', {
        type: 'literal', 
        store: data
      });
    }
  }
  
  function 
 () {
    //
    // If we don't have a callback and the current 
    // store is capable of loading synchronously
    // then do so.
    //
    if (!callback) {
      mergeSources(loadBatch(self.sources));
      return loadBatch(getStores());
    }
    
    loadBatch(self.sources, function (err, data) {
      if (err) {
        return callback(err);
      }
      
      mergeSources(data);
      return loadBatch(getStores(), callback);
    });
  }
  
  return self.sources.length 
    ? loadSources()
    : loadBatch(getStores(), callback);
};

//
// ### function save (value, callback) 
// #### @value {Object} **Optional** Config object to set for this instance
// #### @callback {function} Continuation to respond to when complete.
// Removes any existing configuration settings that may exist in this
// instance and then adds all key-value pairs in `value`. 
//
Provider.prototype.save = function (value, callback) {
  if (!callback && typeof value === 'function') {
    callback = value;
    value = null;
  }
  
  var self = this,
      names = Object.keys(this.stores);
  
  function saveStoreSync(name) {
    var store = self.stores[name];
    
    //
    // If the `store` doesn't have a `saveSync` method, 
    // just ignore it and continue. 
    //
    return store.saveSync
      ? store.saveSync()
      : null;
  }
  
  function saveStore(name, next) {
    var store = self.stores[name];
    
    //
    // If the `store` doesn't have a `save` or saveSync` 
    // method(s), just ignore it and continue. 
    //
    if (!store.save && !store.saveSync) {
      return next();
    }
    
    return store.saveSync
      ? next(null, store.saveSync())
      : store.save(next);
  }
  
  //
  // If we don't have a callback and the current 
  // store is capable of saving synchronously
  // then do so.
  //
  if (!callback) {
    return common.merge(names.map(saveStoreSync));
  }
  
  async.map(names, saveStore, function (err, objs) {
    return err ? callback(err) : callback();
  });  
};

//
// ### @private function _execute (action, syncLength, [arguments])
// #### @action {string} Action to execute on `this.store`.
// #### @syncLength {number} Function length of the sync version.
// #### @arguments {Array} Arguments array to apply to the action
// Executes the specified `action` on all stores for this instance, ensuring a callback supplied
// to a synchronous store function is still invoked.
//
Provider.prototype._execute = function (action, syncLength /* [arguments] */) {
  var args = Array.prototype.slice.call(arguments, 2),
      callback = typeof args[args.length - 1] === 'function' && args.pop(),
      destructive = ['set', 'clear', 'merge'].indexOf(action) !== -1,
      self = this,
      response;
  
  function runAction (name, next) {
    var store = self.stores[name];

    if (destructive && store.readOnly) {
      return next();
    }

    return store[action].length > syncLength
      ? store[action].apply(store, args.concat(next))
      : next(null, store[action].apply(store, args));
  }
  
  if (callback) {
    return async.forEach(Object.keys(this.stores), runAction, function (err) {
      return err ? callback(err) : callback();
    });
  }


  Object.keys(this.stores).forEach(function (name) {
    if (typeof response === 'undefined') {
      var store = self.stores[name];

      if (destructive && store.readOnly) {
        return;
      }

      response = store[action].apply(store, args);
    }
  });
    
  return response;
}

//
// Throw the `err` if a callback is not supplied
//
function onError(err, callback) {
  if (callback) {
    return callback(err);
  }
  
  throw err;
}
loadSources