{-# LANGUAGE RecordWildCards #-}

module Eventful.Projection
  ( Projection (..)
  , latestProjection
  , allProjections
  , getLatestProjection
  , getLatestGlobalProjection
  , serializedProjection
  )
  where

import Data.Foldable (foldl')
import Data.List (scanl')
import Data.Maybe (fromMaybe)

import Eventful.Serializer
import Eventful.Store.Class
import Eventful.UUID

-- | A 'Projection' is a piece of @state@ that is constructed only from
-- @event@s. A Projection is how you reconstruct event sourced state from the
-- ordered stream of events that constitute that state. The "seed" of a
-- Projection is the initial state before any events are applied. The event
-- handler for a projection is the function that actually modifies state based
-- on the given event.
data Projection state event
  = Projection
  { projectionSeed :: state
    -- ^ Initial state of a projection
  , projectionEventHandler :: state -> event -> state
    -- ^ The function that applies and event to the current state, producing a
    -- new state.
  }

-- | Computes the latest state of a 'Projection' from some events.
latestProjection :: (Foldable t) => Projection state event -> t event -> state
latestProjection (Projection seed handler) = foldl' handler seed

-- | Given a list of events, produce all the Projections that were ever
-- produced. Just a 'scanl' using 'projectionEventHandler'. This function is
-- useful for testing 'Projection's; you can easily assert that all the states
-- of a Projection are valid given a list of events.
allProjections :: Projection state event -> [event] -> [state]
allProjections (Projection seed handler) = scanl' handler seed

-- | Gets the latest projection from a store by using 'getEvents' and then
-- applying the events using the Projection's event handler.
getLatestProjection
  :: (Monad m)
  => EventStore serialized m
  -> Projection proj serialized
  -> UUID
  -> m (proj, EventVersion)
getLatestProjection store proj uuid = do
  events <- getEvents store uuid Nothing
  let
    latestVersion = maxEventVersion events
    latestProj = latestProjection proj $ storedEventEvent <$> events
  return (latestProj, latestVersion)
  where
    maxEventVersion [] = -1
    maxEventVersion es = maximum $ storedEventVersion <$> es

-- | Gets globally ordered events from the event store and builds a
-- 'Projection' based on 'ProjectionEvent'. Optionally accepts the current
-- projection state as an argument.
getLatestGlobalProjection
  :: (Monad m)
  => GloballyOrderedEventStore serialized m
  -> Projection proj (ProjectionEvent serialized)
  -> Maybe (proj, SequenceNumber)
  -> m (proj, SequenceNumber)
getLatestGlobalProjection store proj mCurrentState = do
  let
    currentState = fromMaybe (projectionSeed proj) $ fst <$> mCurrentState
    startingSequenceNumber = maybe 0 (+1) $ snd <$> mCurrentState
  events <- getSequencedEvents store startingSequenceNumber
  let
    projectionEvents = globallyOrderedEventToProjectionEvent <$> events
    latestState = foldl' (projectionEventHandler proj) currentState projectionEvents
    latestSeq =
      case events of
        [] -> startingSequenceNumber
        _ -> globallyOrderedEventSequenceNumber $ last events
  return (latestState, latestSeq)

-- | Use a 'Serializer' to wrap a 'Projection' with event type @event@ so it
-- uses the @serialized@ type.
serializedProjection
  :: Projection state event
  -> Serializer event serialized
  -> Projection state serialized
serializedProjection (Projection seed eventHandler) Serializer{..} =
  Projection seed serializedHandler
  where
    -- Try to deserialize the event and apply the handler. If we can't
    -- deserialize, then just return the state.
    serializedHandler state = maybe state (eventHandler state) . deserialize