module Data.Blockchain.Crypto.Hash
    ( Hash
    , ToHash(..)
    , hashToHex
    , fromByteString
    , unsafeFromByteString
    ) where

import qualified Crypto.Hash               as Crypto
import qualified Data.Aeson                as Aeson
import qualified Data.ByteArray.Encoding   as Byte
import qualified Data.ByteString           as BS
import qualified Data.ByteString.Lazy      as Lazy
import qualified Data.Hashable             as H
import qualified Data.Maybe                as Maybe
import qualified Data.Text                 as Text
import qualified Data.Text.Encoding        as Text

import qualified Data.Blockchain.Types.Hex as Hex

-- Types -----------------------------------------------------------------------------------------------------

newtype Hash a = Hash { unHash :: Crypto.Digest Crypto.SHA256 }
  deriving (Eq, Ord)

instance H.Hashable (Hash a) where
    hashWithSalt _ = H.hash . show

instance Show (Hash a) where
    show = show . unHash

instance Aeson.ToJSON (Hash a) where
    toJSON = Aeson.String . Text.pack . show . unHash

instance Aeson.FromJSON (Hash a) where
    parseJSON = Aeson.withText "Hash" $
        maybe (fail "Invalid bytestring hash") return . fromByteString . Text.encodeUtf8

instance Monoid (Hash a) where
    mempty = Hash $ Crypto.hash (mempty :: BS.ByteString)
    mappend (Hash h1) (Hash h2) = Hash $ Crypto.hashFinalize $ Crypto.hashUpdates Crypto.hashInit [h1, h2]

class ToHash a where
    hash :: a -> Hash a

    default hash :: Aeson.ToJSON a => a -> Hash a
    hash = Hash . Crypto.hash . Lazy.toStrict . Aeson.encode

instance ToHash BS.ByteString where
    hash = Hash . Crypto.hash

-- Utils -----------------------------------------------------------------------------------------------------

-- Note: ignore all possible invariants that `Numeric.readHex` would normally need to check.
-- We are only converting string-ified hashes, which should always be valid hex strings.
hashToHex :: Hash a -> Hex.Hex256
hashToHex = Maybe.fromMaybe (error "Unexpected hex conversion failure") . Hex.hex256 . show . unHash

fromByteString :: BS.ByteString -> Maybe (Hash a)
fromByteString bs = case Byte.convertFromBase Byte.Base16 bs of
    Left _    -> Nothing
    Right bs' -> Hash <$> Crypto.digestFromByteString (bs' :: BS.ByteString)

unsafeFromByteString :: BS.ByteString -> Hash a
unsafeFromByteString = Maybe.fromMaybe (error "Invalid hash string") . fromByteString