{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DisambiguateRecordFields #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeFamilies #-}

module RON.Schema.TH.Struct (mkReplicatedStructLww) where

import           RON.Prelude

import qualified Data.ByteString.Char8 as BSC
import           Data.Char (toTitle)
import qualified Data.Map.Strict as Map
import qualified Data.Text as Text
import           Language.Haskell.TH (Exp (VarE), bindS, conP, conT, doE, listE,
                                      noBindS, recC, recConE, sigD, varE, varP,
                                      varT)
import qualified Language.Haskell.TH as TH
import           Language.Haskell.TH.Syntax (liftData)

import           RON.Data (MonadObjectState, ObjectStateT,
                           Replicated (encoding), ReplicatedAsObject, getObject,
                           getObjectStateChunk, newObject, objectEncoding,
                           objectOpType)
import           RON.Data.LWW (lwwType)
import qualified RON.Data.LWW as LWW
import           RON.Error (MonadE, errorContext)
import           RON.Event (ReplicaClock)
import           RON.Schema as X
import           RON.Schema.TH.Common (liftText, mkGuideType, mkNameT, valDP)
import           RON.Types (Object (Object), UUID)
import           RON.Util (Instance (Instance))
import qualified RON.UUID as UUID

data Field' = Field'
    { haskellName :: Text
    , ronName     :: UUID
    , ronType     :: RonType
    , var         :: TH.Name
    }

mkReplicatedStructLww :: StructLww 'Resolved -> TH.DecsQ
mkReplicatedStructLww StructLww{name, fields, annotations} = do
    fields' <- for (Map.assocs fields) $ \(haskellName, Field{ronType}) ->
        case UUID.mkName . BSC.pack $ Text.unpack haskellName of
            Just ronName -> do
                var <- TH.newName $ Text.unpack haskellName
                pure Field'{haskellName, ronName, ronType, var}
            Nothing -> fail $
                "Field name is not representable in RON: " ++ show haskellName
    dataType <- mkDataType name' fields annotations
    [instanceReplicated] <- mkInstanceReplicated structType
    [instanceReplicatedAsObject] <-
        mkInstanceReplicatedAsObject name fields' annotations
    accessors <- fold <$> traverse (mkAccessors structType annotations) fields'
    pure $
        dataType : instanceReplicated : instanceReplicatedAsObject : accessors
  where
    name' = mkNameT name
    structType = conT name'

mkDataType
    :: TH.Name -> Map Text (Field Resolved) -> StructAnnotations -> TH.DecQ
mkDataType name fields annotations = TH.dataD (TH.cxt []) name [] Nothing
    [recC name
        [ TH.varBangType (mkNameT $ mkHaskellFieldName annotations fieldName) $
            TH.bangType (TH.bang TH.sourceNoUnpack TH.sourceStrict) $
            mkGuideType ronType
        | (fieldName, Field ronType) <- Map.assocs fields
        ]]
    []

mkInstanceReplicated :: TH.TypeQ -> TH.DecsQ
mkInstanceReplicated structType = [d|
    instance Replicated $structType where
        encoding = objectEncoding
    |]

mkInstanceReplicatedAsObject
    :: Text -> [Field'] -> StructAnnotations -> TH.DecsQ
mkInstanceReplicatedAsObject name fields annotations = do
    ops <- TH.newName "ops"
    let fieldsToUnpack =
            [ bindS (varP var)
                [| LWW.viewField $(liftData ronName) $(varE ops) |]
            | Field'{var, ronName} <- fields
            ]
    let getObjectImpl = doE
            $   bindS (varP ops) [| getObjectStateChunk |]
            :   fieldsToUnpack
            ++  [noBindS [| pure $consE |]]
    [d| instance ReplicatedAsObject $structType where
            objectOpType = lwwType
            newObject $consP = Object <$> LWW.newObject $fieldsToPack
            getObject =
                errorContext $(liftText errCtx) $getObjectImpl
        |]
  where
    name' = mkNameT name
    structType = conT name'
    fieldsToPack = listE
        [ [| ($(liftData ronName), Instance $(varE var)) |]
        | Field'{var, ronName} <- fields
        ]
    errCtx = "getObject @" <> name <> ":\n"
    consE = recConE name'
        [ pure (fieldName, VarE var)
        | Field'{haskellName, var} <- fields
        , let fieldName = mkNameT $ mkHaskellFieldName annotations haskellName
        ]
    consP = conP name' [varP var | Field'{var} <- fields]

mkHaskellFieldName :: StructAnnotations -> Text -> Text
mkHaskellFieldName annotations base = prefix <> base' where
    StructAnnotations
            { haskellFieldPrefix        = prefix
            , haskellFieldCaseTransform = caseTransform
            }
        = annotations
    base' = case caseTransform of
        Nothing        -> base
        Just TitleCase -> case Text.uncons base of
            Nothing            -> base
            Just (b, baseTail) -> Text.cons (toTitle b) baseTail

mkAccessors :: TH.TypeQ -> StructAnnotations -> Field' -> TH.DecsQ
mkAccessors structType annotations field' = do
    a <- varT <$> TH.newName "a"
    m <- varT <$> TH.newName "m"
    let assignF =
            [ sigD assign [t|
                (ReplicaClock $m, MonadE $m, MonadObjectState $structType $m)
                => $fieldGuideType -> $m ()
                |]
            , valDP assign [| LWW.assignField $(liftData ronName) |]
            ]
        readF =
            [ sigD read [t|
                (MonadE $m, MonadObjectState $structType $m)
                => $m $fieldGuideType
                |]
            , valDP read [| LWW.readField $(liftData ronName) |]
            ]
        zoomF =
            [ sigD zoom [t|
                MonadE $m
                => ObjectStateT $(mkGuideType ronType) $m $a
                -> ObjectStateT $structType                  $m $a
                |]
            , valDP zoom [| LWW.zoomField $(liftData ronName) |]
            ]
    sequenceA $ assignF ++ readF ++ zoomF
  where
    Field'{haskellName, ronName, ronType} = field'
    fieldGuideType = mkGuideType ronType
    assign = mkNameT $ mkHaskellFieldName annotations haskellName <> "_assign"
    read   = mkNameT $ mkHaskellFieldName annotations haskellName <> "_read"
    zoom   = mkNameT $ mkHaskellFieldName annotations haskellName <> "_zoom"