{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}

-- | A way to synchronise an item with safe merge conflicts.
--
-- The item is "zero or one" value.
-- One could say that @Item a = Maybe a@ but there are so such types here.
-- This methaphor just serves as explanation
--
--
-- The setup is as follows:
--
-- * A central server is set up to synchronise with
-- * Each client synchronises with the central server, but never with eachother
--
--
-- = A client should operate as follows:
--
-- The clients starts with an 'initialClientItem'.
--
-- * The client produces a 'ItemSyncRequest' with 'makeItemSyncRequest'.
-- * The client sends that request to the central server and gets a 'ItemSyncResponse'.
-- * The client then updates its local store with 'mergeItemSyncResponseRaw' or 'mergeItemSyncResponseIgnoreProblems'.
--
--
-- = The central server should operate as follows:
--
-- The server starts with an 'initialServerItem'.
--
-- * The server accepts a 'ItemSyncRequest'.
-- * The server performs operations according to the functionality of 'processServerItemSync'.
-- * The server respons with a 'ItemSyncResponse'.
--
--
--
-- WARNING:
-- This whole approach can break down if a server resets its server times
-- or if a client syncs with two different servers using the same server times.
module Data.Mergeful.Item
  ( initialClientItem
  , initialItemSyncRequest
  , makeItemSyncRequest
  , mergeItemSyncResponseRaw
  , ItemMergeResult(..)
  , mergeItemSyncResponseIgnoreProblems
  , mergeIgnoringProblems
  , mergeFromServer
  , ItemMergeStrategy(..)
  , mergeUsingStrategy
    -- * Server side
  , initialServerItem
  , processServerItemSync
    -- * Types, for reference
  , ClientItem(..)
  , ItemSyncRequest(..)
  , ItemSyncResponse(..)
  , ServerItem(..)
  ) where

import GHC.Generics (Generic)

import Data.Aeson as JSON
import Data.Validity

import Control.Applicative

import Data.Mergeful.Timed

data ClientItem a
  -- | There is no item on the client side
  = ClientEmpty
  -- | There is is an item but the server is not aware of it yet.
  | ClientAdded !a
  -- | There is is an item and it has been synced with the server.
  | ClientItemSynced !(Timed a)
  -- | There is is an item and it has been synced with the server, but it has since been modified.
  | ClientItemSyncedButChanged !(Timed a)
  -- | There was an item, and it has been deleted locally, but the server has not been made aware of this.
  | ClientDeleted !ServerTime
  deriving (Show, Eq, Generic)

instance Validity a => Validity (ClientItem a)

instance FromJSON a => FromJSON (ClientItem a) where
  parseJSON =
    withObject "ClientItem" $ \o -> do
      typ <- o .: "type"
      case typ :: String of
        "empty" -> pure ClientEmpty
        "added" -> ClientAdded <$> o .: "value"
        "synced" -> ClientItemSynced <$> (Timed <$> o .: "value" <*> o .: "time")
        "changed" -> ClientItemSyncedButChanged <$> (Timed <$> o .: "value" <*> o .: "time")
        "deleted" -> ClientDeleted <$> o .: "time"
        _ -> fail "unknown item type"

instance ToJSON a => ToJSON (ClientItem a) where
  toJSON ci =
    object $
    case ci of
      ClientEmpty -> ["type" .= ("empty" :: String)]
      ClientAdded a -> ["type" .= ("added" :: String), "value" .= a]
      ClientItemSynced Timed {..} ->
        ["type" .= ("synced" :: String), "value" .= timedValue, "time" .= timedTime]
      ClientItemSyncedButChanged Timed {..} ->
        ["type" .= ("changed" :: String), "value" .= timedValue, "time" .= timedTime]
      ClientDeleted t -> ["type" .= ("deleted" :: String), "time" .= t]

-- | A client item to start with.
--
-- It contains no value.
initialClientItem :: ClientItem a
initialClientItem = ClientEmpty

data ServerItem a
  -- | There is no item on the server side
  = ServerEmpty
  -- | There is an item on the server side, and it was last synced at the given 'ServerTime'.
  | ServerFull !(Timed a)
  deriving (Show, Eq, Generic)

instance Validity a => Validity (ServerItem a)

instance FromJSON a => FromJSON (ServerItem a) where
  parseJSON =
    withObject "ServerItem" $ \o ->
      ServerFull <$> (Timed <$> o .: "value" <*> o .: "time") <|> pure ServerEmpty

instance ToJSON a => ToJSON (ServerItem a) where
  toJSON si =
    object $
    case si of
      ServerEmpty -> []
      ServerFull Timed {..} -> ["value" .= timedValue, "time" .= timedTime]

-- | A server item to start with.
--
-- It contains no value.
initialServerItem :: ServerItem a
initialServerItem = ServerEmpty

data ItemSyncRequest a
  -- | There is no item locally
  = ItemSyncRequestPoll
  -- | There is an item locally that has not been synced to the server yet.
  | ItemSyncRequestNew !a
  -- | There is an item locally that was synced at the given 'ServerTime'
  | ItemSyncRequestKnown !ServerTime
  -- | There is an item locally that was synced at the given 'ServerTime'
  -- but it has been changed since then.
  | ItemSyncRequestKnownButChanged !(Timed a)
  -- | There was an item locally that has been deleted but the
  -- deletion wasn't synced to the server yet.
  | ItemSyncRequestDeletedLocally !ServerTime
  deriving (Show, Eq, Generic)

instance Validity a => Validity (ItemSyncRequest a)

instance FromJSON a => FromJSON (ItemSyncRequest a) where
  parseJSON =
    withObject "ItemSyncRequest" $ \o -> do
      typ <- o .: "type"
      case typ :: String of
        "empty" -> pure ItemSyncRequestPoll
        "added" -> ItemSyncRequestNew <$> o .: "value"
        "synced" -> ItemSyncRequestKnown <$> o .: "time"
        "changed" -> ItemSyncRequestKnownButChanged <$> (Timed <$> o .: "value" <*> o .: "time")
        "deleted" -> ItemSyncRequestDeletedLocally <$> o .: "time"
        _ -> fail "unknown item type"

instance ToJSON a => ToJSON (ItemSyncRequest a) where
  toJSON ci =
    object $
    let o n rest = ("type" .= (n :: String)) : rest
        oe n = o n []
     in case ci of
          ItemSyncRequestPoll -> oe "empty"
          ItemSyncRequestNew a -> o "added" ["value" .= a]
          ItemSyncRequestKnown t -> o "synced" ["time" .= t]
          ItemSyncRequestKnownButChanged Timed {..} ->
            o "changed" ["value" .= timedValue, "time" .= timedTime]
          ItemSyncRequestDeletedLocally t -> o "deleted" ["time" .= t]

-- | An intial 'ItemSyncRequest' to start with.
--
-- It just asks the server to send over whatever it knows.
initialItemSyncRequest :: ItemSyncRequest a
initialItemSyncRequest = ItemSyncRequestPoll

data ItemSyncResponse a
  -- | The client and server are fully in sync, and both empty
  --
  -- Nothing needs to be done at the client side.
  = ItemSyncResponseInSyncEmpty
  -- | The client and server are fully in sync.
  --
  -- Nothing needs to be done at the client side.
  | ItemSyncResponseInSyncFull
  -- | The client added an item and server has succesfully been made aware of that.
  --
  -- The client needs to update its server time
  | ItemSyncResponseClientAdded !ServerTime
  -- | The client changed an item and server has succesfully been made aware of that.
  --
  -- The client needs to update its server time
  | ItemSyncResponseClientChanged !ServerTime
  -- | The client deleted an item and server has succesfully been made aware of that.
  --
  -- The client can delete it from its deleted items
  | ItemSyncResponseClientDeleted
  -- | This item has been added on the server side
  --
  -- The client should add it too.
  | ItemSyncResponseServerAdded !(Timed a)
  -- | This item has been modified on the server side.
  --
  -- The client should modify it too.
  | ItemSyncResponseServerChanged !(Timed a)
  -- | The item was deleted on the server side
  --
  -- The client should delete it too.
  | ItemSyncResponseServerDeleted
  -- | A conflict occurred.
  --
  -- The server and the client both have an item, but it is different.
  -- The server kept its part, the client can either take whatever the server gave them
  -- or deal with the conflict somehow, and then try to re-sync.
  | ItemSyncResponseConflict !(Timed a) -- ^ The item at the server side
  -- | A conflict occurred.
  --
  -- The server has an item but the client does not.
  -- The server kept its part, the client can either take whatever the server gave them
  -- or deal with the conflict somehow, and then try to re-sync.
  | ItemSyncResponseConflictClientDeleted !(Timed a) -- ^ The item at the server side
  -- | A conflict occurred.
  --
  -- The client has a (modified) item but the server does not have any item.
  -- The server left its item deleted, the client can either delete its item too
  -- or deal with the conflict somehow, and then try to re-sync.
  | ItemSyncResponseConflictServerDeleted
  deriving (Show, Eq, Generic)

instance Validity a => Validity (ItemSyncResponse a)

instance FromJSON a => FromJSON (ItemSyncResponse a) where
  parseJSON =
    withObject "ItemSyncResponse" $ \o -> do
      typ <- o .: "type"
      case typ :: String of
        "in-sync-empty" -> pure ItemSyncResponseInSyncEmpty
        "in-sync-full" -> pure ItemSyncResponseInSyncFull
        "client-added" -> ItemSyncResponseClientAdded <$> o .: "time"
        "client-changed" -> ItemSyncResponseClientChanged <$> o .: "time"
        "client-deleted" -> pure ItemSyncResponseClientDeleted
        "server-added" -> ItemSyncResponseServerAdded <$> (Timed <$> o .: "value" <*> o .: "time")
        "server-changed" ->
          ItemSyncResponseServerChanged <$> (Timed <$> o .: "value" <*> o .: "time")
        "server-deleted" -> pure ItemSyncResponseServerDeleted
        "conflict" -> ItemSyncResponseConflict <$> o .: "value"
        "conflict-client-deleted" -> ItemSyncResponseConflictClientDeleted <$> o .: "value"
        "conflict-server-deleted" -> pure ItemSyncResponseConflictServerDeleted
        _ -> fail "unknown type"

instance ToJSON a => ToJSON (ItemSyncResponse a) where
  toJSON isr =
    object $
    let o s rest = ("type" .= (s :: String)) : rest
        oe s = o s []
     in case isr of
          ItemSyncResponseInSyncEmpty -> oe "in-sync-empty"
          ItemSyncResponseInSyncFull -> oe "in-sync-full"
          ItemSyncResponseClientAdded t -> o "client-added" ["time" .= t]
          ItemSyncResponseClientChanged t -> o "client-changed" ["time" .= t]
          ItemSyncResponseClientDeleted -> oe "client-deleted"
          ItemSyncResponseServerAdded Timed {..} ->
            o "server-added" ["value" .= timedValue, "time" .= timedTime]
          ItemSyncResponseServerChanged Timed {..} ->
            o "server-changed" ["value" .= timedValue, "time" .= timedTime]
          ItemSyncResponseServerDeleted -> oe "server-deleted"
          ItemSyncResponseConflict a -> o "conflict" ["value" .= a]
          ItemSyncResponseConflictClientDeleted a -> o "conflict-client-deleted" ["value" .= a]
          ItemSyncResponseConflictServerDeleted -> oe "conflict-server-deleted"

-- | Produce an 'ItemSyncRequest' from a 'ClientItem'.
--
-- Send this to the server for synchronisation.
makeItemSyncRequest :: ClientItem a -> ItemSyncRequest a
makeItemSyncRequest cs =
  case cs of
    ClientEmpty -> ItemSyncRequestPoll
    ClientAdded i -> ItemSyncRequestNew i
    ClientItemSynced t -> ItemSyncRequestKnown (timedTime t)
    ClientItemSyncedButChanged t -> ItemSyncRequestKnownButChanged t
    ClientDeleted st -> ItemSyncRequestDeletedLocally st

data ItemMergeResult a
  -- | The merger went succesfully, no conflicts or desyncs
  = MergeSuccess !(ClientItem a)
  -- | There was a merge conflict. The server and client had different, conflicting versions.
  | MergeConflict !a !(Timed a) -- ^ The item at the server side
  -- | There was a merge conflict. The client had deleted the item while the server had modified it.
  | MergeConflictClientDeleted !(Timed a) -- ^ The item at the server side
  -- | There was a merge conflict. The server had deleted the item while the client had modified it.
  | MergeConflictServerDeleted !a -- ^ The item at the client side
  -- | The server responded with a response that did not make sense given the client's request.
  | MergeMismatch
  deriving (Show, Eq, Generic)

instance Validity a => Validity (ItemMergeResult a)

-- | Merge an 'ItemSyncResponse' into the current 'ClientItem'.
--
-- This function will not make any decisions about what to do with
-- conflicts or mismatches between the request and the response.
-- It only produces a 'ItemMergeResult' so you can decide what to do with it.
mergeItemSyncResponseRaw :: ClientItem a -> ItemSyncResponse a -> ItemMergeResult a
mergeItemSyncResponseRaw cs sr =
  case cs of
    ClientEmpty ->
      case sr of
        ItemSyncResponseInSyncEmpty -> MergeSuccess cs
        ItemSyncResponseServerAdded t -> MergeSuccess $ ClientItemSynced t
        _ -> MergeMismatch
    ClientAdded ci ->
      case sr of
        ItemSyncResponseClientAdded st ->
          MergeSuccess $ ClientItemSynced $ Timed {timedValue = ci, timedTime = st}
        ItemSyncResponseConflict si -> MergeConflict ci si
        _ -> MergeMismatch
    ClientItemSynced t ->
      case sr of
        ItemSyncResponseInSyncFull -> MergeSuccess $ ClientItemSynced t
        ItemSyncResponseServerChanged st -> MergeSuccess $ ClientItemSynced st
        ItemSyncResponseServerDeleted -> MergeSuccess ClientEmpty
        _ -> MergeMismatch
    ClientItemSyncedButChanged ct ->
      case sr of
        ItemSyncResponseClientChanged st -> MergeSuccess $ ClientItemSynced $ ct {timedTime = st}
        ItemSyncResponseConflict si -> MergeConflict (timedValue ct) si
        ItemSyncResponseConflictServerDeleted -> MergeConflictServerDeleted (timedValue ct)
        _ -> MergeMismatch
    ClientDeleted _ ->
      case sr of
        ItemSyncResponseClientDeleted -> MergeSuccess ClientEmpty
        ItemSyncResponseConflictClientDeleted si -> MergeConflictClientDeleted si
        _ -> MergeMismatch

-- | Merge an 'ItemSyncResponse' into the current 'ClientItem'.
--
-- This function ignores any problems that may occur.
-- In the case of a conclict, it will just not update the client item.
-- The next sync request will then produce a conflict again.
--
-- > mergeItemSyncResponseIgnoreProblems cs = mergeIgnoringProblems cs . mergeItemSyncResponseRaw cs
mergeItemSyncResponseIgnoreProblems :: ClientItem a -> ItemSyncResponse a -> ClientItem a
mergeItemSyncResponseIgnoreProblems cs = mergeIgnoringProblems cs . mergeItemSyncResponseRaw cs

-- | A strategy to merge conflicts for item synchronisation
data ItemMergeStrategy a =
  ItemMergeStrategy
    { itemMergeStrategyMergeChangeConflict :: a -> Timed a -> Timed a
      -- ^ How to merge modification conflicts
    , itemMergeStrategyMergeClientDeletedConflict :: Timed a -> Maybe (Timed a)
      -- ^ How to merge conflicts where the client deleted an item that the server modified
    , itemMergeStrategyMergeServerDeletedConflict :: a -> Maybe a
      -- ^ How to merge conflicts where the server deleted an item that the client modified
    }
  deriving (Generic)

-- | Ignore any merge problems in a 'ItemMergeResult'.
--
-- This function just returns the original 'ClientItem' if anything other than 'MergeSuccess' occurs.
--
-- This function ignores any problems that may occur.
-- In the case of a conclict, it will just not update the client item.
-- The next sync request will then produce a conflict again.
--
-- Pro: does not lose data
--
-- __Con: Clients will diverge when a conflict occurs__
mergeIgnoringProblems :: ClientItem a -> ItemMergeResult a -> ClientItem a
mergeIgnoringProblems cs mr =
  case mr of
    MergeSuccess cs' -> cs'
    MergeConflict _ _ -> cs
    MergeConflictServerDeleted _ -> cs
    MergeConflictClientDeleted _ -> cs
    MergeMismatch -> cs

-- | Resolve an 'ItemMergeResult' using a given merge strategy.
--
-- This function ignores 'MergeMismatch' and will just return the original 'ClientItem' in that case.
--
-- In order for clients to converge on the same item correctly, this function must be:
--
-- * Associative
-- * Idempotent
-- * The same on all clients
mergeUsingStrategy :: ItemMergeStrategy a -> ClientItem a -> ItemMergeResult a -> ClientItem a
mergeUsingStrategy ItemMergeStrategy {..} cs mr =
  case mr of
    MergeSuccess cs' -> cs'
    MergeConflict a1 a2 -> ClientItemSynced $ itemMergeStrategyMergeChangeConflict a1 a2
    MergeConflictClientDeleted sa ->
      case itemMergeStrategyMergeClientDeletedConflict sa of
        Nothing -> ClientEmpty
        Just t -> ClientItemSynced t
    MergeConflictServerDeleted ca ->
      case itemMergeStrategyMergeServerDeletedConflict ca of
        Nothing -> ClientEmpty
        Just a -> ClientAdded a
    MergeMismatch -> cs

-- | Resolve an 'ItemMergeResult' by taking whatever the server gave the client.
--
-- Pro: Clients will converge on the same value.
--
-- __Con: Conflicting updates will be lost.__
mergeFromServer :: ClientItem a -> ItemMergeResult a -> ClientItem a
mergeFromServer =
  mergeUsingStrategy
    ItemMergeStrategy
      { itemMergeStrategyMergeChangeConflict = \_ serverItem -> serverItem
      , itemMergeStrategyMergeClientDeletedConflict = \serverItem -> Just serverItem
      , itemMergeStrategyMergeServerDeletedConflict = \_ -> Nothing
      }

-- | Serve an 'ItemSyncRequest' using the current 'ServerItem', producing an 'ItemSyncResponse' and a new 'ServerItem'.
processServerItemSync :: ServerItem a -> ItemSyncRequest a -> (ItemSyncResponse a, ServerItem a)
processServerItemSync store sr =
  case store of
    ServerEmpty ->
      let t = initialServerTime
       in case sr of
            ItemSyncRequestPoll -> (ItemSyncResponseInSyncEmpty, store)
            ItemSyncRequestNew ci ->
              (ItemSyncResponseClientAdded t, ServerFull $ Timed {timedValue = ci, timedTime = t})
            ItemSyncRequestKnown _
             -- This indicates that the server synced with another client and was told to
             -- delete its item.
             --
             -- Given that the client indicates that it did not change anything locally,
             -- the server will just instruct the client to delete its item too.
             -- No conflict here.
             -> (ItemSyncResponseServerDeleted, store)
            ItemSyncRequestKnownButChanged _
             -- This indicates that the server synced with another client and was told to
             -- delete its item.
             --
             -- Given that the client indicates that it *did* change its item locally,
             -- there is a conflict.
             -> (ItemSyncResponseConflictServerDeleted, store)
            ItemSyncRequestDeletedLocally _
             -- This means that the server synced with another client,
             -- was instructed to delete its item by that client,
             -- and is now being told to delete its item again.
             --
             -- That's fine, it will just remain deleted.
             -- No conflict here
             -> (ItemSyncResponseClientDeleted, store)
    ServerFull t@(Timed si st) ->
      let st' = incrementServerTime st
       in case sr of
            ItemSyncRequestPoll
              -- The client is empty but the server is not.
              -- This means that the server has synced with another client before,
              -- so we can just send the item to the client.
             -> (ItemSyncResponseServerAdded (Timed {timedValue = si, timedTime = st}), store)
            ItemSyncRequestNew _
              -- The client has a newly added item, so it thought it was empty before that,
              -- but the server has already synced with another client before.
              -- This indicates a conflict.
              -- The server is always right, so the item at the server will remain unmodified.
              -- The client will receive the conflict.
             -> (ItemSyncResponseConflict t, store)
            ItemSyncRequestKnown ct ->
              if ct >= st
                -- The client time is equal to the server time.
                -- The client indicates that the item was not modified at their side.
                -- This means that the items are in sync.
                -- (Unless the server somehow modified the item but not its server time,
                -- which would beconsidered a bug.)
                then (ItemSyncResponseInSyncFull, store)
                -- The client time is less than the server time
                -- That means that the server has synced with another client in the meantime.
                -- Since the client indicates that the item was not modified at their side,
                -- we can just send it back to the client to have them update their version.
                -- No conflict here.
                else ( ItemSyncResponseServerChanged (Timed {timedValue = si, timedTime = st})
                     , store)
            ItemSyncRequestKnownButChanged (Timed {timedValue = ci, timedTime = ct}) ->
              if ct >= st
                -- The client time is equal to the server time.
                -- The client indicates that the item *was* modified at their side.
                -- This means that the server needs to be updated.
                then ( ItemSyncResponseClientChanged st'
                     , ServerFull (Timed {timedValue = ci, timedTime = st'}))
                -- The client time is less than the server time
                -- That means that the server has synced with another client in the meantime.
                -- Since the client indicates that the item *was* modified at their side,
                -- there is a conflict.
                else (ItemSyncResponseConflict t, store)
            ItemSyncRequestDeletedLocally ct ->
              if ct >= st
                -- The client time is equal to the server time.
                -- The client indicates that the item was deleted on their side.
                -- This means that the server item needs to be deleted as well.
                then (ItemSyncResponseClientDeleted, ServerEmpty)
                -- The client time is less than the server time
                -- That means that the server has synced with another client in the meantime.
                -- Since the client indicates that the item was deleted at their side,
                -- there is a conflict.
                else (ItemSyncResponseConflictClientDeleted t, store)