{-# LANGUAGE UnicodeSyntax #-}
{-# LANGUAGE DeriveDataTypeable #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}

module Data.Timeout
  ( TimedOut(..)
  , TimeoutUnit(..)
  , aTimeoutUnit
  , timeoutUnitNanos
  , Timeout(..)
  , aTimeout
  , (#)
  , (#>)
  , (#<)
  , instantly
  ) where

import Prelude hiding (print)
import Data.Typeable (Typeable)
import Data.Ix (Ix)
import Data.Word (Word64)
import Data.Proxy (Proxy(..))
import Data.Monoid (mconcat)
import Data.Textual (Printable(..), Textual(..))
import qualified Data.Textual as DT
import Data.Textual.Fractional (Sign(..), Decimal(..), Optional(..),
                                fractional')
import Text.Printer (Printer(char7, string7), (<>))
import Text.Parser.Combinators ((<?>), unexpected)
import qualified Text.Parser.Char as PC
import Control.Applicative
import Control.Monad (when)
import Control.Exception (Exception)

-- | Exception that is raised when an operation times out.
--   Not used by the package itself, it is here so that users don't need to
--   roll their own exception type every time.
data TimedOut = TimedOut deriving (Typeable, Eq, Show)

instance Exception TimedOut

-- | Timeout unit.
data TimeoutUnit = NanoSecond
                 | MicroSecond
                 | MilliSecond
                 | Second
                 | Minute
                 | Hour
                 | Day
                 | Week
                 deriving (Typeable, Show, Read, Eq, Ord, Bounded, Ix, Enum)

-- | 'TimeoutUnit' proxy value.
aTimeoutUnit  Proxy TimeoutUnit
aTimeoutUnit = Proxy

instance Printable TimeoutUnit where
  print NanoSecond  = string7 "ns"
  print MicroSecond = string7 "us"
  print MilliSecond = string7 "ms"
  print Second      = char7 's'
  print Minute      = char7 'm'
  print Hour        = char7 'h'
  print Day         = char7 'd'
  print Week        = char7 'w'
  {-# INLINABLE print #-}

instance Textual TimeoutUnit where
  textual = (<?> "timeout unit") $ do
    c  PC.oneOf "numshdw" 
    case c of
      'n'  PC.char 's' *> pure NanoSecond
      'u'  PC.char 's' *> pure MicroSecond
      'm'  maybe Minute (const MilliSecond) <$> optional (PC.char 's')
      's'  pure Second
      'h'  pure Hour
      'd'  pure Day
      _    pure Week

-- | Amount of nanoseconds in a timeout unit.
timeoutUnitNanos  TimeoutUnit  Word64
timeoutUnitNanos NanoSecond  = 1
timeoutUnitNanos MicroSecond = 1000
timeoutUnitNanos MilliSecond = 1000000
timeoutUnitNanos Second      = 1000000000
timeoutUnitNanos Minute      = 60 * 1000000000
timeoutUnitNanos Hour        = 60 * 60 * 1000000000
timeoutUnitNanos Day         = 24 * 60 * 60 * 1000000000
timeoutUnitNanos Week        = 7 * 24 * 60 * 60 * 1000000000
{-# INLINABLE timeoutUnitNanos #-}

-- | Timeout in nanoseconds. The 'Printable' instance renders timeouts as
--   series of /Amount/@Unit@ tokens, e.g.
--
--   @
--      'DT.toString' (/1/ \# 'Day' + /1500/ \# 'MilliSecond') = /"1d1s500ms"/
--   @
--
--   The full list of timeout unit abbreviations:
--  
--     * 'NanoSecond' - /ns/
--
--     * 'MicroSecond' - /us/
--
--     * 'MilliSecond' - /ms/
--
--     * 'Second' - /s/
--
--     * 'Minute' - /m/
--
--     * 'Hour' - /h/
--
--     * 'Day' - /d/
--
--     * 'Week' - /w/
--
--   The 'Textual' instance accepts this syntax and allows decimal
--   fractions to be used as amounts:
--
--   @
--     'fmap' 'DT.toString' ('DT.fromStringAs' 'aTimeout' /"1m1.5s0.2us"/) = 'Just' /"1m1s500ms200ns"/
--   @
newtype Timeout = Timeout Word64
  deriving (Typeable, Show, Read,  Eq, Ord, Bounded, Ix, Enum,
            Num, Real, Integral)

-- | 'Timeout' proxy value.
aTimeout  Proxy Timeout
aTimeout = Proxy

infix 9 #
infix 8 #>, #<

-- | Convert the given number of timeout units to 'Timeout'.
(#)  Word64  TimeoutUnit  Timeout
n # u = Timeout $ n * timeoutUnitNanos u
{-# INLINE (#) #-}

-- | Extract number of units (rounding up).
(#>)  Timeout  TimeoutUnit  Word64
(Timeout tt) #> u = if r == 0 then q else q + 1
  where (q, r) = tt `quotRem` timeoutUnitNanos u
{-# INLINE (#>) #-}

-- | Extract number of units (rounding down).
(#<)  Timeout  TimeoutUnit  Word64
(Timeout tt) #< u = tt `quot` timeoutUnitNanos u
{-# INLINE (#<) #-}

instance Printable Timeout where
  print (Timeout tt) =
      if null ss then string7 "0ns" else mconcat (reverse ss)
    where
      ss = snd $ ($ enumFrom NanoSecond) $ (`foldr` (tt, [])) $ \u (t, ss') 
        let (q, r) = t `quotRem` timeoutUnitNanos u in
          (r, if q == 0 then ss' else (print q <> print u) : ss')

instance Textual Timeout where
  textual = (<?> "timeout") $ do
      a  amount
      u  textual
      let r = a * fromIntegral (timeoutUnitNanos u)  Rational
      if u == minBound then result r else go u r
    where
      amount = fractional' (pure NonNegative) Decimal Required
                           (PC.char '.' *> pure ()) (pure Nothing)
      go u r = do
        ma  optional amount
        case ma of
          Nothing  result r
          Just a  do
            u'  textual
            when (u' >= u) $ unexpected "timeout units must get smaller"
            let r' = r + a * fromIntegral (timeoutUnitNanos u')
            if u' == minBound then result r' else go u' r'
      result r = let c = ceiling r  Integer in
                   if (c  Integer) > fromIntegral (maxBound  Word64)
                   then return maxBound
                   else return $ Timeout $ fromIntegral c

-- | Zero timeout. The event in question should occur immediately.
instantly  Timeout
instantly = Timeout 0
{-# INLINE instantly #-}