--------------------------------------------------------------------------------

-- Copyright © 2010-2012 Bas van Dijk & Roel van Dijk
-- Copyright © 2018 DFINITY Stiftung
--
-- All rights reserved.
--
-- Redistribution and use in source and binary forms, with or without
-- modification, are permitted provided that the following conditions are
-- met:
--
--     * Redistributions of source code must retain the above copyright
--       notice, this list of conditions and the following disclaimer.
--
--     * Redistributions in binary form must reproduce the above
--       copyright notice, this list of conditions and the following
--       disclaimer in the documentation and/or other materials provided
--       with the distribution.
--
--     * The names of Bas van Dijk, Roel van Dijk and the names of
--       contributors may NOT be used to endorse or promote products
--       derived from this software without specific prior written
--       permission.
--
-- THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
-- "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
-- LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
-- A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
-- OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
-- SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
-- LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
-- DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
-- THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
-- (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-- OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

--------------------------------------------------------------------------------

{-# LANGUAGE DeriveDataTypeable #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE NoImplicitPrelude #-}
{-# LANGUAGE UndecidableInstances #-}

--------------------------------------------------------------------------------

-- |
-- Module     : Control.Concurrent.Classy.Lock
-- Copyright  : © 2010-2011 Bas van Dijk & Roel van Dijk
--            , © 2018 DFINITY Stiftung
-- Maintainer : DFINITY USA Research <team@dfinity.org>
--
-- This module provides the 'Lock' synchronisation mechanism. It was inspired by
-- the Python and Java @Lock@ objects and should behave in a similar way. See:
--
-- <http://docs.python.org/3.1/library/threading.html#lock-objects>
--
-- and:
--
-- <http://java.sun.com/javase/7/docs/api/java/util/concurrent/locks/Lock.html>
--
-- All functions are /exception safe/. Throwing asynchronous exceptions will not
-- compromise the internal state of a 'Lock'.

--------------------------------------------------------------------------------

module Control.Concurrent.Classy.Lock
  ( -- * @Lock@
    Lock

    -- * Creating locks
  , newLock
  , newAcquired

    -- * Locking and unlocking
  , acquire
  , tryAcquire
  , release

    -- * Convenience functions
  , with
  , tryWith
  , wait

    -- * Querying locks
  , locked
  ) where

--------------------------------------------------------------------------------

import           Control.Applicative            (pure, (<*>))
import           Control.Monad                  (when)
import           Data.Bool                      (Bool, not)
import           Data.Eq                        (Eq((==)))
import           Data.Function                  (($), (.))
import           Data.Functor                   (fmap, (<$>))
import           Data.Maybe                     (Maybe(Just, Nothing), isJust)
import           Data.Typeable                  (Typeable)
import           Prelude                        (error)

import qualified Control.Concurrent.Classy.MVar as MVar
import           Control.Monad.Catch            (bracket_, mask, onException)
import           Control.Monad.Conc.Class       (MonadConc(MVar))

--------------------------------------------------------------------------------

-- | A lock is in one of two states: \"locked\" or \"unlocked\".
--
-- @since 1.6.2.0
newtype Lock m
  = Lock
    { _fromLock :: MVar m ()
    }
  deriving (Typeable)

instance (Eq (MVar m ())) => Eq (Lock m) where
  (==) (Lock a) (Lock b) = a == b

--------------------------------------------------------------------------------

-- | Create a lock in the \"unlocked\" state.
--
-- @since 1.6.2.0
newLock :: (MonadConc m) => m (Lock m)
newLock = Lock <$> MVar.newMVar ()

-- | Create a lock in the \"locked\" state.
--
-- @since 1.6.2.0
newAcquired :: (MonadConc m) => m (Lock m)
newAcquired = Lock <$> MVar.newEmptyMVar

--------------------------------------------------------------------------------

-- |
-- Acquires the 'Lock'. Blocks if another thread has acquired the 'Lock'.
--
-- @acquire@ behaves as follows:
--
-- * When the state is \"unlocked\" @acquire@ changes the state to \"locked\".
--
-- * When the state is \"locked\" @acquire@ /blocks/ until a call to 'release'
--   in another thread wakes the calling thread. Upon awakening it will change
--   the state to \"locked\".
--
-- There are two further important properties of @acquire@:
--
-- * @acquire@ is single-wakeup. That is, if there are multiple threads blocked
--   on @acquire@ and the lock is released, only one thread will be woken up.
--   The runtime guarantees that the woken thread completes its @acquire@
--   operation.
--
-- * When multiple threads are blocked on @acquire@, they are woken up in FIFO
--   order. This is useful for providing fairness properties of abstractions
--   built using locks. Note that this differs from the Python implementation
--   where the wake-up order is undefined.
--
-- @since 1.6.2.0
acquire :: (MonadConc m) => Lock m -> m ()
acquire = MVar.takeMVar . _fromLock

-- |
-- A non-blocking 'acquire'.
--
-- * When the state is \"unlocked\" @tryAcquire@ changes the state to \"locked\"
--   and returns 'True'.
--
-- * When the state is \"locked\" @tryAcquire@ leaves the state unchanged and
--   returns 'False'.
--
-- @since 1.6.2.0
tryAcquire :: (MonadConc m) => Lock m -> m Bool
tryAcquire = fmap isJust . MVar.tryTakeMVar . _fromLock

-- |
-- @release@ changes the state to \"unlocked\" and returns immediately.
--
-- Note that it is an error to release a lock in the \"unlocked\" state!
--
-- If there are any threads blocked on 'acquire' the thread that first called
-- @acquire@ will be woken up.
--
-- @since 1.6.2.0
release :: (MonadConc m) => Lock m -> m ()
release (Lock mv) = do
  b <- MVar.tryPutMVar mv ()
  when (not b) $
    error "Control.Concurrent.Classy.Lock.release: cannot release an unlocked Lock!"

--------------------------------------------------------------------------------

-- |
-- A convenience function which first acquires the lock and then performs the
-- computation. When the computation terminates, whether normally or by raising an
-- exception, the lock is released.
--
-- Note that: @with = 'bracket_' '<$>' 'acquire' '<*>' 'release'@.
--
-- @since 1.6.2.0
with :: (MonadConc m) => Lock m -> m a -> m a
with = bracket_ <$> acquire <*> release

-- |
-- A non-blocking 'with'. @tryWith@ is a convenience function which first tries
-- to acquire the lock. If that fails, 'Nothing' is returned. If it succeeds,
-- the computation is performed. When the computation terminates, whether
-- normally or by raising an exception, the lock is released and 'Just' the
-- result of the computation is returned.
--
-- @since 1.6.2.0
tryWith :: (MonadConc m) => Lock m -> m a -> m (Maybe a)
tryWith l a = mask $ \restore -> do
  acquired <- tryAcquire l
  if acquired
    then do r <- restore a `onException` release l
            release l
            pure (Just r)
    else pure Nothing

-- |
-- * When the state is \"locked\", @wait@ /blocks/ until a call to 'release'
--   in another thread changes it to \"unlocked\".
--
-- * @wait@ is multiple-wakeup, so when multiple waiters are blocked on
--   a @Lock@, all of them are woken up at the same time.
--
-- * When the state is \"unlocked\" @wait@ returns immediately.
--
-- @wait@ does not alter the state of the lock.
--
-- @since 1.6.2.0
wait :: (MonadConc m) => Lock m -> m ()
wait (Lock mv) = MVar.readMVar mv

--------------------------------------------------------------------------------

-- |
-- Determines if the lock is in the \"locked\" state.
--
-- Note that this is only a snapshot of the state. By the time a program reacts
-- on its result it may already be out of date.
--
-- @since 1.6.2.0
locked :: (MonadConc m) => Lock m -> m Bool
locked = MVar.isEmptyMVar . _fromLock

--------------------------------------------------------------------------------