module System.Xen.Store
	( XsHandle
	, XsError(..)
	, Perm(..)
	, XsPerms(..)
	, XsPath
	, XsData
	, XsWatchCallback
	
	, initiateXS
	, terminateXS
	, withXS
	, tryXS
	
	, withTransaction
	, xsWrite
	, xsRead
	, xsMkdir
	, xsRm
	, xsDirectory
	, xsGetPerms
	, xsSetPerms
	, xsGetDomainPath
	, xsWatch
	, xsUnwatch
	) where
import Data.Word
import Data.List (intersperse)
import Data.ByteString (ByteString)
import qualified Data.ByteString as B
import Data.ByteString.Char8 as BC (pack, unpack)
import Data.Serialize
import Data.IORef
import Data.Typeable
import Control.Concurrent
import Control.Exception (catchJust, tryJust, bracketOnError, bracket, throwIO, Exception(..))
import qualified Control.Exception as E
import Control.Applicative ((<$>))
import Control.Monad
import System.IO
import Network.Socket
data Operation =
	  Debug
	| Directory
	| Read
	| GetPerms
	| Watch
	| Unwatch
	| TransactionStart
	| TransactionEnd
	| Introduce
	| Release
	| GetDomainPath
	| Write
	| Mkdir
	| Rm
	| SetPerms
	| WatchEvent
	| Error
	| IsIntroduced
	| Resume
	| SetTarget
	| Restrict
	deriving (Show,Eq,Enum)
data PacketHeader = PacketHeader
	{ pTransactionId :: Word32
	, pRequestId     :: Word32
	, pType          :: Operation
	, pLength        :: Word32
	} deriving (Show,Eq)
type PacketData = ByteString
type Packet = (PacketHeader, PacketData)
type XsPath = ByteString
type XsData = ByteString
type XsWatchCallback = XsPath -> IO ()
data Perm = PermNone | PermRead | PermWrite | PermRDWR
	deriving (Show,Eq)
data XsError = ErrorNoEnt | ErrorAgain | ErrorInval | ErrorOther ByteString
	deriving (Show,Eq,Typeable)
instance Exception XsError
data XsPerms = XsPerms
	{ permOwner :: Word32
	, permOther :: Perm
	, permACL   :: [(Word32,Perm)]
	}
instance Serialize Perm where
	put PermNone  = putWord8 $ fromIntegral $ fromEnum 'n'
	put PermRead  = putWord8 $ fromIntegral $ fromEnum 'r'
	put PermWrite = putWord8 $ fromIntegral $ fromEnum 'w'
	put PermRDWR  = putWord8 $ fromIntegral $ fromEnum 'b'
	get           = getWord8 >>= \w -> case toEnum $ fromIntegral w of
		'n' -> return PermNone
		'r' -> return PermRead
		'w' -> return PermWrite
		'b' -> return PermRDWR
		_   -> error "unknown permission"
		
instance Serialize XsPerms where
	put p = do
		let l = map putOne $ (permOwner p, permOther p) : permACL p
		putByteString $ B.intercalate "\0" l
		where putOne (did,perm) = B.concat [BC.pack $ show did, encode perm]
	get = do
		b <- remaining >>= getByteString
		let l = map getOne $ B.split 0 b
		case l of
			(owner, other) : acls -> return $ XsPerms owner other acls
			[]                    -> error "wrong permission format"
		where getOne b = do
			let (did, perm) = B.splitAt (B.length b  1) b
			case decode perm of
				Left err -> error err
				Right r  -> (read $ BC.unpack did, r)
instance Serialize PacketHeader where
	put p = 
	        (putWord32host $ fromIntegral $ fromEnum $ pType p) >>
	        (putWord32host $ pRequestId p)                      >>
	        (putWord32host $ pTransactionId p)                  >>
	        (putWord32host $ pLength p)
	get = do
		op  <- toEnum . fromIntegral <$> getWord32host
		rid <- getWord32host
		tid <- getWord32host
		len <- getWord32host
		return $ PacketHeader tid rid op len
data XsHandleState = XsHandleState
	{ xshWatchs  :: [((XsPath, XsData), XsWatchCallback)]
	, xshTID     :: Word32
	}
data XsHandle = XsHandle
	{ unXsHandle :: Handle
	, xshGrab    :: MVar Int
	, xshState   :: IORef XsHandleState
	}
initiateXS :: IO XsHandle
initiateXS = bracketOnError (socket AF_UNIX Stream 0) sClose $ \sock -> do
	connect sock (SockAddrUnix "/var/run/xenstored/socket")
	handle <- socketToHandle sock ReadWriteMode
	mvar   <- newMVar 0
	xshs   <- newIORef (XsHandleState [] 0)
	return $ XsHandle handle mvar xshs
terminateXS :: XsHandle -> IO ()
terminateXS = hClose . unXsHandle
withXS :: (XsHandle -> IO a) -> IO a
withXS f = bracket initiateXS terminateXS f
tryXS :: IO a -> IO (Maybe a)
tryXS f = either (const Nothing) Just <$> tryJust (\e -> if e == ErrorNoEnt then Just () else Nothing) f
modifyState xsh f = modifyIORef (xshState xsh) f
registerWatch :: XsHandle -> XsPath -> XsData -> XsWatchCallback -> IO ()
registerWatch xsh p d f =
	modifyState xsh (\st -> st { xshWatchs = ((p,d), f) : xshWatchs st })
unregisterWatch :: XsHandle -> XsPath -> XsData -> IO ()
unregisterWatch xsh p d =
	modifyState xsh (\st -> st { xshWatchs = filter (\z -> (p,d) /= fst z) $ xshWatchs st })
setTransaction xsh tid = modifyState xsh (\st -> st { xshTID = tid })
clearTransaction xsh   = setTransaction xsh 0
payloadArgs, payloadArgs0 :: [ByteString] -> ByteString
payloadArgs args = B.concat (intersperse "\0" args)
payloadArgs0 args = payloadArgs args `B.append` "\0"
packet :: Operation -> ByteString -> Packet
packet op payload = (PacketHeader 0 0 op (fromIntegral $ B.length payload), payload)
packetRead, packetMkdir, packetRm, packetDirectory, packetGetPerms :: XsPath -> Packet
packetRead      path = packet Read $ payloadArgs0 [path]
packetDirectory path = packet Directory $ payloadArgs0 [path]
packetGetPerms  path = packet GetPerms $ payloadArgs0 [path]
packetMkdir     path = packet Mkdir $ payloadArgs0 [path]
packetRm        path = packet Rm $ payloadArgs0 [path]
packetWrite, packetWatch, packetUnwatch :: XsPath -> XsData -> Packet
packetWatch   path b = packet Watch $ payloadArgs0 [path,b]
packetUnwatch path b = packet Unwatch $ payloadArgs0 [path,b]
packetWrite   path b = packet Write $ payloadArgs [path,b]
packetGetDomainPath :: Word32 -> Packet
packetGetDomainPath domid = packet GetDomainPath $ payloadArgs0 [BC.pack $ show domid]
packetTransactionStart :: Packet
packetTransactionStart = packet TransactionStart $ payloadArgs0 []
packetTransactionEnd :: Bool -> Packet
packetTransactionEnd commit = packet TransactionEnd $ payloadArgs0 [ if commit then "T" else "F" ]
packetSetPerms :: XsPath -> XsPerms -> Packet
packetSetPerms path perms = packet SetPerms $ payloadArgs0 [path, encode perms]
rpc :: XsHandle -> Packet -> IO Packet
rpc xsh (hdr, payload) = do
	let h = unXsHandle xsh
	grabbing $ do
		tid <- xshTID <$> readIORef (xshState xsh)
		B.hPut h (encodeHeader tid) >> B.hPut h payload >> hFlush h
		recvPacket h
	where
		encodeHeader tid = encode (hdr { pTransactionId = tid })
		recvPacket h = do
			(Right rhdr) <- decode <$> B.hGet h 16
			rpay         <- B.hGet h (fromIntegral $ pLength rhdr)
			case pType rhdr of
				Error      -> throwIO $ case rpay of
					"ENOENT\0" -> ErrorNoEnt
					"EAGAIN\0" -> ErrorAgain
					"EINVAL\0" -> ErrorInval
					_          -> ErrorOther rpay
				WatchEvent -> do
					callWatch rpay
					recvPacket h
				_          -> return (rhdr, rpay)
		callWatch rpay = case B.split 0 rpay of	
			[p,d] -> do
				w <- xshWatchs <$> readIORef (xshState xsh)
				case lookup (p,d) w of
					Nothing -> return ()
					Just f  -> f p
			_     -> 
				return ()
	
		grabbing f = do
			modifyMVar (xshGrab xsh) $ \v -> f >>= \r -> return (v+1, r)
withTransaction :: XsHandle -> IO a -> IO a
withTransaction xsh f = toRun
	where
		toRun = do
			xsTransactionStart xsh
			r <- E.catch f (\e -> xsTransactionEnd xsh False >> throwIOExn e)
			again <- catchJust (\e -> if e == ErrorAgain then Just () else Nothing)
			                   (xsTransactionEnd xsh True >> return False)
			                   (\_ -> return True)
			if again then toRun else return r
		throwIOExn :: E.SomeException -> IO a
		throwIOExn = throwIO
ack ((_, payload)) = when (payload /= "OK") $ do
	error "unexpected reply, expecting OK"
xsTransactionEnd :: XsHandle -> Bool -> IO ()
xsTransactionEnd xsh b = rpc xsh (packetTransactionEnd b) >>= ack >> clearTransaction xsh
xsTransactionStart :: XsHandle -> IO ()
xsTransactionStart xsh = rpc xsh packetTransactionStart >>= setTransaction xsh . read . BC.unpack . snd
xsWrite :: XsHandle -> XsPath -> XsData -> IO ()
xsWrite xsh p d = rpc xsh (packetWrite p d) >>= ack
xsUnwatch :: XsHandle -> XsPath -> XsData -> IO ()
xsUnwatch xsh p d = rpc xsh (packetUnwatch p d) >>= ack >> unregisterWatch xsh p d
xsWatch :: XsHandle -> XsPath -> XsData -> XsWatchCallback -> IO ()
xsWatch xsh p d f = rpc xsh (packetWatch p d) >>= ack >> registerWatch xsh p d f
xsRead :: XsHandle -> XsPath -> IO XsData
xsRead xsh p = snd <$> rpc xsh (packetRead p)
xsMkdir :: XsHandle -> XsPath -> IO ()
xsMkdir xsh p = rpc xsh (packetMkdir p) >>= ack
xsRm :: XsHandle -> XsPath -> IO ()
xsRm xsh p = rpc xsh (packetRm p) >>= ack
xsDirectory :: XsHandle -> XsPath -> IO [XsPath]
xsDirectory xsh p = B.split 0 . snd <$> rpc xsh (packetDirectory p)
xsGetPerms :: XsHandle -> XsPath -> IO XsPerms
xsGetPerms xsh p = do
	perms <- decode . snd <$> rpc xsh (packetGetPerms p)
	either error return perms
xsSetPerms :: XsHandle -> XsPath -> XsPerms -> IO ()
xsSetPerms xsh p perms = rpc xsh (packetSetPerms p perms) >>= ack
xsGetDomainPath :: XsHandle -> Word32 -> IO XsPath
xsGetDomainPath xsh d = snd <$> rpc xsh (packetGetDomainPath d)