// fancy-pants parsing of argv, originally forked
// from minimist: https://www.npmjs.com/package/minimist
var camelCase = require('camelcase')
var path = require('path')

function increment (orig) {
  return orig !== undefined ? orig + 1 : 0
}

module.exports = function (args, opts) {
  if (!opts) opts = {}

  var error = null
  var flags = { arrays: {}, bools: {}, strings: {}, counts: {}, normalize: {}, configs: {} }

  ;[].concat(opts['array']).filter(Boolean).forEach(function (key) {
    flags.arrays[key] = true
  })

  ;[].concat(opts['boolean']).filter(Boolean).forEach(function (key) {
    flags.bools[key] = true
  })

  ;[].concat(opts.string).filter(Boolean).forEach(function (key) {
    flags.strings[key] = true
  })

  ;[].concat(opts.count).filter(Boolean).forEach(function (key) {
    flags.counts[key] = true
  })

  ;[].concat(opts.normalize).filter(Boolean).forEach(function (key) {
    flags.normalize[key] = true
  })

  ;[].concat(opts.config).filter(Boolean).forEach(function (key) {
    flags.configs[key] = true
  })

  var aliases = {}
  var newAliases = {}

  extendAliases(opts.key)
  extendAliases(opts.alias)

  var defaults = opts['default'] || {}
  Object.keys(defaults).forEach(function (key) {
    if (/-/.test(key) && !opts.alias[key]) {
      aliases[key] = aliases[key] || []
    }
    (aliases[key] || []).forEach(function (alias) {
      defaults[alias] = defaults[key]
    })
  })

  var argv = { _: [] }

  Object.keys(flags.bools).forEach(function (key) {
    setArg(key, !(key in defaults) ? false : defaults[key])
  })

  var notFlags = []
  if (args.indexOf('--') !== -1) {
    notFlags = args.slice(args.indexOf('--') + 1)
    args = args.slice(0, args.indexOf('--'))
  }

  for (var i = 0; i < args.length; i++) {
    var arg = args[i]
    var broken
    var key
    var letters
    var m
    var next
    var value

    // -- seperated by =
    if (arg.match(/^--.+=/)) {
      // Using [\s\S] instead of . because js doesn't support the
      // 'dotall' regex modifier. See:
      // http://stackoverflow.com/a/1068308/13216
      m = arg.match(/^--([^=]+)=([\s\S]*)$/)

      // nargs format = '--f=monkey washing cat'
      if (checkAllAliases(m[1], opts.narg)) {
        args.splice(i + 1, m[1], m[2])
        i = eatNargs(i, m[1], args)
      // arrays format = '--f=a b c'
      } else if (checkAllAliases(m[1], flags.arrays) && args.length > i + 1) {
        args.splice(i + 1, m[1], m[2])
        i = eatArray(i, m[1], args)
      } else {
        setArg(m[1], m[2])
      }
    } else if (arg.match(/^--no-.+/)) {
      key = arg.match(/^--no-(.+)/)[1]
      setArg(key, false)

    // -- seperated by space.
    } else if (arg.match(/^--.+/)) {
      key = arg.match(/^--(.+)/)[1]

      // nargs format = '--foo a b c'
      if (checkAllAliases(key, opts.narg)) {
        i = eatNargs(i, key, args)
      // array format = '--foo a b c'
      } else if (checkAllAliases(key, flags.arrays) && args.length > i + 1) {
        i = eatArray(i, key, args)
      } else {
        next = args[i + 1]

        if (next !== undefined && !next.match(/^-/) &&
          !checkAllAliases(key, flags.bools) &&
          !checkAllAliases(key, flags.counts)) {
          setArg(key, next)
          i++
        } else if (/^(true|false)$/.test(next)) {
          setArg(key, next)
          i++
        } else {
          setArg(key, defaultForType(guessType(key, flags)))
        }
      }

    // dot-notation flag seperated by '='.
    } else if (arg.match(/^-.\..+=/)) {
      m = arg.match(/^-([^=]+)=([\s\S]*)$/)
      setArg(m[1], m[2])

    // dot-notation flag seperated by space.
    } else if (arg.match(/^-.\..+/)) {
      next = args[i + 1]
      key = arg.match(/^-(.\..+)/)[1]

      if (next !== undefined && !next.match(/^-/) &&
        !checkAllAliases(key, flags.bools) &&
        !checkAllAliases(key, flags.counts)) {
        setArg(key, next)
        i++
      } else {
        setArg(key, defaultForType(guessType(key, flags)))
      }
    } else if (arg.match(/^-[^-]+/)) {
      letters = arg.slice(1, -1).split('')
      broken = false

      for (var j = 0; j < letters.length; j++) {
        next = arg.slice(j + 2)

        if (letters[j + 1] && letters[j + 1] === '=') {
          value = arg.slice(j + 3)
          key = letters[j]

          // nargs format = '-f=monkey washing cat'
          if (checkAllAliases(letters[j], opts.narg)) {
            args.splice(i + 1, 0, value)
            i = eatNargs(i, key, args)
          // array format = '-f=a b c'
          } else if (checkAllAliases(key, flags.arrays) && args.length > i + 1) {
            args.splice(i + 1, 0, value)
            i = eatArray(i, key, args)
          } else {
            setArg(key, value)
          }

          broken = true
          break
        }

        if (next === '-') {
          setArg(letters[j], next)
          continue
        }

        if (/[A-Za-z]/.test(letters[j]) &&
          /-?\d+(\.\d*)?(e-?\d+)?$/.test(next)) {
          setArg(letters[j], next)
          broken = true
          break
        }

        if (letters[j + 1] && letters[j + 1].match(/\W/)) {
          setArg(letters[j], arg.slice(j + 2))
          broken = true
          break
        } else {
          setArg(letters[j], defaultForType(guessType(letters[j], flags)))
        }
      }

      key = arg.slice(-1)[0]

      if (!broken && key !== '-') {
        // nargs format = '-f a b c'
        if (checkAllAliases(key, opts.narg)) {
          i = eatNargs(i, key, args)
        // array format = '-f a b c'
        } else if (checkAllAliases(key, flags.arrays) && args.length > i + 1) {
          i = eatArray(i, key, args)
        } else {
          if (args[i + 1] && !/^(-|--)[^-]/.test(args[i + 1]) &&
            !checkAllAliases(key, flags.bools) &&
            !checkAllAliases(key, flags.counts)) {
            setArg(key, args[i + 1])
            i++
          } else if (args[i + 1] && /true|false/.test(args[i + 1])) {
            setArg(key, args[i + 1])
            i++
          } else {
            setArg(key, defaultForType(guessType(key, flags)))
          }
        }
      }
    } else {
      argv._.push(
        flags.strings['_'] || !isNumber(arg) ? arg : Number(arg)
      )
    }
  }

  setConfig(argv)
  applyDefaultsAndAliases(argv, aliases, defaults)

  Object.keys(flags.counts).forEach(function (key) {
    setArg(key, defaults[key])
  })

  notFlags.forEach(function (key) {
    argv._.push(key)
  })

  // how many arguments should we consume, based
  // on the nargs option?
  function eatNargs (i, key, args) {
    var toEat = checkAllAliases(key, opts.narg)

    if (args.length - (i + 1) < toEat) error = Error('not enough arguments following: ' + key)

    for (var ii = i + 1; ii < (toEat + i + 1); ii++) {
      setArg(key, args[ii])
    }

    return (i + toEat)
  }

  // if an option is an array, eat all non-hyphenated arguments
  // following it... YUM!
  // e.g., --foo apple banana cat becomes ["apple", "banana", "cat"]
  function eatArray (i, key, args) {
    for (var ii = i + 1; ii < args.length; ii++) {
      if (/^-/.test(args[ii])) break
      i = ii
      setArg(key, args[ii])
    }

    return i
  }

  function setArg (key, val) {
    // handle parsing boolean arguments --foo=true --bar false.
    if (checkAllAliases(key, flags.bools) || checkAllAliases(key, flags.counts)) {
      if (typeof val === 'string') val = val === 'true'
    }

    if (/-/.test(key) && !(aliases[key] && aliases[key].length)) {
      var c = camelCase(key)
      aliases[key] = [c]
      newAliases[c] = true
    }

    var value = !checkAllAliases(key, flags.strings) && isNumber(val) ? Number(val) : val

    if (checkAllAliases(key, flags.counts)) {
      value = increment
    }

    var splitKey = key.split('.')
    setKey(argv, splitKey, value)

    ;(aliases[splitKey[0]] || []).forEach(function (x) {
      x = x.split('.')

      // handle populating dot notation for both
      // the key and its aliases.
      if (splitKey.length > 1) {
        var a = [].concat(splitKey)
        a.shift() // nuke the old key.
        x = x.concat(a)
      }

      setKey(argv, x, value)
    })

    var keys = [key].concat(aliases[key] || [])
    for (var i = 0, l = keys.length; i < l; i++) {
      if (flags.normalize[keys[i]]) {
        keys.forEach(function (key) {
          argv.__defineSetter__(key, function (v) {
            val = path.normalize(v)
          })

          argv.__defineGetter__(key, function () {
            return typeof val === 'string' ?
            path.normalize(val) : val
          })
        })
        break
      }
    }
  }

  // set args from config.json file, this should be
  // applied last so that defaults can be applied.
  function setConfig (argv) {
    var configLookup = {}

    // expand defaults/aliases, in-case any happen to reference
    // the config.json file.
    applyDefaultsAndAliases(configLookup, aliases, defaults)

    Object.keys(flags.configs).forEach(function (configKey) {
      var configPath = argv[configKey] || configLookup[configKey]
      if (configPath) {
        try {
          var config = require(path.resolve(process.cwd(), configPath))

          Object.keys(config).forEach(function (key) {
            // setting arguments via CLI takes precedence over
            // values within the config file.
            if (argv[key] === undefined) {
              delete argv[key]
              setArg(key, config[key])
            }
          })
        } catch (ex) {
          if (argv[configKey]) error = Error('invalid json config file: ' + configPath)
        }
      }
    })
  }

  function applyDefaultsAndAliases (obj, aliases, defaults) {
    Object.keys(defaults).forEach(function (key) {
      if (!hasKey(obj, key.split('.'))) {
        setKey(obj, key.split('.'), defaults[key])

        ;(aliases[key] || []).forEach(function (x) {
          setKey(obj, x.split('.'), defaults[key])
        })
      }
    })
  }

  function hasKey (obj, keys) {
    var o = obj
    keys.slice(0, -1).forEach(function (key) {
      o = (o[key] || {})
    })

    var key = keys[keys.length - 1]
    return key in o
  }

  function setKey (obj, keys, value) {
    var o = obj
    keys.slice(0, -1).forEach(function (key) {
      if (o[key] === undefined) o[key] = {}
      o = o[key]
    })

    var key = keys[keys.length - 1]
    if (value === increment) {
      o[key] = increment(o[key])
    } else if (o[key] === undefined && checkAllAliases(key, flags.arrays)) {
      o[key] = Array.isArray(value) ? value : [value]
    } else if (o[key] === undefined || typeof o[key] === 'boolean') {
      o[key] = value
    } else if (Array.isArray(o[key])) {
      o[key].push(value)
    } else {
      o[key] = [ o[key], value ]
    }
  }

  // extend the aliases list with inferred aliases.
  function extendAliases (obj) {
    Object.keys(obj || {}).forEach(function (key) {
      aliases[key] = [].concat(opts.alias[key] || [])
      // For "--option-name", also set argv.optionName
      aliases[key].concat(key).forEach(function (x) {
        if (/-/.test(x)) {
          var c = camelCase(x)
          aliases[key].push(c)
          newAliases[c] = true
        }
      })
      aliases[key].forEach(function (x) {
        aliases[x] = [key].concat(aliases[key].filter(function (y) {
          return x !== y
        }))
      })
    })
  }

  // check if a flag is set for any of a key's aliases.
  function checkAllAliases (key, flag) {
    var isSet = false
    var toCheck = [].concat(aliases[key] || [], key)

    toCheck.forEach(function (key) {
      if (flag[key]) isSet = flag[key]
    })

    return isSet
  }

  // return a default value, given the type of a flag.,
  // e.g., key of type 'string' will default to '', rather than 'true'.
  function defaultForType (type) {
    var def = {
      boolean: true,
      string: '',
      array: []
    }

    return def[type]
  }

  // given a flag, enforce a default type.
  function guessType (key, flags) {
    var type = 'boolean'

    if (flags.strings && flags.strings[key]) type = 'string'
    else if (flags.arrays && flags.arrays[key]) type = 'array'

    return type
  }

  function isNumber (x) {
    if (typeof x === 'number') return true
    if (/^0x[0-9a-f]+$/i.test(x)) return true
    return /^[-+]?(?:\d+(?:\.\d*)?|\.\d+)(e[-+]?\d+)?$/.test(x)
  }

  return {
    argv: argv,
    aliases: aliases,
    error: error,
    newAliases: newAliases
  }
}
