'use strict'

var path = require('path')

var parentRequire = require('parent-require')
var figlet = require('figlet')
var chalk = require('chalk')

module.exports = new Yargonaut()

function Yargonaut () {
  var self = this
  var yargs = hack()
  var fonts = {}
  var styles = {}
  var fontWorkaround = {}
  var styleWorkaround = {}
  var ocdf = null
  var defaultFont = 'Standard'

  var yargsKeys = {
    // supporting fonts and styles
    'Commands:': { transform: wholeString, error: false },
    'Options:': { transform: wholeString, error: false },
    'Examples:': { transform: wholeString, error: false },
    'Positionals:': { transform: wholeString, error: false },
    'Not enough non-option arguments: got %s, need at least %s': { transform: upToFirstColon, error: true },
    'Too many non-option arguments: got %s, maximum of %s': { transform: upToFirstColon, error: true },
    'Missing argument value: %s': { transform: upToFirstColon, error: true, plural: 'Missing argument values: %s' },
    'Missing required argument: %s': { transform: upToFirstColon, error: true, plural: 'Missing required arguments: %s' },
    'Unknown argument: %s': { transform: upToFirstColon, error: true, plural: 'Unknown arguments: %s' },
    'Invalid values:': { transform: wholeString, error: true },
    'Argument check failed: %s': { transform: upToFirstColon, error: true },
    'Implications failed:': { transform: wholeString, error: true },
    'Not enough arguments following: %s': { transform: upToFirstColon, error: true },
    'Invalid JSON config file: %s': { transform: upToFirstColon, error: true },
    'Did you mean %s?': { transform: wholeString, error: true },
    'Arguments %s and %s are mutually exclusive': { transform: wholeString, error: true },
    // supporting styles only (by default)
    'boolean': { transform: null, error: null },
    'count': { transform: null, error: null },
    'string': { transform: null, error: null },
    'array': { transform: null, error: null },
    'required': { transform: null, error: null },
    'default:': { transform: null, error: null },
    'choices:': { transform: null, error: null },
    'aliases:': { transform: null, error: null },
    'generated-value': { transform: null, error: null },
    'Argument: %s, Given: %s, Choices: %s': { transform: null, error: true }
  }

  // chainable config API methods
  self.help = function (font) {
    applyFont(font, self.getHelpKeys())
    return self
  }

  self.errors = function (font) {
    applyFont(font, self.getErrorKeys())
    return self
  }

  self.font = function (font, key) {
    var keys = key ? [].concat(key) : self.getAllKeys()
    applyFont(font, keys, key !== undefined)
    return self
  }

  self.helpStyle = function (style) {
    applyStyle(style, self.getHelpKeys())
    return self
  }

  self.errorsStyle = function (style) {
    applyStyle(style, self.getErrorKeys())
    return self
  }

  self.style = function (style, key) {
    var keys = key ? [].concat(key) : self.getAllKeys()
    applyStyle(style, keys)
    return self
  }

  self.transformWholeString = function (key) {
    var keys = key ? [].concat(key) : self.getAllKeys()
    applyTransform(wholeString, keys)
    return self
  }

  self.transformUpToFirstColon = function (key) {
    var keys = key ? [].concat(key) : self.getAllKeys()
    applyTransform(upToFirstColon, keys)
    return self
  }

  self.ocd = function (f) {
    if (typeof f === 'function') ocdf = f
    return self
  }

  // getter API methods
  self.getAllKeys = function () {
    return Object.keys(yargsKeys)
  }

  self.getHelpKeys = function () {
    return self.getAllKeys().filter(function (key) {
      return yargsKeys[key].error === false
    })
  }

  self.getErrorKeys = function () {
    return self.getAllKeys().filter(function (key) {
      return yargsKeys[key].error === true
    })
  }

  // methods for playing with fonts
  self.asFont = function (text, font, throwErr) {
    try {
      return figlet.textSync(text, { font: font || defaultFont })
    } catch (err) {
      if (throwErr) throw err
      return text
    }
  }

  self.listFonts = function () {
    return figlet.fontsSync()
  }

  self.printFont = function (font, text, throwErr) {
    font = font || defaultFont
    console.log('Font: ' + font + '\n' + self.asFont(text || font, font, throwErr))
  }

  self.printFonts = function (text, throwErr) {
    self.listFonts().forEach(function (f) {
      self.printFont(f, text, throwErr)
    })
  }

  self.figlet = function () {
    return figlet
  }

  self.chalk = function () {
    return chalk
  }

  // private transforms
  function wholeString (str) {
    return { render: str, nonRender: '' }
  }

  function upToFirstColon (str) {
    var firstColon = str.indexOf(':') + 1
    return { render: str.substring(0, firstColon), nonRender: '\n  ' + str.substring(firstColon) }
  }

  // private config methods
  function applyStyle (style, keys) {
    if (typeof style !== 'string') return
    keys.forEach(function (k) {
      styles[k] = style
    })
    if (yargs && yargs.updateLocale) applyStyleWorkaround(keys)
  }

  function applyFont (font, keys, force) {
    if (typeof font !== 'string') font = defaultFont
    keys.forEach(function (k) {
      fonts[k] = font
      if (!yargsKeys[k]) yargsKeys[k] = { transform: wholeString, error: null }
      else if (force && yargsKeys[k].transform === null) yargsKeys[k].transform = wholeString
    })
    if (yargs && yargs.updateLocale) applyFontWorkaround(keys)
  }

  function applyStyleWorkaround (keys) {
    var u = {}
    keys.forEach(function (k) {
      // cache non-font styled text in styleWorkaround
      if (yargsKeys[k] && yargsKeys[k].plural) {
        styleWorkaround[k] = {
          one: doStyle(k, k, styles[k]),
          other: doStyle(yargsKeys[k].plural, yargsKeys[k].plural, styles[k])
        }
      } else {
        styleWorkaround[k] = doStyle(k, k, styles[k])
      }
      u[k] = styleWorkaround[k]

      // style font-rendered text if defined
      if (fontWorkaround[k]) {
        if (typeof fontWorkaround[k] === 'string') u[k] = doStyle(k, fontWorkaround[k], styles[k])
        else {
          u[k] = {
            one: doStyle(k, fontWorkaround[k].one, styles[k]),
            other: doStyle(yargsKeys[k].plural, fontWorkaround[k].other, styles[k])
          }
        }
      }
    })
    yargs.updateLocale(u)
  }

  function applyFontWorkaround (keys) {
    var u = {}
    keys.forEach(function (k) {
      // cache non-styled font text in fontWorkaround
      if (yargsKeys[k] && yargsKeys[k].plural) {
        fontWorkaround[k] = {
          one: doRender(k, k, fonts[k]),
          other: doRender(yargsKeys[k].plural, yargsKeys[k].plural, fonts[k])
        }
      } else {
        fontWorkaround[k] = doRender(k, k, fonts[k])
      }
      u[k] = fontWorkaround[k]

      // render styled text if defined
      if (styleWorkaround[k]) {
        if (typeof styleWorkaround[k] === 'string') u[k] = doRender(k, styleWorkaround[k], fonts[k])
        else {
          u[k] = {
            one: doRender(k, styleWorkaround[k].one, fonts[k]),
            other: doRender(yargsKeys[k].plural, styleWorkaround[k].other, fonts[k])
          }
        }
      }
    })
    yargs.updateLocale(u)
  }

  function applyTransform (transform, keys) {
    keys.forEach(function (k) {
      if (!yargsKeys[k]) yargsKeys[k] = { transform: transform, error: null }
      else yargsKeys[k].transform = transform
    })
  }

  // private logic to intercept y18n methods
  function doStyle (key, orig, style) {
    var chain = chalk
    style.split('.').forEach(function (s) {
      if (chain[s]) chain = chain[s]
    })
    return typeof chain === 'function' ? chain(orig) : orig
  }

  function doRender (key, orig, font) {
    var fn = yargsKeys[key] ? yargsKeys[key].transform : upToFirstColon
    if (fn === null) return orig
    var split = fn(orig)
    return self.asFont(split.render, font) + split.nonRender
  }

  function hack () {
    var lookingFor = []
    lookingFor.push(path.join('yargs', 'node_modules', 'y18n', 'index.js')) // npm 2 = nested
    lookingFor.push(path.join('y18n', 'index.js')) // npm 3 = flat

    var found = findInModuleCache(lookingFor)

    if (found) {
      // console.log('yargonaut: You loaded yargs before yargonaut, only default locale supported')
      return parentRequireSafe('yargs')
    }

    var Y18n = parentRequireFirstOf(lookingFor)
    found = findInModuleCache(lookingFor)

    if (!found) {
      // console.log('yargonaut: You\'re using a version of yargs that does not support string customization')
      return parentRequireSafe('yargs')
    }

    require.cache[found].exports = function (opts) {
      var z18n = new Y18n(opts)
      var singular = z18n.__
      var plural = z18n.__n

      z18n.__ = function () {
        var orig = singular.apply(z18n, arguments)
        var key = arguments[0]
        if (fonts[key] && yargsKeys[key]) {
          return tweak(
            key,
            orig,
            doRender(key, orig, fonts[key]),
            figlet,
            fonts[key]
          )
        }
        return tweak(key, orig, orig, figlet, fonts[key])
      }

      z18n.__n = function () {
        var orig = plural.apply(z18n, arguments)
        var key = arguments[0]
        if (fonts[key] && yargsKeys[key]) {
          return tweak(
            key,
            orig,
            doRender(key, orig, fonts[key]),
            figlet,
            fonts[key]
          )
        }
        return tweak(key, orig, orig, figlet, fonts[key])
      }

      function tweak (key, origString, newString, figlet, font) {
        if (styles[key]) newString = doStyle(key, newString, styles[key])
        return ocdf ? ocdf(key, origString, newString, figlet, font) : newString
      }

      for (var key in z18n) {
        if (typeof z18n[key] === 'function') {
          z18n[key] = z18n[key].bind(z18n)
        }
      }

      return z18n
    }

    return null
  }

  function findInModuleCache (lookingFor) {
    var found = null
    for (var i = 0, files = Object.keys(require.cache); i < files.length; i++) {
      for (var j = 0; j < lookingFor.length; j++) {
        if (~files[i].lastIndexOf(lookingFor[j])) {
          found = files[i]
          break
        }
      }
      if (found) break
    }
    return found
  }

  function parentRequireFirstOf (array) {
    var success = null
    for (var x = 0; x < array.length; x++) {
      success = parentRequireSafe(array[x])
      if (success) break
    }
    return success
  }

  function parentRequireSafe (m) {
    try {
      return parentRequire(m)
    } catch (err) {
      return null
    }
  }
}
