module Data.Git.Parser
    (
      Parser
    , P.Result(..)
    , eitherParse
    , eitherParseChunks
    , maybeParse
    , maybeParseChunks
    -- * Specific functions
    , word32
    , ref
    , referenceBin
    , referenceHex
    , vlf
    , tillEOL
    , skipEOL
    , skipASCII
    , takeUntilASCII
    , decimal
    , takeWhile1
    , string
    -- * Simple re-export
    , P.anyByte
    , P.byte
    , P.bytes
    , P.take
    , P.takeWhile
    , P.parse
    , P.parseFeed
    , P.takeAll
    , P.hasMore
    ) where

import qualified Data.ByteArray.Parse as P
import           Data.ByteArray (ByteArray)

import           Data.Bits
import           Data.Word (Word8, Word32)
import           Data.Char (isDigit)

import qualified Data.ByteString as B
import qualified Data.ByteString.Char8 as BC

import           Data.Git.Ref
import           Data.Git.Internal
import           Data.Git.Imports

type Parser = P.Parser B.ByteString

vlf :: Parser [Word8]
vlf = do
    bs <- P.takeWhile (\w -> w `testBit` 7)
    l  <- P.anyByte
    return $ (map (\w -> w `clearBit` 7) $ B.unpack bs) ++ [l]

word32 :: Parser Word32
word32 = be32 <$> P.take 4

ref, referenceBin, referenceHex :: HashAlgorithm hash => Parser (Ref hash)
ref = referenceBin
referenceBin = takeDigestSize hashAlg 1 fromBinary
referenceHex = takeDigestSize hashAlg 2 fromHex

takeDigestSize :: HashAlgorithm hash => hash -> Int -> (B.ByteString -> Ref hash) -> Parser (Ref hash)
takeDigestSize alg modifier constr = constr <$> P.take (modifier * hashDigestSize alg)

decimal :: (Read n, Num n) => Parser n
decimal = toNum <$> P.takeWhile (\x -> isDigit $ toEnum (fromIntegral x))
  where toNum = read . BC.unpack

string :: B.ByteString -> Parser ()
string = P.bytes

maybeParse :: Parser a -> B.ByteString -> Maybe a
maybeParse f = toMaybe . P.parse f

maybeParseChunks :: Parser a -> [B.ByteString] -> Maybe a
maybeParseChunks p []     = toMaybe $ P.parse p B.empty
maybeParseChunks p (i:is) = loop (P.parse p i) is
  where
    loop (P.ParseOK _ a) []     = Just a
    loop (P.ParseMore c) []     = toMaybe $ c Nothing
    loop (P.ParseMore c) (x:xs) = loop (c $ Just x) xs
    loop _               _      = Nothing

toMaybe :: P.Result t a -> Maybe a
toMaybe (P.ParseOK _ a) = Just a
toMaybe (P.ParseMore c) = toMaybe (c Nothing)
toMaybe _               = Nothing

eitherParse :: Parser a -> B.ByteString -> Either String a
eitherParse f = toEither . P.parse f

eitherParseChunks :: Show a => Parser a -> [B.ByteString] -> Either String a
eitherParseChunks p []     = toEither $ P.parse p B.empty
eitherParseChunks p (i:is) = loop (P.parse p i) is
  where
    loop (P.ParseOK _ a) []     = Right a
    loop (P.ParseMore c) []     = toEither $ c Nothing
    loop (P.ParseMore c) (x:xs) = loop (c $ Just x) xs
    loop ps              l      = Left ("eitherParseChunk: error: " <> show ps <> " : " <> show l)

toEither :: P.Result t b -> Either String b
toEither (P.ParseOK _ a) = Right a
toEither (P.ParseFail e) = Left e
toEither (P.ParseMore c) = toEither (c Nothing)

takeUntilASCII :: ByteArray byteArray => Char -> P.Parser byteArray byteArray
takeUntilASCII char = P.takeWhile (\c -> if fromEnum c < 0x80 then fromEnum c /= fromEnum char else True)

tillEOL :: Parser B.ByteString
tillEOL = P.takeWhile (/= asciiEOL)

skipEOL :: Parser ()
skipEOL = P.byte asciiEOL >> return ()

skipASCII :: Char -> Parser ()
skipASCII c
    | cp < 0x80 = P.byte (fromIntegral cp) >> return ()
    | otherwise = error ("skipASCII: " ++ show c ++ " not a valid ASCII character")
  where
    cp = fromEnum c

asciiEOL :: Word8
asciiEOL = fromIntegral $ fromEnum '\n'

isByte :: ByteArray byteArray => (Word8 -> Bool) -> P.Parser byteArray Word8
isByte predicate = do
    b <- P.anyByte
    if predicate b then return b else fail "isByte"

takeWhile1 :: (Word8 -> Bool) -> Parser B.ByteString
takeWhile1 predicate =
    B.cons <$> isByte predicate <*> P.takeWhile predicate