const ascii = require('./lib/ascii')
const base64 = require('./lib/base64')
const hex = require('./lib/hex')
const utf8 = require('./lib/utf8')
const utf16le = require('./lib/utf16le')

const LE = new Uint8Array(Uint16Array.of(0xff).buffer)[0] === 0xff

function codecFor(encoding) {
  switch (encoding) {
    case 'ascii':
      return ascii
    case 'base64':
      return base64
    case 'hex':
      return hex
    case 'utf8':
    case 'utf-8':
    case undefined:
    case null:
      return utf8
    case 'ucs2':
    case 'ucs-2':
    case 'utf16le':
    case 'utf-16le':
      return utf16le
    default:
      throw new Error(`Unknown encoding '${encoding}'`)
  }
}

function isBuffer(value) {
  return value instanceof Uint8Array
}

function isEncoding(encoding) {
  try {
    codecFor(encoding)
    return true
  } catch {
    return false
  }
}

function alloc(size, fill, encoding) {
  const buffer = new Uint8Array(size)
  if (fill !== undefined) {
    exports.fill(buffer, fill, 0, buffer.byteLength, encoding)
  }
  return buffer
}

function allocUnsafe(size) {
  return new Uint8Array(size)
}

function allocUnsafeSlow(size) {
  return new Uint8Array(size)
}

function byteLength(string, encoding) {
  return codecFor(encoding).byteLength(string)
}

function compare(a, b) {
  if (a === b) return 0

  const len = Math.min(a.byteLength, b.byteLength)

  a = new DataView(a.buffer, a.byteOffset, a.byteLength)
  b = new DataView(b.buffer, b.byteOffset, b.byteLength)

  let i = 0

  for (let n = len - (len % 4); i < n; i += 4) {
    const x = a.getUint32(i, LE)
    const y = b.getUint32(i, LE)
    if (x !== y) break
  }

  for (; i < len; i++) {
    const x = a.getUint8(i)
    const y = b.getUint8(i)
    if (x < y) return -1
    if (x > y) return 1
  }

  return a.byteLength > b.byteLength ? 1 : a.byteLength < b.byteLength ? -1 : 0
}

function concat(buffers, length) {
  if (length === undefined) {
    length = buffers.reduce((len, buffer) => len + buffer.byteLength, 0)
  }

  const result = new Uint8Array(length)

  let offset = 0

  for (const buffer of buffers) {
    if (offset + buffer.byteLength > result.byteLength) {
      result.set(buffer.subarray(0, result.byteLength - offset), offset)
      return result
    }

    result.set(buffer, offset)
    offset += buffer.byteLength
  }

  return result
}

function copy(
  source,
  target,
  targetStart = 0,
  sourceStart = 0,
  sourceEnd = source.byteLength
) {
  if (targetStart < 0) targetStart = 0
  if (targetStart >= target.byteLength) return 0

  const targetLength = target.byteLength - targetStart

  if (sourceStart < 0) sourceStart = 0
  if (sourceStart >= source.byteLength) return 0

  if (sourceEnd <= sourceStart) return 0
  if (sourceEnd > source.byteLength) sourceEnd = source.byteLength

  if (sourceEnd - sourceStart > targetLength) {
    sourceEnd = sourceStart + targetLength
  }

  const sourceLength = sourceEnd - sourceStart

  if (source === target) {
    target.copyWithin(targetStart, sourceStart, sourceEnd)
  } else {
    if (sourceStart !== 0 || sourceEnd !== source.byteLength) {
      source = source.subarray(sourceStart, sourceEnd)
    }

    target.set(source, targetStart)
  }

  return sourceLength
}

function equals(a, b) {
  if (a === b) return true
  if (a.byteLength !== b.byteLength) return false

  return compare(a, b) === 0
}

function fill(
  buffer,
  value,
  offset = 0,
  end = buffer.byteLength,
  encoding = 'utf8'
) {
  if (typeof value === 'string') {
    if (typeof offset === 'string') {
      // fill(string, encoding)
      encoding = offset
      offset = 0
      end = buffer.byteLength
    } else if (typeof end === 'string') {
      // fill(string, offset, encoding)
      encoding = end
      end = buffer.byteLength
    }
  } else if (typeof value === 'number') {
    value = value & 0xff
  } else if (typeof value === 'boolean') {
    value = +value
  }

  if (offset < 0) offset = 0
  if (offset >= buffer.byteLength) return buffer

  if (end <= offset) return buffer
  if (end > buffer.byteLength) end = buffer.byteLength

  if (typeof value === 'number') return buffer.fill(value, offset, end)

  if (typeof value === 'string') value = exports.from(value, encoding)

  const len = value.byteLength

  for (let i = 0, n = end - offset; i < n; ++i) {
    buffer[i + offset] = value[i % len]
  }

  return buffer
}

function from(value, encodingOrOffset, length) {
  // from(string, encoding)
  if (typeof value === 'string') return fromString(value, encodingOrOffset)

  // from(array)
  if (Array.isArray(value)) return fromArray(value)

  // from(buffer)
  if (ArrayBuffer.isView(value)) return fromBuffer(value)

  // from(arrayBuffer[, byteOffset[, length]])
  return fromArrayBuffer(value, encodingOrOffset, length)
}

function fromString(string, encoding) {
  const codec = codecFor(encoding)
  const buffer = new Uint8Array(codec.byteLength(string))
  codec.write(buffer, string)
  return buffer
}

function fromArray(array) {
  const buffer = new Uint8Array(array.length)
  buffer.set(array)
  return buffer
}

function fromBuffer(buffer) {
  const copy = new Uint8Array(buffer.byteLength)
  copy.set(buffer)
  return copy
}

function fromArrayBuffer(arrayBuffer, byteOffset, length) {
  return new Uint8Array(arrayBuffer, byteOffset, length)
}

function includes(buffer, value, byteOffset, encoding) {
  return indexOf(buffer, value, byteOffset, encoding) !== -1
}

function indexOf(buffer, value, byteOffset, encoding) {
  return bidirectionalIndexOf(
    buffer,
    value,
    byteOffset,
    encoding,
    true /* first */
  )
}

function lastIndexOf(buffer, value, byteOffset, encoding) {
  return bidirectionalIndexOf(
    buffer,
    value,
    byteOffset,
    encoding,
    false /* last */
  )
}

function bidirectionalIndexOf(buffer, value, byteOffset, encoding, first) {
  if (buffer.byteLength === 0) return -1

  if (typeof byteOffset === 'string') {
    encoding = byteOffset
    byteOffset = 0
  } else if (byteOffset === undefined) {
    byteOffset = first ? 0 : buffer.length - 1
  } else if (byteOffset < 0) {
    byteOffset += buffer.byteLength
  }

  if (byteOffset >= buffer.byteLength) {
    if (first) return -1
    else byteOffset = buffer.byteLength - 1
  } else if (byteOffset < 0) {
    if (first) byteOffset = 0
    else return -1
  }

  if (typeof value === 'string') {
    value = from(value, encoding)
  } else if (typeof value === 'number') {
    value = value & 0xff

    if (first) {
      return buffer.indexOf(value, byteOffset)
    } else {
      return buffer.lastIndexOf(value, byteOffset)
    }
  }

  if (value.byteLength === 0) return -1

  if (first) {
    let foundIndex = -1

    for (let i = byteOffset; i < buffer.byteLength; i++) {
      if (buffer[i] === value[foundIndex === -1 ? 0 : i - foundIndex]) {
        if (foundIndex === -1) foundIndex = i
        if (i - foundIndex + 1 === value.byteLength) return foundIndex
      } else {
        if (foundIndex !== -1) i -= i - foundIndex
        foundIndex = -1
      }
    }
  } else {
    if (byteOffset + value.byteLength > buffer.byteLength) {
      byteOffset = buffer.byteLength - value.byteLength
    }

    for (let i = byteOffset; i >= 0; i--) {
      let found = true

      for (let j = 0; j < value.byteLength; j++) {
        if (buffer[i + j] !== value[j]) {
          found = false
          break
        }
      }

      if (found) return i
    }
  }

  return -1
}

function swap(buffer, n, m) {
  const i = buffer[n]
  buffer[n] = buffer[m]
  buffer[m] = i
}

function swap16(buffer) {
  const len = buffer.byteLength

  if (len % 2 !== 0)
    throw new RangeError('Buffer size must be a multiple of 16-bits')

  for (let i = 0; i < len; i += 2) swap(buffer, i, i + 1)

  return buffer
}

function swap32(buffer) {
  const len = buffer.byteLength

  if (len % 4 !== 0)
    throw new RangeError('Buffer size must be a multiple of 32-bits')

  for (let i = 0; i < len; i += 4) {
    swap(buffer, i, i + 3)
    swap(buffer, i + 1, i + 2)
  }

  return buffer
}

function swap64(buffer) {
  const len = buffer.byteLength

  if (len % 8 !== 0)
    throw new RangeError('Buffer size must be a multiple of 64-bits')

  for (let i = 0; i < len; i += 8) {
    swap(buffer, i, i + 7)
    swap(buffer, i + 1, i + 6)
    swap(buffer, i + 2, i + 5)
    swap(buffer, i + 3, i + 4)
  }

  return buffer
}

function toBuffer(buffer) {
  return buffer
}

function toString(
  buffer,
  encoding = 'utf8',
  start = 0,
  end = buffer.byteLength
) {
  // toString(buffer)
  if (arguments.length === 1) return utf8.toString(buffer)

  // toString(buffer, encoding)
  if (arguments.length === 2) return codecFor(encoding).toString(buffer)

  if (start < 0) start = 0
  if (start >= buffer.byteLength) return ''

  if (end <= start) return ''
  if (end > buffer.byteLength) end = buffer.byteLength

  if (start !== 0 || end !== buffer.byteLength) {
    buffer = buffer.subarray(start, end)
  }

  return codecFor(encoding).toString(buffer)
}

function write(buffer, string, offset, length, encoding) {
  // write(buffer, string)
  if (arguments.length === 2) return utf8.write(buffer, string)

  if (typeof offset === 'string') {
    // write(buffer, string, encoding)
    encoding = offset
    offset = 0
    length = buffer.byteLength
  } else if (typeof length === 'string') {
    // write(buffer, string, offset, encoding)
    encoding = length
    length = buffer.byteLength - offset
  }

  length = Math.min(length, exports.byteLength(string, encoding))

  let start = offset
  if (start < 0) start = 0
  if (start >= buffer.byteLength) return 0

  let end = offset + length
  if (end <= start) return 0
  if (end > buffer.byteLength) end = buffer.byteLength

  if (start !== 0 || end !== buffer.byteLength) {
    buffer = buffer.subarray(start, end)
  }

  return codecFor(encoding).write(buffer, string)
}

function readDoubleBE(buffer, offset = 0) {
  const view = new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength)
  return view.getFloat64(offset, false)
}

function readDoubleLE(buffer, offset = 0) {
  const view = new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength)
  return view.getFloat64(offset, true)
}

function readFloatBE(buffer, offset = 0) {
  const view = new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength)
  return view.getFloat32(offset, false)
}

function readFloatLE(buffer, offset = 0) {
  const view = new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength)
  return view.getFloat32(offset, true)
}

function readInt32BE(buffer, offset = 0) {
  const view = new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength)
  return view.getInt32(offset, false)
}

function readInt32LE(buffer, offset = 0) {
  const view = new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength)
  return view.getInt32(offset, true)
}

function readUInt32BE(buffer, offset = 0) {
  const view = new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength)
  return view.getUint32(offset, false)
}

function readUInt32LE(buffer, offset = 0) {
  const view = new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength)
  return view.getUint32(offset, true)
}

function writeDoubleBE(buffer, value, offset = 0) {
  const view = new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength)
  view.setFloat64(offset, value, false)
  return offset + 8
}

function writeDoubleLE(buffer, value, offset = 0) {
  const view = new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength)
  view.setFloat64(offset, value, true)
  return offset + 8
}

function writeFloatBE(buffer, value, offset = 0) {
  const view = new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength)
  view.setFloat32(offset, value, false)
  return offset + 4
}

function writeFloatLE(buffer, value, offset = 0) {
  const view = new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength)
  view.setFloat32(offset, value, true)
  return offset + 4
}

function writeInt32BE(buffer, value, offset = 0) {
  const view = new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength)
  view.setInt32(offset, value, false)
  return offset + 4
}

function writeInt32LE(buffer, value, offset = 0) {
  const view = new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength)
  view.setInt32(offset, value, true)
  return offset + 4
}

function writeUInt32BE(buffer, value, offset = 0) {
  const view = new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength)
  view.setUint32(offset, value, false)
  return offset + 4
}

function writeUInt32LE(buffer, value, offset = 0) {
  const view = new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength)
  view.setUint32(offset, value, true)
  return offset + 4
}

module.exports = exports = {
  isBuffer,
  isEncoding,
  alloc,
  allocUnsafe,
  allocUnsafeSlow,
  byteLength,
  compare,
  concat,
  copy,
  equals,
  fill,
  from,
  includes,
  indexOf,
  lastIndexOf,
  swap16,
  swap32,
  swap64,
  toBuffer,
  toString,
  write,
  readDoubleBE,
  readDoubleLE,
  readFloatBE,
  readFloatLE,
  readInt32BE,
  readInt32LE,
  readUInt32BE,
  readUInt32LE,
  writeDoubleBE,
  writeDoubleLE,
  writeFloatBE,
  writeFloatLE,
  writeInt32BE,
  writeInt32LE,
  writeUInt32BE,
  writeUInt32LE
}
