text: // # Settings API
  // RESTful API for the Setting resource
  var _            = require('lodash'),
      dataProvider = require('../models'),
      Promise      = require('bluebird'),
      config       = require('../config'),
      canThis      = require('../permissions').canThis,
      errors       = require('../errors'),
      utils        = require('./utils'),
  
      docName      = 'settings',
      settings,
  
      updateConfigTheme,
      updateSettingsCache,
      settingsFilter,
      filterPaths,
      readSettingsResult,
      settingsResult,
      canEditAllSettings,
      populateDefaultSetting,
      hasPopulatedDefaults = false,
  
      /**
       * ## Cache
       * Holds cached settings
       * @private
       * @type {{}}
       */
      settingsCache = {};
  
  /**
  * ### Updates Config Theme Settings
  * Maintains the cache of theme specific variables that are reliant on settings.
  * @private
  */
  updateConfigTheme = function () {
      config.set({
          theme: {
              title: settingsCache.title.value || '',
              description: settingsCache.description.value || '',
              logo: settingsCache.logo.value || '',
              cover: settingsCache.cover.value || ''
          }
      });
  };
  
  /**
   * ### Update Settings Cache
   * Maintain the internal cache of the settings object
   * @public
   * @param {Object} settings
   * @returns {Settings}
   */
  updateSettingsCache = function (settings) {
      settings = settings || {};
  
      if (!_.isEmpty(settings)) {
          _.map(settings, function (setting, key) {
              settingsCache[key] = setting;
          });
  
          updateConfigTheme();
  
          return Promise.resolve(settingsCache);
      }
  
      return dataProvider.Settings.findAll()
          .then(function (result) {
              settingsCache = readSettingsResult(result.models);
  
              updateConfigTheme();
  
              return settingsCache;
          });
  };
  
  // ## Helpers
  
  /**
   * ### Settings Filter
   * Filters an object based on a given filter object
   * @private
   * @param {Object} settings
   * @param {String} filter
   * @returns {*}
   */
  settingsFilter = function (settings, filter) {
      return _.object(_.filter(_.pairs(settings), function (setting) {
          if (filter) {
              return _.some(filter.split(','), function (f) {
                  return setting[1].type === f;
              });
          }
          return true;
      }));
  };
  
  /**
   * ### Filter Paths
   * Normalizes paths read by require-tree so that the apps and themes modules can use them. Creates an empty
   * array (res), and populates it with useful info about the read packages like name, whether they're active
   * (comparison with the second argument), and if they have a package.json, that, otherwise false
   * @private
   * @param   {object}            paths       as returned by require-tree()
   * @param   {array/string}      active      as read from the settings object
   * @returns {Array}                         of objects with useful info about apps / themes
   */
  filterPaths = function (paths, active) {
      var pathKeys = Object.keys(paths),
          res = [],
          item;
  
      // turn active into an array (so themes and apps can be checked the same)
      if (!Array.isArray(active)) {
          active = [active];
      }
  
      _.each(pathKeys, function (key) {
          // do not include hidden files or _messages
          if (key.indexOf('.') !== 0 &&
                  key !== '_messages' &&
                  key !== 'README.md'
                  ) {
              item = {
                  name: key
              };
              if (paths[key].hasOwnProperty('package.json')) {
                  item.package = paths[key]['package.json'];
              } else {
                  item.package = false;
              }
  
              if (_.indexOf(active, key) !== -1) {
                  item.active = true;
              }
              res.push(item);
          }
      });
      return res;
  };
  
  /**
   * ### Read Settings Result
   * @private
   * @param {Array} settingsModels
   * @returns {Settings}
   */
  readSettingsResult = function (settingsModels) {
      var settings = _.reduce(settingsModels, function (memo, member) {
              if (!memo.hasOwnProperty(member.attributes.key)) {
                  memo[member.attributes.key] = member.attributes;
              }
  
              return memo;
          }, {}),
          themes = config.paths.availableThemes,
          apps = config.paths.availableApps,
          res;
  
      if (settings.activeTheme && themes) {
          res = filterPaths(themes, settings.activeTheme.value);
  
          settings.availableThemes = {
              key: 'availableThemes',
              value: res,
              type: 'theme'
          };
      }
  
      if (settings.activeApps && apps) {
          res = filterPaths(apps, JSON.parse(settings.activeApps.value));
  
          settings.availableApps = {
              key: 'availableApps',
              value: res,
              type: 'app'
          };
      }
  
      return settings;
  };
  
  /**
   * ### Settings Result
   * @private
   * @param {Object} settings
   * @param {String} type
   * @returns {{settings: *}}
   */
  settingsResult = function (settings, type) {
      var filteredSettings = _.values(settingsFilter(settings, type)),
          result = {
              settings: filteredSettings,
              meta: {}
          };
  
      if (type) {
          result.meta.filters = {
              type: type
          };
      }
  
      return result;
  };
  
  /**
   * ### Populate Default Setting
   * @private
   * @param {String} key
   * @returns Promise(Setting)
   */
  populateDefaultSetting = function (key) {
      // Call populateDefault and update the settings cache
      return dataProvider.Settings.populateDefault(key).then(function (defaultSetting) {
          // Process the default result and add to settings cache
          var readResult = readSettingsResult([defaultSetting]);
  
          // Add to the settings cache
          return updateSettingsCache(readResult).then(function () {
              // Get the result from the cache with permission checks
          });
      }).catch(function (err) {
          // Pass along NotFoundError
          if (typeof err === errors.NotFoundError) {
              return Promise.reject(err);
          }
  
          // TODO: Different kind of error?
          return Promise.reject(new errors.NotFoundError('Problem finding setting: ' + key));
      });
  };
  
  /**
   * ### Can Edit All Settings
   * Check that this edit request is allowed for all settings requested to be updated
   * @private
   * @param {Object} settingsInfo
   * @returns {*}
   */
  canEditAllSettings = function (settingsInfo, options) {
      var checkSettingPermissions = function (setting) {
              if (setting.type === 'core' && !(options.context && options.context.internal)) {
                  return Promise.reject(
                      new errors.NoPermissionError('Attempted to access core setting from external request')
                  );
              }
  
              return canThis(options.context).edit.setting(setting.key).catch(function () {
                  return Promise.reject(new errors.NoPermissionError('You do not have permission to edit settings.'));
              });
          },
          checks = _.map(settingsInfo, function (settingInfo) {
              var setting = settingsCache[settingInfo.key];
  
              if (!setting) {
                  // Try to populate a default setting if not in the cache
                  return populateDefaultSetting(settingInfo.key).then(function (defaultSetting) {
                      // Get the result from the cache with permission checks
                      return checkSettingPermissions(defaultSetting);
                  });
              }
  
              return checkSettingPermissions(setting);
          });
  
      return Promise.all(checks);
  };
  
  /**
   * ## Settings API Methods
   *
   * **See:** [API Methods](index.js.html#api%20methods)
   */
  settings = {
  
      /**
       * ### Browse
       * @param {Object} options
       * @returns {*}
       */
      browse: function browse(options) {
          // First, check if we have populated the settings from default-settings yet
          if (!hasPopulatedDefaults) {
              return dataProvider.Settings.populateDefaults().then(function () {
                  hasPopulatedDefaults = true;
                  return settings.browse(options);
              });
          }
  
          options = options || {};
  
          var result = settingsResult(settingsCache, options.type);
  
          // If there is no context, return only blog settings
          if (!options.context) {
              return Promise.resolve(_.filter(result.settings, function (setting) { return setting.type === 'blog'; }));
          }
  
          // Otherwise return whatever this context is allowed to browse
          return canThis(options.context).browse.setting().then(function () {
              // Omit core settings unless internal request
              if (!options.context.internal) {
                  result.settings = _.filter(result.settings, function (setting) { return setting.type !== 'core'; });
              }
  
              return result;
          });
      },
  
      /**
       * ### Read
       * @param {Object} options
       * @returns {*}
       */
      read: function read(options) {
          if (_.isString(options)) {
              options = {key: options};
          }
  
          var getSettingsResult = function () {
                  var setting = settingsCache[options.key],
                      result = {};
  
                  result[options.key] = setting;
  
                  if (setting.type === 'core' && !(options.context && options.context.internal)) {
                      return Promise.reject(
                          new errors.NoPermissionError('Attempted to access core setting from external request')
                      );
                  }
  
                  if (setting.type === 'blog') {
                      return Promise.resolve(settingsResult(result));
                  }
  
                  return canThis(options.context).read.setting(options.key).then(function () {
                      return settingsResult(result);
                  }, function () {
                      return Promise.reject(new errors.NoPermissionError('You do not have permission to read settings.'));
                  });
              };
  
          // If the setting is not already in the cache
          if (!settingsCache[options.key]) {
              // Try to populate the setting from default-settings file
              return populateDefaultSetting(options.key).then(function () {
                  // Get the result from the cache with permission checks
                  return getSettingsResult();
              });
          }
  
          // Get the result from the cache with permission checks
          return getSettingsResult();
      },
  
      /**
       * ### Edit
       * Update properties of a post
       * @param {{settings: }} object Setting or a single string name
       * @param {{id (required), include,...}} options (optional) or a single string value
       * @return {Promise(Setting)} Edited Setting
       */
      edit: function edit(object, options) {
          options = options || {};
          var self = this,
              type;
  
          // Allow shorthand syntax where a single key and value are passed to edit instead of object and options
          if (_.isString(object)) {
              object = {settings: [{key: object, value: options}]};
          }
  
          // clean data
          _.each(object.settings, function (setting) {
              if (!_.isString(setting.value)) {
                  setting.value = JSON.stringify(setting.value);
              }
          });
  
          type = _.find(object.settings, function (setting) { return setting.key === 'type'; });
          if (_.isObject(type)) {
              type = type.value;
          }
  
          object.settings = _.reject(object.settings, function (setting) {
              return setting.key === 'type' || setting.key === 'availableThemes' || setting.key === 'availableApps';
          });
  
          return canEditAllSettings(object.settings, options).then(function () {
              return utils.checkObject(object, docName).then(function (checkedData) {
                  options.user = self.user;
                  return dataProvider.Settings.edit(checkedData.settings, options);
              }).then(function (result) {
                  var readResult = readSettingsResult(result);
  
                  return updateSettingsCache(readResult).then(function () {
                      return settingsResult(readResult, type);
                  });
              });
          });
      }
  };
  
  module.exports = settings;
  module.exports.updateSettingsCache = updateSettingsCache;
  