# LANGUAGE CPP , BangPatterns , TupleSections , DeriveGeneric #

module GhcMod.Exe.Find
  ( Symbol
  , SymbolDb
  , loadSymbolDb
  , lookupSymbol
  , dumpSymbol
  , findSymbol
  , lookupSym
  , isOutdated
  * Load ' SymbolDb ' asynchronously
  , AsyncSymbolDb
  , newAsyncSymbolDb
  , getAsyncSymbolDb
  ) where

import qualified GHC as G
import FastString
import Module
import OccName
import HscTypes
import Exception

import GhcMod.Convert
import GhcMod.Gap
import GhcMod.Monad
import GhcMod.Output
import GhcMod.Types
import GhcMod.Utils
import GhcMod.World
import GhcMod.LightGhc

import Control.Applicative
import Control.DeepSeq
import Control.Monad
import Control.Monad.Trans.Control
import Control.Concurrent

import Data.List
import Data.Binary
import Data.Function
import qualified Data.ByteString as BS
import qualified Data.ByteString.Lazy as LBS
import Data.IORef

import System.Directory.ModTime
import System.IO.Unsafe

import GHC.Generics (Generic)

import Data.Map (Map)
import qualified Data.Map as M
import Data.Set (Set)
import qualified Data.Set as S
import GhcMod.PathsAndFiles
import System.Directory
import Prelude

----------------------------------------------------------------

-- | Type of function and operation names.
type Symbol = BS.ByteString
type ModuleNameBS = BS.ByteString

  | Database from ' Symbol ' to \['ModuleString'\ ] .
data SymbolDb = SymbolDb
  { sdTable             :: Map Symbol (Set ModuleNameBS)
  , sdTimestamp         :: ModTime
  } deriving (Generic)

#if __GLASGOW_HASKELL__ >= 708
instance Binary SymbolDb
#else
instance Binary SymbolDb where
  put (SymbolDb a b) = put a >> put b
  get = do
    a <- get
    b <- get
    return (SymbolDb a b)
#endif
instance NFData SymbolDb

isOutdated :: IOish m => SymbolDb -> GhcModT m Bool
isOutdated db =
  isOlderThan (sdTimestamp db) <$> timedPackageCaches

----------------------------------------------------------------

  | Looking up ' SymbolDb ' with ' Symbol ' to \['ModuleString'\ ]
--   which will be concatenated. 'loadSymbolDb' is called internally.
findSymbol :: IOish m => String -> GhcModT m String
findSymbol sym = loadSymbolDb' >>= lookupSymbol sym

  | Looking up ' SymbolDb ' with ' Symbol ' to \['ModuleString'\ ]
--   which will be concatenated.
lookupSymbol :: IOish m => String -> SymbolDb -> GhcModT m String
lookupSymbol sym db = convert' $ lookupSym (fastStringToByteString $ mkFastString sym) db

lookupSym :: Symbol -> SymbolDb -> [ModuleString]
lookupSym sym db = map (ModuleString . unpackFS . mkFastStringByteString') $ S.toList $ M.findWithDefault S.empty sym $ sdTable db

---------------------------------------------------------------

loadSymbolDb' :: IOish m => GhcModT m SymbolDb
loadSymbolDb' = do
  cache <- symbolCache <$> cradle
  let doLoad True = do
        db <- decode <$> liftIO (LBS.readFile cache)
        outdated <- isOutdated db
        if outdated
        then doLoad False
        else return db
      doLoad False = do
        db <- loadSymbolDb
        liftIO $ LBS.writeFile cache $ encode db
        return db
  doLoad =<< liftIO (doesFileExist cache)

-- | Loading a file and creates 'SymbolDb'.
loadSymbolDb :: IOish m => GhcModT m SymbolDb
loadSymbolDb = do
  ghcMod <- liftIO ghcModExecutable
  readProc <- gmReadProcess'
  out <- liftIO $ readProc ghcMod ["--verbose", "error", "dumpsym"] ""
  return $!! decode out

----------------------------------------------------------------
  used ' ghc - mod dumpsym '

  | Dumps a ' Binary ' representation of ' SymbolDb ' to stdout
dumpSymbol :: IOish m => GhcModT m ()
dumpSymbol = do
  ts <- liftIO getCurrentModTime
  st <- runGmPkgGhc $ getGlobalSymbolTable
  liftIO . LBS.putStr $ encode SymbolDb {
      sdTable = st
    , sdTimestamp = ts
    }

-- | Check whether given file is older than any file from the given set.
-- Returns True if given file does not exist.
isOlderThan :: ModTime -> [TimedFile] -> Bool
isOlderThan tCache files =
  any (tCache <=) $ map tfTime files -- including equal just in case

-- | Browsing all functions in all system modules.
getGlobalSymbolTable :: (G.GhcMonad m, MonadIO m)
                     => m (Map Symbol (Set ModuleNameBS))
getGlobalSymbolTable =
  foldM extend M.empty =<< (listVisibleModules <$> G.getSessionDynFlags)

extend :: (G.GhcMonad m, MonadIO m)
       => Map Symbol (Set ModuleNameBS)
       -> Module
       -> m (Map Symbol (Set ModuleNameBS))
extend mm mdl = do
  hsc_env <- G.getSession
  eps <- liftIO $ readIORef $ hsc_EPS hsc_env
  modinfo <- liftIO $ unsafeInterleaveIO $ runLightGhc hsc_env $ do
    G.getModuleInfo mdl <* liftIO (writeIORef (hsc_EPS hsc_env) eps)
  return $ M.unionWith S.union mm $ extractBindings modinfo mdl

extractBindings :: Maybe G.ModuleInfo
                -> G.Module
                -> Map Symbol (Set ModuleNameBS)
extractBindings Nothing  _   = M.empty
extractBindings (Just inf) mdl = M.fromList $ do
  name <- G.modInfoExports inf
  let sym = fastStringToByteString $ occNameFS $ G.getOccName name
      mdls = S.singleton $ fastStringToByteString $ moduleNameFS $ moduleName mdl
  return (sym, mdls)

mkFastStringByteString' :: BS.ByteString -> FastString
#if !MIN_VERSION_ghc(7,8,0)
fastStringToByteString :: FastString -> BS.ByteString
fastStringToByteString = BS.pack . bytesFS

mkFastStringByteString' = mkFastStringByteList . BS.unpack
#elif __GLASGOW_HASKELL__ == 708
mkFastStringByteString' = unsafePerformIO . mkFastStringByteString
#else
mkFastStringByteString' = mkFastStringByteString
#endif

----------------------------------------------------------------

data AsyncSymbolDb = AsyncSymbolDb (MVar (Either SomeException SymbolDb))

asyncLoadSymbolDb :: IOish m
                  => MVar (Either SomeException SymbolDb)
                  -> GhcModT m ()
asyncLoadSymbolDb mv = void $
    liftBaseWith $ \run -> forkIO $ void $ run $ do
      edb <- gtry loadSymbolDb
      liftIO $ putMVar mv edb

newAsyncSymbolDb :: IOish m => GhcModT m AsyncSymbolDb
newAsyncSymbolDb = do
    mv <- liftIO newEmptyMVar
    asyncLoadSymbolDb mv
    return $ AsyncSymbolDb mv

getAsyncSymbolDb :: forall m. IOish m => AsyncSymbolDb -> GhcModT m SymbolDb
getAsyncSymbolDb (AsyncSymbolDb mv) = do
  db <- liftIO $ handleEx <$> takeMVar mv
  outdated <- isOutdated db
  if outdated
    then do
      asyncLoadSymbolDb mv
      liftIO $ handleEx <$> readMVar mv
    else do
      liftIO $ putMVar mv $ Right db
      return db
 where
   handleEx edb =
     case edb of
       Left ex -> throw ex
       Right db -> db

--------------------------------------------------------------
 | Type of function and operation names.
--------------------------------------------------------------
   which will be concatenated. 'loadSymbolDb' is called internally.
   which will be concatenated.
-------------------------------------------------------------
 | Loading a file and creates 'SymbolDb'.
--------------------------------------------------------------
 | Check whether given file is older than any file from the given set.
 Returns True if given file does not exist.
 including equal just in case
 | Browsing all functions in all system modules.
--------------------------------------------------------------
# LANGUAGE CPP , BangPatterns , TupleSections , DeriveGeneric #

module GhcMod.Exe.Find
  ( Symbol
  , SymbolDb
  , loadSymbolDb
  , lookupSymbol
  , dumpSymbol
  , findSymbol
  , lookupSym
  , isOutdated
  * Load ' SymbolDb ' asynchronously
  , AsyncSymbolDb
  , newAsyncSymbolDb
  , getAsyncSymbolDb
  ) where

import qualified GHC as G
import FastString
import Module
import OccName
import HscTypes
import Exception

import GhcMod.Convert
import GhcMod.Gap
import GhcMod.Monad
import GhcMod.Output
import GhcMod.Types
import GhcMod.Utils
import GhcMod.World
import GhcMod.LightGhc

import Control.Applicative
import Control.DeepSeq
import Control.Monad
import Control.Monad.Trans.Control
import Control.Concurrent

import Data.List
import Data.Binary
import Data.Function
import qualified Data.ByteString as BS
import qualified Data.ByteString.Lazy as LBS
import Data.IORef

import System.Directory.ModTime
import System.IO.Unsafe

import GHC.Generics (Generic)

import Data.Map (Map)
import qualified Data.Map as M
import Data.Set (Set)
import qualified Data.Set as S
import GhcMod.PathsAndFiles
import System.Directory
import Prelude


type Symbol = BS.ByteString
type ModuleNameBS = BS.ByteString

  | Database from ' Symbol ' to \['ModuleString'\ ] .
data SymbolDb = SymbolDb
  { sdTable             :: Map Symbol (Set ModuleNameBS)
  , sdTimestamp         :: ModTime
  } deriving (Generic)

#if __GLASGOW_HASKELL__ >= 708
instance Binary SymbolDb
#else
instance Binary SymbolDb where
  put (SymbolDb a b) = put a >> put b
  get = do
    a <- get
    b <- get
    return (SymbolDb a b)
#endif
instance NFData SymbolDb

isOutdated :: IOish m => SymbolDb -> GhcModT m Bool
isOutdated db =
  isOlderThan (sdTimestamp db) <$> timedPackageCaches


  | Looking up ' SymbolDb ' with ' Symbol ' to \['ModuleString'\ ]
findSymbol :: IOish m => String -> GhcModT m String
findSymbol sym = loadSymbolDb' >>= lookupSymbol sym

  | Looking up ' SymbolDb ' with ' Symbol ' to \['ModuleString'\ ]
lookupSymbol :: IOish m => String -> SymbolDb -> GhcModT m String
lookupSymbol sym db = convert' $ lookupSym (fastStringToByteString $ mkFastString sym) db

lookupSym :: Symbol -> SymbolDb -> [ModuleString]
lookupSym sym db = map (ModuleString . unpackFS . mkFastStringByteString') $ S.toList $ M.findWithDefault S.empty sym $ sdTable db


loadSymbolDb' :: IOish m => GhcModT m SymbolDb
loadSymbolDb' = do
  cache <- symbolCache <$> cradle
  let doLoad True = do
        db <- decode <$> liftIO (LBS.readFile cache)
        outdated <- isOutdated db
        if outdated
        then doLoad False
        else return db
      doLoad False = do
        db <- loadSymbolDb
        liftIO $ LBS.writeFile cache $ encode db
        return db
  doLoad =<< liftIO (doesFileExist cache)

loadSymbolDb :: IOish m => GhcModT m SymbolDb
loadSymbolDb = do
  ghcMod <- liftIO ghcModExecutable
  readProc <- gmReadProcess'
  out <- liftIO $ readProc ghcMod ["--verbose", "error", "dumpsym"] ""
  return $!! decode out

  used ' ghc - mod dumpsym '

  | Dumps a ' Binary ' representation of ' SymbolDb ' to stdout
dumpSymbol :: IOish m => GhcModT m ()
dumpSymbol = do
  ts <- liftIO getCurrentModTime
  st <- runGmPkgGhc $ getGlobalSymbolTable
  liftIO . LBS.putStr $ encode SymbolDb {
      sdTable = st
    , sdTimestamp = ts
    }

isOlderThan :: ModTime -> [TimedFile] -> Bool
isOlderThan tCache files =

getGlobalSymbolTable :: (G.GhcMonad m, MonadIO m)
                     => m (Map Symbol (Set ModuleNameBS))
getGlobalSymbolTable =
  foldM extend M.empty =<< (listVisibleModules <$> G.getSessionDynFlags)

extend :: (G.GhcMonad m, MonadIO m)
       => Map Symbol (Set ModuleNameBS)
       -> Module
       -> m (Map Symbol (Set ModuleNameBS))
extend mm mdl = do
  hsc_env <- G.getSession
  eps <- liftIO $ readIORef $ hsc_EPS hsc_env
  modinfo <- liftIO $ unsafeInterleaveIO $ runLightGhc hsc_env $ do
    G.getModuleInfo mdl <* liftIO (writeIORef (hsc_EPS hsc_env) eps)
  return $ M.unionWith S.union mm $ extractBindings modinfo mdl

extractBindings :: Maybe G.ModuleInfo
                -> G.Module
                -> Map Symbol (Set ModuleNameBS)
extractBindings Nothing  _   = M.empty
extractBindings (Just inf) mdl = M.fromList $ do
  name <- G.modInfoExports inf
  let sym = fastStringToByteString $ occNameFS $ G.getOccName name
      mdls = S.singleton $ fastStringToByteString $ moduleNameFS $ moduleName mdl
  return (sym, mdls)

mkFastStringByteString' :: BS.ByteString -> FastString
#if !MIN_VERSION_ghc(7,8,0)
fastStringToByteString :: FastString -> BS.ByteString
fastStringToByteString = BS.pack . bytesFS

mkFastStringByteString' = mkFastStringByteList . BS.unpack
#elif __GLASGOW_HASKELL__ == 708
mkFastStringByteString' = unsafePerformIO . mkFastStringByteString
#else
mkFastStringByteString' = mkFastStringByteString
#endif


data AsyncSymbolDb = AsyncSymbolDb (MVar (Either SomeException SymbolDb))

asyncLoadSymbolDb :: IOish m
                  => MVar (Either SomeException SymbolDb)
                  -> GhcModT m ()
asyncLoadSymbolDb mv = void $
    liftBaseWith $ \run -> forkIO $ void $ run $ do
      edb <- gtry loadSymbolDb
      liftIO $ putMVar mv edb

newAsyncSymbolDb :: IOish m => GhcModT m AsyncSymbolDb
newAsyncSymbolDb = do
    mv <- liftIO newEmptyMVar
    asyncLoadSymbolDb mv
    return $ AsyncSymbolDb mv

getAsyncSymbolDb :: forall m. IOish m => AsyncSymbolDb -> GhcModT m SymbolDb
getAsyncSymbolDb (AsyncSymbolDb mv) = do
  db <- liftIO $ handleEx <$> takeMVar mv
  outdated <- isOutdated db
  if outdated
    then do
      asyncLoadSymbolDb mv
      liftIO $ handleEx <$> readMVar mv
    else do
      liftIO $ putMVar mv $ Right db
      return db
 where
   handleEx edb =
     case edb of
       Left ex -> throw ex
       Right db -> db
