import type { Maybe } from "Maybe"

import { Just, Nothing } from "Maybe"



/**
 * ByteArray is a specialized array type that only contains bytes. It has a
 * fast path to convert to and from string which makes it practical when
 * dealing with raw bytes representing text data.
 *
 * Note: Most IO apis such as File or Http provide both functions that work with
 * String and functions that work with ByteArray.
 *
 */

// TODO: Add ap and chain

#iftarget llvm

/**
 * Initializes a ByteArray with the given capacity.
 *
 * @since 0.23.0
 */
init :: Integer -> ByteArray
export init = extern "madlib__bytearray__initWithCapacity"


/**
 * Return the byte at a given index, it crashes if the index is out of
 * the bounds of the array.
 * @since 0.23.0
 * @example
 * at(1, fromList([1, 2, 3])) // 2
 * at(3, fromList([1, 2, 3])) // crash, it exits with error
 */
unsafeAt :: Integer -> ByteArray -> Byte
export unsafeAt = extern "madlib__bytearray__unsafeAt"


/**
 * Sets a byte at a given index, it crashes if the index is out of
 * the bounds of the array.
 * NB: the input byte array is mutated in place
 *
 * @since 0.23.0
 * @example
 * pipe(
 *   fromList,
 *   set(1, 33),
 * )([1, 2, 3]) // fromList([1, 33, 3])
 */
unsafeSet :: Integer -> Byte -> ByteArray -> ByteArray
export unsafeSet = extern "madlib__bytearray__unsafeSet"


/**
 * Creates a byte array from a list of bytes.
 *
 * @since 0.11.0
 * @example
 * fromList([90, 91, 92])
 */
fromList :: List Byte -> ByteArray
export fromList = extern "madlib__bytearray__fromList"


/**
 * Converts a byte array to a list of bytes.
 *
 * @since 0.11.0
 * @example
 * toList(fromString("abc"))
 */
toList :: ByteArray -> List Byte
export toList = extern "madlib__bytearray__toList"


/**
 * Creates a byte array from a string.
 *
 * @since 0.11.0
 * @example
 * fromString("abc")
 */
fromString :: String -> ByteArray
export fromString = extern "madlib__bytearray__fromString"


/**
 * Converts a byte array to a string
 *
 * @since 0.11.0
 * @example
 * toString(fromList([90, 91, 92]))
 */
toString :: ByteArray -> String
export toString = extern "madlib__bytearray__toString"


/**
 * Returns the length of a ByteArray.
 *
 * @since 0.7.0
 * @example
 * length(fromString("abc")) // 3
 */
length :: ByteArray -> Integer
export length = extern "madlib__bytearray__length"


/**
 * Concatenates two byte arrays
 *
 * @since 0.11.0
 * @example
 * concat(fromString("123"), fromString("456")) // fromString("123456")
 */
concat :: ByteArray -> ByteArray -> ByteArray
export concat = extern "madlib__bytearray__concat"


/**
 * Concatenates two byte arrays with mutation, the second ByteArray
 * is added to the end of the first one.
 *
 * @since 0.23.0
 * @example
 * concat(fromString("123"), fromString("456")) // fromString("123456")
 */
weld :: ByteArray -> ByteArray -> ByteArray
export weld = extern "madlib__bytearray__concatWithMutation"


/**
 * Push a byte at the end of a ByteArray.
 * NB: it mutates the array.
 * 
 * @since 0.23.0
 * @example
 * push(3, fromList([1, 2])) // fromList([1, 2, 3])
 */
push :: Byte -> ByteArray -> ByteArray
export push = extern "madlib__bytearray__pushBackWithMutation"


/**
 * Maps the bytes of a byte array, applying the given function to each byte.
 *
 * @since 0.11.0
 * @example
 * mapBytes(substract(10), fromString("pqr"))
 */
mapBytes :: (Byte -> Byte) -> ByteArray -> ByteArray
export mapBytes = extern "madlib__bytearray__map"


/**
 * Iterate over a byte array, selecting only bytes which are matched by predicate function.
 *
 * @since 0.23.0
 * @example
 * filter((a) => a % 2 == 0, fromList([1, 2, 3, 4, 5, 6])) // fromList([2, 4, 6])
 */
filter :: (Byte -> Boolean) -> ByteArray -> ByteArray
export filter = (predicate, arr) => {
  len = length(arr)
  result = init(len)
  i = 0

  while(i < len) do {
    item = unsafeAt(i, arr)
    if (predicate(item)) do {
      push(item, result)
    }

    i := i + 1
  }

  return result
}


/**
 * Reduces a byte array to a value, given a reducer function, an initial value, and a byte array.
 *
 * @since 0.11.0
 * @example
 * reduce(add, 0, fromList([1, 2, 3])) // 6
 */
reduce :: (a -> Byte -> a) -> a -> ByteArray -> a
export reduce = extern "madlib__bytearray__reduce"

#elseif js

/**
 * Initializes a ByteArray with the given capacity.
 *
 * @since 0.23.0
 */
init :: Integer -> ByteArray
export init = () => #- new Uint8Array() -#


/**
 * Return the byte at a given index, it crashes if the index is out of
 * the bounds of the array.
 *
 * @since 0.23.0
 * @example
 * at(1, fromList([1, 2, 3])) // 2
 * at(3, fromList([1, 2, 3])) // crash, it exits with error
 */
unsafeAt :: Integer -> ByteArray -> Byte
export unsafeAt = (index, byteArray) => #- {
  if (index >= byteArray.length) {
    throw `Array out of bounds access\\nYou accessed the index '${index}' but the array currently has length '${byteArray.length}'`
  }
  return byteArray[index]
} -#


/**
 * Sets a byte at a given index, it crashes if the index is out of
 * the bounds of the byte array.
 * NB: the input byte array is mutated in place.
 *
 * @since 0.23.0
 * @example
 * pipe(
 *   fromList,
 *   set(1, 33),
 * )([1, 2, 3]) // fromList([1, 33, 3])
 */
unsafeSet :: Integer -> Byte -> ByteArray -> ByteArray
export unsafeSet = (index, byte, byteArray) => #- {
  if (index >= byteArray.length) {
    throw `Array out of bounds access\\nYou accessed the index '${index}' but the array currently has length '${byteArray.length}'`
  }

  byteArray[index] = byte
  return byteArray
} -#

/**
 * Converts a byte array to a string
 *
 * @since 0.11.0
 * @example
 * toString(fromList([90, 91, 92]))
 */
toString :: ByteArray -> String
export toString = (byteArray) => #- { return new TextDecoder().decode(byteArray) } -#


/**
 * Creates a byte array from a string.
 *
 * @since 0.11.0
 * @example
 * fromString("abc")
 */
fromString :: String -> ByteArray
export fromString = (str) => #- { return new TextEncoder().encode(str) } -#


/**
 * Converts a byte array to a list of bytes.
 *
 * @since 0.11.0
 * @example
 * toList(fromString("abc"))
 */
toList :: ByteArray -> List Byte
export toList = (byteArray) => #- {
  if (byteArray.length === 0) {
    return null
  }

  let current = {}
  let result = current
  byteArray.forEach((byte) => {
    current = current.n = { v: byte, n: null }
  })
  return result.n
} -#


/**
 * Creates a byte array from a list of bytes.
 *
 * @since 0.11.0
 * @example
 * fromList([90, 91, 92])
 */
fromList :: List Byte -> ByteArray
export fromList = (bytes) => #- {
  let bytesArray = []
  while (bytes !== null) {
    bytesArray.push(bytes.v)
    bytes = bytes.n
  }
  return Uint8Array.from(bytesArray)
} -#


/**
 * Concatenates two byte arrays
 *
 * @since 0.11.0
 * @example
 * concat(fromString("123"), fromString("456")) // fromString("123456")
 */
concat :: ByteArray -> ByteArray -> ByteArray
export concat = (byteArray1, byteArray2) => #- {
  const result = new Uint8Array(byteArray1.length + byteArray2.length);
  result.set(byteArray1);
  result.set(byteArray2, byteArray1.length);
  return result;
} -#


/**
 * Concatenates two byte arrays
 *
 * @since 0.23.0
 * @example
 * concat(fromString("123"), fromString("456")) // fromString("123456")
 */
weld :: ByteArray -> ByteArray -> ByteArray
export weld = (byteArray1, byteArray2) => #- {
  const result = new Uint8Array(byteArray1.length + byteArray2.length);
  result.set(byteArray1);
  result.set(byteArray2, byteArray1.length);
  return result;
} -#


/**
 * Concatenates two byte arrays
 *
 * @since 0.23.0
 * @example
 * concat(fromString("123"), fromString("456")) // fromString("123456")
 */
push :: Byte -> ByteArray -> ByteArray
export push = (byte, byteArray) => #- {
  const result = new Uint8Array(byteArray.length + 1);
  result.set([...byteArray, byte]);
  return result;
} -#


/**
 * Maps the bytes of a byte array, applying the given function to each byte.
 *
 * @since 0.11.0
 * @example
 * mapBytes(substract(10), fromString("pqr"))
 */
mapBytes :: (Byte -> Byte) -> ByteArray -> ByteArray
export mapBytes = (f, byteArray) => #- { return byteArray.map(f) } -#


/**
 * Iterate over a byte array, selecting only bytes which are matched by predicate function.
 *
 * @since 0.23.0
 * @example
 * filter((a) => a % 2 == 0, fromList([1, 2, 3, 4, 5, 6])) // fromList([2, 4, 6])
 */
filter :: (Byte -> Boolean) -> ByteArray -> ByteArray
export filter = (predicate, byteArray) => #- byteArray.filter(predicate) -#


/**
 * Reduces a byte array to a value, given a reducer function, an initial value, and a byte array.
 *
 * @since 0.11.0
 * @example
 * reduce(add, 0, fromList([1, 2, 3])) // 6
 */
reduce :: (a -> Byte -> a) -> a -> ByteArray -> a
export reduce = (
  f,
  initialValue,
  byteArray
) => #- { return byteArray.reduce((a, b) => f(a)(b), initialValue) } -#


/**
 * Returns the length of a ByteArray.
 *
 * @since 0.7.0
 * @example
 * length(fromString("abc")) // 3
 */
length :: ByteArray -> Integer
export length = (byteArray) => #- { return byteArray.length } -#

#endif


/**
 * The empty byte array
 *
 * @since 0.11.0
 * @example
 * empty == empty // true
 */
empty :: {} -> ByteArray
export empty = () => fromList([])


/**
 * Return a Maybe of a byte at a given index, Nothing if the index is out of
 * the bounds of the array.
 * @since 0.23.0
 * @example
 * at(1, fromList([1, 2, 3])) // Just(2)
 * at(3, fromList([1, 2, 3])) // Nothing
 */
at :: Integer -> ByteArray -> Maybe Byte
export at = (index, arr) => index > 0 && index < length(arr) ? Just(unsafeAt(index, arr)) : Nothing


/**
 * Sets a byte at a given index. The byte array is unchanged if the index is out of
 * the bounds of the byte array.
 * NB: the input byte array is mutated in place
 * @since 0.23.0
 * @example
 * pipe(
 *   Array.fromList,
 *   Array.set(1, 33),
 * )([1, 2, 3]) // Array.fromList([1, 33, 3])
 */
set :: Integer -> Byte -> ByteArray -> ByteArray
export set = (index, arr, byteArray) => index < length(byteArray)
  ? unsafeSet(index, arr, byteArray)
  : byteArray


/**
 * Cut a contiguous segment from an Array, from start index to end index.
 * @since 0.23.0
 */
slice :: Integer -> Integer -> ByteArray -> ByteArray
export slice = (start, end, arr) => {
  len = length(arr)
  realStart = start < 0 ? start + len : start
  realEnd = end == 0 ? len - 1 : end < 0 ? end + len - 1 : end
  result = init(realStart - realEnd)

  while(realStart <= realEnd) do {
    result := push(unsafeAt(realStart, arr), result)
    realStart := realStart + 1
  }

  return result
}

// TODO: make a showBytes or something with this
// #iftarget js
// instance Show ByteArray {
//   show = (byteArray) => #- {
//     let s = ''
//     let h = '0123456789ABCDEF'
//     byteArray.forEach((v, index) => {
//       if (index % 8 === 0 && index !== 0) { s += ' ' }
//       s += h[v >> 4] + h[v & 15]
//     })
//     return `ByteArray(${s})`
//   } -#
// }
// #elseif llvm
// showByteArray :: ByteArray -> String
// showByteArray = extern "madlib__bytearray__internal__show"
// instance Show ByteArray {
//   show = showByteArray
// }
// #endif
