{-# LANGUAGE FlexibleInstances, MultiParamTypeClasses, FunctionalDependencies, Safe #-}

{-
  This module is part of Chatty.
  Copyleft (c) 2014 Marvin Cohrs

  All wrongs reversed. Sharing is an act of love, not crime.
  Please share Antisplice with everyone you like.

  Chatty is free software: you can redistribute it and/or modify
  it under the terms of the GNU Affero General Public License as published by
  the Free Software Foundation, either version 3 of the License, or
  (at your option) any later version.

  Chatty is distributed in the hope that it will be useful,
  but WITHOUT ANY WARRANTY; without even the implied warranty of
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  GNU Affero General Public License for more details.

  You should have received a copy of the GNU Affero General Public License
  along with Chatty. If not, see <http://www.gnu.org/licenses/>.
-}

-- | Provides a typeclass for all monads that may print text.
module Text.Chatty.Printer where

import Control.Applicative
import Control.Arrow
import Control.Monad
import Control.Monad.State
import Control.Monad.Identity
import System.IO

-- | A typeclass for all monads that may output strings.
class Monad m => ChPrinter m where
  -- | Just print it!
  mprint :: String -> m ()
  -- | Print it, except you are IO.
  mnoecho :: String -> m ()
  mnoecho = mprint
  -- | Flush the buffer.
  mflush :: m ()
  mflush = return ()
  -- | Alternative to mprint that does not mask any characters (depends on the carrier).
  mnomask :: String -> m ()
  mnomask = mprint

instance ChPrinter IO where
  mprint = putStr
  mnoecho _ = return ()
  mflush = hFlush stdout

instance Monad m => ChPrinter (StateT String m) where
  mprint s = modify (++s)

-- | DeafT discards all output (much like >\/dev\/null in the shell)
newtype DeafT m a = Deaf { runDeafT :: m a }

instance Monad m => Monad (DeafT m) where
  return = Deaf . return
  (Deaf d) >>= f = Deaf $ do d' <- d; runDeafT (f d')

instance MonadTrans DeafT where
  lift = Deaf

instance (Functor m, Monad m) => Applicative (DeafT m) where
  pure = return
  (<*>) = ap

instance Functor m => Functor (DeafT m) where
  fmap f (Deaf a) = Deaf $ fmap f a

instance MonadIO m => MonadIO (DeafT m) where
  liftIO = lift . liftIO

instance Monad m => ChPrinter (DeafT m) where
  mprint _ = return ()

-- Definition of OutRedirT + instances
-- | Redirects all output to a given handle (much like >filename in the shell)
newtype OutRedirT m a = OutRedir { runOutRedirT' :: Handle -> m (a,Handle) }
-- | 'OutRedirT' on a blank 'IO' monad
type OutRedir = OutRedirT IO

instance Monad m => Monad (OutRedirT m) where
  return a = OutRedir $ \h -> return (a,h)
  (OutRedir r) >>= f = OutRedir $ \h -> do (a,h') <- r h; runOutRedirT' (f a) h'

instance MonadTrans OutRedirT where
  lift m = OutRedir $ \h -> do a <- m; return (a,h)

instance MonadIO m => MonadIO (OutRedirT m) where
  liftIO = lift . liftIO

instance MonadIO m => ChPrinter (OutRedirT m) where
  mprint s = OutRedir $ \h -> do liftIO $ hPutStr h s; return ((),h)
  mflush = OutRedir $ \h -> do liftIO $ hFlush h; return ((),h)

instance Monad m => Functor (OutRedirT m) where
  fmap f a = OutRedir $ \h -> do (a',h') <- runOutRedirT' a h; return (f a',h')

instance Monad m => Applicative (OutRedirT m) where
  pure = return
  (<*>) = ap

-- | Run 'OutRedirT' with a 'Handle'
runOutRedirT :: Functor m => OutRedirT m a -> Handle -> m a
runOutRedirT m h = fmap fst $ runOutRedirT' m h

-- | Run 'OutRedir' with a 'Handle'
runOutRedir :: OutRedir a -> Handle -> IO a
runOutRedir = runOutRedirT

-- | Run 'OutRedirT' with a 'FilePath'
runOutRedirFT :: (Functor m,MonadIO m) => OutRedirT m a -> FilePath -> IOMode -> m a
runOutRedirFT m fp md
  | md `elem` [AppendMode,WriteMode] = do
    h <- liftIO $ openFile fp md
    a <- runOutRedirT m h
    liftIO $ hClose h
    return a
  | otherwise = error "runOutRedirFT does only accept AppendMode or WriteMode."

-- | Run 'OutRedir' with a 'FilePath'
runOutRedirF :: OutRedir a -> FilePath -> IOMode -> IO a
runOutRedirF = runOutRedirFT

-- Definition of RecorderT + instances
-- | Catches all output (much like VAR=$(...) in the shell)
newtype RecorderT m a = Recorder { runRecorderT' :: [String] -> m (a,[String]) }
-- | 'RecorderT' on the 'Identity'
type Recorder = RecorderT Identity

instance Monad m => Monad (RecorderT m) where
  return a = Recorder $ \s -> return (a,s)
  (Recorder r) >>= f = Recorder $ \s -> do (a,s') <- r s; runRecorderT' (f a) s'

instance MonadTrans RecorderT where
  lift m = Recorder $ \s -> do a <- m; return (a,s)

instance Monad m => ChPrinter (RecorderT m) where
  mprint s = Recorder $ \s' -> return ((),s:s')

instance Monad m => Functor (RecorderT m) where
  fmap f a = Recorder $ \s -> do (a',s') <- runRecorderT' a s; return (f a',s')

instance Monad m => Applicative (RecorderT m) where
  (<*>) = ap
  pure = return

instance MonadIO m => MonadIO (RecorderT m) where
  liftIO = lift . liftIO

-- Helper methods for RecorderT
-- | The recorder state. Use this together with 'replay', 'replayM' or 'replay_'.
newtype Replayable = Replayable [String]
instance Show Replayable where show r = show ((\(Replayable x) -> length x) r) ++ ":" ++ show (replay r)

-- | Replay a recorder state inside a 'Monad'.
replayM :: Monad m => m Replayable -> m String
replayM r = do (Replayable r') <- r; return (concat $ reverse r')

-- | Replay a recorder state in a pure context.
replay :: Replayable -> String
replay (Replayable r) = concat $ reverse r

-- | Replay the current recorder state without leaving the recorder.
replay_ :: Monad m => RecorderT m String
replay_ = Recorder $ \s -> return (concat $ reverse s,s)

-- | Run 'Recorder' and also return its state.
runRecorder :: Recorder a -> (a,Replayable)
runRecorder = second Replayable . runIdentity . flip runRecorderT' []

-- | Run 'RecorderT' and also return its state.
runRecorderT :: (Functor m,Monad m) => RecorderT m a -> m (a,Replayable)
runRecorderT = fmap (second Replayable) . flip runRecorderT' []

-- | Line-terminating alternative to 'mprint'
mprintLn :: ChPrinter m => String -> m ()
mprintLn = mprint . (++"\n")

-- | Line-terminating alternative to 'mnomask'
mnomaskLn :: ChPrinter m => String -> m ()
mnomaskLn = mnomask . (++"\n")

-- Shell-like syntax
-- | Redirection target that discards input.
data DiscardO = DiscardO
-- | Redirection target that records input.
data RecordO = RecordO
-- | Class for all redirection targets.
class RedirectionTarget t mt a r | t -> mt, t a -> r where
  -- | Overwriting redirection.
  (.>.) :: (Functor m,MonadIO m,ChPrinter (mt m)) => mt m a -> t -> m r
  -- | Appending redirection.
  (.>>.) :: (Functor m,MonadIO m,ChPrinter (mt m)) => mt m a -> t -> m r
  (.>>.) = (.>.)
instance RedirectionTarget DiscardO DeafT a a where
  m .>. _ = runDeafT m
instance RedirectionTarget RecordO RecorderT a (a,Replayable) where
  m .>. _ = runRecorderT m
instance RedirectionTarget FilePath OutRedirT a a where
  m .>. fp = runOutRedirFT m fp WriteMode
  m .>>. fp = runOutRedirFT m fp AppendMode
instance RedirectionTarget Handle OutRedirT a a where
  m .>. fp = runOutRedirT m fp