-- | This module contains functions and types for reframing the sequence number
--   and timestamps of 'Frame's.
--
--   This means that this module will allow you to record a set of incoming
--   frames using 'pushFrame'. Whenever you want to extract the timing and
--   sequence number information of the next frame with a given duration,
--   use 'generateFrame'.
module Data.MediaBus.Media.Reframe
  ( initialReframerState
  , runReframer
  , pushStartFrame
  , pushFrame
  , PushFrameError(..)
  , nextFrameAvailableDuration
  , nextFrameTimestamp
  , generateFrame
  , type ReframerT
  , ReframerSt()
  , ReframeError(..)
  , mkReFrameError
  ) where

import Control.Exception
import Control.Monad
import Control.Monad.Trans.State.Strict as State
import Data.MediaBus.Media.Stream
import Data.Typeable

-- | Create an empty initial state.
initialReframerState
  :: (Num d, Num s)
  => ReframerSt s d
initialReframerState = MkTimeFrame 0 0 0

-- | Run state 'ReframerSt' state transformer.
runReframer
  :: Monad m
  => ReframerT m s d a -> ReframerSt s d -> m (a, ReframerSt s d)
runReframer = runStateT

-- | Reset the current timing and sequence number, and start with the given
--   start time.
pushStartFrame
  :: (Num d, Monad m)
  => d -> ReframerT m s d ()
pushStartFrame d = do
  (MkTimeFrame _ s _) <- State.get
  State.put (MkTimeFrame d s d)

-- | Increase the available duration by the duration in the frame,  iff the
--   timestamp of the given frame matches exactly the timestamp after the end
--   of the available period, otherwise do nothing with the state and return
--   'True'.
pushFrame
  :: (Num d, Monad m, Eq d, Ord d)
  => Frame s d d -> ReframerT m s d (Maybe PushFrameError)
pushFrame (MkFrame ts _ dur) = do
  (MkTimeFrame startTs nextSeq endTs) <- State.get
  let endTs' = ts + dur
  when (ts == endTs) (State.put (MkTimeFrame startTs nextSeq endTs'))
  return
    (if | ts < endTs && endTs' < endTs -> Just InputFrameIsLate
        | ts < endTs && endTs' >= endTs -> Just InputFrameOverlaps
        | ts > endTs -> Just InputFrameIsEarly
        | ts == endTs -> Nothing)

-- | Specifies in what way 'pushFrame' failed
data PushFrameError
  = InputFrameIsLate -- ^ The input frame ends before '_endTs'
  | InputFrameOverlaps -- ^ The input frame starts before, and ends after '_endTs'
  | InputFrameIsEarly -- ^ The input frame begins after '_endTs'
  deriving (Eq, Ord, Show, Enum)

-- | Return the duration of the frames recorded with 'pushFrame'.
nextFrameAvailableDuration
  :: (Num d, Monad m)
  => ReframerT m s d d
nextFrameAvailableDuration = do
  (MkTimeFrame startTs _ endTs) <- State.get
  return (endTs - startTs)

-- | Return the timestamp of the frame being build. .
nextFrameTimestamp
  :: (Num d, Monad m)
  => ReframerT m s d d
nextFrameTimestamp = do
  (MkTimeFrame startTs _ _) <- State.get
  return startTs

-- | Try to create a frame with the given duration, and update the state
--   accordingly, the actual duration, that was available is
--   put into the payload field of the frame returned.
--   The start time stamp of the next frame
--   is always incremented by the @wantedDureation@ regardless of wether it was
--   available.
generateFrame
  :: (Num s, Num d, Monad m, Eq d, Ord d, Show d)
  => d -> ReframerT m s d (Frame s d d)
generateFrame wantedDuration = do
  (MkTimeFrame startTs nextSeq endTs) <- State.get
  available <- nextFrameAvailableDuration
  let endTs' = max (startTs + wantedDuration) endTs
  State.put (MkTimeFrame (startTs + wantedDuration) (nextSeq + 1) endTs')
  return (MkFrame startTs nextSeq (wantedDuration `min` available))

-- | Reframer state.
data ReframerSt s d = MkTimeFrame
  { _startTs :: !d
  , _nextSeqNum :: !s
  , _endTs :: !d
  } deriving (Typeable)

instance (Show s, Show d) =>
         Show (ReframerSt s d) where
  showsPrec d MkTimeFrame {_startTs, _nextSeqNum, _endTs} =
    showParen
      (d > 10)
      (showString "reframer-state: " .
       showString "start: " .
       showsPrec 11 _startTs .
       showString ", end: " .
       showsPrec 11 _endTs . showString ", next-sn: " . showsPrec 11 _nextSeqNum)

-- | The 'ReframerSt' 'StateT' transformer
type ReframerT m s d a = StateT (ReframerSt s d) m a

-- | The exception type for 'encodeLinearToAacC'
data ReframeError s d = MkReframeError
  { reframeError :: String
  , reframeErrorRequestedOutput :: Maybe d
  , reframeErrorSt :: ReframerSt s d
  } deriving (Typeable)

instance (Show s, Show d) =>
         Show (ReframeError s d) where
  showsPrec d MkReframeError { reframeError
                             , reframeErrorRequestedOutput
                             , reframeErrorSt
                             } =
    showParen
      (d > 10)
      (showString "reframe-error: " .
       showsPrec 11 reframeError .
       maybe
         id
         (\ro -> showString ", requested: " . showsPrec 11 ro)
         reframeErrorRequestedOutput .
       showString ", " . showsPrec 11 reframeErrorSt)

instance (Show s, Typeable s, Show d, Typeable d) =>
         Exception (ReframeError s d)

-- | Utility function to generate a 'ReframeError' with the current state.
mkReFrameError
  :: Monad m
  => String -> Maybe d -> ReframerT m s d (ReframeError s d)
mkReFrameError msg requestedOutputDuration =
  MkReframeError msg requestedOutputDuration <$> State.get