{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeApplications #-}

-- | LWW-per-field RDT
module RON.Data.LWW
    ( LwwPerField (..)
    , assignField
    , lwwType
    , newObject
    , readField
    , viewField
    , zoomField
    ) where

import qualified Data.Map.Strict as Map

import           RON.Data.Internal (Reducible, Replicated, collectFrame,
                                    fromRon, getObjectStateChunk, mkStateChunk,
                                    newRon, reducibleOpType, stateFromChunk,
                                    stateToChunk)
import           RON.Error (MonadE, errorContext)
import           RON.Event (ReplicaClock, advanceToUuid, getEventUuid)
import           RON.Types (Atom (AUuid), Object (..), Op (..), StateChunk (..),
                            StateFrame, UUID)
import           RON.Util (Instance (Instance))
import qualified RON.UUID as UUID

-- | Last-Write-Wins: select an op with latter event
lww :: Op -> Op -> Op
lww = maxOn opId

-- | Untyped LWW. Implementation: a map from 'opRef' to the original op.
newtype LwwPerField = LwwPerField (Map UUID Op)
    deriving (Eq, Monoid, Show)

instance Semigroup LwwPerField where
    LwwPerField fields1 <> LwwPerField fields2 =
        LwwPerField $ Map.unionWith lww fields1 fields2

instance Reducible LwwPerField where
    reducibleOpType = lwwType

    stateFromChunk ops =
        LwwPerField $ Map.fromListWith lww [(refId, op) | op@Op{refId} <- ops]

    stateToChunk (LwwPerField fields) = mkStateChunk lwwType $ Map.elems fields

-- | Name-UUID to use as LWW type marker.
lwwType :: UUID
lwwType = $(UUID.liftName "lww")

-- | Create LWW object from a list of named fields.
newObject :: ReplicaClock m => [(UUID, Instance Replicated)] -> m (Object a)
newObject fields = collectFrame $ do
    payloads <- for fields $ \(_, Instance value) -> newRon value
    event <- lift getEventUuid
    tell $
        Map.singleton event $
        StateChunk
            { stateType = lwwType
            , stateVersion = event
            , stateBody =
                [Op event name p | ((name, _), p) <- zip fields payloads]
            }
    pure event

-- | Decode field value
viewField
    :: (Replicated a, MonadE m)
    => UUID        -- ^ Field name
    -> StateChunk  -- ^ LWW object chunk
    -> StateFrame
    -> m a
viewField field StateChunk{..} frame =
    errorContext ("LWW.viewField " <> show field) $ do
        let ops = filter (\Op{refId} -> refId == field) stateBody
        Op{payload} <- case ops of
            []   -> throwError "no field in lww chunk"
            [op] -> pure op
            _    -> throwError "unreduced state"
        fromRon payload frame

-- | Decode field value
readField
    :: (MonadE m, MonadState (Object a) m, Replicated b)
    => UUID  -- ^ Field name
    -> m b
readField field = do
    obj@Object{..} <- get
    stateChunk <- getObjectStateChunk obj
    viewField field stateChunk frame

-- | Assign a value to a field
assignField
    :: forall a b m
    . (Replicated b, ReplicaClock m, MonadE m, MonadState (Object a) m)
    => UUID  -- ^ Field name
    -> b     -- ^ Value (from untyped world)
    -> m ()
assignField field value = do
    obj@Object{id, frame} <- get
    StateChunk{..} <- getObjectStateChunk obj
    advanceToUuid stateVersion
    let chunk = filter (\Op{refId} -> refId /= field) stateBody
    event <- getEventUuid
    (p, frame') <- runWriterT $ newRon value
    let newOp = Op event field p
    let chunk' = sortOn refId $ newOp : chunk
    let state' = StateChunk
            {stateVersion = event, stateBody = chunk', stateType = lwwType}
    put obj{frame = Map.insert id state' frame <> frame'}

-- | Anti-lens to an object inside a specified field
zoomField
    :: MonadE m
    => UUID                       -- ^ Field name
    -> StateT (Object inner) m a  -- ^ Nested object modifier
    -> StateT (Object outer) m a
zoomField field innerModifier =
    errorContext ("LWW.zoomField" <> show field) $ do
        obj@Object{..} <- get
        StateChunk{..} <- getObjectStateChunk obj
        let ops = filter (\Op{refId} -> refId == field) stateBody
        Op{payload} <- case ops of
            []   -> throwError "empty chunk"
            [op] -> pure op
            _    -> throwError "unreduced state"
        innerObjectId <- errorContext "inner object" $ case payload of
            [AUuid oid] -> pure oid
            _           -> throwError "Expected object UUID"
        let innerObject = Object innerObjectId frame
        (a, Object{frame = frame'}) <-
            lift $ runStateT innerModifier innerObject
        put obj{frame = frame'}
        pure a