package gs.std.v0.core

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

/** Opaque type representing a SHA-256 hash.
  */
opaque type SHA256 = Array[Byte]

/** Opaque type representing a SHA-256 hash.
  */
object SHA256:

  /** SHA-256 hashes are exactly 32 bytes.
    */
  final val NumberOfBytes: ByteCount = ByteCount(32)

  /** The algorithm name is "SHA-256".
    */
  final val Algorithm: String = "SHA-256"

  /** Instantiate a [[SHA256]] 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 32 bytes.
    * @return
    *   The new [[SHA256]] instance.
    */
  def fromBytes(bytes: Array[Byte]): SHA256 =
    if NumberOfBytes.equal(bytes.length) then bytes
    else
      throw IllegalArgumentException(
        s"SHA-256 values must be exactly $NumberOfBytes bytes."
      )

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

  /** Calculate the SHA-256 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 [[SHA256]].
    */
  def calculate(
    data: String,
    charset: Charset = StandardCharsets.UTF_8
  ): SHA256 =
    calculate(data.getBytes(charset))

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

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

    /** Get the byte at the given index (0 to 31).
      *
      * Throws an exception if an out-of-bound index is given.
      *
      * @param index
      *   The byte index (0 to 31).
      * @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 SHA-256 bound of $NumberOfBytes bytes."
        )
      else sha.apply(index)

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

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

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

end SHA256
