/* global Bare */
const { Readable, Writable } = require('bare-stream')
const Signal = require('bare-signals')
const binding = require('./binding')
const constants = require('./lib/constants')

const defaultReadBufferSize = 65536

exports.ReadStream = class TTYReadStream extends Readable {
  constructor(fd, opts = {}) {
    super()

    const { readBufferSize = defaultReadBufferSize, allowHalfOpen = true } =
      opts

    this._state = 0

    this._allowHalfOpen = allowHalfOpen

    this._pendingDestroy = null

    this._buffer = Buffer.alloc(readBufferSize)

    this._handle = binding.init(
      fd,
      this._buffer,
      this,
      noop,
      this._onread,
      this._onclose
    )
  }

  get isTTY() {
    return true
  }

  setMode(mode) {
    binding.setMode(this._handle, mode)
    return this
  }

  setRawMode(mode) {
    return this.setMode(mode ? constants.mode.RAW : constants.mode.NORMAL)
  }

  _read() {
    if ((this._state & constants.state.READING) === 0) {
      this._state |= constants.state.READING
      binding.resume(this._handle)
    }
  }

  _predestroy() {
    if (this._state & constants.state.CLOSING) return
    this._state |= constants.state.CLOSING
    binding.close(this._handle)
  }

  _destroy(err, cb) {
    if (this._state & constants.state.CLOSING) return cb(err)
    this._state |= constants.state.CLOSING
    this._pendingDestroy = cb
    binding.close(this._handle)
  }

  _continueDestroy() {
    if (this._pendingDestroy === null) return
    const cb = this._pendingDestroy
    this._pendingDestroy = null
    cb(null)
  }

  _onread(err, read) {
    if (err) {
      this.destroy(err)
      return
    }

    if (read === 0) {
      this.push(null)
      if (this._allowHalfOpen === false) this.end()
      return
    }

    const copy = Buffer.allocUnsafe(read)
    copy.set(this._buffer.subarray(0, read))

    if (this.push(copy) === false && this.destroying === false) {
      this._state &= ~constants.state.READING
      binding.pause(this._handle)
    }
  }

  _onclose() {
    this._handle = null
    this._continueDestroy()
  }
}

exports.WriteStream = class TTYWriteStream extends Writable {
  constructor(fd, opts = {}) {
    super()

    this._state = 0
    this._size = null

    this._pendingWrite = null
    this._pendingDestroy = null

    this._handle = binding.init(
      fd,
      empty,
      this,
      this._onwrite,
      noop,
      this._onclose
    )

    this._size = this.getWindowSize()

    if (TTYWriteStream._streams.size === 0) TTYWriteStream._resize.start()
    TTYWriteStream._streams.add(this)
  }

  get isTTY() {
    return true
  }

  get columns() {
    return this._size[0]
  }

  get rows() {
    return this._size[1]
  }

  getWindowSize() {
    return binding.getWindowSize(this._handle)
  }

  _writev(batch, cb) {
    this._pendingWrite = [cb, batch]
    binding.writev(
      this._handle,
      batch.map(({ chunk }) => chunk)
    )
  }

  _predestroy() {
    if (this._state & constants.state.CLOSING) return
    this._state |= constants.state.CLOSING
    binding.close(this._handle)
    TTYWriteStream._streams.delete(this)
    if (TTYWriteStream._streams.size === 0) TTYWriteStream._resize.stop()
  }

  _destroy(err, cb) {
    if (this._state & constants.state.CLOSING) return cb(err)
    this._state |= constants.state.CLOSING
    this._pendingDestroy = cb
    binding.close(this._handle)
    TTYWriteStream._streams.delete(this)
    if (TTYWriteStream._streams.size === 0) TTYWriteStream._resize.stop()
  }

  _continueWrite(err) {
    if (this._pendingWrite === null) return
    const cb = this._pendingWrite[0]
    this._pendingWrite = null
    cb(err)
  }

  _continueDestroy() {
    if (this._pendingDestroy === null) return
    const cb = this._pendingDestroy
    this._pendingDestroy = null
    cb(null)
  }

  _onwrite(err) {
    this._continueWrite(err)
  }

  _onclose() {
    this._handle = null
    this._continueDestroy()
  }

  _onresize() {
    this._size = this.getWindowSize()
    this.emit('resize')
  }

  static _streams = new Set()

  static _resize = new Signal('SIGWINCH')
}

exports.constants = constants

exports.isTTY = binding.isTTY

exports.isatty = exports.isTTY // For Node.js compatibility

exports.WriteStream._resize
  .on('signal', () => {
    for (const stream of exports.WriteStream._streams) {
      stream._onresize()
    }
  })
  .unref()

const empty = Buffer.alloc(0)

function noop() {}