const fs = require('fs')
const { promisify } = require('util')
const { resolve, relative, normalize, dirname } = require('path')
const stat = promisify(fs.stat)
const readFile = promisify(fs.readFile)
const writeFile = promisify(fs.writeFile)
const mkdir = promisify(fs.mkdir)
const fsreaddir = promisify(fs.readdir)
const chmod = promisify(fs.chmod)
const rmrf = require('rimraf')
const micromatch = require('micromatch')

/**
 * Type-checkers
 */
function isBoolean(b) {
  return typeof b === 'boolean'
}
function isNumber(n) {
  return typeof n === 'number' && !Number.isNaN(n)
}
function isObject(o) {
  return o !== null && typeof o === 'object'
}
function isString(s) {
  return typeof s === 'string'
}
function isUndefined(u) {
  return typeof u === 'undefined'
}
function isFunction(f) {
  return typeof f === 'function'
}

function match(input, patterns, options) {
  input = input || Object.keys(this._files)
  if (!(input && input.length)) return []
  options = Object.assign({ dot: true }, options || {}, {
    // required to convert forward to backslashes on Windows and match the file keys properly
    format: normalize
  })
  return micromatch(input, patterns, options).sort()
}

function writeStream(path) {
  return fs.createWriteStream(path, 'utf-8')
}
/**
 * Recursively remove a directory
 * @param {string} p
 * @returns Promise
 */
function rm(p) {
  return new Promise(function (resolve, reject) {
    /* istanbul ignore else */
    if (Object.prototype.hasOwnProperty.call(fs, 'rm')) {
      fs.rm(
        p,
        {
          recursive: true,
          force: true
        },
        (err) => {
          if (err) reject(err)
          else resolve()
        }
      )
    } else {
      // Node 14.14- compat
      rmrf(p, { glob: { dot: true } }, (err) => {
        if (err) reject(err)
        else resolve()
      })
    }
  })
}

/**
 * Recursive readdir with support for ignores
 * @private
 * @param {String} dir
 * @param {Array<String|Function>} ignores
 */
function readdir(dir, ignores) {
  if (Array.isArray(ignores)) {
    ignores = {
      str: ignores.filter(isString),
      fn: ignores.filter(isFunction)
    }
  }

  if (isString(dir)) {
    dir = {
      current: dir,
      root: dir
    }
  }
  const result = fsreaddir(dir.current)
    .then((children) => {
      const filtered = []

      children.forEach((child) => {
        const res = resolve(dir.current, child)
        const rel = relative(dir.root, res)

        if (!match(rel, ignores.str).length) {
          filtered.push(
            stat(res).then((stat) => {
              // it would be better to put this check together with the previous if,
              // but that would break backwards-compatibility with Metalsmith 2.3.0
              // as the stat object needs to be passed to ignore fns
              if (ignores.fn.some((fn) => fn(rel, stat))) {
                return null
              }
              if (stat.isDirectory()) {
                return readdir({ current: res, root: dir.root }, ignores)
              }
              return res
            })
          )
        }
      })
      return Promise.all(filtered)
    })
    .then((files) => {
      const result = files
        // @TODO: this is a catch-all & can probably be finetuned with some more tests
        .filter((file) => !(file instanceof Error) && file !== null)
        .reduce((all, file) => all.concat(file), [])
        .sort()
      return result
    })
    .catch((err) => {
      throw err
    })

  return result
}

/**
 * Run `fn` in parallel on #`concurrency` number of `items`, spread over #`items / concurrency` sequential batches
 * @private
 * @param {() => Promise} fn
 * @param {*[]} items
 * @param {number} concurrency
 * @returns {Promise<*[]>}
 */
function batchAsync(fn, items, concurrency) {
  let batches = Promise.resolve([])
  items = [...items]

  while (items.length) {
    const slice = items.splice(0, concurrency)
    batches = batches.then((previousBatch) => {
      return Promise.all(slice.map((...args) => fn(...args))).then((currentBatch) => [
        ...previousBatch,
        ...currentBatch
      ])
    })
  }

  return batches
}

/**
 * Output a file and create any non-existing directories in the process
 * @private
 **/
function outputFile(file, data, mode) {
  return mkdir(dirname(file), { recursive: true })
    .then(() => writeFile(file, data))
    .then(() => (mode ? chmod(file, mode) : Promise.resolve()))
}

const helpers = {
  isBoolean,
  isNumber,
  isString,
  isObject,
  isUndefined,
  isFunction,
  match,
  rm,
  readdir,
  outputFile,
  stat,
  readFile,
  batchAsync,
  writeStream
}

module.exports = helpers
