{-# LANGUAGE OverloadedStrings          #-}
{-# LANGUAGE TemplateHaskell            #-}

{- |

Unstable, may change without warning.

Utility funcs to support "Exigo.Persistent.TH".

-}

module Exigo.Persistent.TH.Internal
  where

import           Database.Persist           ( FieldDef(..)
                                            , EntityDef(..)
                                            , FieldType(FTTypeCon)
                                            , HaskellName(..) )
import           Data.Char
import           Data.Monoid                ( (<>) )
import qualified Data.Text as T
import           Data.Text                  ( Text, cons, uncons )
import           Database.Persist.TH        ( MkPersistSettings(..) )
import           Language.Haskell.TH.Syntax

import           Exigo.Types ( AssessmentMetadata(..) )

{-# ANN module ("HLint: ignore Use camelCase" :: String) #-}

-- | create a function which returns at runtime the
-- assessment metadata passed in at compile-time.
--
-- i.e., if @mkSaveAssessmentMetadata myFunc mData@ is called,
-- it creates a function like
--
-- @
-- myFunc :: AssessmentMetadata
-- myFunc = mData
-- @
mkSaveAssessmentMetadata :: String -> AssessmentMetadata -> Q [Dec]
mkSaveAssessmentMetadata name' mData' = do
  let name = mkName name'
  mData <- lift mData'
  return [ SigD name $ ConT ''AssessmentMetadata
         , ValD (VarP name) (NormalB mData) []
         ]

-- | create a function which returns all the question-type field
--  accessors for Marks.
--
-- i.e., @mkQuestionFieldsAccessor sqlSettings myName@
-- in a call to @share@
-- should produce a result something like
--
-- @
-- myNHame :: [Marks -> Double]
-- myName = [ marksQ1a, marksQ1b, marksQ1c .. ]
-- @
--
-- where the accessors are in the order they appear
-- in the EntityDef.
mkQuestionFieldsAccessor :: MkPersistSettings -> String -> [EntityDef] -> Q [Dec]
mkQuestionFieldsAccessor mpSettings nm es = do
    let es' = filter isMarks es
    case es' of
      [] -> error "no entities with name 'Marks' - did you call mkQuestionFieldsAccessor intentionally?"
      [_] -> concat <$> mapM (mkAcc nm) es'
      _   -> error "multiple entities with name 'Marks' - did you call mkQuestionFieldsAccessor intentionally?"
  where

    mkAcc :: String -> EntityDef -> Q [Dec]
    mkAcc name' e = do
      let name = mkName name'
          -- the various field accessors: q1a, q1b, etc
          qFields = map (VarE . mkName . accessorName mpSettings . fName) $
                      filter isQField $ entityFields e
          funcType = x_to_listXT (marks_to_xT (ConT ''Double))
          funcSig = SigD name funcType
          func = FunD name [normalClause [] (ListE qFields)]
      return [ funcSig, func ]


    -- does this look like a "q1a" etc field?
    -- viz., it starts with a 'q', and doesn't contain
    -- the string "Comments" in its name, and
    -- is of type Double
    isQField :: FieldDef -> Bool
    isQField f = let n = unHaskellName $ fieldHaskell f
                 in "q" `T.isPrefixOf` n
                    && not ("comments" `T.isInfixOf` n)
                    && fieldType f == FTTypeCon Nothing "Double"


-- | create a function which returns all the comment-type field
--  accessors for Marks.
--
-- i.e., @mkCommentFieldsAccessor myName@
--
-- should produce a result something like
--
-- @
-- myNHame :: [Marks -> Maybe Text]
-- myName = [ marksQ1aComments, marksQ1bComments, marksQ1cComments .. ]
-- @
--
-- where the accessors are in the order they appear
-- in the EntityDef.
mkCommentFieldsAccessor ::
  MkPersistSettings -> String -> [EntityDef] -> Q [Dec]
mkCommentFieldsAccessor mpSettings nm es =
  let es' = filter isMarks es
  in concat <$> mapM (mkAcc nm) es'
  where

    mkAcc :: String -> EntityDef -> Q [Dec]
    mkAcc name' e = do
      let name = mkName name'
          -- the various field accessors: marksQ1aComments, etc
          qFields = map (VarE . mkName . accessorName mpSettings . fName) $
                      filter isCommentsField $ entityFields e
          funcType = x_to_listXT (marks_to_xT ( x_to_maybeXT $ ConT ''Text ))
          funcSig = SigD name funcType
          func = FunD name [normalClause [] (ListE qFields)]
      return [ funcSig, func ]

    -- does this look like a "q1a" etc field?
    -- viz., it starts with a 'q', and doesn't contain
    -- the string "Comments" in its name, and
    -- is of type Double
    isCommentsField :: FieldDef -> Bool
    isCommentsField f = let n = unHaskellName $ fieldHaskell f
                 in  "comments" `T.isInfixOf` T.map toLower n
                    && fieldType f == FTTypeCon Nothing "Text"
                    && "Maybe" `elem` fieldAttrs f


-- | given some field from an entty def -- e.g. "q1a" --
-- get the actual accessor name (i.e. "marksQ1a")
accessorName :: MkPersistSettings -> Text -> String
accessorName mpSettings f =
  T.unpack $ recName mpSettings (HaskellName "Marks") (HaskellName f)

-- | is this the "Marks" entity?
isMarks :: EntityDef -> Bool
isMarks e = let eNm = unHaskellName $ entityHaskell e
            in eNm == "Marks"

fName :: FieldDef -> Text
fName = unHaskellName . fieldHaskell

-- the "Marks" type
marksT :: Type
marksT = ConT $ mkName "Marks"

-- given type X, construct type [X]
x_to_listXT :: Type -> Type
x_to_listXT = AppT ListT

-- given type X, construct type [X]
x_to_maybeXT :: Type -> Type
x_to_maybeXT = AppT (ConT ''Maybe)

-- the type "Marks -> x"
marks_to_xT :: Type -> Type
marks_to_xT = AppT (AppT ArrowT marksT)

normalClause :: [Pat] -> Exp -> Clause
normalClause p e = Clause p (NormalB e) []

-- make first letter lowercase
lowerFirst :: Text -> Text
lowerFirst t =
    case uncons t of
        Just (a, b) -> cons (toLower a) b
        Nothing -> t

-- make first letter uppercase
upperFirst :: Text -> Text
upperFirst t =
    case uncons t of
        Just (a, b) -> cons (toUpper a) b
        Nothing -> t

-- make a ... record name for a field, with no underscore?
recNameNoUnderscore :: MkPersistSettings -> HaskellName -> HaskellName -> Text
recNameNoUnderscore mps dt f
  | mpsPrefixFields mps = lowerFirst (unHaskellName dt) <>  upperFirst ft
  | otherwise           = lowerFirst ft
  where
    ft = unHaskellName f

-- | name for a record field
--
-- If we call @recName datatypeName fieldName@
-- this returns a 'qualified field name' (i.e. data type name
-- is prepended, and the whole thing is camelCased.
--
-- i.e.
-- @
-- recName "MyType" "myfield" == "myTypeMyField"
-- @
-- (modulo name constructors).
recName :: MkPersistSettings -> HaskellName -> HaskellName -> Text
recName mps dt f =
    addUnderscore $ recNameNoUnderscore mps dt f
  where
    addUnderscore
        | mpsGenerateLenses mps = ("_" <>)
        | otherwise = id