text: const merge = require('lodash.merge')
  const path = require('path')
  
  const ConfigurationProxyHandler = {
    get(target, key) {
      if (target.has && target.has(key)) {
        const value = target.immutable === true ? Object.freeze(target.get(key)) : target.get(key)
        return new Proxy(value, ConfigurationProxyHandler)
      }
      else {
        return target.immutable === true ? Object.freeze(target[key]) : target[key]
      }
    }
  }
  
  module.exports = class Configuration extends Map {
    constructor(configTree = {}, processEnv = {}) {
      const config = Configuration.buildConfig(configTree, processEnv.NODE_ENV)
      const configEntries = Object.entries(Configuration.flattenTree(config))
      super(configEntries)
  
      this.immutable = false
      this.env = processEnv
  
      this.get = this.get.bind(this)
      this.set = this.set.bind(this)
      this.entries = this.entries.bind(this)
      this.has = this.has.bind(this)
  
      return new Proxy(this, ConfigurationProxyHandler)
    }
  
    set(key, value) {
      if (this.immutable === true) {
        throw new IllegalAccessError('Cannot set properties directly on config. Use .set(key, value) (immutable)')
      }
      return super.set(key, value)
    }
  
    /**
     * Merge tree into this configuration. Return overwritten keys
     */
    merge(configTree) {
      const configEntries = Object.entries(Configuration.flattenTree(configTree))
      return configEntries.map(([key, value]) => {
        const hasKey = this.has(key)
  
        //if the key already exist and it's an object, we need to merge values before setting in on the map or we lose some data
        if (hasKey) {
          const currentValue = this.get(key)
          if (typeof currentValue === 'object' && !Array.isArray(currentValue)) {
            value = merge(currentValue, value)
          }
        }
        this.set(key, value)
  
        return { hasKey, key }
      })
    }
  
    /**
     * Prevent changes to the app configuration
     */
    freeze() {
      this.immutable = true
    }
  
    /**
     * Allow changes to the app configuration
     */
    unfreeze() {
      this.immutable = false
    }
  
    /**
     * Flattens configuration tree
     */
    static flattenTree(tree = {}) {
      const toReturn = {}
  
      Object.entries(tree).forEach(([k, v]) => {
        if (typeof v === 'object' && v !== null) {
          const flatObject = Configuration.flattenTree(v)
          Object.keys(flatObject).forEach(flatKey => {
            toReturn[`${k}.${flatKey}`] = flatObject[flatKey]
          })
        }
        toReturn[k] = v
      })
      return toReturn
    }
  
    /**
     * Copy and merge the provided configuration into a new object, decorated with
     * necessary default and environment-specific values.
     */
    static buildConfig(initialConfig = {}, nodeEnv) {
      const root = path.resolve(path.dirname(require.main.filename))
      const temp = path.resolve(root, '.tmp')
      const envConfig = initialConfig.env && initialConfig.env[nodeEnv] || {}
  
      const configTemplate = {
        main: {
          maxListeners: 128,
          packs: [],
          paths: {
            root: root,
            temp: temp,
            sockets: path.resolve(temp, 'sockets'),
            logs: path.resolve(temp, 'log')
          },
          freezeConfig: true,
          createPaths: true
        }
      }
  
      return merge(configTemplate, initialConfig, envConfig, { env: nodeEnv })
    }
  }
  
  