-- |
-- Module      : Data.Git.Delta
-- License     : BSD-style
-- Maintainer  : Vincent Hanquez <vincent@snarc.org>
-- Stability   : experimental
-- Portability : unix
--
module Data.Git.Delta
    ( Delta(..)
    , DeltaCmd(..)
    , deltaParse
    , deltaRead
    , deltaApply
    ) where

import Data.Attoparsec
import qualified Data.Attoparsec as A
import qualified Data.Attoparsec.Lazy as AL
import Data.ByteString (ByteString)
import qualified Data.ByteString as B
import qualified Data.ByteString.Lazy as L
import Data.Bits
import Data.Word

import Control.Applicative ((<$>), many)

-- | a delta is a source size, a destination size and a list of delta cmd
data Delta = Delta Word64 Word64 [DeltaCmd]
    deriving (Show,Eq)

-- | possible commands in a delta
data DeltaCmd =
      DeltaCopy ByteString   -- command to insert this bytestring
    | DeltaSrc Word64 Word64 -- command to copy from source (offset, size)
    deriving (Show,Eq)

-- | parse a delta.
-- format is 2 variable sizes, followed by delta cmds. for each cmd:
-- * if first byte MSB is set, we copy from source.
-- * otherwise, we copy from delta.
-- * extensions are not handled.
deltaParse = do
    srcSize <- getDeltaHdrSize
    resSize <- getDeltaHdrSize
    dcmds   <- many (anyWord8 >>= parseWithCmd)
    return $ Delta srcSize resSize dcmds
  where
        getDeltaHdrSize = do
            z <- A.takeWhile (\w -> w `testBit` 7)
            l <- anyWord8
            return $ unbytes 0 $ (map (\w -> w `clearBit` 7) (B.unpack z) ++ [l])
        -- use a foldl ..
        unbytes _  []     = 0
        unbytes sh (x:xs) = (fromIntegral x) `shiftL` sh + unbytes (sh+7) xs
        -- parse one command, either an extension, a copy from src, or a copy from delta.
        parseWithCmd cmd
            | cmd == 0        = error "delta extension not supported"
            | cmd `testBit` 7 = do
                o1 <- word8cond (cmd `testBit` 0) 0
                o2 <- word8cond (cmd `testBit` 1) 8
                o3 <- word8cond (cmd `testBit` 2) 16 
                o4 <- word8cond (cmd `testBit` 3) 24
                s1 <- word8cond (cmd `testBit` 4) 0
                s2 <- word8cond (cmd `testBit` 5) 8
                s3 <- word8cond (cmd `testBit` 6) 16 
                let offset = o1 .|. o2 .|. o3 .|. o4
                let size   = s1 .|. s2 .|. s3
                return $ DeltaSrc offset (if size == 0 then 0x10000 else size)
            | otherwise       = DeltaCopy <$> A.take (fromIntegral cmd)
        word8cond cond sh =
            if cond then (flip shiftL sh . fromIntegral) <$> anyWord8 else return 0

-- | read one delta from a lazy bytestring.
deltaRead = AL.maybeResult . AL.parse deltaParse

-- | apply a delta on a lazy bytestring, returning a new bytestring.
deltaApply :: L.ByteString -> Delta -> L.ByteString
deltaApply src (Delta srcSize _ deltaCmds)
    | L.length src /= fromIntegral srcSize = error "source size do not match"
    | otherwise                            = -- FIXME use a bytestring builder here.
        L.fromChunks $ concatMap resolve deltaCmds
      where resolve (DeltaSrc o s) = L.toChunks $ takeAt (fromIntegral s) (fromIntegral o) src
            resolve (DeltaCopy b)  = [b]
            takeAt sz at = L.take sz . L.drop at