-- |
-- Module      : Control.Auto.Interval
-- Description : Tools for working with "interval" semantics: "On or off"
--               'Auto's.
-- Copyright   : (c) Justin Le 2015
-- License     : MIT
-- Maintainer  : justin@jle.im
-- Stability   : unstable
-- Portability : portable
--
--
-- This module provides combinators and utilities for working with the
-- semantic concept of "intervals": an 'Auto' whose output stream is "on"
-- or "off" for (conceputally) contiguous chunks of time.
--

module Control.Auto.Interval (
  -- * Intervals
  -- $intervals
    Interval
  , Interval'
  -- * Static 'Interval's
  , off
  , toOn
  , fromInterval
  , fromIntervalWith
  , onFor
  , offFor
  , window
  -- * Filter 'Interval's
  , whenI
  , unlessI
  -- * Choice
  , (<|!>)
  , (<|?>)
  , chooseInterval
  , choose
  -- * Blip-based 'Interval's
  , after
  , before
  , between
  , hold
  , hold_
  , holdFor
  , holdFor_
  -- * Stretching
  , holdJusts
  , holdJusts_
  -- * Composition with 'Interval'
  , during
  , compI
  , bindI
  ) where

import Control.Applicative
import Control.Arrow
import Control.Auto.Blip.Internal
import Control.Auto.Core
import Control.Category
import Control.Monad              (join)
import Data.Maybe
import Data.Profunctor
import Data.Serialize
import Prelude hiding             ((.), id, mapM)

-- $intervals
--
-- An auto that exhibits this "interval" behavior is represented with the
-- 'Interval' type synonym:
--
-- @
-- type 'Interval' m a b = 'Auto' m a ('Maybe' b)
-- type 'Interval''  a b = 'Auto''  a ('Maybe' b)
-- @
--
-- So, the compiler sees an @'Interval' m a b@ as if it were an @'Auto'
-- m a ('Maybe' b)@.  If it helps you reason about type signatures and type
-- inference, you can make the substitution in your head too!
--
-- An @'Interval' m a b@ takes an input stream of @a@s and output stream of
-- @b@s that are "on" and "off" for chunks at a time; 'Nothing' is
-- interpreted as "off", and @'Just' x@ is interpreted as "on" with a value
-- of @x@.
--
-- A classic example is @'onFor' :: 'Int' -> 'Interval' m a a@.  With
-- @'onFor' n@, the output stream behaves exactly like the input stream for
-- the first @n@ steps, then is "off" forever after:
--
-- >>> streamAuto' (onFor 3) [1..7]
-- [Just 1, Just 2, Just 3, Nothing, Nothing, Nothing, Nothing]
--
-- == Motivation
--
-- Intervals happen to particularly useful when used with the various
-- /switching/ combinators from "Control.Auto.Switch".
--
-- You might find it useful to "sequence" 'Auto's such that they "switch"
-- from one to the other, dynamically.  For example, an 'Auto' that acts
-- like @'pure' 100@ for three steps, and then like 'count' for the rest:
--
-- >>> let a1 = (onFor 3 . pure 100) --> count
-- >>> take 8 . streamAuto' a1 $ repeat ()
-- [100, 100, 100, 0, 1, 2, 3, 4]
--
-- (Recall that @'pure' x@ is the 'Auto' that ignores the input stream and
-- gives an output stream of constant @x@s)
--
-- Or in reverse, an 'Auto' that behaves like 'count' until the count is
-- above 3, then switches to @'pure' 100@
--
-- >>> let a2 = (whenI (<= 3) . count) --> pure 100
-- >>> take 8 . streamAuto' a2 $ repeat ()
-- [0, 1, 2, 3, 100, 100, 100, 100]
--
-- That's just a small example using one switching combinator, '-->'.  But
-- hopefully it demonstrates that one powerful motivation behind
-- "intervals" being a "thing" is because of how it works with switches.
--
-- Another neat motivation is that intervals work pretty well with the
-- 'Blip' semantic tool, as well.
--
-- The following 'Interval' will be "off" and suppress all of its input
-- (from 'count') /until/ the blip stream produced by @'inB' 3@ emits
-- something, then it'll allow 'count' to pass, beginning with the step
-- during which the blip was emmitted.
--
-- >>> let a3 = after . (count &&& inB 3)
-- >>> let a3 = proc () -> do
--             c   <- count -< ()
--             blp <- inB 3 -< ()
--             after -< (c, blp)
-- >>> take 5 . streamAuto' a3 $ repeat ()
-- [Nothing, Nothing, Just 2, Just 3, Just 4]
--
-- Intervals are also used for things that want their 'Auto's to "signal"
-- when they are "off".  'Interval' is the universal language for, "you can
-- be done with me", when it is needed.  For example, the 'interactAuto'
-- loop takes an 'Interval String String', and "turns off" on the first
-- 'Nothing' or "off" value.  'gather' keeps a collection of 'Interval's,
-- and removes them whenever they output a 'Nothing'/turn "off".
--
-- == The Contract
--
-- So, why have an 'Interval' type, and not always just use 'Auto'?
--
-- You can say that, if you are given an 'Interval', then it comes with
-- a "contract" (by documentation) that the 'Auto' will obey /interval
-- semantics/.
--
-- @'Auto' m a ('Maybe' b)@ can mean a lot of things and represent a lot of
-- things.
--
-- However, if you offer something of an 'Interval' type, or if you find
-- something of an 'Interval' type, it comes with some sort of assurance
-- that that 'Auto' will /behave/ like an interval: on and off for
-- contiguous periods of time.
--
-- In addition, this allows us to further clarify /what our functions
-- expect/.  By saying that a function expects an 'Interval':
--
-- @
-- chooseInterval :: [Interval m a b]
--                -> Interval m a b
-- @
--
-- 'chooseInterval' has the ability to "state" that it /expects/ things
-- that follow interval semantics in order to "function" properly and in
-- order to properly "return" an 'Interval'.
--
-- Of course, this is not enforced by the compiler.  However, it's useful
-- to create a way to clearly state that what you are offering or what you
-- are expecting does indeed follow this useful pattern.
--
-- == Combinators
--
-- === Converting back into normal streams
--
-- You can take an incoming interval stream and output a "normal"
-- "always-on" stream by using the 'fromInterval' and 'fromIntervalWith'
-- 'Auto's, analogous to 'fromMaybe' and 'maybe' from "Data.Maybe",
-- respectively:
--
-- >>> let a = fromIntervalWith "off" show . onFor 2
-- >>> streamAuto' a [1..5]
-- ["1", "2", "off", "off", "off"]
--
-- You can also use '<|!>', coming up next....
--
-- === Choice
--
-- You can "choose" between interval streams, with choice combinators like
-- '<|?>' and '<|!>'.
--
-- >>> let a = onFor 2 . pure "hello"
--        <|!> onFor 4 . pure "world"
--        <|!> pure "goodbye!"
-- >>> take 6 . streamAuto' a $ repeat ()
-- ["hello", "hello", "world", "world", "goodbye!", "goodbye!"]
--
-- The above could also be written with 'choose':
--
-- >>> let a = choose (pure "goodbye!")
--                    [ onFor 2 . pure "hello"
--                    , onFor 4 . pure "world"
--                    ]
--
-- === Composition
--
-- Another tool that makes 'Interval's powerful is the ability to compose
-- them.
--
-- If you have an @'Auto' m a b@ and an @'Auto' m b c@, then you can
-- compose them with '.'.
--
-- If you have an @'Auto' m a b@ and an @'Interval' m b c@, then you can
-- compose them by throwing in a 'toOn' in the chain, or @'fmap' 'Just'@:
--
-- @
-- a               :: 'Auto' m a b
-- i               :: 'Interval' m b c
-- i . 'toOn' . a    :: 'Interval' m a c
-- 'fmap' 'Just' a     :: 'Interval' m a b
-- i . 'fmap' 'Just' a :: 'Interval' m a c
-- @
--
-- If you have an @'Interval' m a b@ and an @'Auto' m b c@, you can "lift"
-- the second 'Auto' to be an 'Auto' that only "acts" on "on"/'Just'
-- outputs of the 'Interval':
--
-- @
--     i            :: 'Interval' m a b
--     a            :: 'Auto' m b c
--     'during' a     :: 'Auto' m ('Maybe' a) ('Maybe' b)
--     'during' a . i :: 'Interval' m a c
-- @
--
-- Finally, the kleisli composition: if you have an @'Interval' m a b@ and
-- an @'Interval' m b c@, you can use 'compI': (or also 'bindI')
--
-- @
--     i1            :: 'Interval' m a b
--     i2            :: 'Interval' m b c
--     i2 ``compI`` i1 :: 'Interval' m a b c
--     'bindI' i2 . i1 :: 'Interval' m a b c
-- @
--
-- >>> let a1 = whenI (< 5) `compI` offFor 2
-- >>> streamAuto' a1 [1..6]
-- [Nothing, Nothing, Just 3, Just 4, Nothing, Nothing]
--
-- The implementation works so that any "on"/'Just' inputs will step the
-- lifted 'Auto' like normal, with the contents of the 'Just', and any
-- "off"/'Nothing' inputs cause the lifted 'Auto' to be skipped.
--
-- 'compI' adds a lot of power to 'Interval' because now you can always
-- work "with 'Interval's", bind them just like normal 'Auto's, and then
-- finally "exit" them after composing and combining many.
--
-- == Warning: Switching
--
-- Note that when any of these combinators "block" (or "inhibit" or
-- "suppress", whatever you call it) their input as a part of a composition
-- pipeline (as in for 'off', 'onFor', 'offFor', etc.), the /input/ 'Auto's
-- are /still stepped/ and "run".  If the inputs had any monad effects,
-- they would too be executed at every step.  In order to "freeze" and not
-- run or step an 'Auto' at all, you have to use switches.
--

infixr 3 <|?>
infixr 3 <|!>
infixr 1 `compI`

-- | Represents a relationship between an input and an output, where the
-- output can be "on" or "off" (using 'Just' and 'Nothing') for contiguous
-- chunks of time.
--
-- \"Just\" a type alias for @'Auto' m a ('Maybe' b)@.  If you ended up here
-- with a link...no worries!  If you see @'Interval' m a b@, just think
-- @'Auto' m a ('Maybe' b)@ for type inference/type checking purposes.
--
-- If you see something of type 'Interval', you can rest assured that it
-- has "interval semantics" --- it is on and off for meaningfully
-- contiguous chunks of time, instead of just on and off willy nilly.  If
-- you have a function that expects an 'Interval', then the function
-- expects its argument to behave in this way.
--
type Interval m a b = Auto m a (Maybe b)

-- | 'Interval', specialized with 'Identity' as its underlying 'Monad'.
-- Analogous to 'Auto'' for 'Auto'.
type Interval'  a b = Auto'  a (Maybe b)

-- | The output stream is alwayas off, regardless of the input.
--
-- Note that any monadic effects of the input 'Auto' when composed with
-- 'off' are still executed, even though their result value is suppressed.
--
-- prop> off == pure Nothing
off :: Interval m a b
off = mkConst Nothing

-- | The output stream is always on, with exactly the value of the
-- corresponding input.
--
-- prop> toOn == arr Just
toOn :: Interval m a a
toOn = mkFunc Just

-- | An "interval collapsing" 'Auto'.  A stream of on/off values comes in;
-- the output is the value of the input when the input is on, and the
-- "default value" when the input is off.
--
-- Much like 'fromMaybe' from "Data.Maybe".
--
-- prop> fromInterval d = arr (fromMaybe d)
fromInterval :: a       -- ^ value to output for "off" periods
             -> Auto m (Maybe a) a
fromInterval d = mkFunc (fromMaybe d)

-- | An "interval collapsing" 'Auto'.  A stream of on/off values comes in;
-- when the input is off, the output is the "default value".  When the
-- input is off, the output is the given function applied to the "on"
-- value.
--
-- Much like 'maybe' from "Data.Maybe".
--
-- prop> fromIntervalWith d f = arr (maybe d f)
fromIntervalWith :: b             -- ^ default value, when input is off
                 -> (a -> b)      -- ^ function to apply when input is on
                 -> Auto m (Maybe a) b
fromIntervalWith d f = mkFunc (maybe d f)

-- | For @'onFor' n@, the first @n@ items in the output stream are always
-- "on" (passing through with exactly the value of the corresponding
-- input); for the rest, the output stream is always "off", suppressing all
-- input values forevermore.
--
-- If a number less than 0 is passed, 0 is used.
--
onFor :: Int      -- ^ amount of steps to stay "on" for
      -> Interval m a a
onFor = mkState f . Just . max 0
  where
    f x (Just i) | i > 0 = (Just x , Just (i - 1))
    f _ _                = (Nothing, Nothing)

-- | For @'offFor' n@, the first @n@ items in the output stream are always
-- "off", suppressing all input; for the rest, the output stream is always
-- "on", outputting exactly the value of the corresponding input.
offFor :: Int     -- ^ amount of steps to be "off" for
       -> Interval m a a
offFor = mkState f . Just . max 0
  where
    f _ (Just i) | i > 0 = (Nothing, Just (i - 1))
    f x _                = (Just x , Nothing     )

-- | A combination of 'onFor' and 'offFor'; for @'window' b e@, the output
-- stream will be "on" from item @b@ to item @e@ inclusive with the value
-- of the corresponding input; for all other times, the output stream is
-- always off, suppressing any input.
window :: Int     -- ^ start of window
       -> Int     -- ^ end of window (inclusive)
       -> Interval m a a
window b e = mkState f (Just 1)
  where
    f _ Nothing              = (Nothing, Nothing)
    f x (Just i) | i > e     = (Nothing, Nothing)
                 | i < b     = (Nothing, Just (i + 1))
                 | otherwise = (Just x , Just (i + 1))

-- | The output is "on" with exactly the value of he corresponding input
-- when the input passes the predicate, and is "off" otherwise.
--
-- >>> let a = whenI (\x -> x >= 2 && x <= 4)
-- >>> streamAuto' a [1..6]
-- [Nothing, Just 2, Just 3, Just 4, Nothing, Nothing]
--
-- Careful when using this; you could exactly create an 'Interval' that
-- "breaks" "interval semantics"; for example, 'whenI even', when you know
-- your input stream does not consist of chunks of even numbers and odd
-- numbers at a time.
--
whenI :: (a -> Bool)   -- ^ interval predicate
     -> Interval m a a
whenI p = mkFunc f
  where
    f x | p x       = Just x
        | otherwise = Nothing

-- | Like 'whenI', but only allows values to pass whenever the input does
-- not satisfy the predicate.  Blocks whenever the predicate is true.
--
-- >>> let a = unlessI (\x -> x < 2 || x > 4)
-- >>> steamAuto' a [1..6]
-- [Nothing, Just 2, Just 3, Just 4, Nothing, Nothing]
--
unlessI :: (a -> Bool)   -- ^ interval predicate
       -> Interval m a a
unlessI p = mkFunc f
  where
    f x | p x       = Nothing
        | otherwise = Just x

-- | Takes two input streams --- a stream of normal values, and a blip
-- stream.  Before the first emitted value of the input blip stream, the
-- output is always "off", suppressing all inputs.  /After/ the first
-- emitted value of the input blip stream, the output is always "on" with
-- the corresponding value of the first input stream.
--
-- >>> let a = after . (count &&& inB 3)
-- >>> take 6 . streamAuto' a $ repeat ()
-- [Nothing, Nothing, Just 2, Just 3, Just 4, Just 5]
--
-- ('count' is the 'Auto' that ignores its input and outputs the current
-- step count at every step, and @'inB' 3@ is the 'Auto' generating
-- a blip stream that emits at the third step.)
--
-- Be careful to remember that in the above example, 'count' is still "run"
-- at every step, and is progressed (and if it were an 'Auto' with monadic
-- effects, they would still be executed).  It just isn't allowed to pass
-- its output values through 'after' until the blip stream emits.
--
after :: Interval m (a, Blip b) a
after = mkState f False
  where
    f (x, _     ) True  = (Just x , True )
    f (x, Blip _) False = (Just x , True )
    f _           False = (Nothing, False)

-- | Takes two input streams --- a stream of normal values, and a blip
-- stream.  Before the first emitted value of the input blip stream, the
-- output is always "on" with the corresponding value of the first input
-- stream.  /After/ the first emitted value of the input blip stream, the
-- output will be "off" forever, suppressing all input.
--
-- >>> let a = before . (count &&& inB 3)
-- >>> take 5 . streamAuto' a $ repeat ()
-- [Just 0, Just 1, Nothing, Nothing, Nothing]
--
-- ('count' is the 'Auto' that ignores its input and outputs the current
-- step count at every step, and @'inB' 3@ is the 'Auto' generating
-- a blip stream that emits at the third step.)
--
-- Be careful to remember that in the above example, 'count' is still "run"
-- at every step, and is progressed (and if it were an 'Auto' with monadic
-- effects, they would still be executed).  It just isn't allowed to pass
-- its output values through 'before' after the blip stream emits.
--
before :: Interval m (a, Blip b) a
before = mkState f False
  where
    f _           True  = (Nothing, True )
    f (_, Blip _) False = (Nothing, True )
    f (x, _     ) False = (Just x , False)

-- | Takes three input streams: a stream of normal values, a blip stream of
-- "turning-on" blips, and a blip stream of "turning-off" blips.  After the
-- first blip stream emits, the output will switch to "on" with the value
-- of the first input stream.  After the second blip stream emits, the
-- output will switch to "off", supressing all inputs.  An emission from
-- the first stream toggles this "on"; an emission from the second stream
-- toggles this "off".
--
-- >>> let a = between . (count &&& (inB 3 &&& inB 5))
-- >>> take 7 . streamAuto' a $ repeat ()
-- [Nothing, Nothing, Just 2, Just 3, Nothing, Nothing, Nothing]
between :: Interval m (a, (Blip b, Blip c)) a
between = mkState f False
  where
    f (_, (_, Blip _)) _     = (Nothing, False)
    f (x, (Blip _, _)) _     = (Just x , True )
    f (x, _          ) True  = (Just x , True )
    f _                False = (Nothing, False)

-- | The output is constantly "on" with the last emitted value of the input
-- blip stream.  However, before the first emitted value, it is "off".
-- value of the input blip stream.  From then on, the output is always the
-- last emitted value
--
-- >>> let a = hold . inB 3
-- >>> streamAuto' a [1..5]
-- [Nothing, Nothing, Just 3, Just 3, Just 3]
--
-- If you want an @'Auto' m ('Blip' a) a@ (no 'Nothing'...just a "default
-- value" before everything else), then you can use 'holdWith' from
-- "Control.Auto.Blip"...or also just 'hold' with '<|!>' or 'fromInterval'.
hold :: Serialize a
     => Interval m (Blip a) a
hold = accum f Nothing
  where
    f x = blip x Just

-- | The non-serializing/non-resuming version of 'hold'.
hold_ :: Interval m (Blip a) a
hold_ = accum_ f Nothing
  where
    f x = blip x Just

-- | For @'holdFor' n@, The output is only "on" if there was an emitted
-- value from the input blip stream in the last @n@ steps.  Otherwise, is
-- off.
--
-- Like 'hold', but it only "holds" the last emitted value for the given
-- number of steps.
--
-- >>> let a = holdFor 2 . inB 3
-- >>> streamAuto' a [1..7]
-- [Nothing, Nothing, Just 3, Just 3, Nothing, Nothing, Nothing]
--
holdFor :: Serialize a
        => Int      -- ^ number of steps to hold the last emitted value for
        -> Interval m (Blip a) a
holdFor n = mkState (_holdForF (max 0 n)) (Nothing, Nothing)

-- | The non-serializing/non-resuming version of 'holdFor'.
holdFor_ :: Int   -- ^ number of steps to hold the last emitted value for
         -> Interval m (Blip a) a
holdFor_ n = mkState_ (_holdForF (max 0 n)) (Nothing, Nothing)

_holdForF :: Int -> Blip a -> (Maybe a, Maybe Int) -> (Maybe a, (Maybe a, Maybe Int))
_holdForF n = f   -- n should be >= 0
  where
    f x s = (y, (y, i))
      where
        (y, i) = case (x, s) of
                   (Blip b,  _    )       -> (Just b , Just n )
                   (_     , (z, Just j )) | j > 1 -> (z, Just (j - 1))
                   _                      -> (Nothing, Nothing)

-- | "Stretches" the last "on"/'Just' input over the entire range of
-- following "off"/'Nothing' inputs.  Holds on to the last 'Just' until
-- another one comes along.
--
-- >>> streamAuto' holdJusts [Nothing, Just 1, Just 3, Nothing, Nothing, Just 5]
-- [Nothing, Just 1, Just 3, Just 3, Just 3, Just 5]
--
holdJusts :: Serialize a
          => Interval m (Maybe a) a
holdJusts = accum (flip (<|>)) Nothing

-- | The non-resuming/non-serializing version of 'holdJusts'.
--
holdJusts_ :: Interval m (Maybe a) a
holdJusts_ = accum_ (flip (<|>)) Nothing

-- | Forks a common input stream between the two 'Interval's and returns,
-- itself, an 'Interval'.  If the output of the first one is "on", the
-- whole thing is on with that output. Otherwise, the output is exactly
-- that of the second one.
--
-- >>> let a = (onFor 2 . pure "hello") <|?> (onFor 4 . pure "world")
-- >>> take 5 . streamAuto' a $ repeat ()
-- [Just "hello", Just "hello", Just "world", Just "world", Nothing]
--
-- You can drop the parentheses, because of precedence; the above could
-- have been written as:
--
-- >>> let a' = onFor 2 . pure "hello" <|?> onFor 4 . pure "world"
--
-- Warning: If your underlying monad produces effects, remember that /both/
-- 'Auto's are run at every step, along with any monadic effects,
-- regardless of whether they are "on" or "off".
--
-- Note that more often than not, '<|!>' is probably more useful.  This
-- is useful only in the case that you really, really want an interval at
-- the end of it all.
--
(<|?>) :: Monad m
       => Interval m a b    -- ^ choice 1
       -> Interval m a b    -- ^ choice 2
       -> Interval m a b
(<|?>) = liftA2 (<|>)

-- | Forks a common input stream between an 'Interval' and an 'Auto', and
-- returns, itself, a normal non-interval 'Auto'..  If the
-- output of the first one is "on", the output of the whole thing is that
-- "on" value.  Otherwise, the output is exactly that of the second one.
--
-- >>> let a1 = (onFor 2 . pure "hello") <|!> pure "world"
-- >>> take 5 . streamAuto' a1 $ repeat ()
-- ["hello", "hello", "world", "world", "world"]
--
-- This one is neat because it associates from the right, so it can be
-- "chained":
--
-- >>> let a2 = onFor 2 . pure "hello"
--         <|!> onFor 4 . pure "world"
--         <|!> pure "goodbye!"
-- >>> take 6 . streamAuto' a2 $ repeat ()
-- ["hello", "hello", "world", "world", "goodbye!", "goodbye!"]
--
-- >  a <|!> b <|!> c
--
-- associates as
--
-- >  a <|!> (b <|!> c)
--
-- So using this, you can "chain" a bunch of choices between intervals, and
-- then at the right-most, "final" one, provide the default behavior.
--
-- Warning: If your underlying monad produces effects, remember that /both/
-- 'Auto's are run at every step, along with any monadic effects,
-- regardless of whether they are "on" or "off".
(<|!>) :: Monad m
       => Interval m a b        -- ^ interval 'Auto'
       -> Auto m a b            -- ^ "normal" 'Auto'
       -> Auto m a b
(<|!>) = liftA2 (flip fromMaybe)

-- | Forks an input stream between all 'Interval's in the list.  The result
-- is an 'Interval' whose output is "on" when any of the original
-- 'Interval's is on, with the value of the /first/ "on" one.
--
-- prop> chooseInterval == foldr (<|?>) off
chooseInterval :: Monad m
               => [Interval m a b]    -- ^ the 'Auto's to run and
                                      --   choose from
               -> Interval m a b
chooseInterval = foldr (<|?>) (pure Nothing)

-- | Forks an input stream between all 'Interval's in the list, plus
-- a "default 'Auto'.  The output is the value of the first "on"
-- 'Interval'; if there isn't any, the output from the "default 'Auto'" is
-- used.
--
-- prop> choose == foldr (<|!>)
choose :: Monad m
       => Auto m a b          -- ^ the 'Auto' to behave like if all
                              --   others are 'Nothing'
       -> [Interval m a b]    -- ^ 'Auto's to run and choose from
       -> Auto m a b
choose = foldr (<|!>)

-- | "Lifts" an @'Auto' m a b@ (transforming @a@s into @b@s) into an
-- @'Auto' m ('Maybe' a) ('Maybe' b)@ (or, @'Interval' m ('Maybe' a) b@,
-- transforming /intervals/ of @a@s into /intervals/ of @b@.
--
-- It does this by running the 'Auto' as normal when the input is "on",
-- and freezing it/being "off" when the input is /off/.
--
-- >>> let a1 = during (sumFrom 0) . onFor 2 . pure 1
-- >>> take 5 . streamAuto' a1 $ repeat ()
-- [Just 1, Just 2, Nothing, Nothing, Nothing]
--
-- >>> let a2 = during (sumFrom 0) . offFor 2 . pure 1
-- >>> take 5 . streamAuto' a2 $ repeat ()
-- [Nothing, Nothing, Just 1, Just 2, Just 3]
--
-- (Remember that @'pure' x@ is the 'Auto' that ignores its input and
-- constantly just pumps out @x@ at every step)
--
-- Note the difference between putting the 'sumFrom' "after" the
-- 'offFor' in the chain with 'during' (like the previous example)
-- and putting the 'sumFrom' "before":
--
-- >>> let a3 = offFor 2 . sumFrom 0 . pure 1
-- >>> take 5 . streamAuto' a3 $ repeat ()
-- [Nothing, Nothing, Just 3, Just 4, Just 5]
--
-- In the first case (with @a2@), the output of @'pure' 1@ was suppressed
-- by 'offFor', and @'during' ('sumFrom' 0)@ was only summing on the times
-- that the 1's were "allowed through"...so it only "starts counting" on
-- the third step.
--
-- In the second case (with @a3@), the output of the @'pure' 1@ is never
-- suppressed, and went straight into the @'sumFrom' 0@.  'sumFrom' is
-- always summing, the entire time.  The final output of that @'sumFrom' 0@
-- is suppressed at the end with @'offFor' 2@.
--
during :: Monad m
       => Auto m a b      -- ^ 'Auto' to lift to work over intervals
       -> Interval m (Maybe a) b
during = dimap to from . right
  where
    from = either (const Nothing) Just
    to   = maybe (Left ()) Right

-- | "Lifts" (more technically, "binds") an @'Interval' m a b@ into
-- an @'Interval' m ('Maybe' a) b@.
--
-- Does this by running the 'Auto' as normal when the input is "on", and
-- freezing it/being "off" when the input is /off/.
--
-- It's kind of like 'during', but the resulting @'Maybe' ('Maybe' b))@ is
-- "joined" back into a @'Maybe' b@.
--
-- prop> bindI a == fmap join (during a)
--
-- This is really an alternative formulation of 'compI'; typically, you
-- will be using 'compI' more often, but this form can also be useful (and
-- slightly more general).  Note that:
--
-- prop> bindI f == compI f id
--
-- This combinator allows you to properly "chain" ("bind") together series
-- of inhibiting 'Auto's.  If you have an @'Interval' m a b@ and an
-- @'Interval' m b c@, you can chain them into an @'Interval' m a c@.
--
-- @
-- f             :: 'Interval' m a b
-- g             :: 'Interval' m b c
-- 'bindI' g . f :: 'Interval' m a c
-- @
--
-- (Users of libraries with built-in inhibition semantics like Yampa and
-- netwire might recognize this as the "default" composition in those other
-- libraries)
--
-- See 'compI' for examples of this use case.
--
bindI :: Monad m
      => Interval m a b       -- ^ 'Interval' to bind
      -> Interval m (Maybe a) b
bindI = fmap join . during

-- | Composes two 'Interval's, the same way that '.' composes two 'Auto's:
--
-- @
-- (.)   :: Auto     m b c -> Auto     m a b -> Auto     m a c
-- compI :: Interval m b c -> Interval m a b -> Interval m a c
-- @
--
-- Basically, if any 'Interval' in the chain is "off", then the entire rest
-- of the chain is "skipped", short-circuiting a la 'Maybe'.
--
-- (Users of libraries with built-in inhibition semantics like Yampa and
-- netwire might recognize this as the "default" composition in those other
-- libraries)
--
-- As a contrived example, how about an 'Auto' that only allows values
-- through during a window...between, say, the second and fourth steps:
--
-- >>> let window' start dur = onFor dur `compI` offFor (start - 1)
-- >>> streamAuto' (window' 2 3)
-- [Nothing, Just 2, Just 3, Just 4, Nothing, Nothing]
--
compI :: Monad m
      => Interval m b c   -- ^ compose this 'Interval'...
      -> Interval m a b   -- ^ ...to this one
      -> Interval m a c
compI f g = fmap join (during f) . g