{-# LANGUAGE GeneralizedNewtypeDeriving #-}

-- | This module defines a state monad which allows the compiler's state (see
--   "FreeC.Environment") to be passed implicitly through the converter.
--
--   There are also utility functions to modify the state and retrieve
--   information stored in the state.
module FreeC.Monad.Converter
  ( -- * State Monad
    Converter
  , runConverter
  , evalConverter
  , execConverter
    -- * State Monad Transformer
  , ConverterT
  , runConverterT
  , evalConverterT
  , execConverterT
  , lift
  , lift'
  , hoist
    -- * Using IO Actions in Converters
  , ConverterIO
    -- * Modifying Environments
  , MonadConverter(..)
  , getEnv
  , inEnv
  , putEnv
  , modifyEnv
  , modifyEnv'
    -- * Encapsulating Environments
  , localEnv
  , moduleEnv
  , shadowVarPats
  ) where

import           Prelude                     hiding ( fail )

import           Control.Monad               ( forM_ )
import           Control.Monad.Fail          ( MonadFail(..) )
import           Control.Monad.Identity      ( Identity(..) )
import           Control.Monad.State
  ( MonadIO(..), MonadState(..), MonadTrans(..), StateT(..), evalStateT
  , execStateT, get, gets, modify, put, state )
import           Data.Composition            ( (.:) )

import           FreeC.Environment
import           FreeC.Environment.Entry
import qualified FreeC.IR.Syntax             as IR
import           FreeC.Monad.Class.Hoistable
import           FreeC.Monad.Reporter

-------------------------------------------------------------------------------
-- State Monad                                                               --
-------------------------------------------------------------------------------
-- | Type synonym for the state monad used by the converter.
--
--   All converter functions usually require the current 'Environment'
--   to perform the conversion. This monad allows these functions to
--   pass the environment around implicitly.
--
--   Additionally the converter can report error messages and warnings to the
--   user if there is a problem while converting.
type Converter = ConverterT Identity

-- | Runs the converter with the given initial environment and
--   returns the converter's result as well as the final environment.
runConverter :: Converter a -> Environment -> Reporter (a, Environment)
runConverter = runConverterT

-- | Runs the converter with the given initial environment and
--   returns the converter's result.
evalConverter :: Converter a -> Environment -> Reporter a
evalConverter = evalConverterT

-- | Runs the converter with the given initial environment and
--   returns the final environment.
execConverter :: Converter a -> Environment -> Reporter Environment
execConverter = execConverterT

-------------------------------------------------------------------------------
-- State Monad Transformer                                                   --
-------------------------------------------------------------------------------
-- | A state monad used by the converter parameterized by the inner monad @m@.
newtype ConverterT m a
  = ConverterT { unwrapConverterT :: StateT Environment (ReporterT m) a }
 deriving ( Functor, Applicative, Monad, MonadState Environment )

-- | Runs the converter with the given initial environment and
--   returns the converter's result as well as the final environment.
runConverterT
  :: Monad m => ConverterT m a -> Environment -> ReporterT m (a, Environment)
runConverterT = runStateT . unwrapConverterT

-- | Runs the converter with the given initial environment and
--   returns the converter's result.
evalConverterT :: Monad m => ConverterT m a -> Environment -> ReporterT m a
evalConverterT = evalStateT . unwrapConverterT

-- | Runs the converter with the given initial environment and
--   returns the final environment.
execConverterT
  :: Monad m => ConverterT m a -> Environment -> ReporterT m Environment
execConverterT = execStateT . unwrapConverterT

-- @MonadTrans@ instance for 'ConverterT'
instance MonadTrans ConverterT where
  lift mx = ConverterT $ StateT $ lift . (mx >>=) . (return .: flip (,))

-- | Converts a reporter to a converter.
lift' :: Monad m => ReporterT m a -> ConverterT m a
lift' mx = ConverterT $ StateT $ (mx >>=) . (return .: flip (,))

-- | The converter monad can be lifted to any converter transformer.
instance Hoistable ConverterT where
  hoist = ConverterT . StateT . (hoist .) . runConverter

-------------------------------------------------------------------------------
-- Using IO Actions in Converters                                            --
-------------------------------------------------------------------------------
-- | A converter with an IO action as its inner monad.
type ConverterIO = ConverterT IO

-- | IO actions can be embedded into converters.
instance MonadIO m => MonadIO (ConverterT m) where
  liftIO = ConverterT . liftIO

-------------------------------------------------------------------------------
-- Modifying Environments                                                    --
-------------------------------------------------------------------------------
-- | Type class for monad a converter can be lifted to. Inside such monads,
--   the functions for modifying the converters environment can be called
--   without lifting them explicitly.
class Monad m => MonadConverter m where
  liftConverter :: Converter a -> m a

-- | Converters can be lifted to arbitrary converter transformers.
instance Monad m => MonadConverter (ConverterT m) where
  liftConverter = hoist

-- | Gets the current environment.
getEnv :: MonadConverter m => m Environment
getEnv = liftConverter get

-- | Gets a specific component of the current environment using the given
--   function to extract the value from the environment.
inEnv :: MonadConverter m => (Environment -> a) -> m a
inEnv = liftConverter . gets

-- | Sets the current environment.
putEnv :: MonadConverter m => Environment -> m ()
putEnv = liftConverter . put

-- | Applies the given function to the environment.
modifyEnv :: MonadConverter m => (Environment -> Environment) -> m ()
modifyEnv = liftConverter . modify

-- | Gets a specific component and modifies the environment.
modifyEnv' :: MonadConverter m => (Environment -> (a, Environment)) -> m a
modifyEnv' = liftConverter . state

-------------------------------------------------------------------------------
-- Encapsulating Environments                                                --
-------------------------------------------------------------------------------
-- | Runs the given converter and returns its result but discards all
--   modifications to the environment.
localEnv :: MonadConverter m => m a -> m a
localEnv converter = do
  env <- getEnv
  x <- converter
  putEnv env
  return x

-- | Like 'localEnv' but modules that are added to the environment are still
--   available afterwards.
moduleEnv :: MonadConverter m => m a -> m a
moduleEnv converter = do
  env <- getEnv
  x <- converter
  ms <- inEnv envAvailableModules
  putEnv env { envAvailableModules = ms }
  return x

-- | Adds entries for variable patterns during the execution of the given
--   converter.
--
--   Unlike 'localEnv', all modifications to the environment are kept
--   (except for added entries), except for the definition of the variables.
shadowVarPats :: MonadConverter m => [IR.VarPat] -> m a -> m a
shadowVarPats varPats converter = do
  oldEntries <- inEnv envEntries
  forM_ varPats $ \varPat -> modifyEnv
    $ addEntry VarEntry
    { entrySrcSpan   = IR.varPatSrcSpan varPat
    , entryIsPure    = False
    , entryIdent     = undefined
    , entryAgdaIdent = undefined
    , entryName      = IR.varPatQName varPat
    , entryType      = IR.varPatType varPat
    }
  x <- converter
  modifyEnv $ \env -> env { envEntries = oldEntries }
  return x

-------------------------------------------------------------------------------
-- Reporting in Converter                                                    --
-------------------------------------------------------------------------------
-- | Promotes a reporter to a converter that produces the same result and
--   ignores the environment.
--
--   This type class instance allows 'report' and 'reportFatal' to be used
--   directly in @do@-blocks of the 'Converter' monad without explicitly
--   lifting reporters.
instance Monad m => MonadReporter (ConverterT m) where
  liftReporter = ConverterT . lift . hoist

-- | Use 'MonadReporter' to lift @fail@ of 'Reporter' to a 'ConverterT'.
instance Monad m => MonadFail (ConverterT m) where
  fail = liftReporter . fail