
var CC = require('config-chain').ConfigChain
var inherits = require('inherits')
var configDefs = require('./config-defs.js')
var types = configDefs.types
var once = require('once')
var fs = require('fs')
var path = require('path')
var nopt = require('nopt')
var ini = require('ini')
var Octal = configDefs.Octal
var mkdirp = require('mkdirp')

exports.load = load
exports.Conf = Conf
exports.loaded = false
exports.rootConf = null
exports.usingBuiltin = false
exports.defs = configDefs
Object.defineProperty(exports, 'defaults', { get: function () {
  return configDefs.defaults
}, enumerable: true })
Object.defineProperty(exports, 'types', { get: function () {
  return configDefs.types
}, enumerable: true })

exports.validate = validate

var myUid = process.env.SUDO_UID !== undefined
          ? process.env.SUDO_UID : (process.getuid && process.getuid())
var myGid = process.env.SUDO_GID !== undefined
          ? process.env.SUDO_GID : (process.getgid && process.getgid())


var loading = false
var loadCbs = []
function load (cli_, builtin_, cb_) {
  var cli, builtin, cb
  for (var i = 0; i < arguments.length; i++)
    switch (typeof arguments[i]) {
      case 'string': builtin = arguments[i]; break
      case 'object': cli = arguments[i]; break
      case 'function': cb = arguments[i]; break
    }

  if (!cb)
    cb = function () {}

  if (exports.loaded) {
    var ret = exports.loaded
    if (cli) {
      ret = new Conf(ret)
      ret.unshift(cli)
    }
    return process.nextTick(cb.bind(null, null, ret))
  }

  // either a fresh object, or a clone of the passed in obj
  if (!cli)
    cli = {}
  else
    cli = Object.keys(cli).reduce(function (c, k) {
      c[k] = cli[k]
      return c
    }, {})

  loadCbs.push(cb)
  if (loading)
    return
  loading = true

  cb = once(function (er, conf) {
    if (!er)
      exports.loaded = conf
    loadCbs.forEach(function (fn) {
      fn(er, conf)
    })
    loadCbs.length = 0
  })

  // check for a builtin if provided.
  exports.usingBuiltin = !!builtin
  var rc = exports.rootConf = new Conf()
  var defaults = configDefs.defaults
  if (builtin)
    rc.addFile(builtin, 'builtin')
  else
    rc.add({}, 'builtin')

  rc.on('load', function () {
    var conf = new Conf(rc)
    conf.usingBuiltin = !!builtin
    conf.add(cli, 'cli')
    conf.addEnv()
    conf.addFile(conf.get('userconfig'), 'user')
    conf.once('error', cb)
    conf.once('load', function () {
      // globalconfig and globalignorefile defaults
      // need to respond to the "prefix" setting up to this point.
      // Eg, `npm config get globalconfig --prefix ~/local` should
      // return `~/local/etc/npmrc`
      // annoying humans and their expectations!
      if (conf.get('prefix')) {
        var etc = path.resolve(conf.get("prefix"), "etc")
        defaults.globalconfig = path.resolve(etc, "npmrc")
        defaults.globalignorefile = path.resolve(etc, "npmignore")
      }
      conf.addFile(conf.get('globalconfig'), 'global')

      // move the builtin into the conf stack now.
      conf.root = defaults
      conf.add(rc.shift(), 'builtin')
      conf.once('load', function () {
        // warn about invalid bits.
        validate(conf)
        exports.loaded = conf
        cb(null, conf)
      })
    })
  })
}


// Basically the same as CC, but:
// 1. Always ini
// 2. Parses environment variable names in field values
// 3. Field values that start with ~/ are replaced with process.env.HOME
// 4. Can inherit from another Conf object, using it as the base.
inherits(Conf, CC)
function Conf (base) {
  if (!(this instanceof Conf))
    return new Conf(base)

  CC.apply(this)

  if (base)
    if (base instanceof Conf)
      this.root = base.list[0] || base.root
    else
      this.root = base
  else
    this.root = configDefs.defaults
}

Conf.prototype.save = function (where, cb) {
  var target = this.sources[where]
  if (!target || !(target.path || target.source) || !target.data)
    return this.emit('error', new Error('bad save target: '+where))

  if (target.source) {
    var pref = target.prefix || ''
    Object.keys(target.data).forEach(function (k) {
      target.source[pref + k] = target.data[k]
    })
    return this
  }

  var data = target.data

  if (typeof data._password === 'string' &&
      typeof data.username === 'string') {
    var auth = data.username + ':' + data._password
    data = Object.keys(data).reduce(function (c, k) {
      if (k === 'username' || k === '_password')
        return c
      c[k] = data[k]
      return c
    }, { _auth: new Buffer(auth, 'utf8').toString('base64') })
    delete data.username
    delete data._password
  }

  data = ini.stringify(data)

  then = then.bind(this)
  done = done.bind(this)
  this._saving ++

  var mode = where === 'user' ? 0600 : 0666
  if (!data.trim())
    fs.unlink(target.path, done)
  else {
    mkdirp(path.dirname(target.path), function (er) {
      if (er)
        return then(er)
      fs.writeFile(target.path, data, 'utf8', function (er) {
        if (er)
          return then(er)
        if (where === 'user' && myUid && myGid)
          fs.chown(target.path, +myUid, +myGid, then)
        else
          then()
      })
    })
  }

  function then (er) {
    if (er)
      return done(er)
    fs.chmod(target.path, mode, done)
  }

  function done (er) {
    if (er) {
      if (cb) return cb(er)
      else return this.emit('error', er)
    }
    this._saving --
    if (this._saving === 0) {
      if (cb) cb()
      this.emit('save')
    }
  }

  return this
}

Conf.prototype.addFile = function (file, name) {
  name = name || file
  var marker = {__source__:name}
  this.sources[name] = { path: file, type: 'ini' }
  this.push(marker)
  this._await()
  fs.readFile(file, 'utf8', function (er, data) {
    if (er) // just ignore missing files.
      return this.add({}, marker)
    this.addString(data, file, 'ini', marker)
  }.bind(this))
  return this
}

// always ini files.
Conf.prototype.parse = function (content, file) {
  return CC.prototype.parse.call(this, content, file, 'ini')
}

Conf.prototype.add = function (data, marker) {
  Object.keys(data).forEach(function (k) {
    data[k] = parseField(data[k], k)
  })
  if (Object.prototype.hasOwnProperty.call(data, '_auth')) {
    var auth = new Buffer(data._auth, 'base64').toString('utf8').split(':')
    var username = auth.shift()
    var password = auth.join(':')
    data.username = username
    data._password = password
  }
  return CC.prototype.add.call(this, data, marker)
}

Conf.prototype.addEnv = function (env) {
  env = env || process.env
  var conf = {}
  Object.keys(env)
    .filter(function (k) { return k.match(/^npm_config_[^_]/i) })
    .forEach(function (k) {
      if (!env[k])
        return

      conf[k.replace(/^npm_config_/i, '')
            .toLowerCase()
            .replace(/_/g, '-')] = env[k]
    })
  return CC.prototype.addEnv.call(this, '', conf, 'env')
}

function parseField (f, k, emptyIsFalse) {
  if (typeof f !== 'string' && !(f instanceof String))
    return f

  // type can be an array or single thing.
  var typeList = [].concat(types[k])
  var isPath = -1 !== typeList.indexOf(path)
  var isBool = -1 !== typeList.indexOf(Boolean)
  var isString = -1 !== typeList.indexOf(String)
  var isNumber = -1 !== typeList.indexOf(Number) ||
                 -1 !== typeList.indexOf(Octal)

  f = (''+f).trim()

  if (f.match(/^".*"$/))
    f = JSON.parse(f)

  if (isBool && !isString && f === '')
    return true

  switch (f) {
    case 'true': return true
    case 'false': return false
    case 'null': return null
    case 'undefined': return undefined
  }

  f = envReplace(f)

  if (isPath) {
    var homePattern = process.platform === 'win32' ? /^~(\/|\\)/ : /^~\//
    if (f.match(homePattern) && process.env.HOME) {
      f = path.resolve(process.env.HOME, f.substr(2))
    }
    f = path.resolve(f)
  }

  if (isNumber && !isNaN(f))
    f = +f

  return f
}

function envReplace (f) {
  if (typeof f !== "string" || !f) return f

  // replace any ${ENV} values with the appropriate environ.
  var envExpr = /(\\*)\$\{([^}]+)\}/g
  return f.replace(envExpr, function (orig, esc, name, i, s) {
    esc = esc.length && esc.length % 2
    if (esc)
      return orig
    if (undefined === process.env[name])
      throw new Error("Failed to replace env in config: "+orig)
    return process.env[name]
  })
}

function validate (cl) {
  // warn about invalid configs at every level.
  cl.list.forEach(function (conf, level) {
    nopt.clean(conf, configDefs.types)
  })
}
