{-# LANGUAGE CPP #-}
-- | Some arguments to functions (types in particular) will not be used in the
--   body. Wouldn't it be useful if these wasn't passed around at all?
--   Fear not, we here perform some analysis and try to remove as many of these
--   occurences as possible.
--
--   We employ the worker/wrapper transform, so if f x1 .. xn = e
--   and we notice that some is not needed we create: f' xj .. xk = e [xi := unit]
--   and f x1 .. xn = f' xj .. xk.
--   i.e we erase them in f' and replace by unit, and the original f function
--   calls the new f'. The idea is that f should be inlined and then peace on earth.
module Agda.Compiler.Epic.Erasure where

import Control.Applicative
import Control.Monad.State
import Data.Map(Map)
import qualified Data.Map as M
import Data.Maybe

import Agda.Compiler.Epic.AuxAST
import Agda.Compiler.Epic.CompileState
import Agda.Compiler.Epic.Interface

import Agda.TypeChecking.Monad.Base (TCM)
import qualified Agda.Syntax.Internal as SI
import qualified Agda.Syntax.Common   as SC
import Agda.TypeChecking.Monad (MonadTCM, reportSDoc)
import Agda.TypeChecking.Pretty as P

#include "../../undefined.h"
import Agda.Utils.Impossible

isIrr :: Relevance -> Bool
isIrr Irr      = True
isIrr Rel      = False

isRel :: Relevance -> Bool
isRel = not . isIrr

-- | Relevance "or"
(||-) :: Relevance -> Relevance -> Relevance
Irr      ||- b      = b
_        ||- _      = Rel
infixr 2 ||-

-- | Relevance "and"
(&&-) :: Relevance -> Relevance -> Relevance
Rel      &&- b      = b
_        &&- _      = Irr
infixr 3 &&-

data ErasureState = ErasureState
  { relevancies :: Map Var [Relevance]
  , funs        :: Map Var Fun
  }

type Erasure = StateT ErasureState

-- | Try to find as many unused variables as possible
erasure :: [Fun] -> Compile TCM [Fun]
erasure fs = do
    orgRel <- gets (relevantArgs . importedModules)
    (rels, erasureState) <- flip runStateT (ErasureState orgRel M.empty) $ do
        mapM_ initiate fs
        fu <- gets funs
        M.mapKeys (fromMaybe __IMPOSSIBLE__ . flip M.lookup fu) <$> step 1
    modifyEI $ \s -> s { relevantArgs = M.mapKeys funName rels }
    concat <$> mapM (\f -> map (rem (relevancies erasureState)) <$> check f (M.lookup f rels)) fs
  where

    rem rels f@Fun{} = f { funExpr = removeUnused rels (funExpr f) }
    rem _    f       = f
    -- | Perform the worker//wrapper transform
    check :: Fun -> Maybe [Relevance] -> Compile TCM [Fun]
    -- If the function is already marked as to inline we don't need to create a
    -- new function. Also If all arguments are relevant there is nothing to do.
    check f@Fun{} (Just rs) | any isIrr rs && not (funInline f) = do
        f' <- (funName f ++) <$> newName
        let args' = pairwiseFilter (map isRel rs) (funArgs f)
            subs  = pairwiseFilter (map isIrr rs) (funArgs f)
            e'    = foldr (\v e -> subst v "primUnit" e) (funExpr f) subs
        return [ Fun { funInline  = True
                     , funName    = funName f
                     , funQName   = funQName f
                     , funComment = funComment f
                     , funArgs    = funArgs f
                     , funExpr    = App f' $ map Var args'
                     }
               , Fun { funInline  = False
                     , funName    = f'
                     , funQName   = Nothing
                     , funComment = funComment f ++ " [ERASED]"
                     , funArgs    = args'
                     , funExpr    = e'
                     }
               ]
    check f _ = return [f]

removeUnused :: Map Var [Relevance] -> Expr -> Expr
removeUnused rels t = let rem = removeUnused rels
                       in case t of
    Var _         -> t
    Lit _         -> t
    Lam v e       -> Lam v (rem e)
    Con tag qn es -> Con tag qn (map rem es)
    App v es      -> case M.lookup v rels of
       Just re -> App v $ zipWith (\r x -> if isIrr r then UNIT else rem x)
                                  (re ++ repeat Rel) es
       Nothing    -> App v $ map rem es
    Case e brs    -> Case (rem e) (map (\br -> br {brExpr = rem $ brExpr br}) brs)
    If a b c      -> If (rem a) (rem b) (rem c)
    Let v e1 e2   -> lett v (rem e1) (rem e2)
    Lazy e        -> lazy (rem e)
    UNIT          -> t
    IMPOSSIBLE    -> t

-- | Initiate a function's relevancies
initiate :: Fun -> Erasure (Compile TCM) ()
initiate f@(Fun _ name mqname _ args _) = do
    let rels = replicate (length args) Irr
    modify $ \s -> s { relevancies = M.insert name rels (relevancies s)
                     , funs        = M.insert name f (funs s)
                     }
initiate f@(EpicFun {funName = name, funQName = mqname}) = case mqname of
    Just qn -> do
        ty <- lift $ getType qn
        let rels = initialRels ty Rel
        return ()
        modify $ \s -> s { relevancies = M.insert name rels (relevancies s)
                         , funs        = M.insert name f (funs s)
                         }
    Nothing -> return ()

initialRels :: SI.Type -> Relevance -> [Relevance]
initialRels ty rel =
    case SI.unEl ty of
        SI.Pi  a b -> mkRel a : initialRels (SI.unAbs b) rel
        _       -> []
  where
    mkRel :: SC.Dom SI.Type -> Relevance
    mkRel a | ignoreForced (SC.domRelevance a) = Irr
    mkRel a = case SI.unEl (SC.unDom a) of
       SI.Sort _ -> Irr
       _         -> rel

ignoreForced :: SC.Relevance -> Bool
ignoreForced SC.Relevant = False
ignoreForced _           = True

-- | Calculate if a variable is relevant in an expression
relevant :: (Functor m, Monad m) => Var -> Expr -> Erasure m Relevance
relevant var expr = case expr of
    Var v  | v == var  -> return Rel
           | otherwise -> return Irr
    Lit _l      -> return Irr
    Lam _ e     -> relevant var e
    Con _ _ es  -> relevants var es
    App v es | v == var  -> return Rel
             | otherwise -> do
                -- The variable is relevant if it is used in a relevant position
                mvrs <- gets (M.lookup v . relevancies)
                case mvrs of
                  Nothing  -> relevants var es
                  Just vrs ->
                      relevants var
                        $ map snd
                        $ filter ((==) Rel . fst)
                        $ zip (vrs ++ repeat Rel) es
                      -- foldr (||-) Irr <$> zipWith (&&-) (vrs ++ repeat Rel) <$> mapM (relevant var) es
    -- {-
    Case e [br@Branch{}] -> do
        cvars <- foldr (||-) Irr <$> mapM (flip relevant $ brExpr br) (brVars br)
        vare  <- relevant var e
        varbr <- relevant var (brExpr br)
        return ((vare &&- cvars) ||- varbr)
    -- -}
    Case e brs  -> (||-) <$> relevant var e  <*> relevants var (map brExpr brs)
    If a b c    -> relevants var [a,b,c]
    Let x e1 e2 -> (||-) <$> ((&&-) <$> relevant var e1 <*> relevant x e2) <*> relevant var e2
    Lazy e      -> relevant var e
    UNIT        -> return Irr
    IMPOSSIBLE  -> return Irr
  where
    relevants :: (Functor m, Monad m) => Var -> [Expr] -> Erasure m Relevance
    relevants v [] = return Irr
    relevants v (e : es) = do
      r <- relevant v e
      case r of
        Rel -> return r
        _   -> relevants v es
    -- relevants v es = return . foldr (\x y -> x ||- y) Irr =<< mapM (relevant v) es

-- | Try to find a fixpoint for all the functions relevance.
step :: Integer -> Erasure (Compile TCM) (Map Var [Relevance])
step nrOfLoops = do
    s  <- get
    newRels <- (M.fromList <$>) $ forM (M.toList (funs s)) $ \(v, f) -> ((,) v <$>) $ do
               let funRels = fromMaybe __IMPOSSIBLE__ $ M.lookup v (relevancies s)
               case f of
                  EpicFun{} -> return funRels
                  Fun{} -> do
                     forM (zip (funArgs f) (funRels ++ repeat Rel)) $ \ (x, rel) -> case rel of
                        Rel -> return Rel
                        Irr -> do
                          lift $ lift $ reportSDoc "epic.erasure" 10 $ P.text "running erasure:" P.<+> (P.text . show) (funQName f)
                          relevant x (funExpr f)
    let relsm = newRels `M.union` relevancies s
    if relevancies s == relsm
       then return newRels
       else do
         put s {relevancies = relsm}
         step (nrOfLoops + 1)

diff :: (Ord k, Eq a) => Map k a -> Map k a -> [(k,(a,a))]
diff m1 m2 = catMaybes $ zipWith (\(k, x) (_, y) -> if x == y then Nothing else Just (k, (x, y))) (M.toList m1) (M.toList m2)
