text: var cont     = require('cont')
  var pull     = require('pull-stream')
  var defer    = require('pull-defer')
  var path     = require('path')
  var explain  = require('explain-error')
  var mkdirp   = require('mkdirp')
  var rimraf   = require('rimraf')
  var fs       = require('fs')
  var glob     = require('pull-glob')
  var paramap  = require('pull-paramap')
  var Notify   = require('pull-notify')
  var Live     = require('pull-live')
  var Write    = require('pull-write-file')
  
  var u = require('./util')
  var createHash = u.createHash
  
  module.exports = function Blobs (config) {
    if (!config) throw Error('multiblob expects config')
  
    var dir
    if('string' === typeof config)
      dir = config, config = {dir: dir}
  
    dir = config.dir
    var alg = config.hash = config.hash || config.alg || 'blake2s'
    var encode = config.encode || u.encode
    var decode = config.decode || u.decode
    var isHash = config.isHash || u.isHash
  
    function toPath (dir, string) {
      if(!string || !isHash(string)) return false
      var d = decode(string)
      var h = d.hash.toString('hex')
      return path.join(dir, d.alg, h.substring(0,2), h.substring(2))
    }
  
    function toHash(filename) {
      var parts = path.relative(dir, filename).split(path.sep)
      var alg = parts.shift()
      return encode(new Buffer(parts.join(''), 'hex'), alg)
    }
  
    var newBlob = Notify()
  
    var empty = u.encode(u.algs[alg]().digest(), alg)
    // MIX: I think this should be encode NOT u.encode ?!
  
    function isEmptyHash(hash) {
      return empty === hash
    }
  
    var n = 0
    var waiting = []
    var tmp = false
  
    function init (cb) {
      if(tmp) return cb()
      else waiting.push(cb)
    }
  
    var stat = u.single(fs.stat)
  
    var tmpdir = path.join(dir, 'tmp')
  
    rimraf(tmpdir, function () {
      mkdirp(tmpdir, function () {
        tmp = true; while(waiting.length) waiting.shift()()
      })
    })
  
    function toMeta(hash, stat) {
      if(!stat) return null
      return {id: hash, size: stat.size, ts: +stat.ctime}
    }
  
    function has (id) {
      return function (cb) {
        if(isEmptyHash(id)) return cb(null, true)
        var p = toPath(dir, id)
        if(!p) return cb(new Error('not a valid blob id:'+id))
        stat(p, function (err, stat) {
          cb(null, !!stat)
        })
      }
    }
  
    function size (id) {
      return function (cb) {
        if(isEmptyHash(id)) return cb(null, 0)
        var p = toPath(dir, id)
        if(!p) return cb(new Error('not a valid blob id:'+id))
        stat(p, function (err, stat) {
          cb(null, stat ? stat.size : null)
        })
      }
    }
  
    var meta = function (id, cb) {
      if(isEmptyHash(id)) return cb(null, {id: id, size: 0, ts: 0})
      stat(toPath(dir, id), function (err, stat) {
        cb(err, toMeta(id, stat))
      })
    }
  
    function createTester (test) {
      return function (ids, cb) {
        var n = !Array.isArray(ids)
        //check if any hashes are invalid.
        var invalid
        if(n ? !isHash(ids) : !ids.every(function (h) {
          if(!isHash(h)) invalid = h
          else return true
        }))
          return cb(new Error('not a valid id:'+invalid))
          
        cont.para(u.toArray(ids).map(test)) (function (err, ary) {
          //will give an error if any hash was invalid.
          if(err) cb(err)
          // This will only error if the hash is not present,
          // so never callback an error.
          // PS. if you have a situation where you never error
          // add a comment like this one to explain why.
          else if(n) cb(null, ary[0])
          else       cb(null, ary)
        })
        return cb
      }
    }
  
    function getSlice(opts) {
      var id = u.getId(opts)
  
      if(isEmptyHash(id)) return pull.empty()
  
      var stream = defer.source()
      stat(toPath(dir, id), function (err, stat) {
        if(err)
          stream.abort(explain(err, 'stat failed'))
  
        else if(opts.size != null && opts.size !== stat.size)
          stream.abort(new Error('incorrect file length,'
            + ' requested:' + opts.size + ' file was:' + stat.size
            + ' for file:' + id
          ))
  
        else if(opts.max != null && opts.max < stat.size)
          stream.abort(new Error('incorrect file length,'
            + ' requested:' + opts.size + ' file was:' + stat.size
            + ' for file:' + id
          ))
  
        else
          stream.resolve(u.readFile(toPath(dir, id), {
            start: opts.start,
            end: opts.end
          }))
      })
  
      return stream
    }
  
  
    return {
      get: function (opts) {
        if(isHash(opts)) {
          if(isEmptyHash(opts)) return pull.empty()
          return u.readFile(toPath(dir, opts))
        }
        var id = u.getId(opts)
  
        if(!isHash(id))
          return pull.error(new Error(
            'multiblob.get: {id} is mandatory'
          ))
  
        return getSlice({id: id, size: opts.size, max: opts.max})
      },
      isEmptyHash: isEmptyHash,
  
      getSlice: function (opts) {
        var id = u.getId(opts)
        if(!isHash(id))
          return pull.error(new Error(
            'multiblob.getSlice: {id} is mandatory'
          ))
  
        if(isNaN(opts.start))
          return pull.error(new Error(
            'multiblob.getSlice: {start} must be a number'
          ))
  
        if(isNaN(opts.end))
          return pull.error(new Error(
            'multiblob.getSlice: {end} must be a number'
          ))
  
        return getSlice(opts)
      },
  
      size: createTester(size),
  
      has: createTester(has),
      meta: meta,
  
      add: function (id, cb) {
        if('function' === typeof id) cb = id, id = null
  
        if(!cb) cb = function (err) {
          if(err) throw explain(err, 'no callback provided')
        }
  
        if(id && !isHash(id)) {
          //abort input stream and callback once source is aborted.
          var err = new Error(`not a valid hash: ${id}`)
          return function (read) {
            read(err, cb)
          }
        }
  
        var deferred = defer.sink()
        init(function () {
          var tmpfile = path.join(dir, 'tmp', Date.now() + '-' + n++)
          var hasher = createHash(alg, true)
          var size = 0
  
          deferred.resolve(pull(
            hasher,
            pull.map(function (data) {
              if('string' === typeof data) data = new Buffer(data, 'utf8')
              size += data.length
              return data
            }),
            Write(tmpfile, function (err) {
              if(err) return cb(explain(err, 'could not write to tmpfile'))
  
              var _id = encode(hasher.digest, alg)
  
              if(id && id !== _id)
                return cb(new Error('actual hash:'+ _id
                  + ' did not match expected id:'+id), _id)
  
              var p = toPath(dir, id || _id)
  
              mkdirp(path.dirname(p), function () {
                fs.rename(tmpfile, p, function (err) {
                  if(err) cb(explain(err, 'could not move file'))
                  else    newBlob({id:toHash(p), size: size, ts: Date.now()}), cb(null, _id)
                })
              })
            })
          ))
        })
  
        return deferred
      },
  
      ls: Live(function old (opts) {
        var long = (opts.size || opts.long || opts.meta)
        return pull(
          glob(path.join(dir, '*', '*', '*')),
          long ? paramap(function (filename, cb) {
            stat(filename, function (err, stat) {
              cb(err, toMeta(toHash(filename), stat))
            })
          }, 32) : pull.map(toHash)
        )
      }, function live (opts) {
        var long = (opts.size || opts.long || opts.meta)
        return long
          ? newBlob.listen()
          : pull(newBlob.listen(), pull.map(function (e) { return e.id }))
      }),
  
      rm: function (id, cb) {
        if(!isHash(id)) cb(new Error('not valid id:'+id))
        else              fs.unlink(toPath(dir, id), cb)
      },
  
      resolve: function (id) {
        if(!isHash(id)) throw new Error('not valid id:'+id)
        return toPath(dir, id)
      }
    }
  }
  
  