//  deps
const paths = require('path');
const async = require('async');

const _ = require('lodash');
const mapValuesDeep = require('deepdash/getMapValuesDeep')(_);

module.exports = class ConfigLoader {
    constructor(
        { hotReload = true, defaultConfig = {}, defaultsCustomizer = null } = { hotReload : true, defaultConfig : {}, defaultsCustomizer : null } )
    {
        this.current = {};

        this.hotReload          = hotReload;
        this.defaultConfig      = defaultConfig;
        this.defaultsCustomizer = defaultsCustomizer;
    }

    init(baseConfigPath, cb) {
        this.baseConfigPath = baseConfigPath;
        return this._reload(baseConfigPath, cb);
    }

    get() {
        return this.current;
    }

    _reload(baseConfigPath, cb) {
        let defaultConfig;
        if (_.isFunction(this.defaultConfig)) {
            defaultConfig = this.defaultConfig();
        } else if (_.isObject(this.defaultConfig)) {
            defaultConfig = this.defaultConfig;
        } else {
            defaultConfig = {};
        }

        //
        //  1 - Fetch base configuration from |baseConfigPath|
        //  2 - Merge with |defaultConfig|
        //  3 - Resolve any includes
        //  4 - Resolve @reference and @environment
        //  5 - Perform any validation
        //
        async.waterfall(
            [
                (callback) => {
                    return this._loadConfigFile(baseConfigPath, callback);
                },
                (config, callback) => {
                    if (_.isFunction(this.defaultsCustomizer)) {
                        const stack = [];
                        const mergedConfig = _.mergeWith(
                            defaultConfig,
                            config,
                            (defaultVal, configVal, key, target, source) => {
                                var path;
                                while (true) {
                                    if (!stack.length) {
                                        stack.push({source, path : []});
                                    }

                                    const prev = stack[stack.length - 1];

                                    if (source === prev.source) {
                                        path = prev.path.concat(key);
                                        stack.push({source : configVal, path});
                                        break;
                                    }

                                    stack.pop();
                                }

                                path = path.join('.');
                                return this.defaultsCustomizer(defaultVal, configVal, key, path);
                            }
                        );

                        return callback(null, mergedConfig);
                    }

                    return callback(null, _.merge(defaultConfig, config));
                },
                (config, callback) => {
                    const configRoot = paths.dirname(baseConfigPath);
                    return this._resolveIncludes(configRoot, config, callback);
                },
                (config, callback) => {
                    config = this._resolveAtSpecs(config);
                    return callback(null, config);
                },
            ],
            (err, config) => {
                if (!err) {
                    this.current = config;
                }
                return cb(err);
            }
        );
    }

    _convertTo(value, type) {
        switch (type) {
            case 'bool' :
            case 'boolean' :
                value = 'true' === value.toLowerCase();
                break;

            case 'number' :
                {
                    const num = parseInt(value);
                    if (!isNaN(num)) {
                        value = num;
                    }
                }
                break;

            case 'object' :
                try {
                    value = JSON.parse(value);
                } catch(e) { }
                break;

            case 'date' :
            case 'time' :
            case 'datetime' :
            case 'timestamp' :
                {
                    const m = moment(value);
                    if (m.isValid()) {
                        value = m;
                    }
                }
                break;

            case 'regex' :
                //	:TODO: What flags to use, etc.?
                break;
        }

        return value;
    }

    _resolveEnvironmentVariable(spec) {
        const [prefix, varName, type, array] = spec.split(':');
        if (!varName) {
            return;
        }

        let value = process.env[varName];
        if (!value) {
            return;
        }

        if ('array' === array) {
            value = value.split(',').map(v => this._convertTo(v, type));
        } else {
            value = this._convertTo(value, type);
        }

        return value;
    }

    _loadConfigFile(filePath, cb) {
        const ConfigCache = require('./config_cache');

        const options = {
            filePath,
            hotReload   : this.hotReload,
            callback    : this._configFileChanged.bind(this),
        };

        ConfigCache.getConfigWithOptions(options, (err, config) => {
            return cb(err, config);
        });
    }

    _configFileChanged({fileName, fileRoot, configCache}) {
        const reCachedPath = paths.join(fileRoot, fileName);
        configCache.getConfig(reCachedPath, (err, config) => {
            if (err) {
                return console.stdout(`Configuration ${reCachedPath} is invalid: ${err.message}`); //  eslint-disable-line no-console
            }

            if (this.configPaths.includes(reCachedPath)) {
                this._reload(this.baseConfigPath, err => {
                    if (!err) {
                        const Events = require('./events.js');
                        Events.emit(Events.getSystemEvents().ConfigChanged);
                    }
                });
            }
        });
    }

    _resolveIncludes(configRoot, config, cb) {
        if (!Array.isArray(config.includes)) {
            return cb(null, config);
        }

        //  If a included file is changed, we need to re-cache, so this
        //  must be tracked...
        const includePaths = config.includes.map(inc => paths.join(configRoot, inc));
        async.eachSeries(includePaths, (includePath, nextIncludePath) => {
            this._loadConfigFile(includePath, (err, includedConfig) => {
                if (err) {
                    return nextIncludePath(err);
                }

                _.defaultsDeep(config, includedConfig);
                return nextIncludePath(null);
            });
        },
        err => {
            this.configPaths = [ this.baseConfigPath, ...includePaths ];
            return cb(err, config);
        });
    }

    _resolveAtSpecs(config) {
        return mapValuesDeep(
            config,
            value => {
                if (_.isString(value) && '@' === value.charAt(0)) {
                    if (value.startsWith('@reference:')) {
                        const refPath = value.slice(11);
                        value = _.get(config, refPath, value);
                    } else if (value.startsWith('@environment:')) {
                        value = this._resolveEnvironmentVariable(value) || value;
                    }
                }

                return value;
            }
        );
    }
};

@@ -64,16 +64,50 @@
 async');
+%0Aconst moment = require('moment');
 %0A%0Aconst 
@@ -1953,24 +1953,71 @@
 ile (true) %7B
+  //  eslint-disable-line no-constant-condition
 %0A           
@@ -4150,16 +4150,64 @@
 tch(e) %7B
+%0A                    //  ignored%0A               
  %7D%0A     
@@ -4739,14 +4739,8 @@
 st %5B
-prefix
 , va
@@ -5567,21 +5567,8 @@
 Root
-, configCache
 %7D) %7B
@@ -5633,247 +5633,8 @@
 e);%0A
-        configCache.getConfig(reCachedPath, (err, config) =%3E %7B%0A            if (err) %7B%0A                return console.stdout(%60Configuration $%7BreCachedPath%7D is invalid: $%7Berr.message%7D%60); //  eslint-disable-line no-console%0A            %7D%0A%0A    
     
@@ -5680,28 +5680,24 @@
 hedPath)) %7B%0A
-    
             
@@ -5747,36 +5747,32 @@
                 
-  
-  
 if (!err) %7B%0A    
@@ -5759,36 +5759,32 @@
     if (!err) %7B%0A
-    
                 
@@ -5846,20 +5846,16 @@
         
-   
- 
 Events.e
@@ -5911,33 +5911,26 @@
             
-    %7D%0A   
+%7D%0A
             
@@ -5921,33 +5921,32 @@
   %7D%0A            
- 
 %7D);%0A            
@@ -5945,25 +5945,9 @@
     
-    %7D%0A        %7D);
+%7D
 %0A   

Tidy up config, fix a couple minor bugs