{-# language ConstraintKinds #-}
{-# language UndecidableInstances #-}
{-# options_ghc -Wno-unused-do-bind #-}


module Nix.Thunk.Basic
  ( NThunkF(..)
  , Deferred(..)
  , deferred
  , MonadBasicThunk
  ) where

import           Nix.Prelude
import           Control.Monad.Ref              ( MonadRef(Ref, newRef, readRef, writeRef)
                                                , MonadAtomicRef(atomicModifyRef)
                                                )
import           Control.Monad.Catch            ( MonadCatch(..)
                                                , MonadThrow(throwM)
                                                )
import qualified Text.Show
import           Nix.Thunk


-- * Data type @Deferred@

-- | Data is computed OR in a lazy thunk state which
-- is still not evaluated.
data Deferred m v = Computed v | Deferred (m v)
  deriving (Functor, Foldable, Traversable)

-- ** Utils

-- | Apply second if @Deferred@, otherwise (@Computed@) - apply first.
-- Analog of @either@ for @Deferred = Computed|Deferred@.
deferred :: (v -> b) -> (m v -> b) -> Deferred m v -> b
deferred f1 f2 =
  \case
    Computed v -> f1 v
    Deferred action -> f2 action
{-# inline deferred #-}


-- * Thunk references & lock handling

-- | Thunk resource reference (@ref-tf: Ref m@), and as such also also hold
-- a @Bool@ lock flag.
type ThunkRef m = Ref m Bool

-- | Reference (@ref-tf: Ref m v@) to a value that the thunk holds.
type ThunkValueRef m v = Ref m (Deferred m v)

-- | @ref-tf@ lock instruction for @Ref m@ (@ThunkRef@).
lockVal :: Bool -> (Bool, Bool)
lockVal = (True, )

-- | @ref-tf@ unlock instruction for @Ref m@ (@ThunkRef@).
unlockVal :: Bool -> (Bool, Bool)
unlockVal = (False, )

-- | Takes @ref-tf: Ref m@ reference, returns Bool result of the operation.
lock
  :: ( MonadBasicThunk m
    , MonadCatch m
    )
  => ThunkRef m
  -> m Bool
lock r = atomicModifyRef r lockVal

-- | Takes @ref-tf: Ref m@ reference, returns Bool result of the operation.
unlock
  :: ( MonadBasicThunk m
    , MonadCatch m
    )
  => ThunkRef m
  -> m Bool
unlock r = atomicModifyRef r unlockVal


-- * Data type for thunks: @NThunkF@

-- | The type of very basic thunks
data NThunkF m v =
  Thunk (ThunkId m) (ThunkRef m) (ThunkValueRef m v)

instance (Eq v, Eq (ThunkId m)) => Eq (NThunkF m v) where
  Thunk x _ _ == Thunk y _ _ = x == y

instance Show (NThunkF m v) where
  show Thunk{} = toString thunkStubText

type MonadBasicThunk m = (MonadThunkId m, MonadAtomicRef m)


-- ** @instance MonadThunk NThunkF@

instance (MonadBasicThunk m, MonadCatch m)
  => MonadThunk (NThunkF m v) m v where

  thunkId :: NThunkF m v -> ThunkId m
  thunkId (Thunk n _ _) = n

  thunk :: m v -> m (NThunkF m v)
  thunk action =
    do
      freshThunkId <- freshId
      liftA2 (Thunk freshThunkId)
        (newRef   False          )
        (newRef $ Deferred action)

  query :: m v -> NThunkF m v -> m v
  query vStub (Thunk _ _ lTValRef) =
    deferred pure (const vStub) =<< readRef lTValRef

  force :: NThunkF m v -> m v
  force = forceMain

  forceEff :: NThunkF m v -> m v
  forceEff = forceMain

  further :: NThunkF m v -> m (NThunkF m v)
  further t@(Thunk _ _ ref) =
    const (pure t) =<< atomicModifyRef ref dup


-- *** United body of `force*`

-- | Always returns computed @m v@.
--
-- Checks if resource is computed,
-- if not - with locking evaluates the resource.
forceMain
  :: forall v m
   . ( MonadBasicThunk m
    , MonadCatch m
    )
  => NThunkF m v
  -> m v
forceMain (Thunk tIdV tRefV tValRefV) =
  deferred pure computeW =<< readRef tValRefV
 where
  computeW :: m v -> m v
  computeW vDefferred =
    do
      locked <- lock tRefV
      bool
        lockFailedV
        (do
          v <- vDefferred `catch` bindFailedW
          writeRef tValRefV $ Computed v  -- Proclaim value computed
          unlockRef
          pure v
        )
        $ not locked
   where
    lockFailedV :: m a
    lockFailedV = throwM $ ThunkLoop $ show tIdV

    bindFailedW :: SomeException -> m b
    bindFailedW (e :: SomeException) =
      do
        unlockRef
        throwM e

    unlockRef :: m Bool
    unlockRef = unlock tRefV
{-# inline forceMain #-} -- it is big function, but internal, and look at its use.



-- ** Kleisli functor HOFs: @instance MonadThunkF NThunkF@

instance (MonadBasicThunk m, MonadCatch m)
  => MonadThunkF (NThunkF m v) m v where

  queryF
    :: (v -> m r)
    -> m r
    -> NThunkF m v
    -> m r
  queryF k n (Thunk _ thunkRef thunkValRef) =
    do
      locked <- lock thunkRef
      bool
        n
        go
        (not locked)
    where
      go =
        do
          eres <- readRef thunkValRef
          res  <-
            deferred
              k
              (const n)
              eres
          unlockRef
          pure res

      unlockRef = unlock thunkRef

  forceF
    :: (v -> m a)
    -> NThunkF m v
    -> m a
  forceF k = k <=< force

  forceEffF
    :: (v -> m r)
    -> NThunkF m v
    -> m r
  forceEffF k = k <=< forceEff

  furtherF
    :: (m v -> m v)
    -> NThunkF m v
    -> m (NThunkF m v)
  furtherF k t@(Thunk _ _ ref) =
    do
      _modifiedIt <- atomicModifyRef ref $
        \x ->
          deferred
            (const (x, x))
            (\ d -> (Deferred (k d), x))
            x
      pure t


