{-# LANGUAGE OverloadedLabels #-}
{-# LANGUAGE OverloadedStrings #-}

module Octane.Utility.Optimizer (optimizeFrames) where

import Data.Function ((&))

import qualified Data.Foldable as Foldable
import qualified Data.IntMap.Strict as IntMap
import qualified Data.Map.Strict as Map
import qualified Data.Text as StrictText
import qualified Octane.Type.CompressedWord as CompressedWord
import qualified Octane.Type.Frame as Frame
import qualified Octane.Type.Replication as Replication
import qualified Octane.Type.State as State
import qualified Octane.Type.Value as Value


-- | Optimizes frames by removing unnecessary replications.
optimizeFrames :: [Frame.Frame] -> [Frame.Frame]
optimizeFrames frames = frames
    & Foldable.foldl'
        (\ (state, fs) f -> let
            newState = updateState f state
            minimalFrame = getDelta state f
            in (newState, minimalFrame : fs))
        (initialState, [])
    & snd
    & reverse


-- { actor id => (alive?, { property name => property value } ) }
type State = IntMap.IntMap (Bool, Map.Map StrictText.Text Value.Value)


initialState :: State
initialState = IntMap.empty


updateState :: Frame.Frame -> State -> State
updateState frame state1 = let
    spawned = frame
        & #replications
        & filter (\ replication -> replication
            & #state
            & (== State.SOpening))
        & map #actorId
        & map CompressedWord.fromCompressedWord
    state2 = spawned
        & foldr
            (IntMap.alter (\ maybeValue -> Just (case maybeValue of
                Nothing -> (True, Map.empty)
                Just (_, properties) -> (True, properties))))
            state1

    destroyed = frame
        & #replications
        & filter (\ replication -> replication
            & #state
            & (== State.SClosing))
        & map #actorId
        & map CompressedWord.fromCompressedWord
    state3 = destroyed
        & foldr
            (IntMap.alter (\ maybeValue -> Just (case maybeValue of
                Nothing -> (False, Map.empty)
                Just (_, properties) -> (False, properties))))
            state2

    updated = frame
        & #replications
        & filter (\ replication -> replication
            & #state
            & (== State.SExisting))
    state4 = updated
        & foldr
            (\ replication -> IntMap.alter
                (\ maybeValue -> Just (case maybeValue of
                    Nothing ->
                        (True, #properties replication)
                    Just (alive, properties) ->
                        ( alive
                        , Map.union
                            (#properties replication)
                            properties
                        )))
                (replication & #actorId & CompressedWord.fromCompressedWord))
            state3

    in state4


getDelta :: State -> Frame.Frame -> Frame.Frame
getDelta state frame = let
    newReplications = frame
        & #replications
        -- Remove replications that aren't actually new.
        & reject (\ replication -> let
            isOpening = #state replication == State.SOpening
            actorId = #actorId replication
            currentState = IntMap.lookup (CompressedWord.fromCompressedWord actorId) state
            isAlive = fmap fst currentState
            wasAlreadyAlive = isAlive == Just True
            in isOpening && wasAlreadyAlive)
        -- Remove properties that haven't changed.
        & map (\ replication ->
            if #state replication == State.SExisting
            then let
                actorId = #actorId replication
                currentState = IntMap.findWithDefault
                    (True, Map.empty) (CompressedWord.fromCompressedWord actorId) state
                currentProperties = snd currentState
                newProperties = #properties replication
                changes = newProperties
                    & Map.filterWithKey (\ name newValue -> let
                        oldValue = Map.lookup name currentProperties
                        in Just newValue /= oldValue)
                in replication { Replication.replicationProperties = changes }
            else replication)
    in frame { Frame.frameReplications = newReplications }


reject :: (a -> Bool) -> [a] -> [a]
reject p xs = filter (\ x -> not (p x)) xs