index.js

const log = require('fliplog')
const Boss = require('likeaboss')
const ChainedMap = require('flipchain/ChainedMapExtendable')
const pkg = require('../package.json')

// @TODO:
// add `read-pkg-up` compatible with monorepo symlinks,
// because cannot store configstore values the same for every repo globally

// https://www.youtube.com/watch?v=SwSle66O5sU
const OFF = `${~315 >>> 3}@@`

let dynamics = {}

/**
 * @param  {Function[]} funcs functions to flow left to right
 * @return {Function} passes args through the functions, bound to this
 */
function flow(...funcs) {
  const length = funcs ? funcs.length : 0
  return function flowing(...args) {
    let index = 0
    // log
    //   .data({length, index, f: funcs[index], args, funcs})
    //   .blue('flow')
    //   .echo()
    // eslint-disable-next-line
    let result = length ? funcs[index].apply(this, args) : args[0]
    while (++index < length) {
      // eslint-disable-next-line
      result = funcs[index].call(this, result)
    }
    return result
  }
}

/**
 * @NOTE: only issue with this, is if the keys are the same...
 * @TODO:
 *  - [ ] could auto define a `get` on every set, and a `set`...
 *  - [ ] make a lightweight, and dynamic version - with plugins
 *
 * changed to obj chain because it doesn't need to be json
 * then we add middleware for observable, ~[immutable], json, file
 */
class ObjChain extends ChainedMap {

  /**
   * @param  {string | Object} data json data
   * @param  {Array<string>} [plugins=[]]
   * @return {ObjChain}
   */
  static init(data, plugins) {
    return new ObjChain(data, plugins)
  }

  /**
   * @param  {string | Object} data
   * @param  {Array<string>} [plugins=[]]
   */
  constructor(data = {}, plugins = []) {
    super()

    // single arg just plugins
    if (Array.isArray(data) === true) {
      if (data[0] && data[0].includes('Plugin') === true) {
        plugins = data
        data = {}
      }
    }

    this.pkg = pkg
    this.data = data || {}

    this.current = null

    // @TODO: each of these, + pre & post
    // @TODO: should use `sortByProp(['index'])`
    this.middleware = {
      setIfNotEmpty: [],
      set: [],
      get: [],
      has: [],
      del: [],
      // val: [],
      override: [],
      init: [],
      merge: [],
      save: [],
    }

    // const aliased = {
    //   write: 'save',
    // }

    this.use = this.use.bind(this)
    this.extend = this.extend.bind(this)

    // plugins.push('MapPlugin')
    this.store.set('debug', false)

    plugins.forEach(plugin => {
      log.magenta('using plugin').data(plugin).echo(this.store.get('debug'))
      if (typeof plugin !== 'string') {
        log
          .magenta('using plugin - non-string')
          .data(plugin)
          .echo(this.store.get('debug'))
        this.use(plugin)
      }
      else if (dynamics.exports[plugin]) {
        log
          .magenta('using plugin - dynamic')
          .data(dynamics.exports[plugin])
          .echo(this.store.get('debug'))
        this.use(dynamics.exports[plugin])
      }
      else {
        // log.quick({plugin, dynamics})
      }
    })

    this.update = this.update.bind(this)
    this.setIfNotEmpty = this.setIfNotEmpty.bind(this)
    this.has = this.has.bind(this)
    this.write = this.save.bind(this)
    this.set = this.update.bind(this)
    this.get = this.val.bind(this)
    this.delete = this.del.bind(this)
    this.remove = this.del.bind(this)
  }

  /**
   * @alias set
   * @param  {string | any} key
   * @param  {Serializable | any} val
   * @return {ObjChain} @chainable
   */
  update(key, val) {
    this.middleware.set.forEach(fn => fn(key, val))
    return this
  }

  /**
   * @param  {string} key key with `.`
   * @return {string} string with escaped `\\.`
   */
  escape(key) {
    return (key = key.replace('.', '\\.'))
  }

  /**
   * @description sets !.has(key)
   * @see ObjChain.has
   * @param  {string | any} key
   * @param  {Serializable | any} val
   * @return {ObjChain} @chainable
   */
  setIfNotEmpty(key, val) {
    if (this.has(key)) return this
    this.set(key, val)
    return this
  }

  /**
   * @param  {string | any} key
   * @return {Boolean}
   */
  has(key) {
    return this.middleware.has.map(fn => fn(key)).includes(true)
  }

  /**
   * @alias delete
   * @alias remove
   * @description delete. remove
   * @param  {string | any} key
   * @return {ObjChain}
   */
  del(key) {
    this.middleware.del.forEach(fn => fn(key))
    return this
  }

  /**
   * @description get a value
   * @param {string | any | null} [key=null]
   * @return {any} current key or named if there is key param
   */
  val(key = null) {
    // log.quick(this.middleware.val)
    // .bind(this)
    // return flow.apply(this, this.middleware.val)(key)
    // return flow.apply(this, this.middleware.val)(key)

    let val = key
    this.middleware.get.forEach(fn => {
      val = fn(key, val)
    })

    // log.quick(val, key)
    return val

    // const args = [key]
    // const funcs = this.middleware.val
    // const length = funcs ? funcs.length : 0
    //
    // let index = 0
    // let result = length ? funcs[index].apply(this, args) : args[0]
    // while (++index < length) {
    //   result = funcs[index].call(this, result)
    // }
    // log.quick(this)
  }

  /**
   * @alias write
   * @description save/write data
   * @return {ObjChain}
   */
  save() {
    this.middleware.save.forEach(fn => fn(this))
    return this
  }

  // --- plugins / middleware ---

  /**
   * @description use middleware
   * @param  {Object} obj
   * @return {ObjChain}
   */
  use(obj) {
    if (typeof obj === 'function' && Object.keys(obj).length === 0) {
      log.blue('use.fn').echo(this.store.get('debug'))
      obj(this) // obj =
      return this
    }

    if (obj.init) {
      obj.init(this)
      // delete obj.init
    }

    const keys = Object.keys(obj)
    log.red('use.keys').data({keys}).echo(this.store.get('debug'))
    for (let k = 0; k < keys.length; k++) {
      const key = keys[k]
      if (key === 'middleware') {
        log.red('key is middlware').echo(this.store.get('debug'))

        Object.keys(obj[key]).forEach(method => {
          let fn = obj[key][method]
          log
            .data({method, key, k, fn, obj})
            .green('using middleware')
            .echo(this.store.get('debug'))

          if (fn && fn.bind !== undefined) fn = fn.bind(this)
          if (this.middleware[method] !== undefined) {
            this.middleware[method].push(fn)
          }
          else {
            log.data({method}).red('middleware for method not found 0.0')
          }
        })
        // continue
      }
      else if (!obj[key] || obj[key].bind === undefined) {
        log.yellow('use.else no bind').data({key}).echo(this.store.get('debug'))
        this[key] = obj[key]
      }
      else {
        log.yellow('use.else').data({key}).echo(this.store.get('debug'))
        this[key] = obj[key].bind(this)
      }
    }

    // log.quick(this.middleware)

    return this
  }

  /**
   * @TODO:
   * - [ ] should validate here
   *  - [ ] .files('array')
   * - [ ] allow nesting
   *  - [ ] (scripts().eh())
   *
   *
   * @description take strings, make them methods to .set on
   * @param  {Array<string>} methods
   * @param  {boolean} [nest=false] nest the .set
   * @return {ObjChain}
   */
  extend(methods, nest = false) {
    if (nest === true) {
      methods.forEach(method => {
        this[method] = (keyPath, data) => {
          this.set(method + '.' + keyPath, data)
          return this
        }
      })

      return this
    }

    methods.forEach(method => {
      const cb = data => {
        this.set(method, data)
        return this
      }

      // @TODO: should make each part a function instead of an obj...
      if (method.includes('.') === true) this.set(method, cb)
      else this[method] = cb
    })

    return this
  }

  /**
   * @return {string}
   */
  toString() {
    return this.middleware.toString.forEach(fn => fn(this)).join('')
  }
}

/**
 * @param  {string | Object} data json data
 * @param  {Array<string>} [plugins=[]]
 * @return {ObjChain}
 */
// function ObjChainable(data, plugins = []) {
//   // console.log({data}, Array.isArray(data), data[0])
//   if (Array.isArray(data) === true) {
//     if (data[0] && data[0].includes('Plugin') === true) {
//       return new ObjChain({}, plugins)
//     }
//   }
//   return new ObjChain(data, plugins)
// }

exports = module.exports = Boss.module(dynamics)
  .dir(__dirname)
  .main(ObjChain)
  .props({pkg})
  .dynamics([
    {name: 'ConfigPlugin', path: './plugins/ConfigPlugin'},
    {name: 'FilePlugin', path: './plugins/FilePlugin'},
    {name: 'KebabPlugin', path: './plugins/KebabPlugin'},
    {name: 'JSONPlugin', path: './plugins/JSONPlugin'},
    {name: 'DotPropPlugin', path: './plugins/DotPropPlugin'},
    {name: 'MapPlugin', path: './plugins/MapPlugin'},
    {name: 'SnapshotPlugin', path: './plugins/SnapshotPlugin'},
    {name: 'DiffPlugin', path: './plugins/DiffPlugin'},
    // {name: 'FlowPlugin', path: './plugins/FlowPlugin'},
    // {name: 'ObservablePlugin', path: './plugins/ObservablePlugin'},
    // {name: 'DebugPlugin', path: './plugins/DebugPlugin'},
    // {name: 'GetSetPlugin', path: './plugins/GetSetPlugin'},
    // {name: 'ProxyPlugin', path: './plugins/ProxyPlugin'},
    // {name: 'ZipPlugin', path: './plugins/ZipPlugin'},
  ])
  .end()