const path = require('bare-path')
const binding = require('./binding')
const errors = require('./lib/errors')
const URLSearchParams = require('./lib/url-search-params')

const kind = Symbol.for('bare.url.kind')

const isWindows = Bare.platform === 'win32'

module.exports = exports = class URL {
  static get [kind]() {
    return 0 // Compatibility version
  }

  constructor(input, base, opts = {}) {
    if (arguments.length === 0) throw errors.INVALID_URL()

    input = String(input)

    if (base !== undefined) base = String(base)

    this._components = new Uint32Array(8)

    this._parse(input, base, opts.throw !== false)

    if (this._href) this._params = new URLSearchParams(this.search, this)
  }

  get [kind]() {
    return URL[kind]
  }

  // https://url.spec.whatwg.org/#dom-url-href

  get href() {
    return this._href
  }

  set href(value) {
    this._update(value)

    this._params._parse(this.search)
  }

  // https://url.spec.whatwg.org/#dom-url-protocol

  get protocol() {
    return this._slice(0, this._components[0]) + ':'
  }

  set protocol(value) {
    this._update(
      this._replace(value.replace(/:+$/, ''), 0, this._components[0])
    )
  }

  // https://url.spec.whatwg.org/#dom-url-username

  get username() {
    return this._slice(this._components[0] + 3 /* :// */, this._components[1])
  }

  set username(value) {
    if (cannotHaveCredentialsOrPort(this)) {
      return
    }

    if (this.username === '') value += '@'

    this._update(
      this._replace(
        value,
        this._components[0] + 3 /* :// */,
        this._components[1]
      )
    )
  }

  // https://url.spec.whatwg.org/#dom-url-password

  get password() {
    return this._href.slice(
      this._components[1] + 1 /* : */,
      this._components[2] - 1 /* @ */
    )
  }

  set password(value) {
    if (cannotHaveCredentialsOrPort(this)) {
      return
    }

    let start = this._components[1] + 1 /* : */
    let end = this._components[2] - 1 /* @ */

    if (this.password === '') {
      value = ':' + value
      start--
    }

    if (this.username === '') {
      value += '@'
      end++
    }

    this._update(this._replace(value, start, end))
  }

  // https://url.spec.whatwg.org/#dom-url-host

  get host() {
    return this._slice(this._components[2], this._components[5])
  }

  set host(value) {
    if (hasOpaquePath(this)) {
      return
    }

    this._update(
      this._replace(
        value,
        this._components[2],
        this._components[value.includes(':') ? 5 : 3]
      )
    )
  }

  // https://url.spec.whatwg.org/#dom-url-hostname

  get hostname() {
    return this._slice(this._components[2], this._components[3])
  }

  set hostname(value) {
    if (hasOpaquePath(this)) {
      return
    }

    this._update(this._replace(value, this._components[2], this._components[3]))
  }

  // https://url.spec.whatwg.org/#dom-url-port

  get port() {
    return this._slice(this._components[3] + 1 /* : */, this._components[5])
  }

  set port(value) {
    if (cannotHaveCredentialsOrPort(this)) {
      return
    }

    let start = this._components[3] + 1 /* : */

    if (this.port === '') {
      value = ':' + value
      start--
    }

    this._update(this._replace(value, start, this._components[5]))
  }

  // https://url.spec.whatwg.org/#dom-url-pathname

  get pathname() {
    return this._slice(this._components[5], this._components[6] - 1 /* ? */)
  }

  set pathname(value) {
    if (hasOpaquePath(this)) {
      return
    }

    if (value[0] !== '/' && value[0] !== '\\') {
      value = '/' + value
    }

    this._update(
      this._replace(value, this._components[5], this._components[6] - 1 /* ? */)
    )
  }

  // https://url.spec.whatwg.org/#dom-url-search

  get search() {
    return this._slice(
      this._components[6] - 1 /* ? */,
      this._components[7] - 1 /* # */
    )
  }

  set search(value) {
    if (value && value[0] !== '?') value = '?' + value

    this._update(
      this._replace(
        value,
        this._components[6] - 1 /* ? */,
        this._components[7] - 1 /* # */
      )
    )

    this._params._parse(this.search)
  }

  // https://url.spec.whatwg.org/#dom-url-searchparams

  get searchParams() {
    return this._params
  }

  // https://url.spec.whatwg.org/#dom-url-hash

  get hash() {
    return this._slice(this._components[7] - 1 /* # */)
  }

  set hash(value) {
    if (value && value[0] !== '#') value = '#' + value

    this._update(this._replace(value, this._components[7] - 1 /* # */))
  }

  toString() {
    return this._href
  }

  toJSON() {
    return this._href
  }

  [Symbol.for('bare.inspect')]() {
    return {
      __proto__: { constructor: URL },

      href: this.href,
      protocol: this.protocol,
      username: this.username,
      password: this.password,
      host: this.host,
      hostname: this.hostname,
      port: this.port,
      pathname: this.pathname,
      search: this.search,
      searchParams: this.searchParams,
      hash: this.hash
    }
  }

  _slice(start, end = this._href.length) {
    return this._href.slice(start, end)
  }

  _replace(replacement, start, end = this._href.length) {
    return this._slice(0, start) + replacement + this._slice(end)
  }

  _parse(input, base, shouldThrow) {
    try {
      this._href = binding.parse(
        String(input),
        base ? String(base) : null,
        this._components,
        shouldThrow
      )
    } catch (err) {
      if (err instanceof TypeError) throw err

      throw errors.INVALID_URL()
    }
  }

  _update(input) {
    try {
      this._parse(input, null, true)
    } catch (err) {
      if (err instanceof TypeError) throw err
    }
  }
}

// https://url.spec.whatwg.org/#url-opaque-path
function hasOpaquePath(url) {
  return url.pathname[0] !== '/'
}

// https://url.spec.whatwg.org/#cannot-have-a-username-password-port
function cannotHaveCredentialsOrPort(url) {
  return url.hostname === '' || url.protocol === 'file:'
}

const URL = exports

exports.URL = URL
exports.URLSearchParams = URLSearchParams

exports.errors = errors

exports.isURL = function isURL(value) {
  if (value instanceof URL) return true

  return (
    typeof value === 'object' && value !== null && value[kind] === URL[kind]
  )
}

// https://url.spec.whatwg.org/#dom-url-parse
exports.parse = function parse(input, base) {
  const url = new URL(input, base, { throw: false })
  return url._href ? url : null
}

// https://url.spec.whatwg.org/#dom-url-canparse
exports.canParse = function canParse(input, base) {
  return binding.canParse(String(input), base ? String(base) : null)
}

exports.fileURLToPath = function fileURLToPath(url) {
  if (typeof url === 'string') {
    url = new URL(url)
  }

  if (url.protocol !== 'file:') {
    throw errors.INVALID_URL_SCHEME('The URL must use the file: protocol')
  }

  if (isWindows) {
    if (/%2f|%5c/i.test(url.pathname)) {
      throw errors.INVALID_FILE_URL_PATH(
        'The file: URL path must not include encoded \\ or / characters'
      )
    }
  } else {
    if (url.hostname) {
      throw errors.INVALID_FILE_URL_HOST(
        "The file: URL host must be 'localhost' or empty"
      )
    }

    if (/%2f/i.test(url.pathname)) {
      throw errors.INVALID_FILE_URL_PATH(
        'The file: URL path must not include encoded / characters'
      )
    }
  }

  const pathname = path.normalize(decodeURIComponent(url.pathname))

  if (isWindows) {
    if (url.hostname) return '\\\\' + url.hostname + pathname

    const letter = pathname.charCodeAt(1) | 0x20

    if (
      letter < 0x61 /* a */ ||
      letter > 0x7a /* z */ ||
      pathname.charCodeAt(2) !== 0x3a /* : */
    ) {
      throw errors.INVALID_FILE_URL_PATH('The file: URL path must be absolute')
    }

    return pathname.slice(1)
  }

  return pathname
}

exports.pathToFileURL = function pathToFileURL(pathname) {
  let resolved = path.resolve(pathname)

  if (pathname[pathname.length - 1] === '/') {
    resolved += '/'
  } else if (isWindows && pathname[pathname.length - 1] === '\\') {
    resolved += '\\'
  }

  resolved = resolved
    .replaceAll('%', '%25') // Must be first
    .replaceAll('#', '%23')
    .replaceAll('?', '%3f')
    .replaceAll('\n', '%0a')
    .replaceAll('\r', '%0d')
    .replaceAll('\t', '%09')

  if (!isWindows) {
    resolved = resolved.replaceAll('\\', '%5c')
  }

  return new URL('file:' + resolved)
}

exports.format = function format(parts) {
  const {
    protocol,
    auth,
    host,
    hostname,
    port,
    pathname,
    search,
    query,
    hash,
    slashes
  } = parts

  let result = ''

  if (typeof protocol === 'string') {
    result += protocol

    if (protocol[protocol.length - 1] !== ':') {
      result += ':'
    }

    if (slashes === true || /https?|ftp|gopher|file/.test(protocol)) {
      result += '//'
    }
  }

  if (typeof auth === 'string') {
    if (host || hostname) result += auth + '@'
  }

  if (typeof host === 'string') result += host
  else {
    result += hostname

    if (port) result += ':' + port
  }

  if (typeof pathname === 'string' && pathname !== '') {
    if (pathname[0] !== '/') result += '/'
    result += pathname
  }

  if (typeof search === 'string') {
    if (search[0] !== '?') result += '?'
    result += search
  } else if (typeof query === 'object' && query !== null) {
    result += '?' + new URLSearchParams(query)
  }

  if (typeof hash === 'string') {
    if (hash[0] !== '#') result += '#'
    result += hash
  }

  return result
}
