{-# LANGUAGE DeriveDataTypeable #-}
module Storage.Hashed.Hash( Hash(..), encodeBase64u, decodeBase64u
                          , encodeBase16, decodeBase16, sha256, rawHash
                          , match ) where

import qualified Bundled.SHA256 as SHA
import qualified Data.ByteString as BS
import qualified Data.ByteString.Internal as BSI
import qualified Data.ByteString.Lazy as BL

import qualified Codec.Binary.Base64Url as B64U
import qualified Codec.Binary.Base16 as B16

import Data.Maybe( isJust, fromJust )
import Data.Char( toLower, toUpper )

import Data.Data( Data )
import Data.Typeable( Typeable )

data Hash = SHA256 !BS.ByteString
          | SHA1 !BS.ByteString
          | NoHash
            deriving (Show, Eq, Ord, Read, Typeable, Data)

base16 :: BS.ByteString -> BS.ByteString
debase16 :: BS.ByteString -> Maybe BS.ByteString
base64u :: BS.ByteString -> BS.ByteString
debase64u :: BS.ByteString -> Maybe BS.ByteString

base16 = BS.pack . map (BSI.c2w . toLower) . B16.encode . BS.unpack
base64u = BS.pack . map BSI.c2w . B64U.encode . BS.unpack
debase64u bs = case B64U.decode $ map BSI.w2c $ BS.unpack bs of
                 Just s -> Just $ BS.pack s
                 Nothing -> Nothing
debase16 bs = case B16.decode $ map (toUpper . BSI.w2c) $ BS.unpack bs of
                Just s -> Just $ BS.pack s
                Nothing -> Nothing

encodeBase64u :: Hash -> BS.ByteString
encodeBase64u (SHA256 bs) = base64u bs
encodeBase64u (SHA1 bs) = base64u bs
encodeBase64u NoHash = BS.empty

-- | Produce a base16 (ascii-hex) encoded string from a hash. This can be
-- turned back into a Hash (see "decodeBase16". This is a loss-less process.
encodeBase16 :: Hash -> BS.ByteString
encodeBase16 (SHA256 bs) = base16 bs
encodeBase16 (SHA1 bs) = base16 bs
encodeBase16 NoHash = BS.empty

-- | Take a base64/url-encoded string and decode it as a "Hash". If the string
-- is malformed, yields NoHash.
decodeBase64u :: BS.ByteString -> Hash
decodeBase64u bs
    | BS.length bs == 44 && isJust (debase64u bs) = SHA256 (fromJust $ debase64u bs)
    | BS.length bs == 28 && isJust (debase64u bs) = SHA1 (fromJust $ debase64u bs)
    | otherwise = NoHash

-- | Take a base16-encoded string and decode it as a "Hash". If the string is
-- malformed, yields NoHash.
decodeBase16 :: BS.ByteString -> Hash
decodeBase16 bs | BS.length bs == 64 && isJust (debase16 bs) = SHA256 (fromJust $ debase16 bs)
                | BS.length bs == 40 && isJust (debase16 bs) = SHA1 (fromJust $ debase16 bs)
                | otherwise = NoHash

-- | Compute a sha256 of a (lazy) ByteString. However, although this works
-- correctly for any bytestring, it is only efficient if the bytestring only
-- has a sigle chunk.
sha256 :: BL.ByteString -> Hash
sha256 bits = SHA256 (SHA.sha256 $ BS.concat $ BL.toChunks bits)

rawHash :: Hash -> BS.ByteString
rawHash NoHash = error "Cannot obtain raw hash from NoHash."
rawHash (SHA1 s) = s
rawHash (SHA256 s) = s

match :: Hash -> Hash -> Bool
NoHash `match` _ = False
_ `match` NoHash = False
x `match` y = x == y
