module Data.Serialize.RLP.Internal (
  RLPEncodeable(..),

  -- * Helper Int functions
  toBigEndian,
  toBigEndianS,
  fromBigEndian,
  fromBigEndianS,

  -- * Helper String functions
  toByteString,
  toByteStringS,
  fromByteString,
  fromByteStringS,

  RLPT(..)
      ) where

import Data.Binary.Get
import Data.Binary.Put

import qualified Data.ByteString              as DBS
import qualified Data.ByteString.Char8        as DBSC
import qualified Data.ByteString.Lazy         as DBSL
import qualified Data.ByteString.Lazy.Char8   as DBSLC

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

-- | The 'RLPT' type represents the result of transforming the
-- initial data into its byte-array representation, taking in
-- account the structure of the fields.
--
-- Fields that can't be directly transformed into a ByteString (such
-- as a type with several fields) should generate a list with the
-- representations of its fields (using the RLPL constructor).
--
-- RLPT represents the T type defined in the Ethereum Yellowpaper for
-- defining the RLP protocol.
data RLPT = RLPL [RLPT] | RLPB DBS.ByteString
  deriving (Show, Eq) -- just for understanding pourposes and for checking with hspec

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

toBigEndian :: Int -> DBSL.ByteString
toBigEndian = DBSLC.dropWhile (== '\NUL') . runPut . putInt64be . fromIntegral

-- | Strict version of 'toBigEndian'
toBigEndianS :: Int -> DBS.ByteString
toBigEndianS = DBSL.toStrict . toBigEndian

fromBigEndian :: DBSL.ByteString -> Int
fromBigEndian bs =  fromIntegral . runGet getInt64be $ bs'
  where bs' = case () of
          _ | DBSL.length bs >= 8 -> bs
            | otherwise -> DBSLC.append (DBSLC.pack $ b) bs
                     where b = take (8 - (fromIntegral . DBSL.length $ bs)) (repeat '\NUL')

-- | Strict version of 'fromBigEndian'
fromBigEndianS :: DBS.ByteString -> Int
fromBigEndianS = fromBigEndian . DBSL.fromStrict

toByteString :: String -> DBSL.ByteString
toByteString = DBSLC.pack

-- | Strict version of 'toByteString'
toByteStringS :: String -> DBS.ByteString
toByteStringS = DBSC.pack

fromByteString :: DBSL.ByteString -> String
fromByteString = DBSLC.unpack

-- | Strict version of 'fromByteString'
fromByteStringS :: DBS.ByteString -> String
fromByteStringS = DBSC.unpack

-- | Internal function for spliting the array in chunks of bytes
rlpSplit :: DBSL.ByteString -> [DBSL.ByteString]
rlpSplit x
  | DBSL.null x        = []
  | DBSL.head x <  192 =
      case () of
        _ | DBSL.head x < 128 -> (DBSL.singleton . DBSL.head $ x) : (rlpSplit $ DBSL.tail x)
          | DBSL.head x < 183 ->
              let size = (fromIntegral $ DBSL.head x) - 128 :: Int in
                let total = size + 1 in
                  (DBSL.take (fromIntegral total) x) : (rlpSplit $ DBSL.drop (fromIntegral total) x)
          | otherwise        ->
                    let sizeSize = (fromIntegral $ DBSL.head x) - 183 :: Int in
                      let size = fromBigEndian . DBSL.take (fromIntegral sizeSize) . DBSL.tail $ x :: Int in
                        let total = sizeSize + size + 1 :: Int in
                          (DBSL.take (fromIntegral total) x) : (rlpSplit $ DBSL.drop (fromIntegral total) x)
  | DBSL.head x == 192 = (DBSL.singleton $ DBSL.head x) : (rlpSplit $ DBSL.tail x)
  | DBSL.head x <  247 =
      let size = (fromIntegral $ DBSL.head x) - 192 :: Int in
        let total = size + 1 in
          (DBSL.take (fromIntegral total) x) : (rlpSplit $ DBSL.drop (fromIntegral total) x)
  | otherwise          =
      let sizeSize = (fromIntegral $ DBSL.head x) - 247 :: Int in
        let size = fromBigEndian . DBSL.take (fromIntegral sizeSize) . DBSL.tail $ x :: Int in
          let total = sizeSize + size + 1 :: Int in
            (DBSL.take (fromIntegral total) x) : (rlpSplit $ DBSL.drop (fromIntegral total) x)

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

-- | The 'RLPEncodeable' class groups the RLPT, ByteString and Int types
-- for transforming them into ByteStrings.
--
-- This class defines only the functions for the types explicitly shown on the
-- Yellow Paper. This class intends to be internal and not be used outside the
-- RLPSerialize class.
class RLPEncodeable a where
  -- Use Put to encode the structure
  rlpEncodeI' :: a -> Put

  -- Mainly run rlpEncodeI'
  rlpEncodeI :: a -> DBSL.ByteString
  rlpEncodeI = runPut . rlpEncodeI'

  -- Use Get to parse the structure
  rlpDecodeI' :: Get a

  -- Mainly run rlpDecodeI'
  rlpDecodeI :: DBSL.ByteString -> Maybe a
  rlpDecodeI x = let r = runGetOrFail rlpDecodeI' x in
                   case r of
                     Left _          -> Nothing
                     Right (_, _, s) -> Just s

--------------------------------------------------------------------------------
-- Instances

instance RLPEncodeable RLPT where
  rlpEncodeI' (RLPB bs) = rlpEncodeI' bs
  rlpEncodeI' (RLPL t) = case () of
                      _ | DBSL.length dat < 56 -> (putWord8 . fromIntegral $ 192 + DBSL.length dat)
                                                  <> (putLazyByteString dat)
                        | otherwise -> (putWord8 . fromIntegral $ 247 + DBSL.length l)
                                       <> (putLazyByteString l)
                                       <> (putLazyByteString dat)
                            where l = toBigEndian . fromIntegral . DBSL.length $ dat
    where dat = DBSL.concat . map rlpEncodeI $ t

  rlpDecodeI' = do
    i <- getWord8
    case () of
      _ | i < 192 -> do         -- ByteArray
            ls <- getRemainingLazyByteString
            return . RLPB . (\(Just x) -> x) . rlpDecodeI $ DBSL.cons i ls
        | i == 192 -> do        -- Empty list
            return $ RLPL []
        | i < 247 -> do         -- Small list
            ls <- getLazyByteString . fromIntegral $ i - 192
            return $ RLPL . map ((\(Just x) -> x) . rlpDecodeI) . rlpSplit $ ls
        | otherwise -> do       -- Big List
            ls <- getLazyByteString . fromIntegral $ i - 247
            let k = fromBigEndian ls
            ls' <- getLazyByteString . fromIntegral $ k
            return $ RLPL . map ((\(Just x) -> x) . rlpDecodeI) . rlpSplit $ ls'

instance RLPEncodeable DBS.ByteString where
  rlpEncodeI' bs
    | (DBS.length bs == 1) && (DBS.head bs < (fromIntegral (128 :: Integer))) = putByteString bs
    | DBS.length bs < 56 = (putWord8 . fromIntegral $ 128 + DBS.length bs)
                           <> (putByteString bs)
    | otherwise = (putWord8 . fromIntegral $ 183 + DBSL.length l)
                  <> (putLazyByteString l)
                  <> (putByteString bs)
        where l = toBigEndian . DBS.length $ bs

  rlpDecodeI' = do
    i <- getWord8
    case () of
      _ | i < 128 -> return $ DBS.singleton i
        | i < 183 -> do
            ls <- getByteString . fromIntegral $ i - 128
            return ls
        | i < 192 -> do
            sbe <- getLazyByteString . fromIntegral $ i - 183
            ls <- getByteString . fromBigEndian $ sbe
            return ls
        | otherwise -> undefined

instance RLPEncodeable Int where
  rlpEncodeI' = rlpEncodeI' . DBSL.toStrict . toBigEndian

  rlpDecodeI' = do
    b <- rlpDecodeI' :: Get DBS.ByteString
    return . fromBigEndian . DBSL.fromStrict $ b