-- |
-- Module      : Data.Packer
-- License     : BSD-style
-- Maintainer  : Vincent Hanquez <vincent@snarc.org>
-- Stability   : experimental
-- Portability : unknown
--
-- Simple packing module.
--
-- This is a tradeoff between a more pure / builder (binary, cereal, builder)
-- and direct access to Storable or pointer manipulation
--
{-# LANGUAGE CPP #-}
module Data.Packer
    (
    -- * Types
      Packing
    , Unpacking
    , OutOfBoundUnpacking(..)
    , OutOfBoundPacking(..)
    , IsolationNotFullyConsumed(..)
    , Hole
    -- * Main methods
    , runUnpacking
    , tryUnpacking
    , runPacking
    , runPackingRes
    -- * Unpacking functions
    , unpackSkip
    , unpackSetPosition
    , unpackGetPosition
    , getWord8
    , getWord16
    , getWord16LE
    , getWord16BE
    , getWord32
    , getWord32LE
    , getWord32BE
    , getWord64
    , getWord64LE
    , getWord64BE
    , getBytes
    , getBytesCopy
    , getBytesWhile
    , getRemaining
    , getRemainingCopy
    , getStorable
    , getFloat32LE
    , getFloat32BE
    , getFloat64LE
    , getFloat64BE
    , isolate
    , endOfInput
    , countRemaining
    -- * Packing functions
    , packGetPosition
    , putWord8
    , putHoleWord8
    , putWord16
    , putWord16LE
    , putWord16BE
    , putHoleWord16
    , putHoleWord16LE
    , putHoleWord16BE
    , putWord32
    , putWord32LE
    , putWord32BE
    , putHoleWord32
    , putHoleWord32LE
    , putHoleWord32BE
    , putWord64
    , putWord64LE
    , putWord64BE
    , putHoleWord64
    , putHoleWord64LE
    , putHoleWord64BE
    , putBytes
    , putStorable
    , putFloat32LE
    , putFloat32BE
    , putFloat64LE
    , putFloat64BE
    , fillHole
    ) where

import Control.Applicative
import Data.Packer.Internal
import Data.Packer.Unsafe
import Data.Packer.IO
import Data.Packer.Endian
import Data.Packer.IEEE754
import Foreign.Ptr
import Foreign.ForeignPtr
import Data.ByteString (ByteString)
import qualified Data.ByteString as B
import qualified Data.ByteString.Internal as B (memcpy, unsafeCreate, toForeignPtr, fromForeignPtr)
import Data.Word
import Foreign.Storable
import System.IO.Unsafe
import qualified Control.Exception as E

#if __GLASGOW_HASKELL__ > 704
unsafeDoIO = unsafeDupablePerformIO
#else
unsafeDoIO = unsafePerformIO
#endif

-- | Peek and do an action on the result. just for convenience
{-# INLINE peekAnd #-}
peekAnd :: Storable a => (a -> b) -> Ptr a -> IO b
peekAnd f p = f <$> peek p

-- | Skip bytes
unpackSkip :: Int -> Unpacking ()
unpackSkip n = unpackCheckAct n (\_ -> return ())

-- | Get a Word8
getWord8 :: Unpacking Word8
getWord8 = unpackCheckAct 1 peek
{-# INLINE getWord8 #-}

-- | Get a Word16 in the host endianess.
--
-- It's recommended to use an explicit endianness (LE or BE)
-- when unserializing format.
getWord16 :: Unpacking Word16
getWord16 = unpackCheckAct 2 (peek . castPtr)
{-# INLINE getWord16 #-}

-- | Get a Word16 serialized in little endian.
getWord16LE :: Unpacking Word16
getWord16LE = unpackCheckAct 2 (peekAnd le16Host . castPtr)
{-# INLINE getWord16LE #-}

-- | Get a Word16 serialized in big endian.
getWord16BE :: Unpacking Word16
getWord16BE = unpackCheckAct 2 (peekAnd be16Host . castPtr)
{-# INLINE getWord16BE #-}

-- | Get a Word32 in the host endianess.
--
-- It's recommended to use an explicit endianness (LE or BE)
-- when unserializing format.
getWord32 :: Unpacking Word32
getWord32 = unpackCheckAct 4 (peek . castPtr)
{-# INLINE getWord32 #-}

-- | Get a Word32 serialized in little endian.
getWord32LE :: Unpacking Word32
getWord32LE = unpackCheckAct 4 (peekAnd le32Host . castPtr)
{-# INLINE getWord32LE #-}

-- | Get a Word32 serialized in big endian.
getWord32BE :: Unpacking Word32
getWord32BE = unpackCheckAct 4 (peekAnd be32Host . castPtr)
{-# INLINE getWord32BE #-}

-- | Get a Word64 in the host endianess.
--
-- It's recommended to use an explicit endianness (LE or BE)
-- when unserializing format.
getWord64 :: Unpacking Word64
getWord64 = unpackCheckAct 8 (peek . castPtr)
{-# INLINE getWord64 #-}

-- | Get a Word64 serialized in little endian.
getWord64LE :: Unpacking Word64
getWord64LE = unpackCheckAct 8 (peekAnd le64Host . castPtr)
{-# INLINE getWord64LE #-}

-- | Get a Word64 serialized in big endian.
getWord64BE :: Unpacking Word64
getWord64BE = unpackCheckAct 8 (peekAnd be64Host . castPtr)
{-# INLINE getWord64BE #-}

-- | Read a Float in little endian IEEE-754 format
getFloat32LE :: Unpacking Float
getFloat32LE = wordToFloat <$> getWord32LE

-- | Read a Float in big endian IEEE-754 format
getFloat32BE :: Unpacking Float
getFloat32BE = wordToFloat <$> getWord32BE

-- | Read a Double in little endian IEEE-754 format
getFloat64LE :: Unpacking Double
getFloat64LE = wordToDouble <$> getWord64LE

-- | Read a Double in big endian IEEE-754 format
getFloat64BE :: Unpacking Double
getFloat64BE = wordToDouble <$> getWord64BE

-- | Get a number of bytes in bytestring format.
--
-- The original block of memory is expected to live for the life of this bytestring,
-- and this is done so by holding the original ForeignPtr.
getBytes :: Int -> Unpacking ByteString
getBytes n = unpackCheckActRef n $ \fptr ptr -> do
                    o <- withForeignPtr fptr $ \origPtr -> return (ptr `minusPtr` origPtr)
                    return $ B.fromForeignPtr fptr o n

-- | Similar to 'getBytes' but copy the bytes to a new bytestring without making reference
-- to the original memory after the copy. this allow the original block of memory to go away.
getBytesCopy :: Int -> Unpacking ByteString
getBytesCopy n = B.copy <$> getBytes n

-- | Get the remaining bytes.
getRemaining :: Unpacking ByteString
getRemaining = unpackGetNbRemaining >>= getBytes

-- | Get the remaining bytes but copy the bytestring and drop any
-- reference from the original function.
getRemainingCopy :: Unpacking ByteString
getRemainingCopy = B.copy <$> getRemaining

-- | Get a number of bytes until in bytestring format.
--
-- this could be made more efficient
getBytesWhile :: (Word8 -> Bool) -> Unpacking (Maybe ByteString)
getBytesWhile predicate = unpackLookahead searchEnd >>= \mn -> maybe (return Nothing) (\n -> Just <$> getBytes n) mn
    where searchEnd :: Ptr Word8 -> Int -> IO (Maybe Int)
          searchEnd ptr sz = loop 0
            where loop :: Int -> IO (Maybe Int)
                  loop i
                     | i >= sz   = return $ Nothing
                     | otherwise = do w <- peek (ptr `plusPtr` i)
                                      if predicate w
                                          then loop (i+1)
                                          else return $ Just i

-- | Get an arbitrary type with the Storable class constraint.
--
-- The Storage method for sizeOf need to be constant size related
-- to the type. It cannot use any fields to define its size.
--
-- The sizeOf method is always going to be called with undefined,
-- so make sure sizeOf doesn't need the value of the type.
getStorable :: Storable a => Unpacking a
getStorable = get_ undefined
    where get_ :: Storable a => a -> Unpacking a
          get_ undefA = unpackCheckAct (sizeOf undefA) (peek . castPtr)

-- | Isolate N bytes from the unpacking, and create an isolated
-- context where only those N bytes are available.
--
-- If the sub unpacker doesn't consume all the bytes available,
-- this function will raises an exception
isolate :: Int -> Unpacking a -> Unpacking a
isolate n subUnpacker = unpackIsolate n subUnpacker

-- | Return True if there are no more bytes to be unpacked.
--
-- No input is consumed.
endOfInput :: Unpacking Bool
endOfInput = (== 0) <$> unpackGetNbRemaining

-- | Return the number of bytes remaining in the current Unpacking.
--
-- No input is consumed.
countRemaining :: Unpacking Int
countRemaining = unpackGetNbRemaining

-- | Put a Word8
putWord8 :: Word8 -> Packing ()
putWord8 w = packCheckAct 1 (\ptr -> poke (castPtr ptr) w)

-- | Put a Word8 Hole
putHoleWord8 :: Packing (Hole Word8)
putHoleWord8 = packHole 1 (\ptr w -> poke (castPtr ptr) w)

-- | Put a Word16 in the host endianess.
--
-- It's recommended to use an explicit endianness (LE or BE)
-- when serializing format.
putWord16 :: Word16 -> Packing ()
putWord16 w = packCheckAct 2 (\ptr -> poke (castPtr ptr) w)
{-# INLINE putWord16 #-}

-- | Put a Word16 serialized in little endian.
putWord16LE :: Word16 -> Packing ()
putWord16LE w = putWord16 (le16Host w)

-- | Put a Word16 serialized in big endian.
putWord16BE :: Word16 -> Packing ()
putWord16BE w = putWord16 (be16Host w)

-- | Put a Word16 Hole
putHoleWord16_ :: (Word16 -> Word16) -> Packing (Hole Word16)
putHoleWord16_ f = packHole 2 (\ptr w -> poke (castPtr ptr) (f w))

putHoleWord16, putHoleWord16BE, putHoleWord16LE :: Packing (Hole Word16)
-- | Put a Word16 Hole in host endian
putHoleWord16 = putHoleWord16_ id

-- | Put a Word16 Hole in big endian
putHoleWord16BE = putHoleWord16_ be16Host

-- | Put a Word16 Hole in little endian
putHoleWord16LE = putHoleWord16_ le16Host

-- | Put a Word32 in the host endianess.
--
-- It's recommended to use an explicit endianness (LE or BE)
-- when serializing format.
putWord32 :: Word32 -> Packing ()
putWord32 w = packCheckAct 4 (\ptr -> poke (castPtr ptr) w)
{-# INLINE putWord32 #-}

-- | Put a Word32 serialized in little endian.
putWord32LE :: Word32 -> Packing ()
putWord32LE w = putWord32 (le32Host w)

-- | Put a Word32 serialized in big endian.
putWord32BE :: Word32 -> Packing ()
putWord32BE w = putWord32 (be32Host w)

-- | Put a Word32 Hole
putHoleWord32_ :: (Word32 -> Word32) -> Packing (Hole Word32)
putHoleWord32_ f = packHole 4 (\ptr w -> poke (castPtr ptr) (f w))

putHoleWord32, putHoleWord32BE, putHoleWord32LE :: Packing (Hole Word32)
-- | Put a Word32 Hole in host endian
putHoleWord32 = putHoleWord32_ id

-- | Put a Word32 Hole in big endian
putHoleWord32BE = putHoleWord32_ be32Host

-- | Put a Word32 Hole in little endian
putHoleWord32LE = putHoleWord32_ le32Host

-- | Put a Word64 in the host endianess.
--
-- It's recommended to use an explicit endianness (LE or BE)
-- when serializing format.
putWord64 :: Word64 -> Packing ()
putWord64 w = packCheckAct 8 (\ptr -> poke (castPtr ptr) w)
{-# INLINE putWord64 #-}

-- | Put a Word64 serialized in little endian.
putWord64LE :: Word64 -> Packing ()
putWord64LE w = putWord64 (le64Host w)

-- | Put a Word64 serialized in big endian.
putWord64BE :: Word64 -> Packing ()
putWord64BE w = putWord64 (be64Host w)

-- | Put a Word64 Hole
putHoleWord64_ :: (Word64 -> Word64) -> Packing (Hole Word64)
putHoleWord64_ f = packHole 8 (\ptr w -> poke (castPtr ptr) (f w))

putHoleWord64, putHoleWord64BE, putHoleWord64LE :: Packing (Hole Word64)
-- | Put a Word64 Hole in host endian
putHoleWord64 = putHoleWord64_ id

-- | Put a Word64 Hole in big endian
putHoleWord64BE = putHoleWord64_ be64Host

-- | Put a Word64 Hole in little endian
putHoleWord64LE = putHoleWord64_ le64Host

-- | Write a Float in little endian IEEE-754 format
putFloat32LE :: Float -> Packing ()
putFloat32LE = putWord32LE . floatToWord

-- | Write a Float in big endian IEEE-754 format
putFloat32BE :: Float -> Packing ()
putFloat32BE = putWord32BE . floatToWord

-- | Write a Double in little endian IEEE-754 format
putFloat64LE :: Double -> Packing ()
putFloat64LE = putWord64LE . doubleToWord

-- | Write a Double in big endian IEEE-754 format
putFloat64BE :: Double -> Packing ()
putFloat64BE = putWord64BE . doubleToWord

-- | Put a Bytestring.
putBytes :: ByteString -> Packing ()
putBytes bs =
    packCheckAct len $ \ptr ->
    withForeignPtr fptr $ \ptr2 ->
    B.memcpy ptr (ptr2 `plusPtr` o) (fromIntegral len)
  where (fptr,o,len) = B.toForeignPtr bs

-- | Put an arbitrary type with the Storable class constraint.
putStorable :: Storable a => a -> Packing ()
putStorable a = packCheckAct (sizeOf a) (\ptr -> poke (castPtr ptr) a)

-- | Unpack a bytestring using a monadic unpack action.
runUnpacking :: Unpacking a -> ByteString -> a
runUnpacking action bs = unsafeDoIO $ runUnpackingIO bs action

-- | Similar to 'runUnpacking' but returns an Either type with an exception type in case of failure.
tryUnpacking :: Unpacking a -> ByteString -> Either E.SomeException a
tryUnpacking action bs = unsafeDoIO $ tryUnpackingIO bs action

-- | Run packing with a buffer created internally with a monadic action and return the bytestring
runPackingRes :: Int -> Packing a -> (a, ByteString)
runPackingRes sz action = unsafeDoIO $ runPackingIO sz action

-- | Run packing with a buffer created internally with a monadic action and return the bytestring
runPacking :: Int -> Packing a -> ByteString
runPacking sz action = snd $ runPackingRes sz action