-- | This module provides functions and types to work on values instantiating
-- 'Storable'. Such values have an associated type for their ID and an
-- associated relation (table name, columns for the ID and columns for the
-- value.)
--
-- The type classes 'MonadSelect' and 'MonadStore' and provided so that code can
-- be written with types such as @MonadSelect m => Int -> m String@ ensuring
-- that the function is read-only. In 'MonadStore', all four operations
-- (@SELECT@, @INSERT@, @UPDATE@ and @DELETE#) can be done.
--
-- In order to be able to use a type with these functions, it should be made an
-- instance of 'Storable' as well as possibly an instance of 'FromRow'/'ToRow'
-- depending on what functions are called. It is also a good idea to define a
-- type specifying the properties (fields) on which we can define conditions.
-- See the demo for an example.

module Database.Seakale.Store
  ( Entity(..)
  , MonadSelect
  , MonadStore
  -- * Operations
  , select
  , select_
  , count
  , getMany
  , getMaybe
  , get
  , insertMany
  , insert
  , updateMany
  , update
  , UpdateSetter
  , (=.)
  , deleteMany
  , delete
  -- * Setup
  , Storable(..)
  , Relation(..)
  , RelationName(..)
  , Column(..)
  -- * Properties
  , Property(..)
  , EntityIDProperty(..)
  -- * SELECT clauses
  , SelectClauses
  , groupBy
  , asc
  , desc
  , limit
  , offset
  -- * Conditions
  , Condition
  , (==.)
  , (/=.)
  , (<=.)
  , (<.)
  , (>=.)
  , (>.)
  , (==#)
  , (/=#)
  , (<=#)
  , (<#)
  , (>=#)
  , (>#)
  , (==~)
  , (/=~)
  , (<=~)
  , (<~)
  , (>=~)
  , (>~)
  , (&&.)
  , (||.)
  , isNull
  , isNotNull
  , inList
  , notInList
  ) where

import           Control.Monad

import           Data.List hiding (groupBy, insert, delete)
import           Data.Maybe
import           Data.Monoid
import qualified Data.ByteString.Char8 as BS

import           Database.Seakale.FromRow
import           Database.Seakale.ToRow
import           Database.Seakale.Types

import           Database.Seakale.Store.Internal
                   hiding (select, count, insert, update, delete)
import qualified Database.Seakale.Store.Internal as I

-- | Select all entities for the corresponding relation.
select :: ( MonadSelect backend m, Storable backend k l a
          , FromRow backend (k :+ l) (Entity a) ) => Condition backend a
       -> SelectClauses backend a -> m [Entity a]
select cond clauses = do
  backend <- getBackend
  I.select (relation backend) cond clauses

-- | Like 'select' but without any other clauses than @WHERE@.
select_ :: ( MonadSelect backend m, Storable backend k l a
          , FromRow backend (k :+ l) (Entity a) ) => Condition backend a
        -> m [Entity a]
select_ cond = select cond mempty

-- | Count the number of rows matching the conditions.
count :: (MonadSelect backend m, Storable backend k l a) => Condition backend a
      -> m Integer
count cond = do
  backend <- getBackend
  I.count (relation backend) cond

-- | Select all entities with the given IDs.
getMany :: ( MonadSelect backend m, Storable backend k l a
           , FromRow backend (k :+ l) (Entity a), ToRow backend k (EntityID a) )
        => [EntityID a] -> m [Entity a]
getMany ids = select_ $ EntityID `inList` ids

-- | Return the value corresponding to the given ID if it exists, otherwise
-- return @Nothing@.
getMaybe :: ( MonadSelect backend m, Storable backend k l a
            , FromRow backend (k :+ l) (Entity a), ToRow backend k (EntityID a)
            ) => EntityID a -> m (Maybe a)
getMaybe i =
  (fmap entityVal . listToMaybe) <$> select (EntityID ==. i) (limit 1)

-- | Return the value corresponding to the given ID if it exists, otherwise
-- throw 'EntityNotFoundError'.
get :: ( MonadSelect backend m, Storable backend k l a
       , FromRow backend (k :+ l) (Entity a), ToRow backend k (EntityID a) )
    => EntityID a -> m a
get i = maybe (throwSeakaleError EntityNotFoundError) return =<< getMaybe i

-- | Insert the given values and return their ID in the same order.
insertMany :: forall backend m k l a.
              ( MonadStore backend m, Storable backend k l a, ToRow backend l a
              , FromRow backend k (EntityID a) ) => [a] -> m [EntityID a]
insertMany = I.insert

-- | Like 'insertMany' but for only one value.
insert :: ( MonadStore backend m, Storable backend k l a, ToRow backend l a
          , FromRow backend k (EntityID a) ) => a -> m (EntityID a)
insert = fmap head . insertMany . pure

-- | Update columns on rows matching the given conditions and return the number
-- of rows affected.
updateMany :: forall backend m k l a.
              (MonadStore backend m, Storable backend k l a)
           => UpdateSetter backend a -> Condition backend a -> m Integer
updateMany setter cond = I.update setter cond

-- | Update columns on the row with the given ID.
update :: ( MonadStore backend m, Storable backend k l a
          , ToRow backend k (EntityID a) )
       => EntityID a -> UpdateSetter backend a -> m ()
update i setter = do
  n <- updateMany setter $ EntityID ==. i
  unless (n == 1) $ throwSeakaleError EntityNotFoundError

-- | Delete rows matching the given conditions.
deleteMany :: forall backend m k l a.
              (MonadStore backend m, Storable backend k l a)
           => Condition backend a -> m Integer
deleteMany = I.delete

-- | Delete the row with the given ID.
delete :: ( MonadStore backend m, Storable backend k l a
          , ToRow backend k (EntityID a) ) => EntityID a -> m ()
delete i = do
  n <- deleteMany $ EntityID ==. i
  unless (n == 1) $ throwSeakaleError EntityNotFoundError

-- | Specify that the type @f@ specify properties of @a@. These values of type
-- @f@ can then be used to create 'Condition's on type @a@. The type parameters
-- 'n' and 'b' in the class definition are, respectively, the number of rows
-- taken by this property and the associated type.
--
-- See the following example:
--
-- > data User = User
-- >   { userFirstName :: String
-- >   , userLastName  :: String
-- >   }
-- >
-- > data UserProperty b n a where
-- >   UserFirstName :: UserProperty b One String
-- >   UserLastName  :: UserProperty b One String
-- >
-- > UserFirstName ==. "Marie" &&. UserLastName ==. "Curie"
-- >   :: Condition backend User
class Property backend a f | f -> a where
  toColumns :: backend -> f backend n b -> Vector n Column

-- | Property of any value instantiating 'Storable' and selecting its ID.
-- This can be used to easily create 'Condition's on any type such as
-- @EntityID ==. UserID 42@.
data EntityIDProperty a backend :: Nat -> * -> * where
  EntityID :: forall backend k l a. Storable backend k l a
           => EntityIDProperty a backend k (EntityID a)

instance Property backend a (EntityIDProperty a) where
  toColumns backend x@EntityID = go (relation backend) x
    where
      go :: Relation backend k l a -> EntityIDProperty a backend k (EntityID a)
         -> Vector k Column
      go Relation{..} _ = relationIDColumns

(==.) :: (Property backend a f, ToRow backend n b) => f backend n b -> b
      -> Condition backend a
(==.) prop vals =
  buildCondition "=" (flip toColumns prop) (flip toRow vals)

(/=.) :: (Property backend a f, ToRow backend n b) => f backend n b -> b
      -> Condition backend a
(/=.) prop vals =
  buildCondition "<>" (flip toColumns prop) (flip toRow vals)

(<=.) :: (Property backend a f, ToRow backend n b) => f backend n b -> b
      -> Condition backend a
(<=.) prop vals =
  buildCondition "<=" (flip toColumns prop) (flip toRow vals)

(<.) :: (Property backend a f, ToRow backend n b) => f backend n b -> b
     -> Condition backend a
(<.) prop vals =
  buildCondition "<" (flip toColumns prop) (flip toRow vals)

(>=.) :: (Property backend a f, ToRow backend n b) => f backend n b -> b
      -> Condition backend a
(>=.) prop vals =
  buildCondition ">=" (flip toColumns prop) (flip toRow vals)

(>.) :: (Property backend a f, ToRow backend n b) => f backend n b -> b
     -> Condition backend a
(>.) cols vals =
  buildCondition ">" (flip toColumns cols) (flip toRow vals)

(==#) :: (Property backend a f, Property backend a g)
      => f backend n b -> g backend n b -> Condition backend a
(==#) prop1 prop2 =
  buildCondition' "=" (flip toColumns prop1) (flip toColumns prop2)

(/=#) :: (Property backend a f, Property backend a g)
      => f backend n b -> g backend n b -> Condition backend a
(/=#) prop1 prop2 =
  buildCondition' "<>" (flip toColumns prop1) (flip toColumns prop2)

(<#) :: (Property backend a f, Property backend a g)
      => f backend n b -> g backend n b -> Condition backend a
(<#) prop1 prop2 =
  buildCondition' "<" (flip toColumns prop1) (flip toColumns prop2)

(<=#) :: (Property backend a f, Property backend a g)
      => f backend n b -> g backend n b -> Condition backend a
(<=#) prop1 prop2 =
  buildCondition' "<=" (flip toColumns prop1) (flip toColumns prop2)

(>#) :: (Property backend a f, Property backend a g)
      => f backend n b -> g backend n b -> Condition backend a
(>#) prop1 prop2 =
  buildCondition' ">" (flip toColumns prop1) (flip toColumns prop2)

(>=#) :: (Property backend a f, Property backend a g)
      => f backend n b -> g backend n b -> Condition backend a
(>=#) prop1 prop2 =
  buildCondition' ">=" (flip toColumns prop1) (flip toColumns prop2)

(==~) :: (Property backend a f, Property backend a g)
      => f backend n b -> g backend n c -> Condition backend a
(==~) prop1 prop2 =
  buildCondition' "=" (flip toColumns prop1) (flip toColumns prop2)

(/=~) :: (Property backend a f, Property backend a g)
      => f backend n b -> g backend n c -> Condition backend a
(/=~) prop1 prop2 =
  buildCondition' "<>" (flip toColumns prop1) (flip toColumns prop2)

(<~) :: (Property backend a f, Property backend a g)
      => f backend n b -> g backend n c -> Condition backend a
(<~) prop1 prop2 =
  buildCondition' "<" (flip toColumns prop1) (flip toColumns prop2)

(<=~) :: (Property backend a f, Property backend a g)
      => f backend n b -> g backend n c -> Condition backend a
(<=~) prop1 prop2 =
  buildCondition' "<=" (flip toColumns prop1) (flip toColumns prop2)

(>~) :: (Property backend a f, Property backend a g)
      => f backend n b -> g backend n c -> Condition backend a
(>~) prop1 prop2 =
  buildCondition' ">" (flip toColumns prop1) (flip toColumns prop2)

(>=~) :: (Property backend a f, Property backend a g)
      => f backend n b -> g backend n c -> Condition backend a
(>=~) prop1 prop2 =
  buildCondition' ">=" (flip toColumns prop1) (flip toColumns prop2)

(&&.) :: Condition backend a -> Condition backend a -> Condition backend a
(&&.) = mappend

(||.) :: Condition backend a -> Condition backend a -> Condition backend a
(||.) = combineConditions "OR"

infix 4 ==., /=., <=., <., >=., >.
infix 4 ==#, /=#, <=#, <#, >=#, >#
infix 4 ==~, /=~, <=~, <~, >=~, >~
infixr 2 &&., ||.

isNull :: Property backend a f => f backend n b -> Condition backend a
isNull prop = Condition $ \prefix backend ->
  let bs = mconcat . intersperse " AND "
           . map (\col -> unColumn col prefix <> " IS NULL")
           . vectorToList $ toColumns backend prop
  in (Plain bs EmptyQuery, Nil)

isNotNull :: Property backend a f => f backend n b -> Condition backend a
isNotNull prop = Condition $ \prefix backend ->
  let bs = mconcat . intersperse " AND "
           . map (\col -> unColumn col prefix <> " IS NOT NULL")
           . vectorToList $ toColumns backend prop
  in (Plain bs EmptyQuery, Nil)

listHelper :: (Property backend a f, ToRow backend n b) => BS.ByteString
           -> f backend n b -> [b] -> Condition backend a
listHelper op prop values =
  let step value (suffix, (Condition f)) =
        (", ",) $ Condition $ \prefix backend ->
          let (req, dat) = f prefix backend
              valueList = mconcat $ intersperse ", " $ vectorToList $
                            toRow backend value
          in (Hole (Plain suffix req), Cons ("(" <> valueList <> ")") dat)

      (_, cond) = foldr step ("", mempty) values

  in case cond of
       Condition f -> Condition $ \prefix backend ->
         let (req, dat) = f prefix backend
             colList = mconcat $ intersperse ", " $ map (flip unColumn prefix) $
                         vectorToList $ toColumns backend prop
             req' =  Plain ("(" <> colList <> ") " <> op <> " (") req
                          `qappendZero` Plain ")" EmptyQuery
         in (req', dat)

inList :: (Property backend a f, ToRow backend n b) => f backend n b -> [b]
       -> Condition backend a
inList _ [] = Condition $ \_ _ -> (Plain "1=0" EmptyQuery, Nil)
inList prop values = listHelper "IN" prop values

notInList :: (Property backend a f, ToRow backend n b) => f backend n b -> [b]
          -> Condition backend a
notInList _ [] = Condition $ \_ _ -> (Plain "1=1" EmptyQuery, Nil)
notInList prop values = listHelper "NOT IN" prop values

groupBy :: Property backend a f => f backend n b -> SelectClauses backend a
groupBy prop = mempty { selectGroupBy = vectorToList . flip toColumns prop }

asc :: Property backend a f => f backend n b -> SelectClauses backend a
asc prop =
  let f backend = map (,Asc) $ vectorToList (toColumns backend prop)
  in mempty { selectOrderBy = f }

desc :: Property backend a f => f backend n b -> SelectClauses backend a
desc prop =
  let f backend = map (,Desc) $ vectorToList (toColumns backend prop)
  in mempty { selectOrderBy = f }

limit :: Int -> SelectClauses backend a
limit n = mempty { selectLimit = Just n }

offset :: Int -> SelectClauses backend a
offset n = mempty { selectOffset = Just n }

(=.) :: (Property backend a f, ToRow backend n b)
     => f backend n b -> b -> UpdateSetter backend a
(=.) col value = UpdateSetter $ \backend ->
  vzip (toColumns backend col) (toRow backend value)