'use strict'
/* eslint-disable */

/**
 * Parted (https://github.com/chjj/parted)
 * A streaming multipart state parser.
 * Copyright (c) 2011, Christopher Jeffrey. (MIT Licensed)
 */

const fs = require('node:fs')
const path = require('node:path')
const EventEmitter = require('node:events').EventEmitter
const StringDecoder = require('node:string_decoder').StringDecoder
const set = require('qs').set
const each = Array.prototype.forEach

/**
 * Character Constants
 */

const DASH = '-'.charCodeAt(0)
const CR = '\r'.charCodeAt(0)
const LF = '\n'.charCodeAt(0)
const COLON = ':'.charCodeAt(0)
const SPACE = ' '.charCodeAt(0)

/**
 * Parser
 */

const Parser = function (type, options) {
  if (!(this instanceof Parser)) {
    return new Parser(type, options)
  }

  EventEmitter.call(this)

  this.writable = true
  this.readable = true

  this.options = options || {}

  const key = grab(type, 'boundary')
  if (!key) {
    return this._error('No boundary key found.')
  }

  this.key = Buffer.allocUnsafe('\r\n--' + key)

  this._key = {}
  each.call(this.key, function (ch) {
    this._key[ch] = true
  }, this)

  this.state = 'start'
  this.pending = 0
  this.written = 0
  this.writtenDisk = 0
  this.buff = Buffer.allocUnsafe(200)

  this.preamble = true
  this.epilogue = false

  this._reset()
}

Parser.prototype.__proto__ = EventEmitter.prototype

/**
 * Parsing
 */

Parser.prototype.write = function (data) {
  if (!this.writable ||
      this.epilogue) return

  try {
    this._parse(data)
  } catch (e) {
    this._error(e)
  }

  return true
}

Parser.prototype.end = function (data) {
  if (!this.writable) return

  if (data) this.write(data)

  if (!this.epilogue) {
    return this._error('Message underflow.')
  }

  return true
}

Parser.prototype._parse = function (data) {
  let i = 0
  let len = data.length
  const buff = this.buff
  const key = this.key
  let ch
  let val
  let j

  for (; i < len; i++) {
    if (this.pos >= 200) {
      return this._error('Potential buffer overflow.')
    }

    ch = data[i]

    switch (this.state) {
      case 'start':
        switch (ch) {
          case DASH:
            this.pos = 3
            this.state = 'key'
            break
          default:
            break
        }
        break
      case 'key':
        if (this.pos === key.length) {
          this.state = 'key_end'
          i--
        } else if (ch !== key[this.pos]) {
          if (this.preamble) {
            this.state = 'start'
            i--
          } else {
            this.state = 'body'
            val = this.pos - i
            if (val > 0) {
              this._write(key.slice(0, val))
            }
            i--
          }
        } else {
          this.pos++
        }
        break
      case 'key_end':
        switch (ch) {
          case CR:
            this.state = 'key_line_end'
            break
          case DASH:
            this.state = 'key_dash_end'
            break
          default:
            return this._error('Expected CR or DASH.')
        }
        break
      case 'key_line_end':
        switch (ch) {
          case LF:
            if (this.preamble) {
              this.preamble = false
            } else {
              this._finish()
            }
            this.state = 'header_name'
            this.pos = 0
            break
          default:
            return this._error('Expected CR.')
        }
        break
      case 'key_dash_end':
        switch (ch) {
          case DASH:
            this.epilogue = true
            this._finish()
            return
          default:
            return this._error('Expected DASH.')
        }
      case 'header_name':
        switch (ch) {
          case COLON:
            this.header = buff.toString('ascii', 0, this.pos)
            this.pos = 0
            this.state = 'header_val'
            break
          default:
            buff[this.pos++] = ch | 32
            break
        }
        break
      case 'header_val':
        switch (ch) {
          case CR:
            this.state = 'header_val_end'
            break
          case SPACE:
            if (this.pos === 0) {
              break
            }
            // FALL-THROUGH
          default:
            buff[this.pos++] = ch
            break
        }
        break
      case 'header_val_end':
        switch (ch) {
          case LF:
            val = buff.toString('ascii', 0, this.pos)
            this._header(this.header, val)
            this.pos = 0
            this.state = 'header_end'
            break
          default:
            return this._error('Expected LF.')
        }
        break
      case 'header_end':
        switch (ch) {
          case CR:
            this.state = 'head_end'
            break
          default:
            this.state = 'header_name'
            i--
            break
        }
        break
      case 'head_end':
        switch (ch) {
          case LF:
            this.state = 'body'
            i++
            if (i >= len) return
            data = data.slice(i)
            i = -1
            len = data.length
            break
          default:
            return this._error('Expected LF.')
        }
        break
      case 'body':
        switch (ch) {
          case CR:
            if (i > 0) {
              this._write(data.slice(0, i))
            }
            this.pos = 1
            this.state = 'key'
            data = data.slice(i)
            i = 0
            len = data.length
            break
          default:
            // boyer-moore-like algorithm
            // at felixge's suggestion
            while ((j = i + key.length - 1) < len) {
              if (this._key[data[j]]) break
              i = j
            }
            break
        }
        break
    }
  }

  if (this.state === 'body') {
    this._write(data)
  }
}

Parser.prototype._header = function (name, val) {
  /* if (name === 'content-disposition') {
    this.field = grab(val, 'name');
    this.file = grab(val, 'filename');

    if (this.file) {
      this.data = stream(this.file, this.options.path);
    } else {
      this.decode = new StringDecoder('utf8');
      this.data = '';
    }
  } */

  return this.emit('header', name, val)
}

Parser.prototype._write = function (data) {
  /* if (this.data == null) {
    return this._error('No disposition.');
  }

  if (this.file) {
    this.data.write(data);
    this.writtenDisk += data.length;
  } else {
    this.data += this.decode.write(data);
    this.written += data.length;
  } */

  this.emit('data', data)
}

Parser.prototype._reset = function () {
  this.pos = 0
  this.decode = null
  this.field = null
  this.data = null
  this.file = null
  this.header = null
}

Parser.prototype._error = function (err) {
  this.destroy()
  this.emit('error', typeof err === 'string'
    ? new Error(err)
    : err)
}

Parser.prototype.destroy = function (err) {
  this.writable = false
  this.readable = false
  this._reset()
}

Parser.prototype._finish = function () {
  const self = this
  const field = this.field
  const data = this.data
  const file = this.file
  let part

  this.pending++

  this._reset()

  if (data && data.path) {
    part = data.path
    data.end(next)
  } else {
    part = data
    next()
  }

  function next () {
    if (!self.readable) return

    self.pending--

    self.emit('part', field, part)

    if (data && data.path) {
      self.emit('file', field, part, file)
    }

    if (self.epilogue && !self.pending) {
      self.emit('end')
      self.destroy()
    }
  }
}

/**
 * Uploads
 */

Parser.root = process.platform === 'win32'
  ? 'C:/Temp'
  : '/tmp'

/**
 * Middleware
 */

Parser.middleware = function (options) {
  options = options || {}
  return function (req, res, next) {
    if (options.ensureBody) {
      req.body = {}
    }

    if (req.method === 'GET' ||
        req.method === 'HEAD' ||
        req._multipart) return next()

    req._multipart = true

    let type = req.headers['content-type']

    if (type) type = type.split(';', 1)[0].trim().toLowerCase()

    if (type === 'multipart/form-data') {
      Parser.handle(req, res, next, options)
    } else {
      next()
    }
  }
}

/**
 * Handler
 */

Parser.handle = function (req, res, next, options) {
  const parser = new Parser(req.headers['content-type'], options)
  const diskLimit = options.diskLimit
  const limit = options.limit
  const parts = {}
  const files = {}

  parser.on('error', function (err) {
    req.destroy()
    next(err)
  })

  parser.on('part', function (field, part) {
    set(parts, field, part)
  })

  parser.on('file', function (field, path, name) {
    set(files, field, {
      path,
      name,
      toString: function () {
        return path
      }
    })
  })

  parser.on('data', function () {
    if (this.writtenDisk > diskLimit || this.written > limit) {
      this.emit('error', new Error('Overflow.'))
      this.destroy()
    }
  })

  parser.on('end', next)

  req.body = parts
  req.files = files
  req.pipe(parser)
}

/**
 * Helpers
 */

const isWindows = process.platform === 'win32'

const stream = function (name, dir) {
  const ext = path.extname(name) || ''
  var name = path.basename(name, ext) || ''
  var dir = dir || Parser.root
  let tag

  tag = Math.random().toString(36).substring(2)

  name = name.substring(0, 200) + '.' + tag
  name = path.join(dir, name) + ext.substring(0, 6)
  name = name.replace(/\0/g, '')

  if (isWindows) {
    name = name.replace(/[:*<>|"?]/g, '')
  }

  return fs.createWriteStream(name)
}

var grab = function (str, name) {
  if (!str) return

  const rx = new RegExp('\\b' + name + '\\s*=\\s*("[^"]+"|\'[^\']+\'|[^;,]+)', 'i')
  const cap = rx.exec(str)

  if (cap) {
    return cap[1].trim().replace(/^['"]|['"]$/g, '')
  }
}

/**
 * Expose
 */

module.exports = Parser
