class_base_print.js

import ora from 'ora'
import lodash from 'lodash'
import fs from 'fs-extra'
import Sprintf from 'sprintf-js'
const { sprintf } = Sprintf
let unknownLangWarning = false

const { isString, last, isPlainObject, get, without, reverse, map } = lodash

/**
 * Print class. Use sprintf to interpolate pattern and variable. Support
 * many methods to display things on screen including {@link https://github.com/sindresorhus/ora|ora} based spinner.
 *
 * It also serve as the foundation of Bajo's I18n lightweight system.
 *
 * @class
 */
class Print {
  /**
   * Class constructor
   *
   * @param {Object} plugin - Plugin instance
   * @param {Object} [opts={}] - Options to pass to {@link https://github.com/sindresorhus/ora|ora}
   */
  constructor (plugin, opts = {}) {
    this.opts = opts
    this.plugin = plugin
    this.app = plugin.app
    this.startTime = this.plugin.app.bajo.lib.dayjs()
    this.setOpts()
    this.ora = ora(this.opts)
    this.intl = {}
  }

  /**
   * Initialize print engine and read plugin's translation files
   *
   * @method
   */
  init = () => {
    for (const l of this.plugin.app.bajo.config.intl.supported) {
      this.intl[l] = {}
      const path = `${this.plugin.dir.pkg}/extend/bajo/intl/${l}.json`
      if (!fs.existsSync(path)) continue
      const trans = fs.readFileSync(path, 'utf8')
      try {
        this.intl[l] = JSON.parse(trans)
      } catch (err) {}
    }
  }

  /**
   * Interpolate and translate text according to the chosen language
   *
   * @method
   * @param {string} text - Text pattern to translate. See {@link https://github.com/alexei/sprintf.js|sprintf} for all supported token & format
   * @param {...any} [args] - Variables to interpolate with text pattern above. If the last argument is an object, it will be use to override default translation option. Example: to force language to 'id', pass the last argument as "{ lang: 'id' }"
   * @returns {string} Interpolated & translated text
   */
  write = (text, ...args) => {
    const opts = last(args)
    let lang = this.plugin.app.bajo.config.lang
    if (isPlainObject(opts)) {
      args.pop()
      if (opts.lang) lang = opts.lang
    }
    const { fallback, supported } = this.plugin.app.bajo.config.intl
    if (!unknownLangWarning && !supported.includes(lang)) {
      unknownLangWarning = true
      this.plugin.app.bajo.log.warn('unsupportedLangFallbackTo%s', fallback)
    }
    const plugins = reverse(without([...this.app.getPluginNames()], this.plugin.name))
    plugins.unshift(this.plugin.name)
    plugins.push('bajo')

    let trans
    for (const p of plugins) {
      const root = get(this, `plugin.app.${p}.print.intl.${lang}`, {})
      trans = get(root, text)
      if (trans) break
    }
    if (!trans) {
      for (const p of plugins) {
        const root = get(this, `plugin.app.${p}.print.intl.${fallback}`, {})
        trans = get(root, text)
        if (trans) break
      }
    }
    if (!trans) trans = text
    const params = map(args, a => {
      if (!isString(a)) return a
      return a
    })
    return sprintf(trans, ...params)
  }

  /**
   * Set spinner options
   *
   * @method
   * @param {any[]} [args=[]] - Array of options. If the last argument is an object, it will be used to override ora options
   */
  setOpts = (args = []) => {
    const config = this.plugin.app.bajo.config
    let opts = {}
    if (isPlainObject(args.slice(-1)[0])) opts = args.pop()
    this.opts.isSilent = !!(config.silent || this.opts.isSilent)
    this.opts = this.plugin.lib.aneka.defaultsDeep(opts, this.opts)
  }

  /**
   * Set spinner text
   *
   * @method
   * @param {string} text - Text to use
   * @param {...any} [args] - Any variable to interpolate text. If the last argument is an object, it will be used to override ora options
   * @returns {Object} Return itself, usefull to chain methods
   */
  setText = (text, ...args) => {
    text = this.write(text, ...args)
    this.setOpts(args)
    const prefixes = []
    const texts = []
    if (this.opts.showDatetime) prefixes.push('[' + this.plugin.app.bajo.lib.dayjs().toISOString() + ']')
    if (this.opts.showCounter) texts.push('[' + this.getElapsed() + ']')
    if (prefixes.length > 0) this.ora.prefixText = this.ora.prefixText + prefixes.join(' ')
    if (texts.length > 0) text = texts.join(' ') + ' ' + text
    this.ora.text = text
    return this
  }

  /**
   * Get elapsed time since print instance is created
   *
   * @method
   * @param {string} [unit=hms] - Unit's time. Put 'hms' (default) to get hour, minute, second format or of any format supported by {@link https://day.js.org/docs/en/display/difference|dayjs}
   * @returns {string} Elapsed time since start
   */
  getElapsed = (unit = 'hms') => {
    const u = unit === 'hms' ? 'second' : unit
    const elapsed = this.plugin.lib.dayjs().diff(this.startTime, u)
    return unit === 'hms' ? this.plugin.lib.aneka.secToHms(elapsed) : elapsed
  }

  /**
   * Start the spinner
   *
   * @method
   * @param {string} text - Text to use
   * @param {...any} [args] - Any variable to interpolate text. If the last argument is an object, it will be used to override ora options
   * @returns {Object} Return itself, usefull to chain methods
   */
  start = (text, ...args) => {
    this.setOpts(args)
    this.setText(text, ...args)
    this.ora.start()
    return this
  }

  /**
   * Stop the spinner
   *
   * @method
   * @returns {Object} Return itself, usefull to chain methods
   */
  stop = () => {
    this.ora.stop()
    return this
  }

  /**
   * Print success message, prefixed with a check icon
   *
   * @method
   * @param {string} text - Text to use
   * @param {...any} [args] - Any variable to interpolate text. If the last argument is an object, it will be used to override ora options
   * @returns {Object} Return itself, usefull to chain methods
   */
  succeed = (text, ...args) => {
    this.setText(text, ...args)
    this.ora.succeed()
    return this
  }

  /**
   * Print failed message, prefixed with a cross icon
   *
   * @method
   * @param {string} text - Text to use
   * @param {...any} [args] - Any variable to interpolate text. If the last argument is an object, it will be used to override ora options
   * @returns {Object} Return itself, usefull to chain methods
   */
  fail = (text, ...args) => {
    this.setText(text, ...args)
    this.ora.fail()
    return this
  }

  /**
   * Print warning message, prefixed with a warn icon
   *
   * @method
   * @param {string} text - Text to use
   * @param {...any} [args] - Any variable to interpolate text. If the last argument is an object, it will be used to override ora options
   * @returns {Object} Return itself, usefull to chain methods
   */
  warn = (text, ...args) => {
    this.setText(text, ...args)
    this.ora.warn()
    return this
  }

  /**
   * Print failed message, prefixed with an info icon
   *
   * @method
   * @param {string} text - Text to use
   * @param {...any} [args] - Any variable to interpolate text. If the last argument is an object, it will be used to override ora options
   * @returns {Object} Return itself, usefull to chain methods
   */
  info = (text, ...args) => {
    this.setText(text, ...args)
    this.ora.info()
    return this
  }

  /**
   * Clear spinner text
   *
   * @method
   * @returns {Object} Return itself, usefull to chain methods
   */
  clear = () => {
    this.ora.clear()
    return this
  }

  /**
   * Force render spinner
   *
   * @method
   * @returns {Object} Return itself, usefull to chain methods
   */
  render = () => {
    this.ora.render()
    return this
  }

  /**
   * Print failed message, prefixed with a cross icon and terminate the app process
   *
   * @method
   * @param {string} text - Text to use
   * @param {...any} [args] - Any variable to interpolate text. If the last argument is an object, it will be used to override ora options
   */
  fatal = (text, ...args) => {
    this.setText(text, ...args)
    this.ora.fail()
    process.kill(process.pid, 'SIGINT')
  }

  /**
   * Create a new spinner
   *
   * @method
   * @returns {Object} Return new instance
   */
  spinner = () => {
    return new Print(this.plugin)
  }
}

export default Print