text: const argv         = require('optimist').argv
      , fs           = require('fs')
      , path         = require('path')
      , map          = require('map-async')
      , msee         = require('msee')
      , chalk        = require('chalk')
      , inherits     = require('util').inherits
      , EventEmitter = require('events').EventEmitter
  
  /* jshint -W079 */
  const showMenu         = require('./exerciseMenu')
      , showLanguageMenu = require('./languageMenu')
      , print            = require('./print-text')
      , util             = require('./util')
      , i18n             = require('./i18n')
  /* jshint +W079 */
  
  const defaultWidth = 65
  
  function Workshopper (options) {
    if (!(this instanceof Workshopper))
      return new Workshopper(options)
  
    EventEmitter.call(this)
  
    var handled = false
      , exercise
      , mode = argv._[0]
  
    if (typeof options != 'object')
      throw new TypeError('need to provide an options object')
  
    if (typeof options.name != 'string')
      throw new TypeError('need to provide a `name` String option')
  
    this.appName       = options.name
    this.appDir        = util.assertDir(options, 'appDir')
    this.exerciseDir   = util.assertDir(options, 'exerciseDir', options.appDir, 'exercises')
    this.globalDataDir = util.userDir('.config', 'workshopper')
    this.dataDir       = util.userDir('.config', this.appName)
  
    util.assertFile(options, 'menuJson', options.exerciseDir, 'menu.json')
  
    if (!options.languages) {
      // In case a workshopper didn't define a any language
      options.languages = ['en']
    }
  
    this.defaultLang = options.languages[0]
    // optional
    this.menuOptions = options.menu
    // helpFile is additional to the usage in usage.txt
    this.helpFile    = options.helpFile
    // optional
    this.footerFile  =   options.footerFile === false
                       ? []
                       : [options.footerFile, path.join(__dirname, './i18n/footer/{lang}.md')]
    this.width       = typeof options.width == 'number'
        ? options.width
        : defaultWidth
    // an `onComplete` hook function *must* call the callback given to it when it's finished, async or not
    this.onComplete  = typeof options.onComplete == 'function' && options.onComplete
    this.exercises   = require(options.menuJson).filter(function (e) {
      return !/^\/\//.test(e)
    })
  
  
    try {
      this.lang = i18n.chooseLang(
          this.globalDataDir
        , this.dataDir
        , argv.l || argv.lang
        , this.defaultLang
        , options.languages
      )
    } catch (e) {
      if (e instanceof TypeError)  // In case the language couldn't be selected
        console.log(e.message)
      else
        console.error(e.stack)
      process.exit(1)
    }
  
    this.i18n      = i18n.init(options, this.exercises, this.lang, this.globalDataDir)
    this.__        = this.i18n.__
    this.__n       = this.i18n.__n
    this.languages = this.i18n.languages
  
    // backwards compatibility for title and subtitle
    this.__defineGetter__('title', this.__.bind(this, 'title'))
    this.__defineGetter__('subtitle', this.__.bind(this, 'subtitle'))
  
    if (argv.v || argv.version || mode == 'version')
      return console.log(
          this.appName
        + '@'
        + require(path.join(this.appDir, 'package.json')).version
      )
  
    if (argv.h || argv.help || mode == 'help')
      return this._printHelp()
  
    this.current = this.getData('current')
  
    if (options.menuItems && !options.commands)
      options.commands = options.menuItems
  
    if (Array.isArray(options.commands)) {
      options.commands.forEach(function (item) {
        if (mode == item.name
            || argv[item.name]
            || (item.short && argv[item.short])) {
          handled = true
          return item.handler(this)
        }
      }.bind(this))
  
      if (handled)
        return
  
      this.commands = options.commands
    }
  
    if (mode == 'list') {
      return this.exercises.forEach(function (name) {
        console.log(this.__('exercise.' + name))
      }.bind(this))
    }
  
    if (mode == 'current')
      return console.log(this.__('exercise.' + this.current))
  
    if (mode == 'select' || mode == 'print') {
      var selected = argv._.length > 1 ? argv._.slice(1).join(' ') : this.current
      if (/[0-9]+/.test(selected)) {
        selected = this.exercises[parseInt(selected-1, 10)] || selected
      } else {
        selected = this.exercises.filter(function (exercise) {
          return selected === this.__('exercise.' + exercise)
        }.bind(this))[0] || selected;
      }
      onselect.call(this, selected)
      return
    }
  
    if (mode == 'verify' || mode == 'run') {
      exercise = this.current && this.loadExercise(this.current)
  
      if (!this.current)
        return error(this.__('error.exercise.none_active'))
  
      if (!exercise)
        return error(this.__('error.exercise.missing', {name: name}))
  
      if (exercise.requireSubmission !== false && argv._.length == 1)
        return error(this.__('ui.usage', {appName: this.appName, mode: mode}))
  
      return this.execute(exercise, mode, argv._.slice(1))
    }
  
    if (argv._[0] == 'next') {
      var remainingAfterCurrent = this.exercises.slice(this.exercises.indexOf(this.current))
  
      var completed = this.getData('completed')
  
      if (!completed)
        return error(this.__('error.exercise.none_active') + '\n')
  
      var incompleteAfterCurrent = remainingAfterCurrent.filter(function (elem) {
        return completed.indexOf(elem) < 0
      })
  
      if (incompleteAfterCurrent.length === 0)
        return console.log(this.__('error.no_uncomplete_left') + '\n')
  
      return onselect.call(this, incompleteAfterCurrent[0])
    }
  
    if (mode == 'reset') {
      this.reset()
      return console.log(this.__('progress.reset', {title: this.__('title')}))
    }
  
    this.printMenu()
  }
  
  inherits(Workshopper, EventEmitter)
  
  
  Workshopper.prototype.end = function (mode, pass, exercise, callback) {
    exercise.end(mode, pass, function (err) {
      if (err)
        return error(this.__('error.cleanup', {err: err.message || err}))
  
      setImmediate(callback || function () {
        process.exit(pass ? 0 : -1)
      })
    }.bind(this))
  }
  
  
  // overall exercise fail
  Workshopper.prototype.exerciseFail = function (mode, exercise) {
    console.log('\n' + chalk.bold.red('# ' + this.__('solution.fail.title')) + '\n')
    console.log(this.__('solution.fail.message', {name: this.__('exercise.' + exercise.name)}))
  
    this.end(mode, false, exercise)
  }
  
  
  // overall exercise pass
  Workshopper.prototype.exercisePass = function (mode, exercise) {
    console.log('\n' + chalk.bold.green('# ' + this.__('solution.pass.title')) + '\n')
    console.log(chalk.bold(this.__('solution.pass.message', {name: this.__('exercise.' + exercise.name)})) + '\n')
  
    var done = function done () {
      var completed = this.getData('completed') || []
        , remaining
  
      this.updateData('completed', function (xs) {
        if (!xs)
          xs = []
  
        return xs.indexOf(exercise.name) >= 0 ? xs : xs.concat(exercise.name)
      })
  
      completed = this.getData('completed') || []
  
      remaining = this.exercises.length - completed.length
  
      if (remaining === 0) {
        if (this.onComplete)
          return this.onComplete(this.end.bind(this, mode, true, exercise))
        else
          console.log(this.__('progress.finished'))
      } else {
        console.log(this.__n('progress.remaining', remaining))
        console.log(this.__('ui.return', {appName: this.appName}))
      }
  
      this.end(mode, true, exercise)
    }.bind(this)
  
    if (exercise.hideSolutions)
      return done()
  
    exercise.getSolutionFiles(function (err, files) {
      if (err)
        return error(this.__('solution.notes.load_error', {err: err.message || err}))
      if (!files.length)
        return done()
  
      console.log(this.__('solution.notes.compare'))
  
      function processSolutionFile (file, callback) {
        fs.readFile(file, 'utf8', function (err, content) {
          if (err)
            return callback(err)
  
          var filename = path.basename(file)
  
          // code fencing is necessary for msee to render the solution as code
          content = msee.parse('```js\n' + content + '\n```')
          callback(null, { name: filename, content: content })
        })
      }
  
      function printSolutions (err, solutions) {
        if (err)
          return error(this.__('solution.notes.load_error', {err: err.message || err}))
  
        solutions.forEach(function (file, i) {
          console.log(chalk.yellow(util.repeat('\u2500', 80)))
  
          if (solutions.length > 1)
            console.log(chalk.bold.yellow(file.name + ':') + '\n')
  
          console.log(file.content.replace(/^\n/m, '').replace(/\n$/m, ''))
  
          if (i == solutions.length - 1)
            console.log(chalk.yellow(util.repeat('\u2500', 80)) + '\n')
        }.bind(this))
  
        done()
      }
  
      map(files, processSolutionFile, printSolutions.bind(this))
    }.bind(this))
  }
  
  
  // single 'pass' event for a validation
  function onpass (msg) {
    console.log(chalk.green.bold('\u2713 ') + msg)
  }
  
  
  // single 'fail' event for validation
  function onfail (msg) {
    console.log(chalk.red.bold('\u2717 ') + msg)
  }
  
  
  Workshopper.prototype.execute = function (exercise, mode, args) {
    // individual validation events
    exercise.on('pass', onpass)
    exercise.on('fail', onfail)
    exercise.on('pass', this.emit.bind(this, 'pass', exercise, mode))
    exercise.on('fail', this.emit.bind(this, 'fail', exercise, mode))
  
  
    function done (err, pass) {
      var errback
  
      if (err) {
        // if there was an error then we need to do this after cleanup
        errback = function () {
          error(this.__('error.exercise.unexpected_error', {mode: mode, err: (err.message || err) }))
        }.bind(this)
      }
  
      if (mode == 'run' || err)
        return this.end(mode, true, exercise, errback) // clean up
  
      if (!pass)
        return this.exerciseFail(mode, exercise)
  
      this.exercisePass(mode, exercise)
    }
  
    exercise[mode](args, done.bind(this))
  }
  
  Workshopper.prototype.selectLanguage = function (lang) {
    this.i18n.change(this.globalDataDir, this.dataDir, lang, this.defaultLang, this.i18n.languages)
    this.lang = lang
    this.printMenu()
  }
  
  Workshopper.prototype.printLanguageMenu = function () {
    var menu = showLanguageMenu({
        name      : this.appName
      , languages : this.i18n.languages
      , lang      : this.lang
      , width     : this.width
      , menu      : this.menuOptions
    }, this.i18n)
  
    menu.on('select', this.selectLanguage.bind(this))
    menu.on('cancel', this.printMenu.bind(this))
    menu.on('exit', this._exit.bind(this))
  }
  
  Workshopper.prototype._exit = function () {
    process.exit(0)
  }
  
  Workshopper.prototype.printMenu = function () {
    var menu = showMenu({
        name          : this.appName
      , languages     : this.i18n.languages
      , width         : this.width
      , completed     : this.getData('completed') || []
      , exercises     : this.exercises
      , extras        : this.commands && this.commands
                          .filter(function (item) {
                            return item.menu !== false
                          })
                          .map(function (item) {
                            return item.name.toLowerCase()
                          })
      , menu          : this.menuOptions
    }, this.i18n)
  
    menu.on('select', onselect.bind(this))
    menu.on('exit', this._exit.bind(this))
    menu.on('language', this.printLanguageMenu.bind(this))
    menu.on('help', this._printHelp.bind(this))
  
    if (this.commands) {
      this.commands.forEach(function (item) {
        menu.on('extra-' + item.name, function () {
          item.handler(this)
        }.bind(this))
      }.bind(this))
    }
  }
  
  
  Workshopper.prototype.getData = function (name) {
    var file = path.resolve(this.dataDir, name + '.json')
    try {
      return JSON.parse(fs.readFileSync(file, 'utf8'))
    } catch (e) {}
    return null
  }
  
  
  Workshopper.prototype.updateData = function (id, fn) {
    var json = {}
      , file
  
    try {
      json = this.getData(id)
    } catch (e) {}
  
    file = path.resolve(this.dataDir, id + '.json')
    fs.writeFileSync(file, JSON.stringify(fn(json)))
  }
  
  
  Workshopper.prototype.reset = function () {
    fs.unlink(path.resolve(this.dataDir, 'completed.json'), function () {})
    fs.unlink(path.resolve(this.dataDir, 'current.json'), function () {})
  }
  
  
  Workshopper.prototype.dirFromName = function (name) {
    return util.dirFromName(this.exerciseDir, name)
  }
  
  
  Workshopper.prototype._printHelp = function () {
    this._printUsage(print.localisedFile.bind(print, this.appName, this.appDir, this.helpFile, this.lang))
  }
  
  
  Workshopper.prototype._printUsage = function (callback) {
    print.localisedFirstFile(this.appName, this.appDir, [
      path.join(__dirname, './i18n/usage/{lang}.txt'),
      path.join(__dirname, './i18n/usage/en.txt')
    ], this.lang, callback)
  }
  
  Workshopper.prototype.getExerciseMeta = function (name) {
    if (!name)
      return false
  
    name = name.toLowerCase().trim()
  
    var number
      , dir
  
    this.exercises.some(function (_name, i) {
      if (_name.toLowerCase().trim() != name)
        return false
  
      number = i + 1
      name   = _name
      return true
    })
  
    if (number === undefined)
      return null
  
    dir = this.dirFromName(name)
  
    return {
        name         : name
      , number       : number
      , dir          : dir
      , id           : util.idFromName(name)
      , exerciseFile : path.join(dir, './exercise.js')
    }
  }
  
  Workshopper.prototype.loadExercise = function (name) {
    var meta = this.getExerciseMeta(name)
      , stat
      , exercise
  
    if (!meta)
      return null
  
    try {
      stat = fs.statSync(meta.exerciseFile)
    } catch (err) {
      return error(this.__('error.exercise.missing_file', {exerciseFile: meta.exerciseFile}))
    }
  
    if (!stat || !stat.isFile())
      return error(this.__('error.exercise.missing_file', {exerciseFile: meta.exerciseFile}))
  
    exercise = require(meta.exerciseFile)
  
    if (!exercise || typeof exercise.init != 'function')
      return error(this.__('error.exercise.not_a_workshopper', {exerciseFile: meta.exerciseFile}))
  
    exercise.init(this, meta.id, meta.name, meta.dir, meta.number)
  
    return exercise
  }
  
  function error () {
    var pr = chalk.bold.red
    console.log(pr.apply(pr, arguments))
    process.exit(-1)
  }
  
  function onselect (name) {
    var exercise = this.loadExercise(name)
  
    if (!exercise)
      return error(this.__('error.exercise.missing', {name: name}))
  
    console.log(
        '\n ' + chalk.green.bold(this.__('title'))
      + '\n' + chalk.green.bold(util.repeat('\u2500', chalk.stripColor(this.__('title')).length + 2))
      + '\n ' + chalk.yellow.bold(this.__('exercise.' + exercise.name))
      + '\n ' + chalk.yellow.italic(this.__('progress.state', {count: exercise.number, amount: this.exercises.length}))
      + '\n'
    )
  
    this.current = exercise.name
  
    this.updateData('current', function () {
      return exercise.name
    })
  
    exercise.prepare(function (err) {
      if (err)
        return error(this.__('error.exercise.preparing', {err: err.message || err}))
  
      exercise.getExerciseText(function (err, type, exerciseText) {
        if (err)
          return error(this.__('error.exercise.loading', {err: err.message || err}))
  
        print.text(this.appName, this.appDir, type, exerciseText)
  
        print.localisedFirstFile(this.appName, this.appDir, this.footerFile, this.lang)
  
      }.bind(this))
    }.bind(this))
  }
  
  
  Workshopper.prototype.error = error
  Workshopper.prototype.print = print
  
  
  module.exports = Workshopper
  