{-# language DataKinds              #-}
{-# language FlexibleContexts       #-}
{-# language FlexibleInstances      #-}
{-# language FunctionalDependencies #-}
{-# language GADTs                  #-}
{-# language KindSignatures         #-}
{-# language ScopedTypeVariables    #-}
{-# language TypeApplications       #-}
{-# language TypeOperators          #-}
{-# language UndecidableInstances   #-}
{-|
Description : Utilities for interoperation between Mu and Persistent

The @persistent@ library, and in particular its quasi-quoters
for entities, generate data types which do not look exactly as
plain records. This module defines some wrappers which modify
the 'ToSchema' and 'FromSchema' derivation to work with them.
-}
module Mu.Adapter.Persistent (
  -- * Wrappers for use with @DerivingVia@
  WithEntityNestedId(..)
, WithEntityPlainId(..)
  -- * Generic utilities
, runDb, Pool, runDbPool
) where

import           Control.Monad.IO.Class
import           Control.Monad.Logger
import           Control.Monad.Trans.Reader
import           Control.Monad.Trans.Resource.Internal
import           Data.Int
import           Data.Pool                             (Pool)
import           Database.Persist.Sql
import           GHC.Generics
import           GHC.TypeLits

import           Mu.Schema
import           Mu.Schema.Class

-- | Wrapper for 'Entity' to be used with @DerivingVia@.
--   This wrappers indicates that the identifier is to be found
--   as the sole field of another object, like in:
--
--   > { id: { key: 3 }, name: "Somebody" }
newtype WithEntityNestedId (ty :: Symbol) (fmap :: Mappings Symbol Symbol) a
  = WithEntityNestedId { unWithEntityNestedId :: a }

-- | Wrapper for 'Entity' to be used with @DerivingVia@.
--   This wrappers indicates that the identifier is to be found
--   in the schema at the same level as other fields, like in:
--
--   > { id: 3, name: "Somebody" }
newtype WithEntityPlainId (ty :: Symbol) (fmap :: Mappings Symbol Symbol) a
  = WithEntityPlainId { unWithEntityPlainId :: a }

instance ( Generic t
         , (sch :/: sty) ~ 'DRecord name (idArg ': args)
         , idArg ~ 'Mu.Schema.FieldDef idArgName ('TPrimitive Int64)
         , Rep t ~ D1 dInfo (C1 cInfo f)
         , GToSchemaRecord sch fmap args f
         , ToBackendKey (PersistEntityBackend t) t
         , PersistEntityBackend t ~ SqlBackend )
         => ToSchema sch sty (WithEntityPlainId sty fmap (Entity t)) where
  toSchema (WithEntityPlainId (Entity key x))
    = TRecord $ Field (FPrimitive (unSqlBackendKey $ toBackendKey key))
      :* toSchemaRecord (Proxy @fmap) (unM1 $ unM1 $ from x)

instance ( Generic t
         , (sch :/: sty) ~ 'DRecord name (nestedIdArg ': args)
         , nestedIdArg ~ 'Mu.Schema.FieldDef fname k
         , ToSchemaKey sch idTy k
         , (sch :/: idTy) ~ 'DRecord idName '[idArg]
         , idArg ~ 'Mu.Schema.FieldDef idArgName ('TPrimitive Int64)
         , Rep t ~ D1 dInfo (C1 cInfo f)
         , GToSchemaRecord sch fmap args f
         , ToBackendKey (PersistEntityBackend t) t
         , PersistEntityBackend t ~ SqlBackend )
         => ToSchema sch sty (WithEntityNestedId sty fmap (Entity t)) where
  toSchema (WithEntityNestedId (Entity key x))
    = TRecord $ Field (toSchemaKey $ FSchematic $ TRecord (Field (FPrimitive key') :* Nil))
      :* toSchemaRecord (Proxy @fmap) (unM1 $ unM1 $ from x)
    where key' = unSqlBackendKey $ toBackendKey key

class ToSchemaKey (sch :: Schema') (idTy :: Symbol) t | sch t -> idTy where
  toSchemaKey :: FieldValue sch ('TSchematic idTy) -> FieldValue sch t
instance ToSchemaKey sch idTy ('TSchematic idTy) where
  toSchemaKey = id
instance ToSchemaKey sch idTy t => ToSchemaKey sch idTy ('TOption t) where
  toSchemaKey = FOption . Just . toSchemaKey

-- | Simple utility to execute a database operation
--   in any monad which supports 'IO' operations.
--   Note that all logging messages are discarded.
runDb :: MonadIO m
      => SqlBackend
      -> ReaderT SqlBackend (NoLoggingT (ResourceT IO)) a
      -> m a
runDb = (liftIO .) . flip runSqlPersistM

-- | Simple utility to execute a database operation
--   in any monad which supports 'IO' operations.
--   Note that all logging messages are discarded.
runDbPool :: MonadIO m
          => Pool SqlBackend
          -> ReaderT SqlBackend (NoLoggingT (ResourceT IO)) a
          -> m a
runDbPool = (liftIO .) . flip runSqlPersistMPool
