text: /*
   *************************************************************************
   * Contact:  http://www.mocoolka.com
   * GNU General Public License Usage
   * This file may be used under the terms of the GNU General Public License version 3.0 as
   * published by the Free Software Foundation and appearing in the file LICENSE included in the
   * packaging of this file.
  
   * Please review the following information to ensure the GNU General Public License version 3.0
   * requirements will be met: http://www.gnu.org/copyleft/gpl.html.
   * The Original Code is Mocoolka Group.
   * The Initial Developer of the Original Code is MoCoolKa SLU
   * All portions are Copyright (C) 2012-2013 MoCoolKa SLU
   * All Rights Reserved.
   * Contributor(s):  fastspeeed
   ************************************************************************
   */
  //noinspection Eslint
  /**
   * Created by fastspeeed@gmail.com on 2016/10/12.
   */
  'use strict';
  var cloneDeep = require('lodash').cloneDeep;
  var path = require('path');
  var utils = require('./utils.js');
  var debug = require('debug')('mocoolka:mk-config-loader');
  var assert = require('assert');
  var g = require('strong-globalize')();
  
  var ConfigLoader = exports;
  
  /**
   * Load application config from `config.json` and friends.
   * @param {String} rootDir Directory where to look for files.
   * @param {String} env Environment, usually `process.env.NODE_ENV`
   * @returns {Object}
   */
  ConfigLoader.loadAppConfig = function(rootDir, env) {
    return loadNamed(rootDir, env, 'config', mergeAppConfig);
  };
  
  /**
   * Load data-sources config from `datasources.json` and friends.
   * @param {String} rootDir Directory where to look for files.
   * @param {String} env Environment, usually `process.env.NODE_ENV`
   * @returns {Object}
   */
  ConfigLoader.loadDataSources = function(rootDir, env) {
    return loadNamed(rootDir, env, 'datasources', mergeDataSourceConfig);
  };
  
  /**
   * Load model config from `model-config.json` and friends.
   * @param {String} rootDir Directory where to look for files.
   * @param {String} env Environment, usually `process.env.NODE_ENV`
   * @returns {Object}
   */
  ConfigLoader.loadModels = function(rootDir, env) {
    return loadNamed(rootDir, env, 'model-config', mergeModelConfig);
  };
  
  /**
   * Load middleware config from `middleware.json` and friends.
   * @param {String} rootDir Directory where to look for files.
   * @param {String} env Environment, usually `process.env.NODE_ENV`
   * @returns {Object}
   */
  ConfigLoader.loadMiddleware = function(rootDir, env) {
    return loadNamed(rootDir, env, 'middleware', mergeMiddlewareConfig);
  };
  
  /**
   * Load component config from `component-config.json` and friends.
   * @param {String} rootDir Directory where to look for files.
   * @param {String} env Environment, usually `process.env.NODE_ENV`
   * @returns {Object}
   */
  ConfigLoader.loadComponents = function(rootDir, env) {
    return loadNamed(rootDir, env, 'component-config', mergeComponentConfig);
  };
  
  /*-- Implementation --*/
  
  /**
   * Load named configuration.
   * @param {String} rootDir Directory where to look for files.
   * @param {String} env Environment, usually `process.env.NODE_ENV`
   * @param {String} name
   * @param {function(target:Object, config:Object, filename:String)} mergeFn
   * @returns {Object}
   */
  function loadNamed(rootDir, env, name, mergeFn) {
    var files = findConfigFiles(rootDir, env, name);
    if (files.length) {
      debug('found %s %s files', env, name);
      files.forEach(function(f) { debug('  %s', f); });
    }
    var configs = loadConfigFiles(files);
    var merged = mergeConfigurations(configs, mergeFn);
  
    debug('merged %s %s configuration %j', env, name, merged);
  
    return merged;
  }
  
  /**
   * Search `appRootDir` for all files containing configuration for `name`.
   * @param {String} appRootDir
   * @param {String} env Environment, usually `process.env.NODE_ENV`
   * @param {String} name
   * @returns {Array.<String>} Array of absolute file paths.
   */
  function findConfigFiles(appRootDir, env, name) {
    var master = ifExists(name + '.json');
    if (!master && (ifExistsWithAnyExt(name + '.local') ||
      ifExistsWithAnyExt(name + '.' + env))) {
      g.warn('WARNING: Main {{config}} file "%s.json" is missing', name);
    }
    if (!master) return [];
  
    var candidates = [
      master,
      ifExistsWithAnyExt(name + '.local'),
      ifExistsWithAnyExt(name + '.' + env)
    ];
  
    return candidates.filter(function(c) { return c !== undefined; });
  
    function ifExists(fileName) {
      var filepath = path.resolve(appRootDir, fileName);
      return utils.fileExistsSync(filepath) ? filepath : undefined;
    }
  
    function ifExistsWithAnyExt(fileName) {
      return ifExists(fileName + '.js') || ifExists(fileName + '.json');
    }
  }
  
  /**
   * Load configuration files into an array of objects.
   * Attach non-enumerable `_filename` property to each object.
   * @param {Array.<String>} files
   * @returns {Array.<Object>}
   */
  function loadConfigFiles(files) {
    return files.map(function(f) {
      var config = cloneDeep(require(f));
      Object.defineProperty(config, '_filename', {
        enumerable: false,
        value: f
      });
      debug('loaded config file %s: %j', f, config);
      return config;
    });
  }
  
  /**
   * Merge multiple configuration objects into a single one.
   * @param {Array.<Object>} configObjects
   * @param {function(target:Object, config:Object, filename:String)} mergeFn
   */
  function mergeConfigurations(configObjects, mergeFn) {
    var result = configObjects.shift() || {};
    while (configObjects.length) {
      var next = configObjects.shift();
      mergeFn(result, next, next._filename);
    }
    return result;
  }
  
  function mergeDataSourceConfig(target, config, fileName) {
    var err = mergeObjects(target, config);
    if (err) {
      throw new Error(g.f('Cannot apply %s: %s', fileName, err));
    }
  }
  
  function mergeModelConfig(target, config, fileName) {
    var err = mergeObjects(target, config);
    if (err) {
      throw new Error(g.f('Cannot apply %s: %s', fileName, err));
    }
  }
  
  function mergeAppConfig(target, config, fileName) {
    var err = mergeObjects(target, config);
    if (err) {
      throw new Error(g.f('Cannot apply %s: %s', fileName, err));
    }
  }
  
  function mergeMiddlewareConfig(target, config, fileName) {
    var err = undefined; // see https://github.com/eslint/eslint/issues/5744
    for (var phase in config) {
      if (phase in target) {
        err = mergePhaseConfig(target[phase], config[phase], phase);
      } else {
        err = g.f('The {{phase}} "%s" is not defined in the main config.', phase);
      }
      if (err)
        throw new Error(g.f('Cannot apply %s: %s', fileName, err));
    }
  }
  
  function mergeNamedItems(arr1, arr2, key) {
    assert(Array.isArray(arr1), g.f('invalid array: %s', arr1));
    assert(Array.isArray(arr2), g.f('invalid array: %s', arr2));
    key = key || 'name';
    var result = [].concat(arr1);
    for (var i = 0, n = arr2.length; i < n; i++) {
      var item = arr2[i];
      var found = false;
      if (item[key]) {
        for (var j = 0, k = result.length; j < k; j++) {
          if (result[j][key] === item[key]) {
            mergeObjects(result[j], item);
            found = true;
            break;
          }
        }
      }
      if (!found) {
        result.push(item);
      }
    }
    return result;
  }
  
  function mergePhaseConfig(target, config, phase) {
    var err = undefined; // see https://github.com/eslint/eslint/issues/5744
    for (var mw in config) {
      if (mw in target) {
        var targetMiddleware = target[mw];
        var configMiddleware = config[mw];
        if (Array.isArray(targetMiddleware) && Array.isArray(configMiddleware)) {
          // Both are arrays, combine them
          target[mw] = mergeNamedItems(targetMiddleware, configMiddleware);
        } else if (Array.isArray(targetMiddleware)) {
          if (typeof configMiddleware === 'object' &&
            Object.keys(configMiddleware).length) {
            // Config side is an non-empty object
            target[mw] = mergeNamedItems(targetMiddleware, [configMiddleware]);
          }
        } else if (Array.isArray(configMiddleware)) {
          if (typeof targetMiddleware === 'object' &&
            Object.keys(targetMiddleware).length) {
            // Target side is an non-empty object
            target[mw] = mergeNamedItems([targetMiddleware], configMiddleware);
          } else {
            // Target side is empty
            target[mw] = configMiddleware;
          }
        } else {
          err = mergeObjects(targetMiddleware, configMiddleware);
        }
      } else {
        err = g.f('The {{middleware}} "%s" in phase "%s"' +
          'is not defined in the main config.', mw, phase);
      }
      if (err) return err;
    }
  }
  
  function mergeComponentConfig(target, config, fileName) {
    var err = mergeObjects(target, config);
    if (err) {
      throw new Error(g.f('Cannot apply %s: %s', fileName, err));
    }
  }
  
  function mergeObjects(target, config, keyPrefix) {
    for (var key in config) {
      var fullKey = keyPrefix ? keyPrefix + '.' + key : key;
      var err = mergeSingleItemOrProperty(target, config, key, fullKey);
      if (err) return err;
    }
    return null; // no error
  }
  
  function mergeSingleItemOrProperty(target, config, key, fullKey) {
    var origValue = target[key];
    var newValue = config[key];
  
    if (!hasCompatibleType(origValue, newValue)) {
      return 'Cannot merge values of incompatible types for the option `' +
        fullKey + '`.';
    }
  
    if (Array.isArray(origValue)) {
      return mergeArrays(origValue, newValue, fullKey);
    }
  
    if (newValue !== null && typeof origValue === 'object') {
      return mergeObjects(origValue, newValue, fullKey);
    }
  
    target[key] = newValue;
    return null; // no error
  }
  
  function mergeArrays(target, config, keyPrefix) {
    if (target.length !== config.length) {
      return 'Cannot merge array values of different length' +
        ' for the option `' + keyPrefix + '`.';
    }
  
    // Use for(;;) to iterate over undefined items, for(in) would skip them.
    for (var ix = 0; ix < target.length; ix++) {
      var fullKey = keyPrefix + '[' + ix + ']';
      var err = mergeSingleItemOrProperty(target, config, ix, fullKey);
      if (err) return err;
    }
  
    return null; // no error
  }
  
  function hasCompatibleType(origValue, newValue) {
    if (origValue === null || origValue === undefined)
      return true;
  
    if (Array.isArray(origValue))
      return Array.isArray(newValue);
  
    if (typeof origValue === 'object')
      return typeof newValue === 'object';
  
    // Note: typeof Array() is 'object' too,
    // we don't need to explicitly check array types
    return typeof newValue !== 'object';
  }
  