-- | Encoding and decoding hex strings

{-# LANGUAGE EmptyDataDecls #-}
module Bitcoin.Misc.HexString where 

--------------------------------------------------------------------------------

import Data.Array 

import Data.Char
import Data.Int
import Data.Word
import Data.Bits

-- import Control.Monad ( liftM )

import Bitcoin.Misc.OctetStream

--------------------------------------------------------------------------------

-- | The phantom type is used to encode endianness
newtype HexString = HexString { unHexString :: String } deriving (Eq,Show)

--------------------------------------------------------------------------------

toHexString :: OctetStream a => a -> HexString
toHexString = toHexString' False

toHexString' :: OctetStream a => Bool -> a -> HexString
toHexString' uppercase x = hexEncode' uppercase (toWord8List x)

toHexStringChars :: OctetStream a => a -> String
toHexStringChars = unHexString . toHexString

fromHexString :: OctetStream a => HexString -> a
fromHexString  = fromWord8List . hexDecode

--------------------------------------------------------------------------------

reverseHexString :: HexString -> HexString
reverseHexString = HexString . unsafeReverseHexString . unHexString

unsafeReverseHexString :: String -> String
unsafeReverseHexString = fromPairs . reverse . toPairs where
  toPairs :: [Char] -> [(Char,Char)]
  toPairs (x:y:rest) = (x,y) : toPairs rest
  toPairs [] = []  
  toPairs _  = error "unsafeReverseHexString/toPairs: odd number of characters"
  fromPairs :: [(Char,Char)] -> [Char]
  fromPairs = concatMap (\(x,y) -> [x,y])

--------------------------------------------------------------------------------
-- * encoding and decoding ByteStrings as (little-endian) hex strings

safeHexDecode :: String -> Maybe [Word8]
safeHexDecode s = if even (length s) && all isHexDigit s then Just (go s) else Nothing where 

  go (x:y:rest) = (shiftL (f x) 4 + f y) : go rest
  go [] = []
  go [x] = error "hexDecode: expecting even number of characters"

  f :: Char -> Word8
  f c | c >= '0' && c <= '9'  =  ordW c - 48
      | c >= 'A' && c <= 'F'  =  ordW c - 65 + 10
      | c >= 'a' && c <= 'f'  =  ordW c - 97 + 10
      | otherwise             =  error "hexDecode: unexpected character"

  ordW :: Char -> Word8
  ordW = fromIntegral . ord

{- 
-- already implemented in Data.Char
isHexDigit :: Char -> Bool
isHexDigit c =  (c >= '0' && c <= '9') 
             || (c >= 'a' && c <= 'f') 
             || (c >= 'A' && c <= 'F') 
-}
 
--------------------------------------------------------------------------------

-- | from "4142" to [0xAB]
hexDecode :: HexString -> [Word8]
hexDecode (HexString s) = case safeHexDecode s of
  Just ws -> ws
  Nothing -> error "hexDecode: input is not a hex string"

-- | from [0xAB] to "4142"
hexEncode :: [Word8] -> HexString 
hexEncode = hexEncode' False

hexEncode' :: Bool -> [Word8] -> HexString 
hexEncode' useCapitalLetters = HexString . concatMap worker where
  worker :: Word8 -> String
  worker w = [ table ! (fromIntegral $ shiftR w 4) , table ! (fromIntegral $ w .&. 15) ]
  table = if useCapitalLetters then capitalHexTable else smallHexTable

showHexWord8 :: Word8 -> String
showHexWord8 w = [ smallHexTable ! (fromIntegral $ shiftR w 4) , smallHexTable ! (fromIntegral $ w .&. 15) ]

capitalHexTable, smallHexTable :: Array Word8 Char
capitalHexTable = listArray (0,15) "0123456789ABCDEF"
smallHexTable   = listArray (0,15) "0123456789abcdef"

--------------------------------------------------------------------------------