{-# OPTIONS_GHC -Wunused-imports #-}

module Agda.TypeChecking.DeadCode (eliminateDeadCode) where

import Control.Monad ((<$!>))
import Control.Monad.Trans

import Data.Maybe
import Data.Monoid (All(..))
import Data.Map (Map)
import qualified Data.Map as Map
import qualified Data.Map.Strict as MapS
import Data.Set (Set)
import qualified Data.Set as Set
import qualified Data.HashMap.Strict as HMap

import Agda.Interaction.Options

import qualified Agda.Syntax.Abstract as A

import Agda.Syntax.Common
import Agda.Syntax.Common.Pretty (prettyShow)
import Agda.Syntax.Internal
import Agda.Syntax.Internal.Names
import Agda.Syntax.Scope.Base

import qualified Agda.Benchmarking as Bench
import qualified Agda.TypeChecking.Monad.Benchmark as Bench

import Agda.TypeChecking.Monad
import Agda.TypeChecking.Reduce

import Agda.Utils.Impossible
import Agda.Utils.Lens

import Agda.Utils.HashTable (HashTable)
import qualified Agda.Utils.HashTable as HT

-- | Run before serialisation to remove any definitions and
-- meta-variables that are not reachable from the module's public
-- interface.
--
-- Things that are reachable only from warnings are removed.

eliminateDeadCode ::
     Map ModuleName a
       -- ^ Exported modules whose telescopes should not be mutilated by the dead-code removal.
  -> BuiltinThings PrimFun
  -> DisplayForms
  -> Signature
  -> LocalMetaStore
  -> TCM (DisplayForms, Signature, RemoteMetaStore)
eliminateDeadCode publicModules bs disp sig ms = Bench.billTo [Bench.DeadCode] $ do
  !patsyn <- getPatternSyns
  !public <- Set.mapMonotonic anameName . publicNames <$> getScope
  !save   <- optSaveMetas <$> pragmaOptions
  !defs   <- if save then return (sig ^. sigDefinitions)
                     else Bench.billTo [Bench.DeadCode, Bench.DeadCodeInstantiateFull]
                          (traverse (\x -> instantiateFull x) (sig ^. sigDefinitions))

  -- #2921: Eliminating definitions with attached COMPILE pragmas results in
  -- the pragmas not being checked. Simple solution: don't eliminate these.
  -- #6022 (Andreas, 2022-09-30): Eliminating cubical primitives can lead to crashes.
   -- Simple solution: retain all primitives (shouldn't be many).
  let hasCompilePragma = not . MapS.null . defCompiledRep
      isPrimitive = \case
        Primitive{}     -> True
        PrimitiveSort{} -> True
        _ -> False
      extraRootsFilter (name, def)
        | hasCompilePragma def || isPrimitive (theDef def) = Just name
        | otherwise = Nothing
      extraRoots =
        Set.fromList $ mapMaybe extraRootsFilter $ HMap.toList defs

      rootSections = Map.elems $ (sig ^. sigSections) `Map.intersection` publicModules
      rootNames = Set.union public extraRoots
      rootMetas =
        if not save then Set.empty else metasIn
          ( bs
          , sig ^. sigSections
          , sig ^. sigRewriteRules
          , HMap.filterWithKey (\x _ -> Set.member x rootNames) disp
          )

  (!rns, !rms) <- Bench.billTo [Bench.DeadCode, Bench.DeadCodeReachable] $ liftIO $
                    reachableFrom rootSections rootNames rootMetas patsyn disp defs ms

  let !dead  = Set.fromList (HMap.keys defs) `Set.difference` rns
      !valid = getAll . namesIn' (All . (`Set.notMember` dead))  -- no used name is dead
      !defs' = HMap.map ( \ d -> d { defDisplay = filter valid (defDisplay d) } )
               $ HMap.filterWithKey (\ x _ -> Set.member x rns) defs
      !disp' = HMap.filter (not . null) $ HMap.map (filter valid) disp
      !ms'   = HMap.fromList $
                mapMaybe
                  (\(m, mv) ->
                    if not (Set.member m rms)
                    then Nothing
                    else Just (m, remoteMetaVariable mv)) $
                MapS.toList ms

  reportSLn "tc.dead" 10 $
    "Removed " ++ show (HMap.size defs - HMap.size defs') ++
    " unused definitions and " ++ show (MapS.size ms - HMap.size ms') ++
    " unused meta-variables."
  reportSLn "tc.dead" 90 $ unlines $
    "Removed the following definitions from the signature:" :
    map (("- " ++) . prettyShow) (Set.toList dead)
  let !sig' = set sigDefinitions defs' sig
  return (disp', sig', ms')

reachableFrom ::
     [Section]      -- ^ Root modules.
  -> Set QName      -- ^ Root names.
  -> Set MetaId     -- ^ Root metas.
  -> A.PatternSynDefns
  -> DisplayForms
  -> Definitions
  -> LocalMetaStore
  -> IO (Set QName, Set MetaId)
reachableFrom sections ids ms psyns disp defs insts = do

  !seenNames <- HT.empty :: IO (HashTable QName ())
  !seenMetas <- HT.empty :: IO (HashTable MetaId ())

  let goName :: QName -> IO ()
      goName !x = HT.lookup seenNames x >>= \case
        Just _ ->
          pure ()
        Nothing -> do
          HT.insert seenNames x ()
          go (HMap.lookup x defs)
          go (PSyn <$!> MapS.lookup x psyns)
          go (HMap.lookup x disp)

      goMeta :: MetaId -> IO ()
      goMeta !m = HT.lookup seenMetas m >>= \case
        Just _ ->
          pure ()
        Nothing -> do
          HT.insert seenMetas m ()
          case MapS.lookup m insts of
            Nothing -> pure ()
            Just mv -> go (instBody (theInstantiation mv))

      go :: NamesIn a => a -> IO ()
      go = namesAndMetasIn' (either goName goMeta)
      {-# INLINE go #-}

  go sections
  foldMap goName ids
  foldMap goMeta ms
  !ids' <- HT.keySet seenNames
  !ms'  <- HT.keySet seenMetas
  pure (ids', ms')


-- | Returns the instantiation.
--
-- Precondition: The instantiation must be of the form @'InstV' inst@.

theInstantiation :: MetaVariable -> Instantiation
theInstantiation mv = case mvInstantiation mv of
  InstV inst                     -> inst
  Open{}                         -> __IMPOSSIBLE__
  OpenInstance{}                 -> __IMPOSSIBLE__
  BlockedConst{}                 -> __IMPOSSIBLE__
  PostponedTypeCheckingProblem{} -> __IMPOSSIBLE__

-- | Converts from 'MetaVariable' to 'RemoteMetaVariable'.
--
-- Precondition: The instantiation must be of the form @'InstV' inst@.

remoteMetaVariable :: MetaVariable -> RemoteMetaVariable
remoteMetaVariable mv = RemoteMetaVariable
  { rmvInstantiation = theInstantiation mv
  , rmvModality      = getModality mv
  , rmvJudgement     = mvJudgement mv
  }
