import debugFactory from 'debug'
import EventEmitter from 'events'

const debug = debugFactory('webtorrent:file-iterator')

/**
 * Async iterator of a torrent file
 *
 * @param {File} file
 * @param {Object} opts
 * @param {number} opts.start iterator slice of file, starting from this byte (inclusive)
 * @param {number} opts.end iterator slice of file, ending with this byte (inclusive)
 */
export default class FileIterator extends EventEmitter {
  constructor (file, { start, end }) {
    super()

    this._torrent = file._torrent

    this._pieceLength = file._torrent.pieceLength

    this._startPiece = (start + file.offset) / this._pieceLength | 0
    this._endPiece = (end + file.offset) / this._pieceLength | 0

    this._piece = this._startPiece
    this._offset = (start + file.offset) - (this._startPiece * this._pieceLength)

    this._missing = end - start + 1
    this._criticalLength = Math.min((1024 * 1024 / this._pieceLength) | 0, 2)

    this._torrent._select(this._startPiece, this._endPiece, 1, null, true)
    this.destroyed = false
  }

  [Symbol.asyncIterator] () {
    return this
  }

  next () {
    return new Promise((resolve, reject) => {
      if (this._missing === 0 || this.destroyed) {
        resolve({ done: true })
        return this.destroy()
      }
      const pump = (index, opts) => {
        if (!this._torrent.bitfield.get(index)) {
          const listener = i => {
            if (i === index || this.destroyed) {
              this._torrent.removeListener('verified', listener)
              pump(index, opts)
            }
          }
          this._torrent.on('verified', listener)
          return this._torrent.critical(index, index + this._criticalLength)
        }

        if (this._torrent.destroyed) return reject(new Error('Torrent removed'))

        this._torrent.store.get(index, opts, (err, buffer) => {
          if (this.destroyed) return resolve({ done: true }) // prevent hanging
          debug('read %s and yielding (length %s) (err %s)', index, buffer?.length, err?.message)

          if (err) return reject(err)

          // prevent re-wrapping outside of promise
          resolve({ value: buffer, done: false })
        })
      }

      const length = Math.min(this._missing, this._pieceLength - this._offset)

      pump(this._piece++, { length, offset: this._offset })
      this._missing -= length
      this._offset = 0
    })
  }

  async return () {
    this.destroy()
    return { done: true }
  }

  async throw (err) {
    throw err
  }

  destroy (cb = () => {}, err) {
    if (this.destroyed) return
    this.destroyed = true
    if (!this._torrent.destroyed) {
      this._torrent._deselect(this._startPiece, this._endPiece, true)
    }
    this.emit('return')
    cb(err)
  }
}
