{-# LANGUAGE CPP #-}
#if __GLASGOW_HASKELL__ >= 702
{-# LANGUAGE Safe #-}
#endif
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE TypeSynonymInstances #-}

module Hails.Database.MongoDB.Structured ( DCRecord(..)
                                         , DCLabeledRecord(..)
                                         , MkToLabeledDocument(..)
                                         , toDocumentP
                                         ) where

import LIO
import LIO.DCLabel

import Hails.Database
import Hails.Database.MongoDB

import Data.Monoid (mappend)
import Control.Monad (liftM)

-- | Class for converting from \"structured\" records to documents
-- (and vice versa).
class DCRecord a where
  -- | Convert a document to a record
  fromDocument :: Monad m => Document DCLabel -> m a
  -- | Convert a record to a document
  toDocument :: a -> Document DCLabel
  -- | Get the collection name for the record
  collectionName :: a -> CollectionName
  -- | Find an object with mathing value for the given key. If the
  -- object does exist but cannot be read (above clearance), this
  -- returns 'Nothing'.
  findBy :: (Val DCLabel v, DatabasePolicy p)
         => p -> CollectionName -> Key -> v -> DC (Maybe a)
  -- | Find an object with given query
  findWhere :: (DatabasePolicy p)
            => p -> Query DCLabel -> DC (Maybe a)
  -- | Insert a record into the database
  insertRecord :: (DatabasePolicy p)
               => p -> a -> DC (Either Failure (Value DCLabel))
  -- | Insert a record into the database
  saveRecord :: (DatabasePolicy p)
             => p -> a -> DC (Either Failure ())
  -- | Delete a record from the database given a matching value for
  -- given key. The deleted record is returned.
  deleteBy :: (Val DCLabel v, DatabasePolicy p)
           => p -> CollectionName -> Key -> v -> DC (Maybe a)
  -- | Delete an object matching the given query.
  -- The deleted record is returned.
  deleteWhere :: (DatabasePolicy p)
              => p -> Selection DCLabel -> DC (Maybe a)
  -- | Same as 'findBy', but using explicit privileges.
  findByP :: (Val DCLabel v, DatabasePolicy p)
          => DCPrivTCB -> p -> CollectionName -> Key -> v -> DC (Maybe a)
  -- | Same as 'findWhere', but using explicit privileges.
  findWhereP :: (DatabasePolicy p)
            => DCPrivTCB -> p -> Query DCLabel -> DC (Maybe a)
  -- | Same as 'insertRecord', but using explicit privileges.
  insertRecordP :: (DatabasePolicy p)
              => DCPrivTCB -> p -> a -> DC (Either Failure (Value DCLabel))
  -- | Same as 'saveRecord', but using explicit privileges.
  saveRecordP :: (DatabasePolicy p)
              => DCPrivTCB -> p -> a -> DC (Either Failure ())
  -- | Same as 'deleteBy', but using explicit privileges.
  deleteByP :: (Val DCLabel v, DatabasePolicy p)
      => DCPrivTCB -> p -> CollectionName -> Key -> v -> DC (Maybe a)
  -- | Same as 'deleteWhere', but using explicit privileges.
  deleteWhereP :: (DatabasePolicy p)
               => DCPrivTCB -> p -> Selection DCLabel -> DC (Maybe a)

  --
  -- Default definitions
  --

  --
  findBy = findByP noPrivs
  --
  findWhere = findWhereP noPrivs
  --
  insertRecord = insertRecordP noPrivs
  --
  saveRecord = saveRecordP noPrivs
  --
  deleteBy = deleteByP noPrivs
  --
  deleteWhere = deleteWhereP noPrivs
  --
  insertRecordP p policy record = do
    let colName = collectionName record
    p' <- getPrivileges
    withDB policy $ insertP (p' `mappend` p)  colName $ toDocument record
  --
  saveRecordP p policy record = do
    let colName = collectionName record
    p' <- getPrivileges
    withDB policy $ saveP (p' `mappend` p) colName $ toDocument record
  --
  findByP p policy colName k v = 
    findWhereP p policy (select [k =: v] colName)
  --
  findWhereP p policy query  = do
    result <- withDB policy $ findOneP p query
    c <- getClearance
    case result of
      Right (Just r) | leqp p (labelOf r) c -> fromDocument `liftM` unlabelP p r
      _ -> return Nothing
  --
  deleteByP p policy colName k v = 
    deleteWhereP p policy (select [k =: v] colName)
  --
  deleteWhereP p policy sel = do
    -- Find with only supplied privileges
    mdoc <- findWhereP p policy $ select (selector sel) (coll sel)
    -- User underlying privileges as well:
    p' <- getPrivileges
    res <- withDB policy $ deleteOneP (p' `mappend` p) sel
    case res of
      Right _ -> return mdoc
      _ -> return Nothing
  --

-- | Class for inserting and saving labeled records.
class DCRecord a => DCLabeledRecord a where
  -- | Insert a labeled record into the database
  insertLabeledRecord :: (MkToLabeledDocument p)
               => p -> DCLabeled a -> DC (Either Failure (Value DCLabel))
  -- | Insert a labeled record into the database
  saveLabeledRecord :: (MkToLabeledDocument p)
             => p -> DCLabeled a -> DC (Either Failure ())
  -- | Same as 'insertLabeledRecord', but using explicit privileges.
  insertLabeledRecordP :: (MkToLabeledDocument p)
    => DCPrivTCB -> p -> DCLabeled a -> DC (Either Failure (Value DCLabel))
  -- | Same as 'saveLabeledRecord', but using explicit privileges.
  saveLabeledRecordP :: (MkToLabeledDocument p)
              => DCPrivTCB -> p -> DCLabeled a -> DC (Either Failure ())

  --
  -- Default definitions for insert/save
  --

  --
  insertLabeledRecord = insertLabeledRecordP noPrivs
  --
  saveLabeledRecord = saveLabeledRecordP noPrivs
  --
  insertLabeledRecordP p policy lrecord = do
    let colName = collectionName (forceType lrecord)
    p' <- getPrivileges
    ldoc <- mkToLabeledDocument policy lrecord
    withDB policy $ insertP (p' `mappend` p)  colName  ldoc
  --
  saveLabeledRecordP p policy lrecord = do
    let colName = collectionName (forceType lrecord)
    p' <- getPrivileges
    ldoc <- mkToLabeledDocument policy lrecord
    withDB policy $ saveP (p' `mappend` p) colName ldoc
  --

-- | Classe used by a database policy to translate a labeled record to
-- a labeled document.
class DatabasePolicy p => MkToLabeledDocument p where
  -- | Given a policy, return a function that can be used to translate
  -- labeled records to labeled documents. It is recommended to simply
  -- create the instance by defining @mkToDocumentP@ as:
  --
  -- > mkToDocumentP (Policy ... priv ..) = toDocumentP priv
  --
  mkToLabeledDocument :: DCRecord a
                      => p
                      -> DCLabeled a
                      -> DC (DCLabeled (Document DCLabel))

-- | Same as 'toDocument' but for uses the policy's privileges to
-- convert a labeled record to a labeled  document.
toDocumentP :: DCRecord a
            => DCPrivTCB -> DCLabeled a -> DC (DCLabeled (Document DCLabel))
toDocumentP privs lr = do
  lcur <- getLabel
  let lres = lostar privs lcur (labelOf lr)
  r <- unlabelP privs lr
  labelP privs lres $ toDocument r


--
-- Misc helpers
--

-- | Get the type of a 'DCLabeled' value
forceType :: DCLabeled a -> a
forceType = undefined