package gs.std.v0.core

import java.nio.charset.Charset
import java.nio.charset.StandardCharsets
import java.security.MessageDigest

/** Opaque type representing a MD5 hash.
  */
opaque type MD5 = Array[Byte]

/** Opaque type representing a MD5 hash.
  */
object MD5:

  /** MD5 hashes are exactly 16 bytes.
    */
  final val NumberOfBytes: ByteCount = ByteCount(16)

  /** The algorithm name is "MD5".
    */
  final val Algorithm: String = "MD5"

  /** Instantiate a [[MD5]] from the given byte array. This function does not
    * know whether the array is actually a calculated hash.
    *
    * Typically used for loading pre-validated hashes (e.g. from a database).
    *
    * @param bytes
    *   The bytes - must contain exactly 16 bytes.
    * @return
    *   The new [[MD5]] instance.
    */
  def fromBytes(bytes: Array[Byte]): MD5 =
    if NumberOfBytes.equal(bytes.length) then bytes
    else
      throw IllegalArgumentException(
        s"MD5 values must be exactly $NumberOfBytes bytes."
      )

  /** Calculate the MD5 hash for the given byte array.
    *
    * @param data
    *   The byte array.
    * @return
    *   The calculated [[MD5]].
    */
  def calculate(data: Array[Byte]): MD5 =
    MessageDigest.getInstance(Algorithm).digest(data)

  /* Calculate the MD5 hash for the given string.
   *
   * @param data The string data.
   * @param charset The character set of the string. Defaults to UTF-8.
   * @return The calculated [[MD5]]. */
  def calculate(
    data: String,
    charset: Charset = StandardCharsets.UTF_8
  ): MD5 =
    calculate(data.getBytes(charset))

  extension (md5: MD5)
    /** @return
      *   The underlying byte array.
      */
    def unwrap(): Array[Byte] = md5

    /** @return
      *   The underlying byte array.
      */
    def toBytes(): Array[Byte] = md5

    /** Get the byte at the given index (0 to 15).
      *
      * Throws an exception if an out-of-bound index is given.
      *
      * @param index
      *   The byte index (0 to 15).
      * @return
      *   The byte at the specified index.
      */
    def getByte(index: Int): Byte =
      if index < 0 || index >= NumberOfBytes.unwrap() then
        throw IndexOutOfBoundsException(
          s"Index $index out of MD5 bound of $NumberOfBytes bytes."
        )
      else md5.apply(index)

    /** Determine if this hash is the same as some other hash. Compares each
      * byte in order.
      *
      * @param other
      *   The [[MD5]] to compare against.
      * @return
      *   True if the hashes are identical, false otherwise.
      */
    def isSame(other: MD5): Boolean = md5.sameElements(other)

    /** @return
      *   This hash encoded to a Base64 string.
      */
    def base64(): EncodedString = Base64Encoder.encode(md5)

    /** @return
      *   This hash encoded to a Hexadecimal string.
      */
    def hex(): EncodedString = HexEncoder.encode(md5)

end MD5
