{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE BlockArguments #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE ExistentialQuantification #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE ImportQualifiedPost #-}
{-# LANGUAGE KindSignatures #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TupleSections #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE UndecidableInstances #-}
{-# OPTIONS_GHC -Wno-unused-top-binds #-}

module Try.FusedEffects.UndoIO (
  main1,
  main,
) where

import Control.Algebra (Algebra (alg), send, type (:+:) (L, R))
import Control.Applicative (Alternative)
import Control.Carrier.Error.Either (Catch, Has, Throw, catchError, runError, throwError)
import Control.Carrier.Fail.Either qualified as Fail
import Control.Carrier.Lift (sendM)
import Control.Carrier.State.Strict (StateC (..), runState)
import Control.Effect.Exception (Lift, bracket, try)
import Control.Exception.Lifted qualified as CL (bracket, catch, try)
import Control.Monad (MonadPlus, replicateM_)
import Control.Monad.Catch (MonadCatch, MonadMask, MonadThrow)
import Control.Monad.Catch qualified as CE
import Control.Monad.Except (runExceptT)
import Control.Monad.Fix (MonadFix)
import Control.Monad.IO.Class (MonadIO (..))
import Control.Monad.State.Strict (MonadTrans (..))
import Control.Monad.Trans.Control (MonadBaseControl)
import Control.Monad.Trans.Except (ExceptT)
import Control.Monad.Trans.Writer (WriterT (..))
import Control.Monad.Trans.Writer qualified as W (WriterT)
import Control.Monad.Writer.Class qualified as WC (MonadWriter, tell)
import Data.Data (Typeable)
import Data.Foldable (sequenceA_)
import Data.Functor (($>))
import Data.Functor.Identity (Identity (..))
import Data.Kind (Type)

data WriterStack w (m :: Type -> Type) k where
  Tell :: w -> WriterStack w m ()
  Listen :: m a -> WriterStack w m (w, a)
  Censor :: (w -> w) -> m a -> WriterStack w m a

tell :: forall w m sig. Has (WriterStack w) sig m => w -> m ()
tell = send . Tell

listen :: Has (WriterStack w) sig m => m a -> m (w, a)
listen = send . Listen

censor :: Has (WriterStack w) sig m => (w -> w) -> m a -> m a
censor f = send . Censor f

newtype WriterStackC w m a = WriterStackC {runWriterStackC :: StateC w m a}
  deriving (Alternative, Applicative, Functor, Monad, Fail.MonadFail, MonadFix, MonadIO, MonadPlus, MonadTrans)

runWriterStack :: forall w a m. Monoid w => WriterStackC w m a -> m (w, a)
runWriterStack (WriterStackC m) = runState mempty m

execWriterStack :: forall w a m. (Monoid w, Functor m) => WriterStackC w m a -> m w
execWriterStack = fmap fst . runWriterStack

instance (Monoid w, Algebra sig m) => Algebra (WriterStack w :+: sig) (WriterStackC w m) where
  alg hdl sig ctx =
    -- this is just to observe the type of hdl
    let hdl1 = hdl
     in WriterStackC $ case sig of
          L writer -> StateC $ \w -> case writer of
            Tell w1 -> do
              let !w2 = w1 <> w
              return (w2, ctx)
            Listen m -> do
              (w1, a) <- runWriterStack (hdl (m <$ ctx))
              let !w2 = w1 <> w
              return (w2, (w1,) <$> a)
            Censor f m -> do
              (w1, a) <- runWriterStack (hdl (m <$ ctx))
              let !w2 = f w1 <> w
              return (w2, a)
          R other -> alg (runWriterStackC . hdl) (R other) ctx

tell1 :: (Has (WriterStack [a]) sig m) => a -> m ()
tell1 a = tell [a]

l :: [Int]
l = (4 :: Int) <$ ([3, 2, 5] :: [Int]) $> 5

-- >>> l
-- [5,5,5]

type Log = [Int]

sl2 :: Log
sl2 = take 5 $
  (runIdentity . execWriterStack) do
    replicateM_ 1000000 (let !a = tell ([3] :: [Int]) in a)

-- >>> s2
-- [3,3,3,3,3]

someActions :: forall m sig. (MonadIO m, Has (WriterStack [Int]) sig m, Has (WriterStack [IO ()]) sig m) => m ()
someActions = do
  tell @[Int] [1, 2]
  tell @[IO ()] [putStr "world!\n"]
  tell @[IO ()] [putStr "Hello, "]

main1 :: IO ()
main1 = do
  s <- (fst <$>) $ runWriterStack @[IO ()] . runWriterStack @[Int] @() $ someActions
  sequenceA_ s

main :: IO ()
main = print "Hello, world!"

Writer and exceptions

Using Lift IO

data FileError = WriteError FilePath | ReadError FilePath deriving (Typeable, Show)

instance CE.Exception FileError

someActions1 :: forall m sig. (Has (Lift IO) sig m, Has (Throw FileError) sig m, Has (Catch FileError) sig m, Has (WriterStack [IO ()]) sig m) => m ()
someActions1 = flip catchError (\(_ :: FileError) -> tell @[IO ()] [putStr "Not hello, "]) do
  tell @[IO ()] [putStr "world!\n"]
  _ :: Either CE.SomeException () <-
    try $
      bracket
        (sendM $ readFile "SomeFile")
        (\_ -> tell @[IO ()] [print "File is here!"])
        (\_ -> tell @[IO ()] [print "File is not here!"])
  _ <- throwError $ ReadError "No such file"
  tell @[IO ()] [putStr "Hello, "]

main2 :: IO ()
main2 = do
  s <- (fst <$>) $ runWriterStack @[IO ()] . runError @FileError $ someActions1
  sequenceA_ s

main3 :: IO ()
main3 = do
  s <- runError @FileError . runWriterStack @[IO ()] $ someActions1
  case s of
    Left x -> print x
    Right (x, _) -> sequenceA_ x

Trying mtl stack

Still, Writer's log doesn't go into handlers

newtype MyStack e w a = MyStack {runMyStack :: ExceptT e (W.WriterT w IO) a}
  deriving (Functor, Applicative, Monad, WC.MonadWriter w, MonadIO, MonadMask, MonadCatch, MonadThrow)

someActions2 :: MyStack FileError [IO ()] ()
someActions2 = flip CE.catch (\(_ :: FileError) -> WC.tell [putStr "Not hello, "]) do
  WC.tell [putStr "world!\n"]
  _ :: Either CE.SomeException () <-
    CE.try $
      CE.bracket
        (liftIO $ readFile "SomeFile")
        (\_ -> WC.tell [putStr "File is here!"])
        (\_ -> WC.tell [putStr "File is not here!"])
  _ <- CE.throwM $ ReadError "No such file"
  WC.tell [putStr "Hello, "]
  return ()

main4 :: IO ()
main4 = do
  s <- (snd <$>) $ runWriterT . runExceptT . runMyStack $ someActions2
  sequenceA_ s

Trying lifted-base

someActions3 :: forall m sig. (MonadBaseControl IO m, Has (Lift IO) sig m, Has (Throw FileError) sig m, Has (WriterStack [IO ()]) sig m) => m ()
someActions3 = flip CL.catch (\(_ :: FileError) -> tell @[IO ()] [putStr "Not hello, "]) do
  tell @[IO ()] [putStr "world!\n"]
  _ :: Either CE.SomeException () <-
    CL.try $
      CL.bracket
        (sendM $ readFile "SomeFile")
        (\_ -> tell @[IO ()] [print "File is here!"])
        (\_ -> tell @[IO ()] [print "File is not here!"])
  _ <- throwError $ ReadError "No such file"
  tell @[IO ()] [putStr "Hello, "]

-- main5 :: IO ()
-- main5 = do
--   s <- (fst <$>) $ runWriterStack @[IO ()] . runError @FileError $ someActions3
--   sequenceA_ s

increment :: Int -> Int
increment x = x + 1

wrappedInt :: Maybe Int
wrappedInt = Just 3

wrappedIncrement :: Maybe (Int -> Int)
wrappedIncrement = Just increment

s1 :: (Int -> Int) -> (Maybe Int -> Maybe Int)
s1 = fmap

s1' :: (Int -> Int) -> (Maybe Int -> Maybe Int)
s1' = undefined

s2 :: Maybe (Int -> Int) -> (Maybe Int -> Maybe Int)
s2 = (<*>)

s2' :: Maybe (Int -> Int) -> (Maybe Int -> Maybe Int)
s2' = undefined

wrappingIncrement :: Int -> Maybe Int
wrappingIncrement x = Just (increment x)

s3 :: Int -> Maybe Int
s3 = pure

s3' :: Int -> Maybe Int
s3' = undefined

s4 :: Maybe Int -> (Int -> Maybe Int) -> Maybe Int
s4 = (>>=)

s4' :: Maybe Int -> (Int -> Maybe Int) -> Maybe Int
s4' = undefined