{- git long-running filter process
 -
 - As documented in git's gitattributes(5) and
 - Documentation/technical/long-running-process-protocol.txt
 -
 - Copyright 2021 Joey Hess <id@joeyh.name>
 -
 - Licensed under the GNU AGPL version 3 or higher.
 -}

{-# LANGUAGE OverloadedStrings #-}

module Git.FilterProcess (
	WelcomeMessage(..),
	Version(..),
	Capability(..),
	longRunningProcessHandshake,
	longRunningFilterProcessHandshake,
	FilterRequest(..),
	getFilterRequest,
	respondFilterRequest,
) where

import Common
import Git.PktLine

import qualified Data.ByteString as B

{- This is a message like "git-filter-client" or "git-filter-server" -}
data WelcomeMessage = WelcomeMessage PktLine
	deriving (Show)

{- Configuration message, eg "foo=bar" -}
data ConfigValue = ConfigValue String String
	deriving (Show, Eq)

encodeConfigValue :: ConfigValue -> PktLine
encodeConfigValue (ConfigValue k v) = stringPktLine (k <> "=" <> v)

decodeConfigValue :: PktLine -> Maybe ConfigValue
decodeConfigValue pktline =
	let t = pktLineToString pktline
	    (k, v) = break (== '=') t
	in if null v
		then Nothing
		else Just $ ConfigValue k (drop 1 v)

extractConfigValue :: [ConfigValue] -> String -> Maybe String
extractConfigValue [] _ = Nothing
extractConfigValue (ConfigValue k v:cs) wantk
	| k == wantk = Just v
	| otherwise = extractConfigValue cs wantk

data Version = Version Int
	deriving (Show, Eq)

encodeVersion :: Version -> PktLine
encodeVersion (Version n) = encodeConfigValue $ ConfigValue "version" (show n)

decodeVersion :: PktLine -> Maybe Version
decodeVersion pktline = decodeConfigValue pktline >>= \case
	ConfigValue "version" v -> Version <$> readish v
	_ -> Nothing

data Capability = Capability String
	deriving (Show, Eq)

encodeCapability :: Capability -> PktLine
encodeCapability (Capability c) = encodeConfigValue $ 
	ConfigValue "capability" c

decodeCapability :: PktLine -> Maybe Capability
decodeCapability pktline = decodeConfigValue pktline >>= \case
	ConfigValue "capability" c -> Just $ Capability c
	_ -> Nothing

longRunningProcessHandshake
	:: (WelcomeMessage -> Maybe WelcomeMessage)
	-> ([Version] -> [Version])
	-> ([Capability] -> [Capability])
	-> IO (Either String ())
longRunningProcessHandshake respwelcomemessage filterversions filtercapabilities =
	readUntilFlushPkt >>= \case
		[] -> protoerr "no welcome message"
		(welcomemessage:versions) -> 
			checkwelcomemessage welcomemessage $
				checkversion versions $ do
					capabilities <- readUntilFlushPkt
					checkcapabilities capabilities success
  where
	protoerr msg = return $ Left $ "git protocol error: " ++ msg
	success = return (Right ())

	checkwelcomemessage welcomemessage cont =
		case respwelcomemessage (WelcomeMessage welcomemessage) of
			Nothing -> protoerr "unsupported welcome message"
			Just (WelcomeMessage welcomemessage') -> do
				writePktLine stdout welcomemessage'
				cont

	checkversion versions cont = do
		let versions' = filterversions (mapMaybe decodeVersion versions)
		if null versions'
			then protoerr "unsupported protocol version"
			else do
				forM_ versions' $ \v ->
					writePktLine stdout $ encodeVersion v
				writePktLine stdout flushPkt
				cont
	
	checkcapabilities capabilities cont = do
		let capabilities' = filtercapabilities (mapMaybe decodeCapability capabilities)
		if null capabilities'
			then protoerr "unsupported protocol capabilities"
			else do
				forM_ capabilities' $ \c ->
					writePktLine stdout $ encodeCapability c
				writePktLine stdout flushPkt
				cont

longRunningFilterProcessHandshake :: IO (Either String ())
longRunningFilterProcessHandshake =
	longRunningProcessHandshake respwelcomemessage filterversions filtercapabilities
  where
	respwelcomemessage (WelcomeMessage w)
		| pktLineToString w == "git-filter-client" =
			Just $ WelcomeMessage $ stringPktLine "git-filter-server"
		| otherwise = Nothing
	filterversions = filter (== Version 2)
	-- Delay capability is not implemented, so filter it out.
	filtercapabilities = filter (`elem` [Capability "smudge", Capability "clean"])

data FilterRequest = Smudge FilePath | Clean FilePath
	deriving (Show, Eq)

{- Waits for the next FilterRequest to be received. Does not read
 - the content to be filtered, which is what gets sent subsequent to the
 - FilterRequest. Use eg readUntilFlushPkt to read it, before calling
 - respondFilterRequest. -}
getFilterRequest :: IO (Maybe FilterRequest)
getFilterRequest = do
	ps <- readUntilFlushPkt
	let cs = mapMaybe decodeConfigValue ps
	case (extractConfigValue cs "command", extractConfigValue cs "pathname") of
		(Just command, Just pathname)
			| command == "smudge" -> return $ Just $ Smudge pathname
			| command == "clean" -> return $ Just $ Clean pathname
			| otherwise -> return Nothing
		_ -> return Nothing

{- Send a response to a FilterRequest, consisting of the filtered content. -}
respondFilterRequest :: B.ByteString -> IO ()
respondFilterRequest b = do
	writePktLine stdout $ encodeConfigValue $ ConfigValue "status" "success"
	writePktLine stdout flushPkt
	send b
	-- The protocol allows for another list of ConfigValues to be sent
	-- here, but we don't use it. Send another flushPkt to terminate
	-- the empty list.
	writePktLine stdout flushPkt
  where
	send b' = 
		let (pktline, rest) = encodePktLine b'
		in do
			if isFlushPkt pktline
				then return ()
				else writePktLine stdout pktline
			case rest of
				Just b'' -> send b''
				Nothing -> writePktLine stdout flushPkt