{-# LANGUAGE CPP #-}
{-# LANGUAGE ConstraintKinds #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE RecursiveDo #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE PatternSynonyms #-}
{-# LANGUAGE RankNTypes #-}

module Main where

import Data.Foldable
import Control.Lens
import Control.Applicative
import Control.Monad
import Control.Monad.Fix
import Control.Exception
import System.Timeout
import Data.Maybe (isJust)
import Data.Functor.Misc
import qualified Data.Map as Map
import Data.Map (Map)
import qualified Data.IntMap as IntMap
import Data.IntMap (IntMap)
import Data.These
import Data.Align
import Reflex
import Reflex.EventWriter.Base
import Test.Run
import Test.Hspec
import Reflex.Spider.Internal (EventLoopException)
import Data.Witherable (Filterable)

#if defined(MIN_VERSION_these_lens) || (MIN_VERSION_these(0,8,0) && !MIN_VERSION_these(0,9,0))
import Data.These.Lens
#endif

type Widget t m = (MonadHold t m, Reflex t, MonadFix m)

connectDyn :: Widget t m => Event t () -> (Dynamic t a, Dynamic t a) -> m (Dynamic t a)
connectDyn e (d, d') = do
  dd <- holdDyn d (d' <$ e)
  return $ join dd

dynLoop :: Widget t m => (Event t Int, Event t ()) -> m (Event t Int)
dynLoop (e1, e2) = do
  -- "heightBagRemove: Height 2 not present in bag HeightBag {_heightBag_size = 2, _heightBag_contents = fromList [(0,1)]}"
  rec
    d <- count e1
    d' <- connectDyn e2 (d, liftA2 (+) d d')
  return $ updated d'

connectOnCoincidence :: Widget t m => Event t () -> Event t a -> m (Event t a)
connectOnCoincidence click e = do
  d <- holdDyn never (e <$ click)
  return $ coincidence (updated d)

coincidenceLoop :: Widget t m => (Event t Int, Event t ()) -> m (Event t Int)
coincidenceLoop (e1, e2) = do
-- "heightBagRemove: Height 1 not present in bag HeightBag {_heightBag_size = 1, _heightBag_contents = fromList [(0,0)]}"
-- (simpler version of dynLoop)
  rec
    e' <- connectOnCoincidence e2 (updated d)
    d <- count (align e' e1)
  return $ updated d

addHeight :: Reflex t =>  Event t a -> Event t a
addHeight e = leftmost [e4, e4] where
  e1 = leftmost [e, e]
  e2 = leftmost [e1, e1]
  e3 = leftmost [e2, e2]
  e4 = leftmost [e3, e3]

-- Take an existing test and build it inside a
buildLoop :: Widget t m => (forall t m. Widget t m => (Event t Int, Event t ()) -> m (Event t Int)) -> (Event t Int, Event t ()) -> m (Event t Int)
buildLoop test (e1, e2) = switchHold never buildLoop
  where buildLoop = pushAlways (const $ test (e1, e2)) e2

connectButtonPromptly :: Widget t m => Event t () -> Event t a -> m (Event t a)
connectButtonPromptly click e = do
  d <- holdDyn never (e <$ click)
  return (switchDyn d)

connectButton :: Widget t m => Event t () -> Event t a -> m (Event t a)
connectButton click e = do
  d <- hold never (e <$ click)
  return (switch d)

switchLoop01 :: Widget t m => (Event t Int, Event t ()) -> m (Event t Int)
switchLoop01 (e1, e2) = do
  rec
    e' <- connectButton e2 (updated d)
    d <- count (align e' e1)
  return $ updated d

mergeLoop :: forall t m. (Adjustable t m, Widget t m) => (Event t Int, Event t ()) -> m (Event t Int)
mergeLoop (e1, e2) = do
  rec
    (_, e) <- runEventWriterT $
      runWithReplace w (leftmost [w <$ e1])
  return (sum <$> e)
  where
    w = do
      c <- count e1
      tellEvent (updated ((pure <$> c) :: Dynamic t [Int]))

switchLoop02 :: Widget t m => (Event t Int, Event t ()) -> m (Event t Int)
switchLoop02 (e1, e2) = do
  rec
    e' <- connectButton e2 (updated d)
    d <- count (leftmost [e', e1])
  return $ updated d

switchLoop03 :: Widget t m => (Event t Int, Event t ()) -> m (Event t Int)
switchLoop03 (e1, e2) = do
  rec
    e' <- connectButton e2 (addHeight $ updated d)
    d <- count (align e' e1)
  return $ updated d

staticLoop01 :: Widget t m => (Event t Int, Event t ()) -> m (Event t Int)
staticLoop01 (e1, e2) = do
  rec
    d <- foldDyn (+) (0 :: Int) (1 <$ align e1 (updated d))
  return $ updated d

staticLoop02 :: Widget t m => (Event t Int, Event t ()) -> m (Event t Int)
staticLoop02 (e1, e2) = do
  rec
    d <- foldDyn (+) (0 :: Int) (leftmost [e1, updated d])
  return $ updated d

buildStaticLoop :: Widget t m => (Event t Int, Event t ()) -> m (Event t Int)
buildStaticLoop (e1, e2) = switchHold never buildLoop
  where buildLoop = pushAlways (const $ staticLoop01 (e1, e2)) e2

splitThese :: Filterable f => f (These a b) -> (f a, f b)
splitThese f = (mapMaybe (preview here) f,  mapMaybe (preview there) f)

main :: IO ()
main = hspec $ do
  describe "DebugCycles" $ do
    it "throws EventLoopException on switchLoop01" $ do
      check switchLoop01
    it "throws EventLoopException on switchLoop02" $ do
      check switchLoop02
    it "throws EventLoopException on switchLoop03" $ do
      check switchLoop03
    it "throws EventLoopException on buildSwitchLoop" $ do
      check $ buildLoop switchLoop01
    xit "throws EventLoopException on mergeLoop" $ do
      check mergeLoop
    xit "throws EventLoopException on staticLoop01" $ do
      check staticLoop01
    xit "throws EventLoopException on staticLoop02" $ do
      check staticLoop02
    xit "throws EventLoopException on buildStaticLoop" $ do
      check buildStaticLoop
    xit "throws EventLoopException on coincidenceLoop" $ do
      check coincidenceLoop
    xit "throws EventLoopException on dynLoop" $ do
      check dynLoop
    xit "throws EventLoopException on buildCoincidenceLoop" $ do
      check $ buildLoop coincidenceLoop
 where
   milliseconds = (*1000)
   occs = [  This 1, This 2, That (), This 3, That (), This 1 ]
   check test = do
     let action = timeout (milliseconds 50) $ do
                    runApp' (test . splitThese) (Just <$> occs)
     action `shouldThrow` (const True :: Selector EventLoopException)
