'use strict'

const { iteratorMixin } = require('./util')
const { kEnumerableProperty } = require('../../core/util')
const { webidl } = require('../webidl')
const nodeUtil = require('node:util')
const { runtimeFeatures } = require('../../util/runtime-features.js')

const random = runtimeFeatures.has('crypto')
  ? require('node:crypto').randomInt
  : (max) => Math.floor(Math.random() * max)

// https://xhr.spec.whatwg.org/#formdata
class FormData {
  #state = []
  #boundary = null

  constructor (form = undefined) {
    webidl.util.markAsUncloneable(this)

    if (form !== undefined) {
      throw webidl.errors.conversionFailed({
        prefix: 'FormData constructor',
        argument: 'Argument 1',
        types: ['undefined']
      })
    }
  }

  append (name, value, filename = undefined) {
    webidl.brandCheck(this, FormData)

    const prefix = 'FormData.append'
    webidl.argumentLengthCheck(arguments, 2, prefix)

    name = webidl.converters.USVString(name)

    if (arguments.length === 3 || webidl.is.Blob(value)) {
      value = webidl.converters.Blob(value, prefix, 'value')

      if (filename !== undefined) {
        filename = webidl.converters.USVString(filename)
      }
    } else {
      value = webidl.converters.USVString(value)
    }

    // 1. Let value be value if given; otherwise blobValue.

    // 2. Let entry be the result of creating an entry with
    // name, value, and filename if given.
    const entry = makeEntry(name, value, filename)

    // 3. Append entry to this’s entry list.
    this.#state.push(entry)
  }

  delete (name) {
    webidl.brandCheck(this, FormData)

    const prefix = 'FormData.delete'
    webidl.argumentLengthCheck(arguments, 1, prefix)

    name = webidl.converters.USVString(name)

    // The delete(name) method steps are to remove all entries whose name
    // is name from this’s entry list.
    this.#state = this.#state.filter(entry => entry.name !== name)
  }

  get (name) {
    webidl.brandCheck(this, FormData)

    const prefix = 'FormData.get'
    webidl.argumentLengthCheck(arguments, 1, prefix)

    name = webidl.converters.USVString(name)

    // 1. If there is no entry whose name is name in this’s entry list,
    // then return null.
    const idx = this.#state.findIndex((entry) => entry.name === name)
    if (idx === -1) {
      return null
    }

    // 2. Return the value of the first entry whose name is name from
    // this’s entry list.
    return this.#state[idx].value
  }

  getAll (name) {
    webidl.brandCheck(this, FormData)

    const prefix = 'FormData.getAll'
    webidl.argumentLengthCheck(arguments, 1, prefix)

    name = webidl.converters.USVString(name)

    // 1. If there is no entry whose name is name in this’s entry list,
    // then return the empty list.
    // 2. Return the values of all entries whose name is name, in order,
    // from this’s entry list.
    return this.#state
      .filter((entry) => entry.name === name)
      .map((entry) => entry.value)
  }

  has (name) {
    webidl.brandCheck(this, FormData)

    const prefix = 'FormData.has'
    webidl.argumentLengthCheck(arguments, 1, prefix)

    name = webidl.converters.USVString(name)

    // The has(name) method steps are to return true if there is an entry
    // whose name is name in this’s entry list; otherwise false.
    return this.#state.findIndex((entry) => entry.name === name) !== -1
  }

  set (name, value, filename = undefined) {
    webidl.brandCheck(this, FormData)

    const prefix = 'FormData.set'
    webidl.argumentLengthCheck(arguments, 2, prefix)

    name = webidl.converters.USVString(name)

    if (arguments.length === 3 || webidl.is.Blob(value)) {
      value = webidl.converters.Blob(value, prefix, 'value')

      if (filename !== undefined) {
        filename = webidl.converters.USVString(filename)
      }
    } else {
      value = webidl.converters.USVString(value)
    }

    // The set(name, value) and set(name, blobValue, filename) method steps
    // are:

    // 1. Let value be value if given; otherwise blobValue.

    // 2. Let entry be the result of creating an entry with name, value, and
    // filename if given.
    const entry = makeEntry(name, value, filename)

    // 3. If there are entries in this’s entry list whose name is name, then
    // replace the first such entry with entry and remove the others.
    const idx = this.#state.findIndex((entry) => entry.name === name)
    if (idx !== -1) {
      this.#state = [
        ...this.#state.slice(0, idx),
        entry,
        ...this.#state.slice(idx + 1).filter((entry) => entry.name !== name)
      ]
    } else {
      // 4. Otherwise, append entry to this’s entry list.
      this.#state.push(entry)
    }
  }

  [nodeUtil.inspect.custom] (depth, options) {
    const state = this.#state.reduce((a, b) => {
      if (a[b.name]) {
        if (Array.isArray(a[b.name])) {
          a[b.name].push(b.value)
        } else {
          a[b.name] = [a[b.name], b.value]
        }
      } else {
        a[b.name] = b.value
      }

      return a
    }, { __proto__: null })

    options.depth ??= depth
    options.colors ??= true

    const output = nodeUtil.formatWithOptions(options, state)

    // remove [Object null prototype]
    return `FormData ${output.slice(output.indexOf(']') + 2)}`
  }

  /**
   * @param {FormData} formData
   */
  static getFormDataState (formData) {
    return formData.#state
  }

  /**
   * @param {FormData} formData
   * @param {any[]} newState
   */
  static setFormDataState (formData, newState) {
    formData.#state = newState
  }

  /**
   * @param {FormData} formData
   * @returns {string | null}
   */
  static getFormDataBoundary (formData) {
    const boundary = formData.#boundary
    if (boundary != null) return boundary

    // eslint-disable-next-line no-return-assign
    return formData.#boundary = `----formdata-undici-0${`${random(1e11)}`.padStart(11, '0')}`
  }
}

const { getFormDataState, setFormDataState, getFormDataBoundary } = FormData
Reflect.deleteProperty(FormData, 'getFormDataState')
Reflect.deleteProperty(FormData, 'setFormDataState')
Reflect.deleteProperty(FormData, 'getFormDataBoundary')

iteratorMixin('FormData', FormData, getFormDataState, 'name', 'value')

Object.defineProperties(FormData.prototype, {
  append: kEnumerableProperty,
  delete: kEnumerableProperty,
  get: kEnumerableProperty,
  getAll: kEnumerableProperty,
  has: kEnumerableProperty,
  set: kEnumerableProperty,
  [Symbol.toStringTag]: {
    value: 'FormData',
    configurable: true
  }
})

/**
 * @see https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#create-an-entry
 * @param {string} name
 * @param {string|Blob} value
 * @param {?string} filename
 * @returns
 */
function makeEntry (name, value, filename) {
  // 1. Set name to the result of converting name into a scalar value string.
  // Note: This operation was done by the webidl converter USVString.

  // 2. If value is a string, then set value to the result of converting
  //    value into a scalar value string.
  if (typeof value === 'string') {
    // Note: This operation was done by the webidl converter USVString.
  } else {
    // 3. Otherwise:

    // 1. If value is not a File object, then set value to a new File object,
    //    representing the same bytes, whose name attribute value is "blob"
    if (!webidl.is.File(value)) {
      value = new File([value], 'blob', { type: value.type })
    }

    // 2. If filename is given, then set value to a new File object,
    //    representing the same bytes, whose name attribute is filename.
    if (filename !== undefined) {
      /** @type {FilePropertyBag} */
      const options = {
        type: value.type,
        lastModified: value.lastModified
      }

      value = new File([value], filename, options)
    }
  }

  // 4. Return an entry whose name is name and whose value is value.
  return { name, value }
}

webidl.is.FormData = webidl.util.MakeTypeAssertion(FormData)

module.exports = { FormData, makeEntry, setFormDataState, getFormDataBoundary }
