{-# LANGUAGE ScopedTypeVariables, OverloadedStrings #-}
{-# LANGUAGE RecordWildCards, BlockArguments, TupleSections #-}

module Main (main) where

import qualified Paths_ghc_lib as GHC

import GHC hiding (loadModule)
import GHC.Driver.Monad
import GHC.Driver.Session
import GHC.Unit.State
import GHC.Unit.Types
import GHC.Unit.Module.Name
import GHC.Driver.Types
import Data.Time
import GHC.Utils.Fingerprint
import GHC.Iface.Make (mkIface_)
import GHC.Tc.Types
import qualified Data.Map as M
import Control.Monad
import Control.Monad.State
import Data.Maybe
import GHC.Utils.Outputable hiding ((<>))

main :: IO ()
main = do
    libdir <- GHC.getDataDir
    runGhc (Just libdir) do
        setup

        let resolveTarget mod = do
                -- Pretend this does something more interesting
                return Target
                    { targetId = TargetFile ("input/fixity/" <> moduleNameSlashes mod <> ".hs") Nothing
                    , targetAllowObjCode = False
                    , targetContents = Nothing
                    }

        loadDependencies resolveTarget $ mkModuleName "Standalone"
        liftIO $ putStrLn "Success with Standalone.hs"
        loadDependencies resolveTarget $ mkModuleName "Importer"
        liftIO $ putStrLn "Success with Importer.hs"

-- This is very custom, because in my real use case I can't let GHC
-- load modules on its own from files.
loadDependencies :: forall m. (GhcMonad m) => (ModuleName -> m Target) -> ModuleName -> m ()
loadDependencies resolveTarget mod = evalStateT (go mod) []
  where
    go :: ModuleName -> StateT [ModuleName] m ()
    go mod = do
        done <- gets (mod `elem`)
        unless done do
            modify (mod:)
            ms <- lift do
                target <- resolveTarget mod
                addTarget target
                mg <- depanal [] False
                return $ fromMaybe (error "mgLookupModule") $
                    mgLookupModule mg (mkModule mainUnit mod)
            let deps = [mod | (_, L _ mod) <- ms_textual_imps ms]
            mapM_ go deps
            lift $ loadModule ms

setup :: (GhcMonad m) => m ()
setup = do
    dflags <- getSessionDynFlags
    dflags <- return $ gopt_set dflags Opt_NoTypeableBinds
    dflags <- return $ dflags{ hscTarget = HscNothing }
    dflags <- liftIO $ initUnits dflags
    modifySession $ \env -> env{ hsc_dflags = dflags }
    modifySession $ invalidateModSummaryCache

loadModule :: (GhcMonad m) => ModSummary -> m ()
loadModule ms = do
    liftIO $ putStrLn $ unwords ["Loading module", moduleNameString mod]
    tmod <- typecheckModule =<< parseModule ms
    env <- getSession
    let (tcg, details) = tm_internals_ tmod
        iface = mkModIface env tcg details
        mod_info = HomeModInfo iface details Nothing
    liftIO $ putStrLn $ unwords ["Fixities:", showPpr (hsc_dflags env) $ mi_fixities iface]
    modifySession $
        extendHpt mod_info .
        registerModule (mkModule mainUnit mod)
  where
    mod = moduleName . ms_mod $ ms

mkModIface :: HscEnv -> TcGblEnv -> ModDetails -> ModIface
mkModIface env tcg mod_details = fill partial
  where
    empty = emptyFullModIface this_mod

    fill :: PartialModIface -> ModIface
    fill partial = partial
        { mi_decls = map (fingerprint0,) (mi_decls partial)
        , mi_final_exts = mi_final_exts empty
        }

    partial = mkIface_ env
        this_mod hsc_src
        used_th deps rdr_env
        fix_env warns hpc_info
        (imp_trust_own_pkg imports) safe_mode usages
        doc_hdr' doc_map arg_map
        mod_details

    -- Copypasta from GHC.Iface.Make.mkIfaceTc
    this_mod = tcg_mod tcg
    hsc_src = HsSrcFile
    used_th = False
    deps = noDependencies -- TODO: dep tracking
    rdr_env = tcg_rdr_env tcg
    fix_env = tcg_fix_env tcg
    warns = tcg_warns tcg
    hpc_info = emptyHpcInfo False
    imports = tcg_imports tcg
    safe_mode = Sf_Ignore
    usages = [] -- TODO: dep tracking
    doc_hdr' = Nothing
    doc_map = emptyDeclDocMap
    arg_map = emptyArgDocMap

registerModule :: Module -> HscEnv -> HscEnv
registerModule mod env = env
    { hsc_dflags = let dflags = hsc_dflags env in dflags
        { unitState = addModules (unitState dflags)
        }
    }
  where
    addModules us = us
        { moduleNameProvidersMap = M.insert (moduleName mod) (M.singleton mod modOrig) $ moduleNameProvidersMap us
        }
    modOrig = ModOrigin (Just True) [] [] True

extendHpt :: HomeModInfo -> HscEnv -> HscEnv
extendHpt hmi env = env{ hsc_HPT = hpt', hsc_type_env_var = Nothing }
  where
    mod = mi_module . hm_iface $ hmi
    hpt' = addToHpt (hsc_HPT env) (moduleName mod) hmi

invalidateModSummaryCache :: HscEnv -> HscEnv
invalidateModSummaryCache 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) }
