module Data.PropertyList.Binary.Get where
import Control.Applicative
import Control.Monad
import Control.Monad.Trans.Error ()
import Data.Bits
import qualified Data.ByteString.Char8 as BSC8
import qualified Data.ByteString.Lazy as BL
import Data.PropertyList.Binary.Float
import Data.PropertyList.Binary.Types
import Data.Serialize.Get
import qualified Data.Sequence as Seq
import qualified Data.Text as Text
import qualified Data.Text.Encoding as Text
import Data.Time
import qualified Data.Vector.Unboxed as V
import Data.Word
import GHC.Float
rawBPList bs = do
    let headerBS = BL.take 8 bs
    header@(BPListHeader version) <- runGetLazy bplistHeader headerBS
    when (version .&. 0xff00 /= 0x3000) $
        Left "Unsupported bplist version"
    
    let trailerBS = BL.drop (BL.length bs  bplistTrailerBytes) bs
    trailer <- runGetLazy bplistTrailer trailerBS
    
    
    let nOffsets :: Num a => a
        nOffsets        = fromIntegral (numObjects trailer)
        bytesPerOffset  = fromIntegral (offsetIntSize trailer)
        offsetsBS
            = BL.take (nOffsets * fromIntegral bytesPerOffset)
            . BL.drop (fromIntegral (offsetTableOffset trailer))
            $ bs
    offsets <- runGetLazy (replicateM nOffsets (sizedInt bytesPerOffset)) offsetsBS
    
    return (RawBPList bs header (V.fromList offsets) trailer)
readBPListRecords :: BL.ByteString -> Either String (BPListRecords Abs)
readBPListRecords bs = do
    raw <- rawBPList bs
    let tlr  = rawTrailer raw
        ct   = numObjects tlr
        root = topObject tlr
    recs <- mapM (getBPListRecord raw) [0 .. ct  1]
    return (BPListRecords root (Seq.fromList recs))
getBPListRecord (RawBPList bs _hdr offsets tlr) objNum
    | objNum >= 0 && fromIntegral objNum < V.length offsets
    = runGetLazy (bplistRecord objRef) (BL.drop (fromIntegral (offsets V.! fromIntegral objNum)) bs)
    
    | otherwise = Left "getBPListRecord: index out of range"
    where
        objRef = sizedInt (fromIntegral (objectRefSize tlr))
asciiString str = do
    let bs = BSC8.pack str
    bs' <- getByteString (BSC8.length bs)
    if (bs == bs') 
        then return ()
        else fail ("Expecting " ++ show str)
bplistHeaderBytes = 8
bplistHeader = do
    asciiString "bplist"
    BPListHeader <$> getWord16be
bplistTrailerBytes = 32
bplistTrailer =
    const BPListTrailer
        <$> skip 5          
        <*> getWord8        
        <*> getWord8        
        <*> getWord8        
        <*> getWord64be     
        <*> getWord64be     
        <*> getWord64be     
bplistRecord ref = msum
    [ const BPLNull     <$> bplNull
    , BPLBool           <$> bplTrue
    , BPLBool           <$> bplFalse
    , const BPLFill     <$> bplFill
    , BPLInt            <$> bplInt
    , BPLReal           <$> bplFloat32
    , BPLReal           <$> bplFloat64
    , BPLDate           <$> bplDate
    , BPLData           <$> bplData
    , BPLString         <$> bplASCII
    , BPLString         <$> bplUTF16
    , BPLUID            <$> bplUID
    , BPLArray          <$> bplArray ref
    , BPLSet            <$> bplSet ref
    , uncurry BPLDict   <$> bplDict ref
    ]
word8 b = do
    b' <- getWord8
    if b == b'
        then return b
        else fail ("expecting " ++ show b)
bplNull  = word8 0x00
bplTrue  = word8 0x08 >> return False
bplFalse = word8 0x09 >> return True
bplFill  = word8 0x0f
bplInt = do
    sz <- shiftL 1 . fromIntegral <$> halfByte 0x1
    i  <- sizedInt sz
    return (interpretBPLInt sz i)
bplFloat32 = do
    word8 0x22
    float2Double <$> getFloat32be
bplFloat64 = do
    word8 0x23
    getFloat64be
bplDate = do
    word8 0x33
    interpretBPLDate . word64ToDouble <$> getWord64be
bplData = do
    sz <- markerAndSize 0x4
    getByteString sz
bplASCII = do
    sz <- markerAndSize 0x5
    BSC8.unpack <$> getByteString sz
bplUTF16 = do
    sz <- markerAndSize 0x6
    Text.unpack . Text.decodeUtf16BE <$> getByteString (2*sz)
bplUID = do
    sz <- fmap (1+) (halfByte 0x8)
    sizedInt (fromIntegral sz)
bplArray ref = do
    sz <- markerAndSize 0xA
    replicateM sz ref
bplSet ref = do
    sz <- markerAndSize 0xC
    replicateM sz ref
bplDict ref = do
    sz <- markerAndSize 0xD
    ks <- replicateM sz ref
    vs <- replicateM sz ref
    return (ks, vs)
halfByte x = do
    marker <- getWord8
    if marker `shiftR` 4 == x
        then return (marker .&. 0x0f)
        else fail ("expecting marker " ++ show x)
markerAndSize x = do
    marker <- halfByte x
    case marker of
        0xf -> do
            intSz <- shiftL 1 . fromIntegral <$> halfByte 0x1
            sizedInt intSz
        _   -> return (fromIntegral marker)
sizedInt :: (Integral i, Bits i) => Word -> Get i
sizedInt 0 = return 0
sizedInt 1 = fromIntegral <$> getWord8
sizedInt 2 = fromIntegral <$> getWord16be
sizedInt 4 = fromIntegral <$> getWord32be
sizedInt 8 = fromIntegral <$> getWord64be
sizedInt n
    | n < 0     = fail ("sizedInt: negative size: " ++ show n)
    | otherwise = do
        let a = n `shiftR` 1; b = n  a
        x <- sizedInt a
        y <- sizedInt b
        return ((x `shiftL` (fromIntegral b * 8)) .|. y)
interpretBPLInt :: Word -> Integer -> Integer
interpretBPLInt sz i
    | isSigned && testBit i signBit     = i  bit nBits
    | otherwise                         = i
    where
        isSigned = sz >= 8
        nBits = fromIntegral sz * 8
        signBit  = nBits  1
interpretBPLDate :: Double -> UTCTime
interpretBPLDate sec = addUTCTime (realToFrac sec) epoch
    where
        epoch = UTCTime (fromGregorian 2001 1 1) 0
getFloat32be :: Get Float
getFloat32be = do
    d <- getWord32be
    return $! word32ToFloat d
getFloat64be :: Get Double
getFloat64be = do
    d <- getWord64be
    return $! word64ToDouble d