{-# LANGUAGE DataKinds #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE ImportQualifiedPost #-}
{-# LANGUAGE InstanceSigs #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE UndecidableInstances #-}

module Try.Effectful.Dynamic where

import Control.Exception (IOException)
import Control.Monad (replicateM)
import Control.Monad.Catch (catch)
import Control.Monad.Cont (MonadIO (..))
import Data.Char (chr)
import Data.Map.Strict qualified as M
import Effectful (Dispatch (..), DispatchOf, Eff, Effect, IOE, runEff, runPureEff, type (:>))
import Effectful.Dispatch.Dynamic (HasCallStack, interpret, localSeqUnlift, localSeqUnliftIO, reinterpret, send)
import Effectful.Error.Dynamic
import Effectful.State.Static.Local
import GHC.Clock (getMonotonicTime)
import System.IO qualified as IO
import Prelude hiding (readFile)

data FileSystem :: Effect where
  ReadFile :: FilePath -> FileSystem m String
  WriteFile :: FilePath -> String -> FileSystem m ()

type instance DispatchOf FileSystem = Dynamic

readFile :: (HasCallStack, FileSystem :> es) => FilePath -> Eff es String
readFile path = send (ReadFile path)

writeFile :: (HasCallStack, FileSystem :> es) => FilePath -> String -> Eff es ()
writeFile path content = send (WriteFile path content)

newtype FsError = FsError String deriving (Show)

runFileSystemIO :: (IOE :> es, Error FsError :> es) => Eff (FileSystem : es) a -> Eff es a
runFileSystemIO = interpret $ \_ -> \case
  ReadFile path -> adapt $ IO.readFile path
  WriteFile path contents -> adapt $ IO.writeFile path contents
 where
  adapt m = liftIO m `catch` \(e :: IOException) -> throwError . FsError $ show e

runFileSystemPure :: Error FsError :> es => M.Map FilePath String -> Eff (FileSystem : es) a -> Eff es a
runFileSystemPure fs0 = reinterpret (evalState fs0) $ \_ -> \case
  ReadFile path ->
    gets (M.lookup path) >>= \case
      Just contents -> pure contents
      Nothing -> throwError . FsError $ "File not found: " ++ show path
  WriteFile path contents -> modify $ M.insert path contents

action :: (FileSystem :> es) => Eff es Bool
action = do
  file <- readFile "nix-managed.cabal"
  pure $ not (null file)

-- >>>:t action
-- action :: (FileSystem :> es) => Eff es Bool

-- >>>runEff . runError @FsError . runFileSystemIO $ action
-- Right True

-- >>>runPureEff . runErrorNoCallStack @FsError . runFileSystemPure M.empty $ action
-- Left (FsError "File not found: \"nix-managed.cabal\"")

data Profiling :: Effect where
  Profile :: String -> m a -> Profiling m a

type instance DispatchOf Profiling = Dynamic

profile :: (HasCallStack, Profiling :> es) => String -> Eff es a -> Eff es a
profile label action = send (Profile label action)

runProfiling :: IOE :> es => Eff (Profiling : es) a -> Eff es a
runProfiling = interpret $ \env -> \case
  Profile label action -> localSeqUnliftIO env $ \unlift -> do
    t1 <- getMonotonicTime
    r <- unlift action
    t2 <- getMonotonicTime
    putStrLn $ "Action '" ++ label ++ "' took " ++ show (t2 - t1) ++ " seconds."
    pure r

runNoProfiling :: Eff (Profiling : es) a -> Eff es a
runNoProfiling = interpret $ \env -> \case
  Profile label action -> localSeqUnlift env $ \unlift -> unlift action

action1 :: (Profiling :> es, IOE :> es) => Eff es ()
action1 = profile "greet" . liftIO $ putStrLn "Hello!"

testProfiling :: IO ()
testProfiling = runEff . runProfiling $ action1

testNoProfiling :: IO ()
testNoProfiling = runEff . runNoProfiling $ action1

class Monad m => MonadRNG m where
  randomInt :: m Int

randomString :: MonadRNG m => Int -> m String
randomString n = map chr <$> replicateM n randomInt

data RNG :: Effect where
  RandomInt :: RNG m Int

type instance DispatchOf RNG = Dynamic

instance RNG :> es => MonadRNG (Eff es) where
  randomInt :: (RNG :> es) => Eff es Int
  randomInt = send RandomInt

runDummyRNG :: Eff (RNG : es) a -> Eff es a
runDummyRNG = interpret $ \_ -> \case
  RandomInt -> pure 55

testDummy :: IO String
testDummy = runEff . runDummyRNG $ randomString 3