{-# LANGUAGE OverloadedStrings, TemplateHaskell, DeriveDataTypeable, ScopedTypeVariables, FlexibleInstances #-}
module Database.Cypher (
	Cypher,
	Entity,
	CypherResult(..),
	runCypher,
	cypher,
	CypherException(..),
	Hostname,
	Port,
	OneTuple(..)
	) where

import Data.Aeson
import Data.Aeson.TH
import Data.Aeson.Types
import Network.HTTP.Conduit
import Network.HTTP.Types
import Data.Conduit
import Data.Typeable
import Data.Text (Text)
import Data.Tuple.OneTuple
import Control.Exception
import Control.Applicative
import Control.Monad
import Data.Monoid
import Control.Monad.IO.Class (liftIO)
import qualified Data.ByteString as S
import qualified Data.ByteString.Lazy as L

data DBInfo = DBInfo Hostname Port Manager
type Hostname = S.ByteString
type Port = Int

-- | All interaction with Neo4j is done through the Cypher monad. Use 'cypher' to add a query to the monad.
newtype Cypher a = Cypher {
	uncypher :: (DBInfo -> ResourceT IO a)
}

-- | Raw result data returned by Neo4j. Only use this if you care about column headers.
data CypherResult a = CypherResult {
	rescolumns :: [Text],
	resdata :: a
} deriving (Show, Eq)

data CypherRequest = CypherRequest {
	req_query :: Text,
	req_params :: Value
} deriving (Show, Eq)

-- | A neo4j node or edge
data Entity a = Entity {
	entity_id :: Text,
	entity_data :: a
} deriving (Show, Eq)

instance FromJSON a => FromJSON (Entity a) where
	parseJSON (Object v) = Entity <$>
							v .: "self" <*>
							v .: "data"
	parseJSON _ = mempty

instance ToJSON (Entity a) where
	toJSON = toJSON . entity_id

instance FromJSON a => FromJSON (OneTuple a) where
	parseJSON x = do
		[l] <- parseJSON x
		return $ OneTuple l

instance ToJSON a => ToJSON (OneTuple a) where
	toJSON = toJSON . (\x->[x])

$(deriveJSON (drop 3) ''CypherResult)
$(deriveJSON (drop 4) ''CypherRequest)

-- | An error in handling a Cypher query, either in communicating with the server or parsing the result
data CypherException = CypherServerException Status ResponseHeaders L.ByteString | 
					   CypherClientParseException S.ByteString deriving (Show, Typeable)
instance Exception CypherException

throwClientParse bs = throw $ CypherClientParseException $ S.concat $ L.toChunks bs

instance Monad Cypher where
	return a = Cypher (const (return a))
	(Cypher  cmd) >>= f =
		Cypher $ \con-> do
			a <- cmd con
			uncypher (f a) con

class FromCypher a where
	fromCypher :: L.ByteString -> a

instance FromCypher () where
	fromCypher _ = ()

instance FromJSON a => FromCypher (CypherResult a) where
	fromCypher bs = 
		case decode bs of
			Just x -> x
			Nothing -> throwClientParse bs

instance FromJSON a => FromCypher [a] where
	fromCypher bs =
case decode bs of
			Just (CypherResult _ ds) -> ds
			_ -> throwClientParse bs

instance FromJSON a => FromCypher (OneTuple a) where
	fromCypher bs =
		case decode bs of
			Just (CypherResult _ [d]) -> d
			_ -> throwClientParse bs

instance (FromJSON a, FromJSON b) => FromCypher (a,b) where
	fromCypher bs =
		case decode bs of
			Just (CypherResult _ [d]) -> d
			_ -> throwClientParse bs

instance (FromJSON a, FromJSON b, FromJSON c) => FromCypher (a,b,c) where
	fromCypher bs =
		case decode bs of
			Just (CypherResult _ [d]) -> d
			_ -> throwClientParse bs

instance FromJSON a => FromCypher (Maybe a) where
	fromCypher bs =
		case decode bs of
			Just (CypherResult _ [a]) -> Just a
			Just (CypherResult _ []) -> Nothing
			_ -> throwClientParse bs

-- | Perform a cypher query
cypher :: FromCypher a => Text -> Value -> Cypher a
cypher txt params = Cypher $ \(DBInfo h p m)-> do
	let req = def { host = h, port = p,
					path = "db/data/cypher",
					requestBody = RequestBodyLBS (encode $ CypherRequest txt params),
					requestHeaders = headerAccept "application/json" : headerContentType "application/json" : requestHeaders def,
					method = "POST",
					checkStatus = (\_ _-> Nothing)
				  }
	r <- httpLbs req m
	let sci = statusCode (responseStatus r)
	if 200 <= sci && sci < 300 then return (fromCypher (responseBody r))
		else throw $ CypherServerException (responseStatus r) (responseHeaders r) (responseBody r)

-- | Execute some number of cypher queries
runCypher :: Cypher a -> Hostname -> Port -> IO a
runCypher c h p =
	runResourceT $ do
    	manager <- liftIO $ newManager def
    	uncypher c (DBInfo h p manager)