module Network.EasyBitcoin.Internal.ByteString
 where

import Data.Binary
import qualified Data.ByteString as BS
import qualified Data.ByteString.Base16 as B16
import qualified Data.ByteString.Char8 as C ( pack, unpack)
import Control.Monad (guard,(<=<))
import Control.Applicative
import Data.List (unfoldr)
import Data.Bits ((.|.), shiftL, shiftR)
import Data.Binary.Put  (Put, runPut)
import qualified Data.ByteString.Lazy  as BL
import Data.Binary.Get  ( Get, runGetOrFail, getByteString, ByteOffset, runGet)
import Data.Binary.Get ( getWord64be
                       , getWord32be
                       , getWord64le
                       , getWord8
                       , getWord16le
                       , getWord32le
                       , getByteString
                       , Get
                       )
import Data.Binary.Put( putWord64be
                      , putWord32be
                      , putWord32le
                      , putWord64le
                      , putWord16le
                      , putWord8
                      , putByteString
                      )

encode' :: Binary a => a -> BS.ByteString
encode' = toStrictBS . encode



decode' :: Binary a => BS.ByteString -> a
decode' = decode . toLazyBS

-- ByteString helpers
-- | Transforms a lazy bytestring into a strict bytestring
toStrictBS :: BL.ByteString -> BS.ByteString
toStrictBS = BS.concat . BL.toChunks



decodeToMaybe :: Binary a => BS.ByteString -> Maybe a
decodeToMaybe bs = case decodeOrFail $ toLazyBS bs of
                    Left  (lbs,o,err)  -> Nothing
                    Right (lbs,o,res)  -> Just res


integerToBS :: Integer -> BS.ByteString
integerToBS 0 = BS.pack [0]
integerToBS i 
    | i > 0     = BS.pack $ reverse $ unfoldr f i
    | otherwise = error "integerToBS not defined for negative values"
  where 
    f 0 = Nothing
    f x = Just $ (fromInteger x :: Word8, x `shiftR` 8)

-- | Decode a big endian Integer from a bytestring
bsToInteger :: BS.ByteString -> Integer
bsToInteger = (foldr f 0) . reverse . BS.unpack
  where 
    f w n = (toInteger w) .|. shiftL n 8



bsToHex :: BS.ByteString -> String
bsToHex = C.unpack . B16.encode

hexToBS :: String -> Maybe BS.ByteString
hexToBS xs = guard (bad == BS.empty) >> return x
   where
    (x, bad) = B16.decode $ C.pack xs



--bsToString :: BS.ByteString -> String
--bsToString = C.unpack


-- | Strict version of @Data.Binary.runPut@
runPut' :: Put -> BS.ByteString
runPut' = toStrictBS . runPut




-- | Isolate a Data.Binary.Get monad for the next @Int@ bytes. Only the next
-- @Int@ bytes of the input bytestring will be available for the Get monad to
-- consume. This function will fail if the Get monad fails or some of the input
-- is not consumed.
isolate :: Binary a => Int -> Get a -> Get a
isolate i g = do 
                 bs <- getByteString i
                 case runGetOrFail' g bs of
                  Left (_, _, err)           -> fail err
                  Right (unconsumed, _, res)
                    | BS.null unconsumed     -> return res
                    | otherwise              -> fail "Isolate: unconsumed input"





-- | Strict version of @Data.Binary.runGet@
runGet' :: Binary a => Get a -> BS.ByteString -> a
runGet' m = runGet m . toLazyBS






newtype VarInt = VarInt { getVarInt :: Int } deriving (Eq, Show, Read)

instance Binary VarInt where

    get = VarInt <$> ( getWord8 >>= go )
       where
        go 0xff = fromIntegral <$> getWord64le
        go 0xfe = fromIntegral <$> getWord32le
        go 0xfd = fromIntegral <$> getWord16le
        go x    = fromIntegral <$> return x

    put (VarInt x)
        | x < 0xfd        = putWord8 (fromIntegral x)
        | x <= 0xffff     = putWord8 0xfd >> putWord16le (fromIntegral x)
        | x <= 0xffffffff = putWord8 0xfe >> putWord32le (fromIntegral x)
        | otherwise       = putWord8 0xff >> putWord64le (fromIntegral x)

----------------------------------------------------------------------------------------------
-- | Transforms a strict bytestring into a lazy bytestring
toLazyBS :: BS.ByteString -> BL.ByteString
toLazyBS bs = BL.fromChunks [bs]





runGetOrFail' :: Binary a => Get a -> BS.ByteString -> Either (BS.ByteString, ByteOffset, String) (BS.ByteString, ByteOffset, a)
runGetOrFail' m bs = case runGetOrFail m $ toLazyBS bs of
                        Left (lbs,o,err) -> Left (toStrictBS lbs,o,err)
                        Right (lbs,o,res) -> Right (toStrictBS lbs,o,res)