content: {-# LANGUAGE RecursiveDo #-}
  {-# LANGUAGE TypeAbstractions #-}
  
  module Reactimate.Switching (caseOf, manyCaseOf, switch, rSwitch) where
  
  import Control.Arrow
  import Control.Monad (when)
  import Data.Vector qualified as V
  import Reactimate.Basic
  import Reactimate.Handles
  import Reactimate.Signal
  
  -- | 'caseOf' is a powerful combinator to implement switching behavior. It is similar to case expressions, but for signal functions.
  --
  -- The first argument determines some @c@. The second argument takes the @c@ and decides which signal function should be used.
  -- The signal functions generated by the second function keep their state across executions, i.e. when they get selected multiple times,
  -- they keep their state.
  --
  -- Beware that this function should not be used when @c@ has many (~dozens) cases, since the setup phase will be run for each case.
  caseOf :: (Bounded c, Enum c) => Signal es a c -> (c -> Signal es a b) -> Signal es a b
  caseOf @c decider kontSignal = makeSignal $ do
    when (fromEnum (maxBound :: c) - fromEnum (minBound :: c) > 100) $
      fail "You probably do not want to branch with so many cases. Use `manyCaseSignal` if you are really sure."
    unSignal (manyCaseOf decider kontSignal)
  {-# INLINE caseOf #-}
  
  -- | Same as `caseOf` but will not error when you have a @c@ with many cases.
  manyCaseOf :: (Bounded c, Enum c) => Signal es a c -> (c -> Signal es a b) -> Signal es a b
  manyCaseOf deciderSignal kontSignal = makeSignal $ do
    decide <- unSignal deciderSignal
    signals <- V.fromList <$> traverse (\c -> unSignal (kontSignal c)) [minBound .. maxBound]
    pure $ \a -> do
      c <- decide a
      let step = signals V.! fromEnum c
      step a
  {-# INLINE manyCaseOf #-}
  
  -- | Add a signal input for switching. If you feed in a new signal function, it will become active immediately and not run the old one.
  -- After a `Signal` has been switched out, it's outputs might be corrupted due to resource cleanup.
  rSwitch :: Signal es a b -> Signal es (Maybe (Signal es a b), a) b
  rSwitch initial = makeSignal $ do
    switchPoint <- newSwitch (unSignal initial)
    pure $
      ( \(maybeNewSignal, a) -> do
          case maybeNewSignal of
            Nothing -> pure ()
            Just newSignal ->
              updateSwitch switchPoint (unSignal newSignal)
          runSwitch switchPoint a
      )
  {-# INLINE rSwitch #-}
  
  -- | Switch out a signal function with another when you produce a @Just c@ value once. Be aware that each use of 'switch' has a small incremental cost. So if you switch often, use something like 'rSwitch' .
  -- The next signal function will become active instantly. After a `Signal` has been switched out, it's outputs might be corrupted.
  switch :: Signal es a (b, Maybe c) -> (c -> Signal es a b) -> Signal es a b
  switch signal kont = makeSignal $ mdo
    let initialSignal = do
          f <- unSignal signal
          pure $ \a -> do
            (b, mc) <- f a
            case mc of
              Nothing -> pure ()
              Just c -> do
                updateSwitch switchPoint $ unSignal $ kont c
            pure b
    switchPoint <- newSwitch initialSignal
    pure (runSwitch switchPoint)
  
  problematic :: (IOE :> es) => Int -> Signal es Int Int
  problematic threshold =
    switch
      ( arrIO $ \i -> do
          pure (i, if i > threshold then Just i else Nothing)
      )
      ( \i ->
          arrIO
            (\i -> print ("T: " <> show threshold) >> pure i)
            >>> problematic i
      )
  