'use strict' // this file handles outputting usage instructions, // failures, etc. keeps logging in one place. const decamelize = require('./decamelize') const stringWidth = require('string-width') const objFilter = require('./obj-filter') const path = require('path') const setBlocking = require('set-blocking') const YError = require('./yerror')

module.exports = function usage (yargs, y18n) {

const __ = y18n.__
const self = {}

// methods for ouputting/building failure message.
const fails = []
self.failFn = function failFn (f) {
  fails.push(f)
}

let failMessage = null
let showHelpOnFail = true
self.showHelpOnFail = function showHelpOnFailFn (enabled, message) {
  if (typeof enabled === 'string') {
    message = enabled
    enabled = true
  } else if (typeof enabled === 'undefined') {
    enabled = true
  }
  failMessage = message
  showHelpOnFail = enabled
  return self
}

let failureOutput = false
self.fail = function fail (msg, err) {
  const logger = yargs._getLoggerInstance()

  if (fails.length) {
    for (let i = fails.length - 1; i >= 0; --i) {
      fails[i](msg, err, self)
    }
  } else {
    if (yargs.getExitProcess()) setBlocking(true)

    // don't output failure message more than once
    if (!failureOutput) {
      failureOutput = true
      if (showHelpOnFail) {
        yargs.showHelp('error')
        logger.error()
      }
      if (msg || err) logger.error(msg || err)
      if (failMessage) {
        if (msg || err) logger.error('')
        logger.error(failMessage)
      }
    }

    err = err || new YError(msg)
    if (yargs.getExitProcess()) {
      return yargs.exit(1)
    } else if (yargs._hasParseCallback()) {
      return yargs.exit(1, err)
    } else {
      throw err
    }
  }
}

// methods for ouputting/building help (usage) message.
let usages = []
let usageDisabled = false
self.usage = (msg, description) => {
  if (msg === null) {
    usageDisabled = true
    usages = []
    return
  }
  usageDisabled = false
  usages.push([msg, description || ''])
  return self
}
self.getUsage = () => {
  return usages
}
self.getUsageDisabled = () => {
  return usageDisabled
}

self.getPositionalGroupName = () => {
  return __('Positionals:')
}

let examples = []
self.example = (cmd, description) => {
  examples.push([cmd, description || ''])
}

let commands = []
self.command = function command (cmd, description, isDefault, aliases) {
  // the last default wins, so cancel out any previously set default
  if (isDefault) {
    commands = commands.map((cmdArray) => {
      cmdArray[2] = false
      return cmdArray
    })
  }
  commands.push([cmd, description || '', isDefault, aliases])
}
self.getCommands = () => commands

let descriptions = {}
self.describe = function describe (key, desc) {
  if (typeof key === 'object') {
    Object.keys(key).forEach((k) => {
      self.describe(k, key[k])
    })
  } else {
    descriptions[key] = desc
  }
}
self.getDescriptions = () => descriptions

let epilog
self.epilog = (msg) => {
  epilog = msg
}

let wrapSet = false
let wrap
self.wrap = (cols) => {
  wrapSet = true
  wrap = cols
}

function getWrap () {
  if (!wrapSet) {
    wrap = windowWidth()
    wrapSet = true
  }

  return wrap
}

const deferY18nLookupPrefix = '__yargsString__:'
self.deferY18nLookup = str => deferY18nLookupPrefix + str

const defaultGroup = 'Options:'
self.help = function help () {
  normalizeAliases()

  // handle old demanded API
  const base$0 = path.basename(yargs.$0)
  const demandedOptions = yargs.getDemandedOptions()
  const demandedCommands = yargs.getDemandedCommands()
  const groups = yargs.getGroups()
  const options = yargs.getOptions()

  let keys = []
  keys = keys.concat(Object.keys(descriptions))
  keys = keys.concat(Object.keys(demandedOptions))
  keys = keys.concat(Object.keys(demandedCommands))
  keys = keys.concat(Object.keys(options.default))
  keys = keys.filter(filterHiddenOptions)
  keys = Object.keys(keys.reduce((acc, key) => {
    if (key !== '_') acc[key] = true
    return acc
  }, {}))

  const theWrap = getWrap()
  const ui = require('cliui')({
    width: theWrap,
    wrap: !!theWrap
  })

  // the usage string.
  if (!usageDisabled) {
    if (usages.length) {
      // user-defined usage.
      usages.forEach((usage) => {
        ui.div(`${usage[0].replace(/\$0/g, base$0)}`)
        if (usage[1]) {
          ui.div({ text: `${usage[1]}`, padding: [1, 0, 0, 0] })
        }
      })
      ui.div()
    } else if (commands.length) {
      let u = null
      // demonstrate how commands are used.
      if (demandedCommands._) {
        u = `${base$0} <${__('command')}>\n`
      } else {
        u = `${base$0} [${__('command')}]\n`
      }
      ui.div(`${u}`)
    }
  }

  // your application's commands, i.e., non-option
  // arguments populated in '_'.
  if (commands.length) {
    ui.div(__('Commands:'))

    const context = yargs.getContext()
    const parentCommands = context.commands.length ? `${context.commands.join(' ')} ` : ''

    if (yargs.getParserConfiguration()['sort-commands'] === true) {
      commands = commands.sort((a, b) => a[0].localeCompare(b[0]))
    }

    commands.forEach((command) => {
      const commandString = `${base$0} ${parentCommands}${command[0].replace(/^\$0 ?/, '')}` // drop $0 from default commands.
      ui.span(
        {
          text: commandString,
          padding: [0, 2, 0, 2],
          width: maxWidth(commands, theWrap, `${base$0}${parentCommands}`) + 4
        },
        { text: command[1] }
      )
      const hints = []
      if (command[2]) hints.push(`[${__('default:').slice(0, -1)}]`) // TODO hacking around i18n here
      if (command[3] && command[3].length) {
        hints.push(`[${__('aliases:')} ${command[3].join(', ')}]`)
      }
      if (hints.length) {
        ui.div({ text: hints.join(' '), padding: [0, 0, 0, 2], align: 'right' })
      } else {
        ui.div()
      }
    })

    ui.div()
  }

  // perform some cleanup on the keys array, making it
  // only include top-level keys not their aliases.
  const aliasKeys = (Object.keys(options.alias) || [])
    .concat(Object.keys(yargs.parsed.newAliases) || [])

  keys = keys.filter(key => !yargs.parsed.newAliases[key] && aliasKeys.every(alias => (options.alias[alias] || []).indexOf(key) === -1))

  // populate 'Options:' group with any keys that have not
  // explicitly had a group set.
  if (!groups[defaultGroup]) groups[defaultGroup] = []
  addUngroupedKeys(keys, options.alias, groups)

  // display 'Options:' table along with any custom tables:
  Object.keys(groups).forEach((groupName) => {
    if (!groups[groupName].length) return

    // if we've grouped the key 'f', but 'f' aliases 'foobar',
    // normalizedKeys should contain only 'foobar'.
    const normalizedKeys = groups[groupName].filter(filterHiddenOptions).map((key) => {
      if (~aliasKeys.indexOf(key)) return key
      for (let i = 0, aliasKey; (aliasKey = aliasKeys[i]) !== undefined; i++) {
        if (~(options.alias[aliasKey] || []).indexOf(key)) return aliasKey
      }
      return key
    })

    if (normalizedKeys.length < 1) return

    ui.div(__(groupName))

    // actually generate the switches string --foo, -f, --bar.
    const switches = normalizedKeys.reduce((acc, key) => {
      acc[key] = [ key ].concat(options.alias[key] || [])
        .map(sw => {
          // for the special positional group don't
          // add '--' or '-' prefix.
          if (groupName === self.getPositionalGroupName()) return sw
          else return (sw.length > 1 ? '--' : '-') + sw
        })
        .join(', ')

      return acc
    }, {})

    normalizedKeys.forEach((key) => {
      const kswitch = switches[key]
      let desc = descriptions[key] || ''
      let type = null

      if (~desc.lastIndexOf(deferY18nLookupPrefix)) desc = __(desc.substring(deferY18nLookupPrefix.length))

      if (~options.boolean.indexOf(key)) type = `[${__('boolean')}]`
      if (~options.count.indexOf(key)) type = `[${__('count')}]`
      if (~options.string.indexOf(key)) type = `[${__('string')}]`
      if (~options.normalize.indexOf(key)) type = `[${__('string')}]`
      if (~options.array.indexOf(key)) type = `[${__('array')}]`
      if (~options.number.indexOf(key)) type = `[${__('number')}]`

      const extra = [
        type,
        (key in demandedOptions) ? `[${__('required')}]` : null,
        options.choices && options.choices[key] ? `[${__('choices:')} ${
          self.stringifiedValues(options.choices[key])}]` : null,
        defaultString(options.default[key], options.defaultDescription[key])
      ].filter(Boolean).join(' ')

      ui.span(
        { text: kswitch, padding: [0, 2, 0, 2], width: maxWidth(switches, theWrap) + 4 },
        desc
      )

      if (extra) ui.div({ text: extra, padding: [0, 0, 0, 2], align: 'right' })
      else ui.div()
    })

    ui.div()
  })

  // describe some common use-cases for your application.
  if (examples.length) {
    ui.div(__('Examples:'))

    examples.forEach((example) => {
      example[0] = example[0].replace(/\$0/g, base$0)
    })

    examples.forEach((example) => {
      if (example[1] === '') {
        ui.div(
          {
            text: example[0],
            padding: [0, 2, 0, 2]
          }
        )
      } else {
        ui.div(
          {
            text: example[0],
            padding: [0, 2, 0, 2],
            width: maxWidth(examples, theWrap) + 4
          }, {
            text: example[1]
          }
        )
      }
    })

    ui.div()
  }

  // the usage string.
  if (epilog) {
    const e = epilog.replace(/\$0/g, base$0)
    ui.div(`${e}\n`)
  }

  // Remove the trailing white spaces
  return ui.toString().replace(/\s*$/, '')
}

// return the maximum width of a string
// in the left-hand column of a table.
function maxWidth (table, theWrap, modifier) {
  let width = 0

  // table might be of the form [leftColumn],
  // or {key: leftColumn}
  if (!Array.isArray(table)) {
    table = Object.keys(table).map(key => [table[key]])
  }

  table.forEach((v) => {
    width = Math.max(
      stringWidth(modifier ? `${modifier} ${v[0]}` : v[0]),
      width
    )
  })

  // if we've enabled 'wrap' we should limit
  // the max-width of the left-column.
  if (theWrap) width = Math.min(width, parseInt(theWrap * 0.5, 10))

  return width
}

// make sure any options set for aliases,
// are copied to the keys being aliased.
function normalizeAliases () {
  // handle old demanded API
  const demandedOptions = yargs.getDemandedOptions()
  const options = yargs.getOptions()

  ;(Object.keys(options.alias) || []).forEach((key) => {
    options.alias[key].forEach((alias) => {
      // copy descriptions.
      if (descriptions[alias]) self.describe(key, descriptions[alias])
      // copy demanded.
      if (alias in demandedOptions) yargs.demandOption(key, demandedOptions[alias])
      // type messages.
      if (~options.boolean.indexOf(alias)) yargs.boolean(key)
      if (~options.count.indexOf(alias)) yargs.count(key)
      if (~options.string.indexOf(alias)) yargs.string(key)
      if (~options.normalize.indexOf(alias)) yargs.normalize(key)
      if (~options.array.indexOf(alias)) yargs.array(key)
      if (~options.number.indexOf(alias)) yargs.number(key)
    })
  })
}

// given a set of keys, place any keys that are
// ungrouped under the 'Options:' grouping.
function addUngroupedKeys (keys, aliases, groups) {
  let groupedKeys = []
  let toCheck = null
  Object.keys(groups).forEach((group) => {
    groupedKeys = groupedKeys.concat(groups[group])
  })

  keys.forEach((key) => {
    toCheck = [key].concat(aliases[key])
    if (!toCheck.some(k => groupedKeys.indexOf(k) !== -1)) {
      groups[defaultGroup].push(key)
    }
  })
  return groupedKeys
}

function filterHiddenOptions (key) {
  return yargs.getOptions().hiddenOptions.indexOf(key) < 0 || yargs.parsed.argv[yargs.getOptions().showHiddenOpt]
}

self.showHelp = (level) => {
  const logger = yargs._getLoggerInstance()
  if (!level) level = 'error'
  const emit = typeof level === 'function' ? level : logger[level]
  emit(self.help())
}

self.functionDescription = (fn) => {
  const description = fn.name ? decamelize(fn.name, '-') : __('generated-value')
  return ['(', description, ')'].join('')
}

self.stringifiedValues = function stringifiedValues (values, separator) {
  let string = ''
  const sep = separator || ', '
  const array = [].concat(values)

  if (!values || !array.length) return string

  array.forEach((value) => {
    if (string.length) string += sep
    string += JSON.stringify(value)
  })

  return string
}

// format the default-value-string displayed in
// the right-hand column.
function defaultString (value, defaultDescription) {
  let string = `[${__('default:')} `

  if (value === undefined && !defaultDescription) return null

  if (defaultDescription) {
    string += defaultDescription
  } else {
    switch (typeof value) {
      case 'string':
        string += `"${value}"`
        break
      case 'object':
        string += JSON.stringify(value)
        break
      default:
        string += value
    }
  }

  return `${string}]`
}

// guess the width of the console window, max-width 80.
function windowWidth () {
  const maxWidth = 80
  if (typeof process === 'object' && process.stdout && process.stdout.columns) {
    return Math.min(maxWidth, process.stdout.columns)
  } else {
    return maxWidth
  }
}

// logic for displaying application version.
let version = null
self.version = (ver) => {
  version = ver
}

self.showVersion = () => {
  const logger = yargs._getLoggerInstance()
  logger.log(version)
}

self.reset = function reset (localLookup) {
  // do not reset wrap here
  // do not reset fails here
  failMessage = null
  failureOutput = false
  usages = []
  usageDisabled = false
  epilog = undefined
  examples = []
  commands = []
  descriptions = objFilter(descriptions, (k, v) => !localLookup[k])
  return self
}

let frozen
self.freeze = function freeze () {
  frozen = {}
  frozen.failMessage = failMessage
  frozen.failureOutput = failureOutput
  frozen.usages = usages
  frozen.usageDisabled = usageDisabled
  frozen.epilog = epilog
  frozen.examples = examples
  frozen.commands = commands
  frozen.descriptions = descriptions
}
self.unfreeze = function unfreeze () {
  failMessage = frozen.failMessage
  failureOutput = frozen.failureOutput
  usages = frozen.usages
  usageDisabled = frozen.usageDisabled
  epilog = frozen.epilog
  examples = frozen.examples
  commands = frozen.commands
  descriptions = frozen.descriptions
  frozen = undefined
}

return self

}