{-# LANGUAGE FlexibleContexts, GeneralizedNewtypeDeriving #-}
-- | Various internal utilities for beam-th. The usual caveats apply.
module Database.Beam.TH.Internal (
  -- * The MakeTableT monad transformer
  -- ** Definition
  MakeTableT(..),
  -- ** Derived type synonyms
  MakeTable, MakeTableT', MakeTableT'',
  -- ** Helper functions
  runTableT, tellD,
  -- ** Extracting values from a MakeTableT
  vst,
  -- *** Simple and composite names
  name, nameId, nameId', nameT,
  -- * Name utilities
  rename,
  -- * Type and Expression Application Sugar
  (<~>), (<+>), (~>),
  -- * Error handling
  -- ** Constructor types
  invalidConstructor,
  -- ** Table names
  assert, assertMany
  ) where

import Control.Monad (unless)
import qualified Control.Monad.Fail as Fail (MonadFail(..))
import Control.Monad.Identity (Identity)
import Control.Monad.Reader (ReaderT(..), MonadReader, asks)
import Control.Monad.Writer (WriterT(..), MonadWriter, execWriterT, tell)
import Control.Monad.Trans (MonadTrans(..))
import Data.Foldable (traverse_)
import Language.Haskell.TH (Name, mkName, nameBase, reportError, Q, DecsQ, Dec, Type(..), Exp(..))
import Language.Haskell.TH.Syntax (VarBangType)
import Data.Typeable (Typeable)

-- | Rename a 'Name' using a function on 'String's
rename :: (String -> String) -> Name -> Name
rename f = mkName . f . nameBase
{-# INLINE rename #-}

mkNameSelector :: MonadReader (Name, VarBangType) m => String -> m Name
mkNameSelector suffix = asks (rename (++ suffix) . fst)
{-# INLINE mkNameSelector #-}

-- | Extract the 'PrimaryKey' 'VarBangType'
vst :: MonadReader (Name, VarBangType) m => m VarBangType
vst = asks snd
{-# INLINE vst #-}

name, nameId, nameId', nameT :: MonadReader (Name, VarBangType) m => m Name
-- | Get the base name
name = asks fst
-- | Get the name with an \"Id\" suffix
nameId = mkNameSelector "Id"
-- | Get the name with an \"Id'\" suffix
nameId' = mkNameSelector "Id'"
-- | Get the name with a \"T\" suffix
nameT = mkNameSelector "T"
{-# INLINE name #-}
{-# INLINE nameId #-}
{-# INLINE nameId' #-}
{-# INLINE nameT #-}


-- | A monad transformer for writing Template Haskell declarations.
--
-- The Reader contains both the base name of the table and the 'VarBangType'
-- of the primary key field.
--
-- If you can come up with a better name, drop me a line.
newtype MakeTableT m a = MakeTableT { runTable :: WriterT [Dec] (ReaderT (Name, VarBangType) m) a }
                     deriving (Typeable, Functor, Applicative, Monad, MonadReader (Name, VarBangType), MonadWriter [Dec], Fail.MonadFail)
instance MonadTrans MakeTableT where
  lift = MakeTableT . lift . lift
-- | Type synonym for 'MakeTableT' in the 'Identity' monad.
--
-- Only defined for complying with the monad transformer conventions
-- and not actually used.
type MakeTable a = MakeTableT Identity a
-- | Type synonym for 'MakeTableT' in the 'Q' monad.
type MakeTableT' a = MakeTableT Q a
-- | Type synonym for 'MakeTableT' in the 'Q' monad with the empty tuple as the inner type.
-- This is the most common use case.
type MakeTableT'' = MakeTableT' ()

{-
instance Monoid MakeTableT'' where
  mempty = MakeTableT . WriterT . lift . pure $ ((), [])
  mappend = curry (mkt <=< (uncurry ((<*>) . (mappend <$>)) . (f *** f)))
    where
      f = fmap snd . MakeTableT . lift . runWriterT . runTable
      mkt = MakeTableT . WriterT . ReaderT . const . pure . ((),)
-}

-- | Run the table writing sequence (or, the 'MakeTableT' if you prefer).
runTableT ::
  Name             -- ^ The base name of the table, without the trailing \"T\".
  -> VarBangType   -- ^ The primary key field.
  -> MakeTableT' a -- ^ The table writing sequence to be executed. The inner type is ignored.
  -> DecsQ
runTableT n v = flip runReaderT (n, v) . execWriterT . runTable
{-# INLINE runTableT #-}

-- | Write a single 'Dec'
tellD :: MonadWriter [Dec] m => Dec -> m ()
tellD = tell . pure
{-# INLINE tellD #-}

-- $setup
-- >>> let nm = mkName "nm"

-- | Convenient syntactic sugar for application of types.
--
-- >>> ConT nm <~> ConT nm <~> ConT nm
-- AppT (AppT (ConT nm) (ConT nm)) (ConT nm)
(<~>) :: Type -> Type -> Type
a <~> b = AppT a b
{-# INLINE (<~>) #-}

-- | Convenient syntactic sugar for application of expressions.
--
-- >>> ConE nm <+> ConE nm <+> ConE nm
-- AppE (AppE (ConE nm) (ConE nm)) (ConE nm)
(<+>) :: Exp -> Exp -> Exp
a <+> b = AppE a b
{-# INLINE (<+>) #-}

-- | Convenient syntactic sugar for arrows in types.
--
-- >>> StarT ~> StarT
-- AppT (AppT ArrowT StarT) StarT
(~>) :: Type -> Type -> Type
a ~> b = ArrowT <~> a <~> b
{-# INLINE (~>) #-}

infixl 6 <~>, <+>, ~>

-- | Assert a condition related to the table base name and suggest following the naming convention.
assert :: Bool -> String -> Q ()
assert cond msg = unless cond (reportError $ "Table name does not follow convention: " ++ msg ++ "; use 'MyTableNameT' or so")
-- | Assert a list of conditions and associated error messages.
assertMany :: [(Bool, String)] -> Q ()
assertMany = traverse_ (uncurry assert)

-- | Complain about an unknown field in the table.
invalidConstructor :: Fail.MonadFail m => m a
invalidConstructor = Fail.fail "Invalid constructor field; the primary key must be of the form 'Columnar f SomeType'"