{-# LANGUAGE RecordWildCards, TupleSections #-}
module Main (main) where

import qualified Paths_ghc_lib as GHC

import GHC.Driver.Monad
import GHC
    ( runGhc, defaultErrorHandler
    , setTargets
    , parseModule, typecheckModule, desugarModule, coreModule
    )

import GHC.Core.TyCon
import GHC.CoreToStg.Prep
import GHC.Data.Bag
import GHC.Data.FastString
import GHC.Driver.Main (hscSimplify)
import GHC.Driver.Make
import GHC.Driver.Phases
import GHC.Driver.Session
import GHC.Driver.Types
import GHC.Hs.Instances ()
import GHC.Hs.Instances ()
import GHC.Iface.Tidy (tidyProgram)
import GHC.Iface.UpdateIdInfos
import GHC.StgToCmm.Types (CgInfos(..))
import GHC.Types.Avail
import GHC.Types.Id
import GHC.Types.Name
import GHC.Types.Name.Set
import GHC.Unit.Module.Env
import GHC.Unit.Module.Name
import GHC.Unit.State
import GHC.Unit.Types
import GHC.Utils.Error
import GHC.Utils.Outputable (ppr, showSDoc)

import qualified Data.Map as M
import Data.Time
import Control.Monad (unless)
import Data.Either (partitionEithers)
import System.Exit
import System.FilePath

import qualified GHC.Data.EnumSet as EnumSet
import qualified GHC.LanguageExtensions.Type as Ext

main :: IO ()
main = defaultErrorHandler defaultFatalMessager defaultFlushOut $ do
    libDir <- GHC.getDataDir
    runGhc (Just libDir) $ withSourceErrors $ do
        setup
        compileModule =<< prepareModule "Inline"

withSourceErrors :: (GhcMonad m) => m a -> m a
withSourceErrors = handleSourceError $ \e -> do
    printException e
    liftIO $ exitWith $ ExitFailure 1

setup :: (GhcMonad m) => m ()
setup = do
    dflags <- getSessionDynFlags
    dflags <- return $ gopt_set dflags Opt_NoTypeableBinds
    dflags <- return $ dflags
        { hscTarget = HscNothing
        , mainModIs = noMainModule
        , packageDBFlags = [PackageDB $ PkgDbPath primPkgDb, ClearPackageDBs]
        }

    dflags <- liftIO $ initUnits dflags
    modifySession $ \env -> env{ hsc_dflags = dflags }
    invalidateModSummaryCache
  where
    primPkgDb = "/home/mi/.ghcup/ghc/9.0.1/lib/ghc-9.0.1/package.conf.d"

noMainModule :: Module
noMainModule = mkModule HoleUnit $ mkModuleName "Main"

noPrelude :: HscEnv -> HscEnv
noPrelude env = env
    { hsc_dflags = let dflags = hsc_dflags env in dflags
        { extensionFlags = EnumSet.delete Ext.ImplicitPrelude $ extensionFlags dflags
        }
    }

prepareModule :: (GhcMonad m) => String -> m ModSummary
prepareModule modName = do
    let mod = mkModuleName modName
        target = resolve mod
    setTargets [target]

    -- Anything already in the provided map should be left as-is
    providers <- do
        env <- getSession
        return $ moduleNameProvidersMap . unitState . hsc_dflags $ env
    let exclude = M.keys providers

    (errs, mss) <- do
        env <- getSession
        env <- return $ noPrelude env
        liftIO $ partitionEithers <$> downsweep env [] exclude False
    reportErrors errs

    let menv = mkModuleEnv [(ms_mod ms, ms) | ms <- mss]

    let Just ms = lookupModuleEnv menv $ mkModule mainUnit mod
    return ms

reportErrors :: (GhcMonad m) => [ErrorMessages] -> m ()
reportErrors errs = do
    errs <- return $ unionManyBags errs
    unless (isEmptyBag errs) $ throwErrors errs

compileModule
    :: (GhcMonad m)
    => ModSummary
    -> m ()
compileModule ms = do
    pmod <- parseModule ms
    tmod <- typecheckModule pmod
    dmod <- desugarModule tmod
    let mguts = coreModule dmod

    env <- getSession
    mguts' <- liftIO $ hscSimplify env [] mguts
    (cg_guts, details) <- liftIO $ tidyProgram env mguts'

    let binds = cg_binds cg_guts
        tycons = cg_tycons cg_guts
        data_tycons = filter isDataTyCon tycons
        mod = cg_module cg_guts
        modLoc = ms_location ms
    (binds, _ccs) <- liftIO $ corePrepPgm env mod modLoc binds data_tycons

    let cg_info = CgInfos
            { cgNonCafs = mempty
            , cgLFInfos = mempty
            }
        details' = updateModDetailsIdInfos (hsc_dflags env) cg_info details

    let type_env = md_types details'
        lookupVar v = lookupTypeEnv type_env =<< exportedName details' varName v
    liftIO $ case lookupVar "f" of
        Just (AnId f) -> putStrLn $ showSDoc (hsc_dflags env) . ppr $ idUnfolding f
        _ -> putStrLn "No `f' in module"

    return ()

resolve :: ModuleName -> Target
resolve mod = mkTarget $ "input" </> path <.> "src"
  where
    path = moduleNameSlashes mod

    mkTarget filePath = Target
        { targetId = TargetFile filePath (Just $ Cpp HsSrcFile)
        , targetAllowObjCode = False
        , targetContents = Nothing
        }

invalidateModSummaryCache :: (GhcMonad m) => m ()
invalidateModSummaryCache = modifySession $ \env -> env
    { hsc_mod_graph = invalidateMG (hsc_mod_graph env)
    }
  where
    invalidateMG = mapMG invalidateMS
    invalidateMS ms = ms{ ms_hs_date = addUTCTime (-1) (ms_hs_date ms) }

exportedName :: ModDetails -> NameSpace -> String -> Maybe Name
exportedName ModDetails{..} = \ns s ->
    let occ = mkOccNameFS ns (fsLit s)
    in case nameSetElemsStable . filterNameSet (\nm -> occName nm == occ) $ names of
        [nm] -> Just nm
        _ -> Nothing
  where
    names = availsToNameSet md_exports
