{-# OPTIONS_HADDOCK hide #-}
{-# LANGUAGE FlexibleInstances #-}
-- Stream Types:    https://msdn.microsoft.com/en-us/library/dd303435.aspx
-- Data Types:      https://msdn.microsoft.com/en-us/library/dd305325.aspx
-- Data Stream:     https://msdn.microsoft.com/en-us/library/dd340794.aspx


module Database.Tds.Message.DataStream ( TypeInfo (..)
                                       , RawBytes (..)
                                       , getRawBytes
                                       , putRawBytes
                                       , Data (..)
                                       ) where

import Data.Monoid((<>))

import qualified Data.ByteString as B
import qualified Data.ByteString.Lazy as LB

import qualified Data.Text as T
import qualified Data.Text.Encoding as T

import Data.Word (Word8(..),Word16(..),Word32(..),Word64(..))
import Data.Int (Int8(..),Int16(..),Int32(..),Int64(..))

import Data.Binary (Put(..),Get(..),Binary(..))
import qualified Data.Binary.Put as Put
import qualified Data.Binary.Get as Get

import Data.Time (UTCTime(..))
import Data.UUID.Types (UUID)
import qualified Data.UUID.Types as UUID

import Database.Tds.Primitives.Null
import Database.Tds.Primitives.Money
import Database.Tds.Primitives.DateTime
import Database.Tds.Primitives.Float
import Database.Tds.Primitives.Decimal
import Database.Tds.Primitives.Collation





data TypeInfo = TINull       -- 0x1f
              | TIBit        -- 0x32
              | TIInt1       -- 0x30
              | TIInt2       -- 0x34
              | TIInt4       -- 0x38
              | TIInt8       -- 0x7f
              | TIMoney4     -- 0x7a
              | TIMoney8     -- 0x3c
              | TIDateTime4  -- 0x3a
              | TIDateTime8  -- 0x3d
              | TIFlt4       -- 0x3b
              | TIFlt8       -- 0x3e

              | TIBitN        -- 0x68
              | TIIntN1       -- 0x26
              | TIIntN2       -- 0x26
              | TIIntN4       -- 0x26
              | TIIntN8       -- 0x26
              | TIMoneyN4     -- 0x6e
              | TIMoneyN8     -- 0x6e
              | TIDateTimeN4  -- 0x6f
              | TIDateTimeN8  -- 0x6f
              | TIFltN4       -- 0x6d
              | TIFltN8       -- 0x6d

              | TIGUID                     -- 0x24
              | TIDecimalN !Precision !Scale -- 0x6a, 0x37(legacy)
              | TINumericN !Precision !Scale -- 0x6c, 0x3f(legacy)

              | TIChar !Word8                  -- 0x2f(legacy) -- [TODO] test
              | TIVarChar !Word8               -- 0x27(legacy) -- [TODO] test
              | TIBigChar !Word16 !Collation    -- 0xaf
              | TIBigVarChar !Word16 !Collation -- 0xa7
              | TIText !Word32 !Collation       -- 0x23

              | TINChar !Word16 !Collation    -- 0xef
              | TINVarChar !Word16 !Collation -- 0xe7
              | TINText !Word32 !Collation    -- 0x63

              | TIBinary !Word8        -- 0x2d(legacy) -- [TODO] test
              | TIVarBinary !Word8     -- 0x25(legacy) -- [TODO] test
              | TIBigBinary !Word16    -- 0xad
              | TIBigVarBinary !Word16 -- 0xa5
              | TIImage !Word32        -- 0x22
              deriving (Show)


-- https://msdn.microsoft.com/en-us/library/dd358284.aspx
-- https://msdn.microsoft.com/en-us/library/dd305325.aspx
getTypeInfo :: Get TypeInfo
getTypeInfo = f =<< Get.getWord8
  where
    f :: Word8 -> Get TypeInfo
    f 0x1f = return TINull
    f 0x32 = return TIBit
    f 0x30 = return TIInt1
    f 0x34 = return TIInt2
    f 0x38 = return TIInt4
    f 0x7f = return TIInt8
    f 0x7a = return TIMoney4
    f 0x3c = return TIMoney8
    f 0x3a = return TIDateTime4
    f 0x3d = return TIDateTime8
    f 0x3b = return TIFlt4
    f 0x3e = return TIFlt8

    f 0x68 = do
      _ <- Get.getWord8
      return TIBitN

    f 0x26 = do
      len <- Get.getWord8
      case len of
        1 -> return TIIntN1
        2 -> return TIIntN2
        4 -> return TIIntN4
        8 -> return TIIntN8
        _ -> fail "getTypeInfo: invalid data length"

    f 0x6e = do
      len <- Get.getWord8
      case len of
        4 -> return TIMoneyN4
        8 -> return TIMoneyN8
        _ -> fail "getTypeInfo: invalid data length"

    f 0x6f = do
      len <- Get.getWord8
      case len of
        4 -> return TIDateTimeN4
        8 -> return TIDateTimeN8
        _ -> fail "getTypeInfo: invalid data length"

    f 0x6d = do
      len <- Get.getWord8
      case len of
        4 -> return TIFltN4
        8 -> return TIFltN8
        _ -> fail "getTypeInfo: invalid data length"

    f 0x24 = do
      len <- Get.getWord8 -- 0x10 (16byte)
      case len of
        16 -> return TIGUID
        _ -> fail "getTypeInfo: invalid data length"

    f 0x37 = f 0x6a
    f 0x6a = do
      _ <- Get.getWord8
      TIDecimalN <$> Get.getWord8 -- precision
                 <*> Get.getWord8 -- scale

    f 0x3f = f 0x6c
    f 0x6c = do
      _ <- Get.getWord8
      TINumericN <$> Get.getWord8 -- precision
                 <*> Get.getWord8 -- scale

    -- [TODO] test
    -- [MEMO] no collation
    f 0x2f = TIChar <$> Get.getWord8

    -- [TODO] test
    -- [MEMO] no collation
    f 0x27 = TIVarChar <$> Get.getWord8

    f 0xaf = TIBigChar <$> Get.getWord16le
                       <*> getCollation -- collation

    f 0xa7 = TIBigVarChar <$> Get.getWord16le
                          <*> getCollation -- collation

    f 0x23 = TIText <$> Get.getWord32le
                    <*> getCollation -- collation

    f 0xef = TINChar <$> Get.getWord16le
                     <*> getCollation -- collation

    f 0xe7 = TINVarChar <$> Get.getWord16le
                        <*> getCollation -- collation

    f 0x63 = TINText <$> Get.getWord32le
                     <*> getCollation -- collation

    -- [TODO] test
    f 0x2d = TIBinary <$> Get.getWord8

    -- [TODO] test
    f 0x25 = TIVarBinary <$> Get.getWord8

    f 0xad = TIBigBinary <$> Get.getWord16le

    f 0xa5 = TIBigVarBinary <$> Get.getWord16le

    f 0x22 = TIImage <$> Get.getWord32le



-- https://msdn.microsoft.com/en-us/library/dd358284.aspx
-- https://msdn.microsoft.com/en-us/library/dd305325.aspx
putTypeInfo :: TypeInfo -> Put
putTypeInfo (TINull     ) = Put.putWord8 0x1f -- [TODO] test
putTypeInfo (TIBit      ) = Put.putWord8 0x32
putTypeInfo (TIInt1     ) = Put.putWord8 0x30
putTypeInfo (TIInt2     ) = Put.putWord8 0x34
putTypeInfo (TIInt4     ) = Put.putWord8 0x38
putTypeInfo (TIInt8     ) = Put.putWord8 0x7f
putTypeInfo (TIMoney4   ) = Put.putWord8 0x7a
putTypeInfo (TIMoney8   ) = Put.putWord8 0x3c
putTypeInfo (TIDateTime4) = Put.putWord8 0x3a
putTypeInfo (TIDateTime8) = Put.putWord8 0x3d
putTypeInfo (TIFlt4     ) = Put.putWord8 0x3b
putTypeInfo (TIFlt8     ) = Put.putWord8 0x3e

putTypeInfo (TIBitN      ) = Put.putWord8 0x68 >> Put.putWord8 1
putTypeInfo (TIIntN1     ) = Put.putWord8 0x26 >> Put.putWord8 1
putTypeInfo (TIIntN2     ) = Put.putWord8 0x26 >> Put.putWord8 2
putTypeInfo (TIIntN4     ) = Put.putWord8 0x26 >> Put.putWord8 4
putTypeInfo (TIIntN8     ) = Put.putWord8 0x26 >> Put.putWord8 8
putTypeInfo (TIMoneyN4   ) = Put.putWord8 0x6e >> Put.putWord8 4
putTypeInfo (TIMoneyN8   ) = Put.putWord8 0x6e >> Put.putWord8 8
putTypeInfo (TIDateTimeN4) = Put.putWord8 0x6f >> Put.putWord8 4
putTypeInfo (TIDateTimeN8) = Put.putWord8 0x6f >> Put.putWord8 8
putTypeInfo (TIFltN4     ) = Put.putWord8 0x6d >> Put.putWord8 4
putTypeInfo (TIFltN8     ) = Put.putWord8 0x6d >> Put.putWord8 8

putTypeInfo (TIGUID) = Put.putWord8 0x24 >> Put.putWord8 16
putTypeInfo (TIDecimalN p s) = do
  Put.putWord8 0x6a
  Put.putWord8 $ precisionToLen p -- [TODO] test
  Put.putWord8 p
  Put.putWord8 s
putTypeInfo (TINumericN p s) = do
  Put.putWord8 0x6c
  Put.putWord8 $ precisionToLen p -- [TODO] test
  Put.putWord8 p
  Put.putWord8 s


putTypeInfo (TIChar len) = do -- [TODO] test
  Put.putWord8 0x2f
  Put.putWord8 len
  -- [MEMO] no collation

putTypeInfo (TIVarChar len) = do -- [TODO] test
  Put.putWord8 0x27
  Put.putWord8 len
  -- [MEMO] no collation

putTypeInfo (TIBigChar len col) = do
  Put.putWord8 0xaf
  Put.putWord16le len
  putCollation col

putTypeInfo (TIBigVarChar len col) = do
  Put.putWord8 0xa7
  Put.putWord16le len
  putCollation col

putTypeInfo (TIText len col) = do
  Put.putWord8 0x23
  Put.putWord32le len
  putCollation col


putTypeInfo (TINChar len col) = do
  Put.putWord8 0xef
  Put.putWord16le len
  putCollation col

putTypeInfo (TINVarChar len col) = do
  Put.putWord8 0xe7
  Put.putWord16le len
  putCollation col

putTypeInfo (TINText len col) = do
  Put.putWord8 0x63
  Put.putWord32le len
  putCollation col


putTypeInfo (TIBinary len) = do -- [TODO] test
  Put.putWord8 0x2d
  Put.putWord8 len

putTypeInfo (TIVarBinary len) = do -- [TODO] test
  Put.putWord8 0x25
  Put.putWord8 len

putTypeInfo (TIBigBinary len) = do
  Put.putWord8 0xad
  Put.putWord16le len

putTypeInfo (TIBigVarBinary len) = do
  Put.putWord8 0xa5
  Put.putWord16le len

putTypeInfo (TIImage len) = do
  Put.putWord8 0x22
  Put.putWord32le len


instance Binary TypeInfo where
  put = putTypeInfo
  get = getTypeInfo




type RawBytes = Maybe B.ByteString

getRawBytes :: TypeInfo -> Get RawBytes
getRawBytes = f
  where

    get8n :: Get RawBytes
    get8n = do
      len <- Get.getWord8
      if len == 0
        then return Nothing
        else Just <$> (Get.getByteString $ fromIntegral len)

    get8s :: Get RawBytes
    get8s = do
      len <- Get.getWord8
      if len == 0xff
        then return Nothing
        else Just <$> (Get.getByteString $ fromIntegral len)

    get16s :: Get RawBytes
    get16s = do
      len <- Get.getWord16le
      if len == 0xffff
        then return Nothing
        else Just <$> (Get.getByteString $ fromIntegral len)

    get32s :: Get RawBytes
    get32s = do
      len <- Get.getWord32le
      if len == 0xffffffff
        then return Nothing
        else Just <$> (Get.getByteString $ fromIntegral len)

    f :: TypeInfo -> Get RawBytes
    f TINull      = return Nothing
    f TIBit       = Just <$> Get.getByteString 1
    f TIInt1      = Just <$> Get.getByteString 1
    f TIInt2      = Just <$> Get.getByteString 2
    f TIInt4      = Just <$> Get.getByteString 4
    f TIInt8      = Just <$> Get.getByteString 8
    f TIMoney4    = Just <$> Get.getByteString 4
    f TIMoney8    = Just <$> Get.getByteString 8
    f TIDateTime4 = Just <$> Get.getByteString 4
    f TIDateTime8 = Just <$> Get.getByteString 8
    f TIFlt4      = Just <$> Get.getByteString 4
    f TIFlt8      = Just <$> Get.getByteString 8

    f TIBitN  = get8n
    f TIIntN1 = get8n
    f TIIntN2 = get8n
    f TIIntN4 = get8n
    f TIIntN8 = get8n
    f TIMoneyN4 = get8n
    f TIMoneyN8 = get8n
    f TIDateTimeN4 = get8n
    f TIDateTimeN8 = get8n
    f TIFltN4 = get8n
    f TIFltN8 = get8n

    f TIGUID = get8n

    f (TIDecimalN _ _) = get8n
    f (TINumericN _ _) = get8n

    f (TIChar _) = get8s
    f (TIVarChar _) = get8s
    f (TIBigChar _ _) = get16s
    f (TIBigVarChar _ _) = get16s
    f (TIText _ _) = get32s

    f (TINChar _ _) = get16s
    f (TINVarChar _ _) = get16s
    f (TINText _ _) = get32s

    f (TIBinary _) = get8s
    f (TIVarBinary _) = get8s
    f (TIBigBinary _) = get16s
    f (TIBigVarBinary _) = get16s
    f (TIImage _) = get32s


putRawBytes :: TypeInfo -> RawBytes -> Put
putRawBytes = g
  where

    put8n :: RawBytes -> Put
    put8n Nothing = Put.putWord8 0
    put8n (Just bs) = do
      Put.putWord8 $ fromIntegral $ B.length bs
      Put.putByteString  bs

    put8s :: RawBytes -> Put
    put8s Nothing = Put.putWord8 0xff
    put8s (Just bs) = do
      Put.putWord8 $ fromIntegral $ B.length bs
      Put.putByteString  bs

    put16s :: RawBytes -> Put
    put16s Nothing = Put.putWord16le 0xffff
    put16s (Just bs) = do
      Put.putWord16le $ fromIntegral $ B.length bs
      Put.putByteString  bs

    put32s :: RawBytes -> Put
    put32s Nothing = Put.putWord32le 0xffffffff
    put32s (Just bs) = do
      Put.putWord32le $ fromIntegral $ B.length bs
      Put.putByteString  bs


    g :: TypeInfo -> RawBytes -> Put
    g TINull _ = return ()

    g TIBit Nothing        = error "putRawBytes: Nothing is not convertible to TIBit"
    g TIInt1 Nothing       = error "putRawBytes: Nothing is not convertible to TIInt1"
    g TIInt2 Nothing       = error "putRawBytes: Nothing is not convertible to TIInt2"
    g TIInt4 Nothing       = error "putRawBytes: Nothing is not convertible to TIInt4"
    g TIInt8 Nothing       = error "putRawBytes: Nothing is not convertible to TIInt8"
    g TIMoney4 Nothing     = error "putRawBytes: Nothing is not convertible to TIMoney4"
    g TIMoney8 Nothing     = error "putRawBytes: Nothing is not convertible to TIMoney8"
    g TIDateTime4 Nothing  = error "putRawBytes: Nothing is not convertible to TIDateTime4"
    g TIDateTime8 Nothing  = error "putRawBytes: Nothing is not convertible to TIDateTime8"
    g TIFlt4 Nothing       = error "putRawBytes: Nothing is not convertible to TIFlt4"
    g TIFlt8 Nothing       = error "putRawBytes: Nothing is not convertible to TIFlt8"

    g TIBit (Just bs)       = Put.putByteString bs
    g TIInt1 (Just bs)      = Put.putByteString bs
    g TIInt2 (Just bs)      = Put.putByteString bs
    g TIInt4 (Just bs)      = Put.putByteString bs
    g TIInt8 (Just bs)      = Put.putByteString bs
    g TIMoney4 (Just bs)    = Put.putByteString bs
    g TIMoney8 (Just bs)    = Put.putByteString bs
    g TIDateTime4 (Just bs) = Put.putByteString bs
    g TIDateTime8 (Just bs) = Put.putByteString bs
    g TIFlt4 (Just bs)      = Put.putByteString bs
    g TIFlt8 (Just bs)      = Put.putByteString bs

    g TIBitN rb       = put8n rb
    g TIIntN1 rb      = put8n rb
    g TIIntN2 rb      = put8n rb
    g TIIntN4 rb      = put8n rb
    g TIIntN8 rb      = put8n rb
    g TIMoneyN4 rb    = put8n rb
    g TIMoneyN8 rb    = put8n rb
    g TIDateTimeN4 rb = put8n rb
    g TIDateTimeN8 rb = put8n rb
    g TIFltN4 rb      = put8n rb
    g TIFltN8 rb      = put8n rb

    g TIGUID rb = put8n rb

    g (TIDecimalN _ _) rb = put8n rb
    g (TINumericN _ _) rb = put8n rb

    g (TIChar _) rb = put8s rb
    g (TIVarChar _) rb = put8s rb
    g (TIBigChar _ _) rb = put16s rb
    g (TIBigVarChar _ _) rb = put16s rb
    g (TIText _ _) rb = put32s rb

    g (TINChar _ _) rb = put16s rb
    g (TINVarChar _ _) rb = put16s rb
    g (TINText _ _) rb = put32s rb

    g (TIBinary _) rb = put8s rb
    g (TIVarBinary _) rb = put8s rb
    g (TIBigBinary _) rb = put16s rb
    g (TIBigVarBinary _) rb = put16s rb
    g (TIImage _) rb = put32s rb




validNull :: TypeInfo -> a -> a
validNull  = f
  where
    f :: TypeInfo -> a -> a
    f TINull x = x
    f ti _ = error $ "validNull: " <> (show ti) <> " is not convertible from/to Null"



validIntegral :: String -> TypeInfo -> a -> a
validIntegral ht ti x = f ti x
  where
    f :: TypeInfo -> a -> a
    f TIBit x = x
    f TIInt1 x = x
    f TIInt2 x = x
    f TIInt4 x = x
    f TIInt8 x = x
    f TIBitN x = x
    f TIIntN1 x = x
    f TIIntN2 x = x
    f TIIntN4 x = x
    f TIIntN8 x = x
    f _ _ = error $ "validIntegral: " <> (show ti) <> " is not convertible from/to " <> ht

validBool = validIntegral "Bool"
validInt = validIntegral "Int"
validInteger = validIntegral "Integer"
validMaybeBool = validIntegral "(Maybe Bool)"
validMaybeInt = validIntegral "(Maybe Int)"
validMaybeInteger = validIntegral "(Maybe Integer)"

isIntegralN :: TypeInfo -> Bool
isIntegralN = f
  where
    f :: TypeInfo -> Bool
    f TIBit = False
    f TIInt1 = False
    f TIInt2 = False
    f TIInt4 = False
    f TIInt8 = False
    f TIBitN = True
    f TIIntN1 = True
    f TIIntN2 = True
    f TIIntN4 = True
    f TIIntN8 = True


getIntegral :: Integral a => TypeInfo -> Get a
getIntegral TIBit  = fromIntegral <$> Get.getWord8
getIntegral TIInt1 = fromIntegral <$> Get.getInt8
getIntegral TIInt2 = fromIntegral <$> Get.getInt16le
getIntegral TIInt4 = fromIntegral <$> Get.getInt32le
getIntegral TIInt8 = fromIntegral <$> Get.getInt64le
getIntegral TIBitN  = getIntegral TIBit
getIntegral TIIntN1 = getIntegral TIInt1
getIntegral TIIntN2 = getIntegral TIInt2
getIntegral TIIntN4 = getIntegral TIInt4
getIntegral TIIntN8 = getIntegral TIInt8


putIntegral :: Integral a => TypeInfo -> a -> Put
putIntegral TIBit  i = Put.putWord8 $ fromIntegral i
putIntegral TIInt1 i = Put.putInt8 $ fromIntegral i
putIntegral TIInt2 i = Put.putInt16le $ fromIntegral i
putIntegral TIInt4 i = Put.putInt32le $ fromIntegral i
putIntegral TIInt8 i = Put.putInt64le $ fromIntegral i
putIntegral TIBitN  i = putIntegral TIBit i
putIntegral TIIntN1 i = putIntegral TIInt1 i
putIntegral TIIntN2 i = putIntegral TIInt2 i
putIntegral TIIntN4 i = putIntegral TIInt4 i
putIntegral TIIntN8 i = putIntegral TIInt8 i




validMoney :: TypeInfo -> a -> a
validMoney  = f
  where
    f :: TypeInfo -> a -> a
    f TIMoney4 x = x
    f TIMoney8 x = x
    f TIMoneyN4 x = x
    f TIMoneyN8 x = x
    f ti _ = error $ "validMoney: " <> (show ti) <> " is not convertible from/to Money"

isMoneyN :: TypeInfo -> Bool
isMoneyN = f
  where
    f :: TypeInfo -> Bool
    f TIMoney4 = False
    f TIMoney8 = False
    f TIMoneyN4 = True
    f TIMoneyN8 = True

getMoney :: TypeInfo -> Get Money
getMoney TIMoney4 = bytesToMoney4 <$> Get.getInt32le
getMoney TIMoney8 = bytesToMoney8 <$> Get.getInt32le <*> Get.getInt32le
getMoney TIMoneyN4 = getMoney TIMoney4
getMoney TIMoneyN8 = getMoney TIMoney8

putMoney :: TypeInfo -> Money -> Put
putMoney TIMoney4 f = Put.putInt32le $ moneyToBytes4 f
putMoney TIMoney8 f = do
  let (m,l) = moneyToBytes8 f
  Put.putInt32le m
  Put.putInt32le l
putMoney TIMoneyN4 f = putMoney TIMoney4 f
putMoney TIMoneyN8 f = putMoney TIMoney8 f




validUTCTime :: TypeInfo -> a -> a
validUTCTime  = f
  where
    f :: TypeInfo -> a -> a
    f TIDateTime4 x = x
    f TIDateTime8 x = x
    f TIDateTimeN4 x = x
    f TIDateTimeN8 x = x
    f ti _ = error $ "validUTCTime: " <> (show ti) <> " is not convertible from/to UTCTime"

isUTCTimeN :: TypeInfo -> Bool
isUTCTimeN = f
  where
    f :: TypeInfo -> Bool
    f TIDateTime4 = False
    f TIDateTime8 = False
    f TIDateTimeN4 = True
    f TIDateTimeN8 = True

getUTCTime :: TypeInfo -> Get UTCTime
getUTCTime TIDateTime4 = bytesToUtc4 <$> Get.getWord16le <*> Get.getWord16le
getUTCTime TIDateTime8 = bytesToUtc8 <$> Get.getInt32le <*> Get.getWord32le
getUTCTime TIDateTimeN4 = getUTCTime TIDateTime4
getUTCTime TIDateTimeN8 = getUTCTime TIDateTime8

putUTCTime :: TypeInfo -> UTCTime -> Put
putUTCTime TIDateTime4 time = do
  let (wday,wmin) = utcToBytes4 time
  Put.putWord16le wday
  Put.putWord16le wmin
putUTCTime TIDateTime8 time = do
  let (iday,w3hsec) = utcToBytes8 time
  Put.putInt32le iday
  Put.putWord32le w3hsec
putUTCTime TIDateTimeN4 time = putUTCTime TIDateTime4 time
putUTCTime TIDateTimeN8 time = putUTCTime TIDateTime8 time




validFloat' :: String -> TypeInfo -> a -> a
validFloat' hn ti x = f ti x
  where
    f :: TypeInfo -> a -> a
    f TIFlt4 x = x
    f TIFlt8 x = x
    f TIFltN4 x = x
    f TIFltN8 x = x
    f ti _ = error $ "validFloat': " <> (show ti) <> " is not convertible from/to " <> hn

validFloat = validFloat' "Float"
validDouble = validFloat' "Double"
validMaybeFloat = validFloat' "(Maybe Float)"
validMaybeDouble = validFloat' "(Maybe Double)"

isFloatN :: TypeInfo -> Bool
isFloatN = f
  where
    f :: TypeInfo -> Bool
    f TIFlt4 = False
    f TIFlt8 = False
    f TIFltN4 = True
    f TIFltN8 = True

getFloat :: Fractional a => TypeInfo -> Get a
getFloat TIFlt4 = realToFrac . wordToFloat <$> Get.getWord32le
getFloat TIFlt8 = realToFrac . wordToDouble <$> Get.getWord64le
getFloat TIFltN4 = getFloat TIFlt4
getFloat TIFltN8 = getFloat TIFlt8

putFloat :: Real a => TypeInfo -> a -> Put
putFloat TIFlt4 f = Put.putWord32le $ floatToWord $ realToFrac f
putFloat TIFlt8 f = Put.putWord64le $ doubleToWord $ realToFrac f
putFloat TIFltN4 f = putFloat TIFlt4 f
putFloat TIFltN8 f = putFloat TIFlt8 f




validDecimal :: TypeInfo -> a -> a
validDecimal  = f
  where
    f :: TypeInfo -> a -> a
    f (TIDecimalN _ _) x = x
    f (TINumericN _ _) x = x
    f ti _ = error $ "validDecimal: " <> (show ti) <> " is not convertible from/to Decimal"

-- https://msdn.microsoft.com/en-us/library/ee780893.aspx
-- [MEMO] sign byte + signed bytes
getDecimal :: Int -> Precision -> Scale -> Get Decimal
getDecimal len p s =
  bytesToDecimal p s <$> Get.getWord8 <*> (Get.getByteString $ fromIntegral $ len -1)

putDecimal :: Decimal -> Put
putDecimal dec = do -- [TODO] test
  let (s,bs) = decimalToBytes dec
  Put.putWord8 s
  Put.putByteString bs




validUUID :: TypeInfo -> a -> a
validUUID  = f
  where
    f :: TypeInfo -> a -> a
    f TIGUID x = x
    f ti _ = error $ "validUUID: " <> (show ti) <> " is not convertible from/to UUID"




validByteString :: TypeInfo -> a -> a
validByteString  = f
  where
    f :: TypeInfo -> a -> a
    f (TIChar _) x = x
    f (TIVarChar _) x = x
    f (TIBigChar _ _) x = x
    f (TIBigVarChar _ _) x = x
    f (TIText _ _) x = x
    f (TIBinary _) x = x
    f (TIVarBinary _) x = x
    f (TIBigBinary _) x = x
    f (TIBigVarBinary _) x = x
    f (TIImage _) x = x
    f ti _ = error $ "validByteString: " <> (show ti) <> " is not convertible from/to ByteString"




validText :: TypeInfo -> a -> a
validText  = f
  where
    f :: TypeInfo -> a -> a
    f (TINChar _ _) x = x
    f (TINVarChar _ _) x = x
    f (TINText _ _) x= x
    f ti _ = error $ "validText: " <> (show ti) <> " is not convertible from/to Text"




runGet :: Get a -> B.ByteString -> a
runGet f bs = Get.runGet f $ LB.fromStrict bs

runPut :: Put -> B.ByteString
runPut f = LB.toStrict $ Put.runPut f


runGetBool :: TypeInfo -> B.ByteString -> Bool
runGetBool ti bs = (/=0) $ runGet (getIntegral ti) bs

runPutBool :: TypeInfo -> Bool -> B.ByteString
runPutBool ti b = runPut $ putIntegral ti $ if b then 1 else 0



-- [TODO] check nullable flag

class Data a where
  fromRawBytes :: TypeInfo -> RawBytes -> a
  toRawBytes :: TypeInfo -> a -> RawBytes


instance Data Null where
  fromRawBytes ti rb = validNull ti $ f rb
    where
      f Nothing = Null
      f _ = error "Null.fromRawBytes: non-Null value is not convertible to Null"
  toRawBytes ti _ = validNull ti $ Nothing

instance Data Bool where
  fromRawBytes ti (Just bs) = validBool ti $ runGetBool ti bs
  fromRawBytes ti Nothing = validBool ti $ error "Bool.fromRawBytes: Null value is not convertible to Bool"
  toRawBytes ti b = validBool ti $ Just $ runPutBool ti b

instance Data Int where
  fromRawBytes ti (Just bs) = validInt ti $ runGet (getIntegral ti) bs
  fromRawBytes ti Nothing = validInt ti $ error "Int.fromRawBytes: Null value is not convertible to Int"
  toRawBytes ti i = validInt ti $ Just $ runPut $ putIntegral ti i

instance Data Integer where
  fromRawBytes ti (Just bs) = validInteger ti $ runGet (getIntegral ti) bs
  fromRawBytes ti Nothing = validInteger ti $ error "Integer.fromRawBytes: Null value is not convertible to Integer"
  toRawBytes ti i = validInteger ti $ Just $ runPut $ putIntegral ti i

instance Data Money where
  fromRawBytes ti (Just bs) = validMoney ti $ runGet (getMoney ti) bs
  fromRawBytes ti Nothing = validMoney ti $ error "Money.fromRawBytes: Null value is not convertible to Money"
  toRawBytes ti m = validMoney ti $ Just $ runPut $ putMoney ti m

instance Data UTCTime where
  fromRawBytes ti (Just bs) = validUTCTime ti $ runGet (getUTCTime ti) bs
  fromRawBytes ti Nothing = validUTCTime ti $ error "UTCTime.fromRawBytes: Null value is not convertible to UTCTime"
  toRawBytes ti dt = validUTCTime ti $ Just $ runPut $ putUTCTime ti dt

instance Data Float where
  fromRawBytes ti (Just bs) = validFloat ti $ runGet (getFloat ti) bs
  fromRawBytes ti Nothing = validFloat ti $ error "Float.fromRawBytes: Null value is not convertible to Float"
  toRawBytes ti f = validFloat ti $ Just $ runPut $ putFloat ti f

instance Data Double where
  fromRawBytes ti (Just bs) = validDouble ti $ runGet (getFloat ti) bs
  fromRawBytes ti Nothing = validDouble ti $ error "Double.fromRawBytes: Null value is not convertible to Double"
  toRawBytes ti f = validDouble ti $ Just $ runPut $ putFloat ti f

instance Data Decimal where
  fromRawBytes ti (Just bs) = validDecimal ti $
    let (p,s) = ps ti
    in runGet (getDecimal (B.length bs) p s) bs
    where
      ps :: TypeInfo -> (Precision,Scale)
      ps (TIDecimalN p s) = (p,s)
      ps (TINumericN p s) = (p,s)
  fromRawBytes ti Nothing = validDecimal ti $ error "Decimal.fromRawBytes: Null value is not convertible to Decimal"
  toRawBytes ti dec = validDecimal ti $ Just $ runPut $ putDecimal dec

instance Data UUID where
  fromRawBytes ti (Just bs) = validUUID ti $ case UUID.fromByteString $ LB.fromStrict bs of
                                               Nothing -> error "UUID.fromRawBytes: UUID.fromBtyteString error"
                                               Just (uuid) -> uuid
  fromRawBytes ti Nothing = validUUID ti $ error "UUID.fromRawBytes: Null value is not convertible to UUID"
  toRawBytes ti uuid = validUUID ti $ Just $ LB.toStrict $ UUID.toByteString uuid

instance Data B.ByteString where
  fromRawBytes ti (Just bs) = validByteString ti $ bs
  fromRawBytes ti Nothing = validByteString ti $ error "ByteString.fromRawBytes: Null value is not convertible to ByteString"
  toRawBytes ti bs = validByteString ti $ Just $ bs

instance Data T.Text where
  fromRawBytes ti (Just bs) = validText ti $ T.decodeUtf16LE bs
  fromRawBytes ti Nothing = validText ti $ error "Text.fromRawBytes: Null value is not convertible to Text"
  toRawBytes ti t = validText ti $ Just $ T.encodeUtf16LE t



instance Data (Maybe Bool) where
  fromRawBytes ti rb = validMaybeBool ti $ runGetBool ti <$> rb
  toRawBytes ti b = validMaybeBool ti $
                    case b of
                      Nothing | (not . isIntegralN) ti -> error $ "(Maybe Bool).toRawBytes: Nothing is not convertible to " <> (show ti)
                      _ -> runPutBool ti <$> b

instance Data (Maybe Int) where
  fromRawBytes ti rb = validMaybeInt ti $ (runGet (getIntegral ti)) <$> rb
  toRawBytes ti i = validMaybeInt ti $
                    case i of
                      Nothing | (not . isIntegralN) ti -> error $ "(Maybe Int).toRawBytes: Nothing is not convertible to " <> (show ti)
                      _ -> runPut . (putIntegral ti) <$> i

instance Data (Maybe Integer) where
  fromRawBytes ti rb = validMaybeInteger ti $ (runGet (getIntegral ti)) <$> rb
  toRawBytes ti i = validMaybeInteger ti $
                    case i of
                      Nothing | (not . isIntegralN) ti -> error $ "(Maybe Integer).toRawBytes: Nothing is not convertible to " <> (show ti)
                      _ -> runPut . (putIntegral ti) <$> i

instance Data (Maybe Money) where
  fromRawBytes ti rb = validMoney ti $ (runGet (getMoney ti)) <$> rb
  toRawBytes ti m = validMoney ti $
                    case m of
                      Nothing | (not . isMoneyN) ti -> error $ "(Maybe Money).toRawBytes: Nothing is not convertible to " <> (show ti)
                      _ -> runPut . (putMoney ti) <$> m

instance Data (Maybe UTCTime) where
  fromRawBytes ti rb = validUTCTime ti $ (runGet (getUTCTime ti)) <$> rb
  toRawBytes ti dt = validUTCTime ti $
                     case dt of
                       Nothing | (not . isUTCTimeN) ti -> error $ "(Maybe UTCTime).toRawBytes: Nothing is not convertible to " <> (show ti)
                       _ -> runPut . (putUTCTime ti) <$> dt

instance Data (Maybe Float) where
  fromRawBytes ti rb = validMaybeFloat ti $ (runGet (getFloat ti)) <$> rb
  toRawBytes ti f = validMaybeFloat ti $
                    case f of
                      Nothing | (not . isFloatN) ti -> error $ "(Maybe Float).toRawBytes: Nothing is not convertible to " <> (show ti)
                      _ -> runPut . (putFloat ti) <$> f

instance Data (Maybe Double) where
  fromRawBytes ti rb = validMaybeDouble ti $ (runGet (getFloat ti)) <$> rb
  toRawBytes ti f = validMaybeDouble ti $
                    case f of
                      Nothing | (not . isFloatN) ti -> error $ "(Maybe Double).toRawBytes: Nothing is not convertible to " <> (show ti)
                      _ -> runPut . (putFloat ti) <$> f

instance Data (Maybe Decimal) where
  fromRawBytes ti rb = validDecimal ti $
    let (p,s) = ps ti
    in (\bs -> runGet (getDecimal (B.length bs) p s) bs) <$> rb
    where
      ps :: TypeInfo -> (Precision,Scale)
      ps (TIDecimalN p s) = (p,s)
      ps (TINumericN p s) = (p,s)
  toRawBytes ti dec = validDecimal ti $ runPut . putDecimal <$> dec

instance Data (Maybe UUID) where
  fromRawBytes ti rb = validUUID ti $ f <$> rb
    where
      f :: B.ByteString -> UUID
      f bs = case UUID.fromByteString $ LB.fromStrict bs of
               Nothing -> error "(Maybe UUID).fromRawBytes: UUID.fromBtyteString error"
               Just (uuid) -> uuid
  toRawBytes ti m = validUUID ti $ (LB.toStrict . UUID.toByteString) <$> m

instance Data (Maybe B.ByteString) where
  fromRawBytes ti rb = validByteString ti $ rb
  toRawBytes ti bs = validByteString ti $ bs

instance Data (Maybe T.Text) where
  fromRawBytes ti rb = validText ti $ T.decodeUtf16LE <$> rb
  toRawBytes ti t = validText ti $ T.encodeUtf16LE <$> t