{-# LANGUAGE FlexibleInstances, OverloadedStrings, GADTs #-}

-- TODO: operator fixity

-- | Functions from the ReQL (RethinkDB Query Language)

module Database.RethinkDB.Functions where

import Data.Text (Text)
import Control.Monad.State
import Control.Applicative
import Data.Maybe

import Database.RethinkDB.ReQL
import Database.RethinkDB.MapReduce
import Database.RethinkDB.Objects as O

import Database.RethinkDB.Protobuf.Ql2.Term.TermType

import Prelude (($), return, Double, Bool, String)
import qualified Prelude as P

infixl 6 +, -
infixl 7 *, /

-- | Addition or concatenation
--
-- Use the Num instance, or a qualified operator.
--
-- > >>> run h $ 2 + 5 :: IO (Maybe Int)
-- > Just 7
-- > >>> run h $ 2 R.+ 5 :: IO (Maybe Int)
-- > Just 7
-- > >>> run h $ (str "foo") + (str "bar") :: IO (Just String)
-- > Just "foobar"
(+) :: (Expr a, Expr b) => a -> b -> ReQL
(+) a b = op ADD (a, b) ()

-- | Subtraction
--
-- > >>> run h $ 2 - 5 :: IO (Maybe Int)
-- > Just (-3)
(-) :: (Expr a, Expr b) => a -> b -> ReQL
(-) a b = op SUB (a, b) ()

-- | Multiplication
--
-- > >>> run h $ 2 * 5 :: IO (Maybe Int)
-- > Just 10
(*) :: (Expr a, Expr b) => a -> b -> ReQL
(*) a b = op MUL (a, b) ()

-- | Division
--
-- > >>> run h $ 2 R./ 5 :: IO (Maybe Double)
-- > Just 0.4
(/) :: (Expr a, Expr b) => a -> b -> ReQL
(/) a b = op DIV (a, b) ()

-- | Mod
--
-- > >>> run h $ 5 `mod` 2 :: IO (Maybe Int)
-- > Just 1
mod :: (Expr a, Expr b) => a -> b -> ReQL
mod a b = op MOD (a, b) ()

infixr 2 ||
infixr 3 &&

-- | Boolean or
--
-- > >>> run h $ True R.|| False :: IO (Maybe Bool)
-- > Just True
(||) :: (Expr a, Expr b) => a -> b -> ReQL
a || b = op ANY (a, b) ()

-- | Boolean and
--
-- > >>> run h $ True R.&& False :: IO (Maybe Bool)
-- > Just False
(&&) :: (Expr a, Expr b) => a -> b -> ReQL
a && b = op ALL (a, b) ()

infix 4 ==, /=

-- | Test for equality
--
-- > >>> run h $ obj ["a" := 1] R.== obj ["a" := 1] :: IO (Maybe Bool)
-- > Just True
(==) :: (Expr a, Expr b) => a -> b -> ReQL
a == b = op EQ (a, b) ()

-- | Test for inequality
--
-- > >>> run h $ 1 R./= False :: IO (Maybe Bool)
-- > Just True
(/=) :: (Expr a, Expr b) => a -> b -> ReQL
a /= b = op NE (a, b) ()

infix 4 >, <, <=, >=

-- | Greater than
--
-- > >>> run h $ 3 R.> 2 :: IO (Maybe Bool)
-- > Just True
(>) :: (Expr a, Expr b) => a -> b -> ReQL
a > b = op GT (a, b) ()

-- | Lesser than
--
-- > >>> run h $ (str "a") R.< (str "b") :: IO (Maybe Bool)
-- > Just True
(<) :: (Expr a, Expr b) => a -> b -> ReQL
a < b = op LT (a, b) ()

-- | Greater than or equal to
--
-- > >>> run h $ [1] R.>= () :: IO (Maybe Bool)
-- > Just False
(>=) :: (Expr a, Expr b) => a -> b -> ReQL
a >= b = op GE (a, b) ()

-- | Lesser than or equal to
--
-- > >>> run h $ 2 R.<= 2 :: IO (Maybe Bool)
-- > Just True
(<=) :: (Expr a, Expr b) => a -> b -> ReQL
a <= b = op LE (a, b) ()

-- | Negation
--
-- > >>> run h $ R.not False :: IO (Maybe Bool)
-- > Just True
-- > >>> run h $ R.not () :: IO (Maybe Bool)
-- > Just True
not :: (Expr a) => a -> ReQL
not a = op NOT [a] ()

-- * Lists and Streams

-- | The size of a sequence or an array.
--
-- Called /count/ in the official drivers
--
-- > >>> run h $ R.length (table "foo") :: IO (Maybe Int)
-- > Just 17
length :: (Expr a) => a -> ReQL
length e = op COUNT [e] ()

infixr 5 ++

-- | Join two sequences.
--
-- Called /union/ in the official drivers
--
-- > >>> run h $ [1,2,3] R.++ ["a", "b", "c" :: Text] :: IO (Maybe [JSON])
-- > Just [1.0,2.0,3.0,"a","b","c"]
(++) :: (Expr a, Expr b) => a -> b -> ReQL
a ++ b = op UNION (a, b) ()

-- | Map a function over a sequence
--
-- > >>> run h $ R.map (!"a") [obj ["a" := 1], obj ["a" := 2]] :: IO (Maybe [Int])
-- > Just [1,2]
map :: (Expr a, Expr b) => (ReQL -> b) -> a -> ReQL
map f a = op MAP (a, expr P.. f) ()

-- | Filter a sequence given a predicate
--
-- > >>> run h $ R.filter (R.< 4) [3, 1, 4, 1, 5, 9, 2, 6] :: IO (Maybe [Int])
-- > Just [3,1,1,2]
filter :: (Expr predicate, Expr seq) => predicate -> seq -> ReQL
filter f a = op FILTER (a, f) ()

-- | Query all the documents whose value for the given index is in a given range
--
-- > >>> run h $ table "users" # between "id" (Closed $ str "a") (Open $ str "n") :: IO [JSON]
-- > [{"post_count":4.0,"name":"bob","id":"b6a9df6a-b92c-46d1-ae43-1d2dd8ec293c"},{"post_count":4.0,"name":"bill","id":"b2908215-1d3c-4ff5-b9ee-1a003fa9690c"}]
between :: (Expr left, Expr right, Expr seq) => Key -> Bound left -> Bound right -> seq -> ReQL
between i a b e =
  op BETWEEN [expr e, expr $ getBound a, expr $ getBound b]
         ["left_bound" := closedOrOpen a, "right_bound" := closedOrOpen b, "index" := i]

-- | Append a datum to a sequence
--
-- > >>> run h $ append 3 [1, 2] :: IO (Maybe [Int])
-- > Just [1,2,3]
append :: (Expr a, Expr b) => a -> b -> ReQL
append a b = op APPEND (b, a) ()

-- | Map a function of a sequence and concat the results
--
-- > >>> run h $ concatMap id [[1, 2], [3], [4, 5]] :: IO (Maybe [Int])
-- > Just [1,2,3,4,5]
concatMap :: (Expr a, Expr b) => (ReQL -> b) -> a -> ReQL
concatMap f e = op CONCATMAP (e, expr P.. f) ()

-- | SQL-like inner join of two sequences
--
-- > >>> run' h $ innerJoin (\user post -> user!"id" R.== post!"author") (table "users") (table "posts") # mergeLeftRight # without ["id", "author"]
-- > [[{"name":"bob","message":"lorem ipsum"},{"name":"bill","message":"hello"},{"name":"bill","message":"hi"}]]
innerJoin :: (Expr a, Expr b, Expr c) => (ReQL -> ReQL -> c) -> a -> b -> ReQL
innerJoin f a b = op INNER_JOIN (a, b, fmap expr P.. f) ()

-- | SQL-like outer join of two sequences
--
-- > >>> run' h $ outerJoin (\user post -> user!"id" R.== post!"author") (table "users") (table "posts") # mergeLeftRight # without ["id", "author"]
-- > [[{"name":"nancy"},{"name":"bill","message":"hello"},{"name":"bill","message":"hi"},{"name":"bob","message":"lorem ipsum"}]]
outerJoin :: (Expr a, Expr b, Expr c) => (ReQL -> ReQL -> c) -> a -> b -> ReQL
outerJoin f a b = op OUTER_JOIN (a, b, fmap expr P.. f) ()

-- | An efficient inner_join that uses a key for the left table and an index for the right table.
--
-- > >>> run' h $ table "posts" # eqJoin "author" (table "users") "id" # mergeLeftRight # without ["id", "author"]
-- > [[{"name":"bill","message":"hi"},{"name":"bob","message":"lorem ipsum"},{"name":"bill","message":"hello"}]]
eqJoin :: (Expr right, Expr left) => Key -> right -> Key -> left -> ReQL
eqJoin key right index left = op EQ_JOIN (left, key, right) ["index" := index]

-- | Drop elements from the head of a sequence.
--
-- Called /skip/ in the official drivers
--
-- > >>> run h $ R.drop 2 [1, 2, 3, 4] :: IO (Maybe [Int])
-- > Just [3,4]
drop :: (Expr a, Expr b) => a -> b -> ReQL
drop a b = op SKIP (b, a) ()

-- | Limit the size of a sequence.
--
-- Called /limit/ in the official drivers
--
-- > >>> run h $ R.take 2 [1, 2, 3, 4] :: IO (Maybe [Int])
-- > Just [1,2]
take :: (Expr n, Expr seq) => n -> seq -> ReQL
take n s = op LIMIT (s, n) ()

-- | Cut out part of a sequence
--
-- > >>> run h $ slice 2 4 [1, 2, 3, 4, 5] :: IO (Maybe [Int])
-- > Just [3,4]
slice :: (Expr a, Expr b, Expr c) => a -> b -> c -> ReQL
slice n m s = op SLICE (s, n, m) ()

infixl 9 !!

-- | Get the nth value of a sequence or array
--
-- > >>> run h $ [1, 2, 3] !! 0 :: IO (Maybe Int)
-- > Just 1
(!!) :: (Expr a) => a -> ReQL -> ReQL
s !! n = op NTH (s, n) ()

-- | Reduce a sequence to a single value
--
-- > >>> run h $ reduce (+) 0 [1, 2, 3] :: IO (Maybe Int)
-- > Just 6
reduce :: (Expr base, Expr seq, Expr a) => (ReQL -> ReQL -> a) -> base -> seq -> ReQL
reduce f b s = op REDUCE (s, fmap expr P.. f) ["base" := b]

-- | Reduce a non-empty sequence to a single value
--
-- > >>> run h $ reduce1 (+) [1, 2, 3] :: IO (Maybe Int)
-- > Just 6
reduce1 :: (Expr a, Expr s) => (ReQL -> ReQL -> a) -> s -> ReQL
reduce1 f s = op REDUCE (s, fmap expr P.. f) ()

-- | Filter out identical elements of the sequence
--
-- Called /distint/ in the official drivers
--
-- > >>> run h $ nub (table "posts" ! "flag") :: IO (Maybe [String])
-- > Just ["pinned", "deleted"]
nub :: (Expr s) => s -> ReQL
nub s = op DISTINCT [s] ()

-- | Like map but for write queries
--
-- > >>> run' h $ table "users" # replace (without ["post_count"])
-- > >>> run' h $ forEach (table "posts") $ \post -> table "users" # get (post!"author") # update (\user -> obj ["post_count" := (handle 0 (user!"post_count") + 1)])
-- > [{"skipped":0.0,"inserted":0.0,"unchanged":0.0,"deleted":0.0,"replaced":3.0,"errors":0.0}]
forEach :: (Expr s, Expr a) => s -> (ReQL -> a) -> ReQL
forEach s f = op FOREACH (s, expr P.. f) ()

-- | Merge the "left" and "right" attributes of the objects in a sequence.
--
-- Called /zip/ in the official drivers
--
-- > >>> run' h $ table "posts" # eqJoin "author" (table "users") "id" # mergeLeftRight
mergeLeftRight :: (Expr a) => a -> ReQL
mergeLeftRight a = op ZIP [a] ()

-- | Ordering specification for orderBy
data Order =
  Asc { orderAttr :: Key } -- ^ Ascending order
  | Desc { orderAttr :: Key } -- ^ Descending order

-- | Order a sequence by the given keys
--
-- > >>> run' h $ table "users" # orderBy [Desc "post_count", Asc "name"] # pluck ["name", "post_count"]
-- > [[{"post_count":2.0,"name":"bill"},{"post_count":1.0,"name":"bob"},{"name":"nancy"}]]
orderBy :: (Expr s) => [Order] -> s -> ReQL
orderBy o s = ReQL $ do
  s' <- baseReQL (expr s)
  o' <- baseArray $ arr $ P.map buildOrder o
  return $ BaseReQL ORDERBY P.Nothing (s' : o') []
  where
    buildOrder (Asc k) = op ASC [k] ()
    buildOrder (Desc k) = op DESC [k] ()

-- | Turn a grouping function and a reduction function into a grouped map reduce operation
--
-- > >>> run' h $ table "posts" # groupBy (!"author") (reduce1 (\a b -> a + "\n" + b) . R.map (!"message"))
    -- > [[{"group":"b2908215-1d3c-4ff5-b9ee-1a003fa9690c","reduction":"hi\nhello"},{"group":"b6a9df6a-b92c-46d1-ae43-1d2dd8ec293c","reduction":"lorem ipsum"}]]
-- > >>> run' h $ table "users" # groupBy (!"level") (\users -> let pc = users!"post_count" in [avg pc, R.sum pc])
-- > [[{"group":1,"reduction":[1.5,3.0]},{"group":2,"reduction":[0.0,0.0]}]]
groupBy ::
  (Expr group, Expr reduction, Expr seq)
  => (ReQL -> group) -> (ReQL -> reduction) -> seq -> ReQL
groupBy g mr s = ReQL $ do
  (m, r, mf) <- termToMapReduce (expr . mr)
  let gmr = op GROUPED_MAP_REDUCE [expr s, expr $ expr P.. g, expr m, expr r] ()
  baseReQL $ case mf of
    Nothing -> gmr
    Just f -> op MAP [gmr, expr $ \x -> expr $
                      obj ["group" := (x!"group"), "reduction" := f (x!"reduction")]] ()

-- | The sum of a sequence
--
-- > >>> run h $ sum [1, 2, 3] :: IO (Maybe Int)
-- > Just 6
sum :: (Expr s) => s -> ReQL
sum = reduce ((+) :: ReQL -> ReQL -> ReQL) (num 0)

-- | The average of a sequence
--
-- > >>> run h $ avg [1, 2, 3, 4] :: IO (Maybe Double)
-- > Just 2.5
avg :: (Expr s) => s -> ReQL
avg = (\x -> (x!!0) / (x!!1)) .
  reduce (\a b -> [(a!!0) + (b!!0), (a!!1) + (b!!1)]) [num 0, num 0] .
  map (\x -> [x, 1])

-- * Accessors

infixl 9 !

-- | Get a single field from an object
--
-- > >>> run h $ (obj ["foo" := True]) ! "foo" :: IO (Maybe Bool)
-- > Just True
--
-- Or a single field from each object in a sequence
--
-- > >>> run h $ [obj ["foo" := True], obj ["foo" := False]] ! "foo" :: IO (Maybe [Bool])
-- > Just [True,False]
(!) :: (Expr s) => s -> ReQL -> ReQL
s ! k = op GET_FIELD (s, k) ()

-- | Keep only the given attributes
--
-- > >>> run' h $ map obj [["a" := 1, "b" := 2], ["a" := 2, "c" := 7], ["b" := 4]] # pluck ["a"]
-- > [[{"a":1.0},{"a":2.0},{}]]
pluck :: (Expr o) => [ReQL] -> o -> ReQL
pluck ks e = op PLUCK (cons e $ arr (P.map expr ks)) ()

-- | Remove the given attributes from an object
--
-- > >>> run' h $ map obj [["a" := 1, "b" := 2], ["a" := 2, "c" := 7], ["b" := 4]] # without ["a"]
-- > [[{"b":2.0},{"c":7.0},{"b":4.0}]]
without :: (Expr o) => [ReQL] -> o -> ReQL
without ks e = op WITHOUT (cons e $ arr (P.map expr ks)) ()

-- | Test if a sequence contains a given element
--
-- > >>> run' h $ 1 `R.elem` [1,2,3]
-- > [true]
elem :: (Expr x, Expr seq) => x -> seq -> ReQL
elem x s = op CONTAINS (s, x) ()

-- | Merge two objects together
--
-- > >>> run' h $ merge (obj ["a" := 1, "b" := 1]) (obj ["b" := 2, "c" := 2])
-- > [{"a":1.0,"b":2.0,"c":2.0}]
merge :: (Expr a, Expr b) => a -> b -> ReQL
merge a b = op MERGE (a, b) ()

-- | Evaluate a JavaScript expression
--
-- > >>> run h $ js "Math.random()" :: IO (Maybe Double)
-- > Just 0.9119815775193274
-- > >>> run h $ R.map (\x -> js "Math.sin" `apply` [x]) [pi, pi/2] :: IO (Maybe [Double])
-- > Just [1.2246063538223773e-16,1.0]
js :: ReQL -> ReQL
js s = op JAVASCRIPT [s] ()

-- | Called /branch/ in the official drivers
--
-- > >>> run h $ if' (1 R.< 2) 3 4 :: IO (Maybe Int)
-- > Just 3
if' :: (Expr a, Expr b, Expr c) => a -> b -> c -> ReQL
if' a b c = op BRANCH (a, b, c) ()

-- | Abort the query with an error
--
-- > >>> run' h $ R.error (str "haha") R./ 2 + 1
-- > *** Exception: RethinkDBError {errorCode = runtime error, errorTerm = ADD(DIV(ERROR("haha"), 2.0), 1.0), errorMessage = "haha", errorBacktrace = [0,0]}
error :: (Expr s) => s -> ReQL
error m = op ERROR [m] ()

-- | Create a Database reference
--
-- > >>> run' h $ db "test" # info
-- > [{"name":"test","type":"DB"}]
db :: Text -> O.Database
db s = O.Database s

-- | Create a database on the server
--
-- > >>> run' h $ dbCreate "dev"
-- > [{"created":1.0}]
dbCreate :: P.String -> ReQL
dbCreate db_name = op DB_CREATE [str db_name] ()

-- | Drop a database
--
-- > >>> run' h $ dbDrop (db "dev")
-- > [{"dropped":1.0}]
dbDrop :: Database -> ReQL
dbDrop (O.Database name) = op DB_DROP [name] ()

-- | List the databases on the server
--
-- > >>> run h $ dbList :: IO (Maybe [String])
-- > Just ["test"]
dbList :: ReQL
dbList = op DB_LIST () ()

-- | Create an index on the table from the given function
--
-- > >>> run' h $ table "users" # indexCreate "name" (!"name") def
-- > [{"created":1.0}]
indexCreate :: (Expr fun) => P.String -> fun -> IndexCreateOptions -> Table -> ReQL
indexCreate name f opts tbl = op INDEX_CREATE (tbl, str name, f) $ catMaybes [
  ("multi" :=) <$> indexMulti opts]

-- | Drop an index
--
-- > >>> run' h $ table "users" # indexDrop "name"
-- > [{"dropped":1.0}]
indexDrop :: Key -> Table -> ReQL
indexDrop name tbl = op INDEX_DROP (tbl, name) ()

-- | List the indexes on the table
--
-- > >>> run' h $ indexList (table "users")
-- > [["name"]]
indexList :: Table -> ReQL
indexList tbl = op INDEX_LIST [tbl] ()

-- | Retreive documents by their indexed value
--
-- > >>> run' h $ table "users" # getAll "name" ["bob"]
-- > [{"post_count":1.0,"name":"bob","id":"b6a9df6a-b92c-46d1-ae43-1d2dd8ec293c"}]
getAll :: (Expr value) => Key -> [value] -> Table -> ReQL
getAll idx xs tbl = op GET_ALL (expr tbl : P.map expr xs) ["index" := idx]

-- | A table
--
-- > >>> (mapM_ print =<<) $ run' h $ table "users"
-- > {"post_count":0.0,"name":"nancy","id":"8d674d7a-873c-4c0f-8a4a-32a4bd5bdee8"}
-- > {"post_count":1.0,"name":"bob","id":"b6a9df6a-b92c-46d1-ae43-1d2dd8ec293c"}
-- > {"post_count":2.0,"name":"bill","id":"b2908215-1d3c-4ff5-b9ee-1a003fa9690c"}
table :: Text -> Table
table n = O.Table Nothing n Nothing

-- | Create a table on the server
--
-- > >>> run' h $ tableCreate (table "posts") def
-- > >>> run' h $ tableCreate (table "users") def
-- > >>> run' h $ tableCreate (Table (db "prod") "bar" (Just "name")) def{ tableDataCenter = Just "cloud", tableCacheSize = Just 10 }
tableCreate :: Table -> TableCreateOptions -> ReQL
tableCreate (O.Table mdb table_name pkey) opts =
  withQuerySettings $ \QuerySettings{ queryDefaultDatabase = ddb } ->
    op TABLE_CREATE (fromMaybe ddb mdb, table_name) $ catMaybes [
      ("datacenter" :=) <$> tableDataCenter opts,
      ("cache_size" :=) <$> tableCacheSize opts,
      ("primary_key" :=) <$> pkey ]

-- | Drop a table
--
-- > >>> run' h $ tableDrop (table "bar")
-- > [{"dropped":1.0}]
tableDrop :: Table -> ReQL
tableDrop (O.Table mdb table_name _) =
  withQuerySettings $ \QuerySettings{ queryDefaultDatabase = ddb } ->
    op TABLE_DROP (fromMaybe ddb mdb, table_name) ()

-- | List the tables in a database
--
-- > >>> run h $ tableList (db "test") :: IO (Maybe [String])
-- > Just ["foo","posts","users"]
tableList :: Database -> ReQL
tableList name = op TABLE_LIST [name] ()

-- | Get a document by primary key
--
-- > >>> run' h $ table "users" # get "8d674d7a-873c-4c0f-8a4a-32a4bd5bdee8"
-- > [{"post_count":0.0,"name":"nancy","id":"8d674d7a-873c-4c0f-8a4a-32a4bd5bdee8"}]
get :: Expr s => ReQL -> s -> ReQL
get k e = op GET (e, k) ()

-- | Insert a document or a list of documents into a table
--
-- > >>> Just wr@WriteResponse{} <- run h $ table "users" # insert (map (\x -> obj ["name":=x]) ["bill", "bob", "nancy" :: Text])
-- > >>> let Just [bill, bob, nancy] = writeResponseGeneratedKeys wr
-- > >>> run' h $ table "posts" # insert (obj ["author" := bill, "message" := str "hi"])
-- > >>> run' h $ table "posts" # insert (obj ["author" := bill, "message" := str "hello"])
-- > >>> run' h $ table "posts" # insert (obj ["author" := bob, "message" := str "lorem ipsum"])
insert :: (Expr object) => object -> Table -> ReQL
insert a tb = canReturnVals $ op INSERT (tb, a) ()

-- | Like insert, but update existing documents with the same primary key
--
-- > >>> run' h $ table "users" # upsert (obj ["id" := "79bfe377-9593-402a-ad47-f94c76c36a51", "name" := "rupert"])
-- > [{"skipped":0.0,"inserted":0.0,"unchanged":0.0,"deleted":0.0,"replaced":1.0,"errors":0.0}]
upsert :: (Expr table, Expr object) => object -> table -> ReQL
upsert a tb = canReturnVals $ op INSERT (tb, a) ["upsert" := P.True]

-- | Add to or modify the contents of a document
--
-- > >>> run' h $ table "users" # getAll "name" [str "bob"] # update (const $ obj ["name" := str "benjamin"])
-- > [{"skipped":0.0,"inserted":0.0,"unchanged":0.0,"deleted":0.0,"replaced":1.0,"errors":0.0}]
update :: (Expr selection, Expr a) => (ReQL -> a) -> selection -> ReQL
update f s = canNonAtomic $ canReturnVals $ op UPDATE (s, expr . f) ()

-- | Replace a document with another
--
-- > >>> run' h $ replace (\bill -> obj ["name" := str "stoyan", "id" := bill!"id"]) . R.filter ((R.== str "bill") . (!"name")) $ table "users"
-- > [{"skipped":0.0,"inserted":0.0,"unchanged":0.0,"deleted":0.0,"replaced":1.0,"errors":0.0}]
replace :: (Expr selection, Expr a) => (ReQL -> a) -> selection -> ReQL
replace f s = canNonAtomic $ canReturnVals $ op REPLACE (s, expr . f) ()

-- | Delete the documents
--
-- > >>> run' h $ delete . getAll "name" [str "bob"] $ table "users"
-- > [{"skipped":0.0,"inserted":0.0,"unchanged":0.0,"deleted":1.0,"replaced":0.0,"errors":0.0}]
delete :: (Expr selection) => selection -> ReQL
delete s = canReturnVals $ op DELETE [s] ()

-- | Convert a value to a different type
--
-- > >>> run h $ coerceTo "STRING" 1 :: IO (Maybe String)
-- > Just "1"
coerceTo :: (Expr x) => ReQL -> x -> ReQL
coerceTo t a = op COERCE_TO (a, t) ()

-- | Convert a value to an array
--
-- > >>> run h $ asArray $ obj ["a" := 1, "b" := 2] :: IO (Maybe [(String, Int)])
-- > Just [("a",1),("b",2)]
asArray :: Expr x => x -> ReQL
asArray = coerceTo "ARRAY"

-- | Convert a value to a string
--
-- > >>> run h $ asString $ obj ["a" := 1, "b" := 2] :: IO (Maybe String)
-- > Just "{\n\t\"a\":\t1,\n\t\"b\":\t2\n}"
asString :: Expr x => x -> ReQL
asString = coerceTo "STRING"

-- | Convert a value to a number
--
-- > >>> run h $ asNumber (str "34") :: IO (Maybe Int)
-- > Just 34
asNumber :: Expr x => x -> ReQL
asNumber = coerceTo "NUMBER"

-- | Convert a value to an object
--
-- > >>> run' h $ asObject $ [(str "a",1),("b",2)]
-- > [{"a":1.0,"b":2.0}]
asObject :: Expr x => x -> ReQL
asObject = coerceTo "OBJECT"

-- | Convert a value to a boolean
asBool :: Expr x => x -> ReQL
asBool = coerceTo "BOOL"

-- | Like hasFields followed by pluck
--
-- > >>> run' h $ map obj [["a" := 1, "b" := 2], ["a" := 2, "c" := 7], ["b" := 4]] # withFields ["a"]
-- > [[{"a":1.0},{"a":2.0}]]
withFields :: Expr seq => [ReQL] -> seq -> ReQL
withFields p s = op WITH_FIELDS (s, p) ()

-- | The position in the sequence of the elements that match the predicate
--
-- > >>> run h $ indexesOf (=~ "ba.") [str "foo", "bar", "baz"] :: IO (Maybe [Int])
-- > Just [1,2]
indexesOf :: (Expr fun, Expr seq) => fun -> seq -> ReQL
indexesOf f s = op INDEXES_OF (s, f) ()

-- | Test if a sequence is empty
--
-- > >>> run h $ isEmpty [1] :: IO (Maybe Bool)
-- > Just False
isEmpty :: Expr seq => seq -> ReQL
isEmpty s = op IS_EMPTY [s] ()

-- | Select a given number of elements from a sequence with uniform random distribution
--
-- > >>> run h $ sample 3 [0,1,2,3,4,5,6,7,8,9] :: IO (Maybe [Int])
-- > Just [4,3,8]
sample :: (Expr n, Expr seq) => n -> seq -> ReQL
sample n s = op SAMPLE (s, n) ()

-- | Prepend an element to an array
--
-- > >>> run h $ prepend 1 [2,3] :: IO (Maybe [Int])
-- > Just [1,2,3]
prepend :: (Expr datum, Expr array) => datum -> array -> ReQL
prepend d a = op PREPEND (a, d) ()

infixl 9 \\ --

-- | Called /difference/ in the official drivers
--
-- > >>> run h $ [1,2,3,4,5] \\ [2,5] :: IO (Maybe [Int])
-- > Just [1,3,4]
(\\) :: (Expr a, Expr b) => a -> b -> ReQL
a \\ b = op DIFFERENCE (a, b) ()

-- | Insert a datum into an array if it is not yet present
--
-- > >>> run h $ setInsert 3 [1,2,4,4,5] :: IO (Maybe [Int])
-- > Just [1,2,4,5,3]
setInsert :: (Expr datum, Expr array) => datum -> array -> ReQL
setInsert d a = op SET_INSERT (a, d) ()

-- | The union of two sets
--
-- > >>> run h $ [1,2] `setUnion` [2,3]  :: IO (Maybe [Int])
-- > Just [2,3,1]
setUnion :: (Expr a, Expr b) => a -> b -> ReQL
setUnion a b = op SET_UNION (b, a) ()

-- | The intersection of two sets
--
-- > >>> run h $ [1,2] `setIntersection` [2,3]  :: IO (Maybe [Int])
-- > Just [2]
setIntersection :: (Expr a, Expr b) => a -> b -> ReQL
setIntersection a b = op SET_INTERSECTION (b, a) ()

-- | The difference of two sets
--
-- > >>> run h $ [2,3] # setDifference [1,2]  :: IO (Maybe [Int])
-- > Just [3]
setDifference :: (Expr set, Expr remove) => remove -> set -> ReQL
setDifference r s = op SET_DIFFERENCE (s, r) ()

-- | Test if an object has the given fields
--
-- > >>> run h $ hasFields ["a"] $ obj ["a" := 1] :: IO (Maybe Bool)
-- > Just True
hasFields :: (Expr obj) => ReQL -> obj -> ReQL
hasFields p o = op HAS_FIELDS (o, expr p) ()

-- | Insert a datum at the given position in an array
--
-- > >>> run h $ insertAt 1 4 [1,2,3] :: IO (Maybe [Int])
-- > Just [1,4,2,3]
insertAt :: (Expr n, Expr datum, Expr array) => n -> datum -> array -> ReQL
insertAt n d a = op INSERT_AT (a, n, d) ()

-- | Splice an array at a given position inside another array
--
-- > >>> run h $ spliceAt 2 [4,5] [1,2,3] :: IO (Maybe [Int])
-- > Just [1,2,4,5,3]
spliceAt :: (Expr n, Expr replace, Expr array) => n -> replace -> array -> ReQL
spliceAt n s a = op SPLICE_AT (a, n, s) ()

-- | Delete an element from an array
--
-- > >>> run h $ deleteAt 1 [1,2,3] :: IO (Maybe [Int])
-- > Just [1,3]
deleteAt :: (Expr n, Expr array) => n -> array -> ReQL
deleteAt n a = op DELETE_AT (a, n) ()

-- | Change an element in an array
--
-- > >>> run h $ changeAt 1 4 [1,2,3] :: IO (Maybe [Int])
-- > Just [1,4,3]
changeAt :: (Expr n, Expr datum, Expr array) => n -> datum -> array -> ReQL
changeAt n d a = op CHANGE_AT (a, n, d) ()

-- | The list of keys of the given object
--
-- > >>> run h $ keys (obj ["a" := 1, "b" := 2]) :: IO (Maybe [String])
-- > Just ["a","b"]
keys :: Expr obj => obj -> ReQL
keys o = op KEYS [o] ()

-- | Match a string to a regular expression.
--
-- Called /match/ in the official drivers
--
-- > >>> run' h $ str "foobar" =~ "f(.)+[bc](.+)"
-- > [{"groups":[{"start":2.0,"end":3.0,"str":"o"},{"start":4.0,"end":6.0,"str":"ar"}],"start":0.0,"end":6.0,"str":"foobar"}]
(=~) :: (Expr string) => string -> ReQL -> ReQL
s =~ r = op MATCH (s, r) ()

-- | Apply a function to a list of arguments.
--
-- Called /do/ in the official drivers
--
-- > >>> run h $ (\x -> x R.* 2) `apply` [4] :: IO (Maybe Int)
-- > Just 8
apply :: (Expr fun, Expr arg) => fun -> [arg] -> ReQL
f `apply` as = op FUNCALL (expr f : P.map expr as) ()

-- | Catch some expections inside the query.
--
-- Called /default/ in the official drivers
--
-- > >>> run h $ handle 0 $ obj ["a" := 1] ! "b" :: IO (Maybe Int)
-- > Just 0
-- > >>> run h $ handle (expr . id) $ obj ["a" := 1] ! "b" :: IO (Maybe String)
-- > Just "No attribute `b` in object:\n{\n\t\"a\":\t1\n}"
handle :: (Expr handler, Expr reql) => handler -> reql -> ReQL
handle h r = op DEFAULT (r, h) ()

-- | A string representing the type of an expression
--
-- > >>> run h $ typeOf 1 :: IO (Maybe String)
-- > Just "NUMBER"
typeOf :: Expr a => a -> ReQL
typeOf a = op TYPEOF [a] ()

-- | Get information on a given expression. Useful for tables and databases.
--
-- > >>> run' h $ info $ table "foo"
-- > [{"primary_key":"id","name":"foo","indexes":[],"type":"TABLE","db":{"name":"test","type":"DB"}}]
info :: Expr a => a -> ReQL
info a = op INFO [a] ()

-- | Parse a json string into an object
--
-- > >>> run' h $ json "{a:1}"
-- > [{"a":1.0}]
json :: ReQL -> ReQL
json s = op JSON [s] ()

-- | Flipped function application
infixl 8 #
(#) :: (Expr a, Expr b) =>  a -> (a -> b) -> ReQL
x # f = expr (f x)

infixr 9 .
-- | Specialised function composition
(.) :: (Expr a, Expr b, Expr c) =>  (ReQL -> b) -> (ReQL -> a) -> c -> ReQL
(f . g) x = expr (f (expr (g (expr x))))