text: 'use strict'
  const Buffer = require('./buffer.js')
  const MiniPass = require('minipass')
  const Pax = require('./pax.js')
  const Header = require('./header.js')
  const ReadEntry = require('./read-entry.js')
  const fs = require('fs')
  const path = require('path')
  
  const types = require('./types.js')
  const maxReadSize = 16 * 1024 * 1024
  const PROCESS = Symbol('process')
  const FILE = Symbol('file')
  const DIRECTORY = Symbol('directory')
  const SYMLINK = Symbol('symlink')
  const HARDLINK = Symbol('hardlink')
  const HEADER = Symbol('header')
  const READ = Symbol('read')
  const LSTAT = Symbol('lstat')
  const ONLSTAT = Symbol('onlstat')
  const ONREAD = Symbol('onread')
  const ONREADLINK = Symbol('onreadlink')
  const OPENFILE = Symbol('openfile')
  const ONOPENFILE = Symbol('onopenfile')
  const CLOSE = Symbol('close')
  const MODE = Symbol('mode')
  const warner = require('./warn-mixin.js')
  const winchars = require('./winchars.js')
  
  const modeFix = require('./mode-fix.js')
  
  const WriteEntry = warner(class WriteEntry extends MiniPass {
    constructor (p, opt) {
      opt = opt || {}
      super(opt)
      if (typeof p !== 'string')
        throw new TypeError('path is required')
      this.path = p
      // suppress atime, ctime, uid, gid, uname, gname
      this.portable = !!opt.portable
      // until node has builtin pwnam functions, this'll have to do
      this.myuid = process.getuid && process.getuid()
      this.myuser = process.env.USER || ''
      this.maxReadSize = opt.maxReadSize || maxReadSize
      this.linkCache = opt.linkCache || new Map()
      this.statCache = opt.statCache || new Map()
      this.preservePaths = !!opt.preservePaths
      this.cwd = opt.cwd || process.cwd()
      this.strict = !!opt.strict
      this.noPax = !!opt.noPax
      this.noMtime = !!opt.noMtime
      this.mtime = opt.mtime || null
  
      if (typeof opt.onwarn === 'function')
        this.on('warn', opt.onwarn)
  
      if (!this.preservePaths && path.win32.isAbsolute(p)) {
        // absolutes on posix are also absolutes on win32
        // so we only need to test this one to get both
        const parsed = path.win32.parse(p)
        this.warn('stripping ' + parsed.root + ' from absolute path', p)
        this.path = p.substr(parsed.root.length)
      }
  
      this.win32 = !!opt.win32 || process.platform === 'win32'
      if (this.win32) {
        this.path = winchars.decode(this.path.replace(/\\/g, '/'))
        p = p.replace(/\\/g, '/')
      }
  
      this.absolute = opt.absolute || path.resolve(this.cwd, p)
  
      if (this.path === '')
        this.path = './'
  
      if (this.statCache.has(this.absolute))
        this[ONLSTAT](this.statCache.get(this.absolute))
      else
        this[LSTAT]()
    }
  
    [LSTAT] () {
      fs.lstat(this.absolute, (er, stat) => {
        if (er)
          return this.emit('error', er)
        this[ONLSTAT](stat)
      })
    }
  
    [ONLSTAT] (stat) {
      this.statCache.set(this.absolute, stat)
      this.stat = stat
      if (!stat.isFile())
        stat.size = 0
      this.type = getType(stat)
      this.emit('stat', stat)
      this[PROCESS]()
    }
  
    [PROCESS] () {
      switch (this.type) {
        case 'File': return this[FILE]()
        case 'Directory': return this[DIRECTORY]()
        case 'SymbolicLink': return this[SYMLINK]()
        // unsupported types are ignored.
        default: return this.end()
      }
    }
  
    [MODE] (mode) {
      return modeFix(mode, this.type === 'Directory')
    }
  
    [HEADER] () {
      if (this.type === 'Directory' && this.portable)
        this.noMtime = true
  
      this.header = new Header({
        path: this.path,
        linkpath: this.linkpath,
        // only the permissions and setuid/setgid/sticky bitflags
        // not the higher-order bits that specify file type
        mode: this[MODE](this.stat.mode),
        uid: this.portable ? null : this.stat.uid,
        gid: this.portable ? null : this.stat.gid,
        size: this.stat.size,
        mtime: this.noMtime ? null : this.mtime || this.stat.mtime,
        type: this.type,
        uname: this.portable ? null :
          this.stat.uid === this.myuid ? this.myuser : '',
        atime: this.portable ? null : this.stat.atime,
        ctime: this.portable ? null : this.stat.ctime
      })
  
      if (this.header.encode() && !this.noPax)
        this.write(new Pax({
          atime: this.portable ? null : this.header.atime,
          ctime: this.portable ? null : this.header.ctime,
          gid: this.portable ? null : this.header.gid,
          mtime: this.noMtime ? null : this.mtime || this.header.mtime,
          path: this.path,
          linkpath: this.linkpath,
          size: this.header.size,
          uid: this.portable ? null : this.header.uid,
          uname: this.portable ? null : this.header.uname,
          dev: this.portable ? null : this.stat.dev,
          ino: this.portable ? null : this.stat.ino,
          nlink: this.portable ? null : this.stat.nlink
        }).encode())
      this.write(this.header.block)
    }
  
    [DIRECTORY] () {
      if (this.path.substr(-1) !== '/')
        this.path += '/'
      this.stat.size = 0
      this[HEADER]()
      this.end()
    }
  
    [SYMLINK] () {
      fs.readlink(this.absolute, (er, linkpath) => {
        if (er)
          return this.emit('error', er)
        this[ONREADLINK](linkpath)
      })
    }
  
    [ONREADLINK] (linkpath) {
      this.linkpath = linkpath
      this[HEADER]()
      this.end()
    }
  
    [HARDLINK] (linkpath) {
      this.type = 'Link'
      this.linkpath = path.relative(this.cwd, linkpath)
      this.stat.size = 0
      this[HEADER]()
      this.end()
    }
  
    [FILE] () {
      if (this.stat.nlink > 1) {
        const linkKey = this.stat.dev + ':' + this.stat.ino
        if (this.linkCache.has(linkKey)) {
          const linkpath = this.linkCache.get(linkKey)
          if (linkpath.indexOf(this.cwd) === 0)
            return this[HARDLINK](linkpath)
        }
        this.linkCache.set(linkKey, this.absolute)
      }
  
      this[HEADER]()
      if (this.stat.size === 0)
        return this.end()
  
      this[OPENFILE]()
    }
  
    [OPENFILE] () {
      fs.open(this.absolute, 'r', (er, fd) => {
        if (er)
          return this.emit('error', er)
        this[ONOPENFILE](fd)
      })
    }
  
    [ONOPENFILE] (fd) {
      const blockLen = 512 * Math.ceil(this.stat.size / 512)
      const bufLen = Math.min(blockLen, this.maxReadSize)
      const buf = Buffer.allocUnsafe(bufLen)
      this[READ](fd, buf, 0, buf.length, 0, this.stat.size, blockLen)
    }
  
    [READ] (fd, buf, offset, length, pos, remain, blockRemain) {
      fs.read(fd, buf, offset, length, pos, (er, bytesRead) => {
        if (er)
          return this[CLOSE](fd, _ => this.emit('error', er))
        this[ONREAD](fd, buf, offset, length, pos, remain, blockRemain, bytesRead)
      })
    }
  
    [CLOSE] (fd, cb) {
      fs.close(fd, cb)
    }
  
    [ONREAD] (fd, buf, offset, length, pos, remain, blockRemain, bytesRead) {
      if (bytesRead <= 0 && remain > 0) {
        const er = new Error('unexpected EOF')
        er.path = this.absolute
        er.syscall = 'read'
        er.code = 'EOF'
        this.emit('error', er)
      }
  
      // null out the rest of the buffer, if we could fit the block padding
      if (bytesRead === remain) {
        for (let i = bytesRead; i < length && bytesRead < blockRemain; i++) {
          buf[i + offset] = 0
          bytesRead ++
          remain ++
        }
      }
  
      const writeBuf = offset === 0 && bytesRead === buf.length ?
        buf : buf.slice(offset, offset + bytesRead)
      remain -= bytesRead
      blockRemain -= bytesRead
      pos += bytesRead
      offset += bytesRead
  
      this.write(writeBuf)
  
      if (!remain) {
        if (blockRemain)
          this.write(Buffer.alloc(blockRemain))
        this.end()
        this[CLOSE](fd, _ => _)
        return
      }
  
      if (offset >= length) {
        buf = Buffer.allocUnsafe(length)
        offset = 0
      }
      length = buf.length - offset
      this[READ](fd, buf, offset, length, pos, remain, blockRemain)
    }
  })
  
  class WriteEntrySync extends WriteEntry {
    constructor (path, opt) {
      super(path, opt)
    }
  
    [LSTAT] () {
      this[ONLSTAT](fs.lstatSync(this.absolute))
    }
  
    [SYMLINK] () {
      this[ONREADLINK](fs.readlinkSync(this.absolute))
    }
  
    [OPENFILE] () {
      this[ONOPENFILE](fs.openSync(this.absolute, 'r'))
    }
  
    [READ] (fd, buf, offset, length, pos, remain, blockRemain) {
      let threw = true
      try {
        const bytesRead = fs.readSync(fd, buf, offset, length, pos)
        this[ONREAD](fd, buf, offset, length, pos, remain, blockRemain, bytesRead)
        threw = false
      } finally {
        if (threw)
          try { this[CLOSE](fd) } catch (er) {}
      }
    }
  
    [CLOSE] (fd) {
      fs.closeSync(fd)
    }
  }
  
  const WriteEntryTar = warner(class WriteEntryTar extends MiniPass {
    constructor (readEntry, opt) {
      opt = opt || {}
      super(opt)
      this.preservePaths = !!opt.preservePaths
      this.portable = !!opt.portable
      this.strict = !!opt.strict
      this.noPax = !!opt.noPax
      this.noMtime = !!opt.noMtime
  
      this.readEntry = readEntry
      this.type = readEntry.type
      if (this.type === 'Directory' && this.portable)
        this.noMtime = true
  
      this.path = readEntry.path
      this.mode = this[MODE](readEntry.mode)
      this.uid = this.portable ? null : readEntry.uid
      this.gid = this.portable ? null : readEntry.gid
      this.uname = this.portable ? null : readEntry.uname
      this.gname = this.portable ? null : readEntry.gname
      this.size = readEntry.size
      this.mtime = this.noMtime ? null : opt.mtime || readEntry.mtime
      this.atime = this.portable ? null : readEntry.atime
      this.ctime = this.portable ? null : readEntry.ctime
      this.linkpath = readEntry.linkpath
  
      if (typeof opt.onwarn === 'function')
        this.on('warn', opt.onwarn)
  
      if (path.isAbsolute(this.path) && !this.preservePaths) {
        const parsed = path.parse(this.path)
        this.warn(
          'stripping ' + parsed.root + ' from absolute path',
          this.path
        )
        this.path = this.path.substr(parsed.root.length)
      }
  
      this.remain = readEntry.size
      this.blockRemain = readEntry.startBlockSize
  
      this.header = new Header({
        path: this.path,
        linkpath: this.linkpath,
        // only the permissions and setuid/setgid/sticky bitflags
        // not the higher-order bits that specify file type
        mode: this.mode,
        uid: this.portable ? null : this.uid,
        gid: this.portable ? null : this.gid,
        size: this.size,
        mtime: this.noMtime ? null : this.mtime,
        type: this.type,
        uname: this.portable ? null : this.uname,
        atime: this.portable ? null : this.atime,
        ctime: this.portable ? null : this.ctime
      })
  
      if (this.header.encode() && !this.noPax)
        super.write(new Pax({
          atime: this.portable ? null : this.atime,
          ctime: this.portable ? null : this.ctime,
          gid: this.portable ? null : this.gid,
          mtime: this.noMtime ? null : this.mtime,
          path: this.path,
          linkpath: this.linkpath,
          size: this.size,
          uid: this.portable ? null : this.uid,
          uname: this.portable ? null : this.uname,
          dev: this.portable ? null : this.readEntry.dev,
          ino: this.portable ? null : this.readEntry.ino,
          nlink: this.portable ? null : this.readEntry.nlink
        }).encode())
  
      super.write(this.header.block)
      readEntry.pipe(this)
    }
  
    [MODE] (mode) {
      return modeFix(mode, this.type === 'Directory')
    }
  
    write (data) {
      const writeLen = data.length
      if (writeLen > this.blockRemain)
        throw new Error('writing more to entry than is appropriate')
      this.blockRemain -= writeLen
      return super.write(data)
    }
  
    end () {
      if (this.blockRemain)
        this.write(Buffer.alloc(this.blockRemain))
      return super.end()
    }
  })
  
  WriteEntry.Sync = WriteEntrySync
  WriteEntry.Tar = WriteEntryTar
  
  const getType = stat =>
    stat.isFile() ? 'File'
    : stat.isDirectory() ? 'Directory'
    : stat.isSymbolicLink() ? 'SymbolicLink'
    : 'Unsupported'
  
  module.exports = WriteEntry
  