{-# 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
    , TypecheckedModule(..)
    )

import GHC.Driver.Session
import GHC.Unit.Module.Name
import GHC.Unit.State
import GHC.Unit.Types
import GHC.Utils.Error
import GHC.Data.Bag
import GHC.Driver.Make
import GHC.Driver.Phases
import GHC.Driver.Finder
import GHC.Unit.Module.Env
import GHC.Driver.Types
import GHC.Hs.Instances ()
import GHC.Plugins
import GHC.Hs.Doc
import GHC.Tc.Types
import Data.Version
import qualified Data.Map as M
import Data.Time
import GHC.Iface.Make
import GHC.HsToCore.Usage
import GHC.Utils.Fingerprint

import GHC.Data.IOEnv
import Data.IORef
import GHC.Tc.Utils.Monad (updateEps_)
import GHC.Core.InstEnv
import GHC.Core.FamInstEnv


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

        -- These three modules could come from any combination of
        -- units. For this example, we use three different ones; but
        -- changing this even so that all three are in the same unit
        -- (or even mainUnit) doesn't make a difference for the
        -- problem at hand.
        loadModule =<< prepareModule unit1 "Class"
        loadModule =<< prepareModule unit2 "Instance"
        loadModule =<< prepareModule unit3 "Use"

unit1, unit2, unit3 :: Unit
unit1 = RealUnit . Definite . UnitId . fsLit $ "unit1"
unit2 = RealUnit . Definite . UnitId . fsLit $ "unit2"
unit3 = RealUnit . Definite . UnitId . fsLit $ "unit3"

mkUnitInfo :: Unit -> UnitInfo
mkUnitInfo unit = fakeUnitInfo pkgId pkgName uid
  where
    uid = toUnitId unit
    pkgId = PackageId $ unitIdFS uid
    pkgName = PackageName $ unitIdFS uid

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
    mapM_ (modifySession . addUnit . mkUnitInfo) [ unit1, unit2, unit3 ]
  where
    primPkgDb = "/home/mi/.ghcup/ghc/9.0.1/lib/ghc-9.0.1/package.conf.d"

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

withUnitState :: UnitState -> ModSummary -> ModSummary
withUnitState us ms = ms
    { ms_hspp_opts = (ms_hspp_opts ms)
        { unitState = us
        }
    }

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

loadModule :: (GhcMonad m) => ModSummary -> m ()
loadModule ms = do
    liftIO $ putStrLn $ unwords ["loadModule", moduleNameString . moduleName . ms_mod $ ms]

    -- Loading dependencies has changed the unitState, so we
    -- need to refresh the one stored in the ModSummary
    ms <- do
        us <- unitState . hsc_dflags <$> getSession
        return $ withUnitState us ms

    (tmod, iface, details) <- prepareSource ms
    registerModule iface details

    -- It seems to me that this is what should register the
    -- instances. Alas, calling or not calling registerToEps here
    -- doesn't make a difference...
    registerToEps iface details

prepareModule :: (GhcMonad m) => Unit -> String -> m ModSummary
prepareModule unit 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 $ setHomeUnit (toUnitId unit) env
        -- -- If I do this here, inter-unit references aren't resolved correctly.
        -- liftIO $ flushFinderCaches env
        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 unit mod
    return ms

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

prepareSource
    :: (GhcMonad m)
    => ModSummary
    -> m (TypecheckedModule, ModIface, ModDetails)
prepareSource ms = do
    pmod <- parseModule ms
    tmod <- typecheckModule pmod

    env <- getSession
    let (tcg, details) = tm_internals_ tmod
    iface <- liftIO $ mkModIface env tcg details
    return (tmod, iface, details)

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) }

setHomeUnit :: UnitId -> HscEnv -> HscEnv
setHomeUnit unitId env = env
    { hsc_dflags = (hsc_dflags env)
        { homeUnitId = unitId
        }
    }

mkModIface :: HscEnv -> TcGblEnv -> ModDetails -> IO ModIface
mkModIface hsc_env tcg mod_details = do
    let pluginModules = map lpModule (cachedPlugins (hsc_dflags hsc_env))
    deps <- mkDependencies
        unit_id (map mi_module pluginModules) tcg
    putStrLn . showPpr (hsc_dflags hsc_env) $ dep_orphs deps
    putStrLn . showPpr (hsc_dflags hsc_env) $ imp_orphs . tcg_imports $ tcg

    dep_files <- readIORef dependent_files
    let used_names = mkUsedNames tcg
    usages <- mkUsageInfo hsc_env this_mod (imp_mods imports) used_names
              dep_files merged pluginModules
    -- print usages

    let partial = mkIface_ hsc_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
    return $ fill partial
  where
    -- Of course, we should be using mkIfaceTc here directly. However,
    -- that leads to GHC trying to compute fingerprints on its own,
    -- which involves loading the .hi files that we don't generate:
    --
    -- Bad interface file: input/Instance.hi
    -- input/Instance.hi: openBinaryFile: does not exist (No such file or directory)
    --
    -- So instead, we patch GHC to make mkIface_ public and use it directly,
    -- and use dummy fingerprints for now.

    empty = emptyFullModIface this_mod

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

    -- Copypasta from GHC.Iface.Make.mkIfaceTc
    merged = tcg_merged tcg
    dependent_files = tcg_dependent_files tcg
    unit_id = homeUnitId (hsc_dflags hsc_env)
    -- unit_id = toUnitId (moduleUnit this_mod)

    this_mod = tcg_mod tcg
    hsc_src = HsSrcFile
    used_th = False
    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
    doc_hdr' = Nothing
    doc_map = emptyDeclDocMap
    arg_map = emptyArgDocMap

fakeUnitInfo :: PackageId -> PackageName -> UnitId -> UnitInfo
fakeUnitInfo pkgId pkgName unit = GenericUnitInfo
    { unitId = unit
    , unitInstanceOf = Indefinite unit Nothing
    , unitInstantiations = []
    , unitPackageId = pkgId
    , unitPackageName = pkgName
    , unitPackageVersion = makeVersion [0, 0]
    , unitComponentName = Nothing
    , unitAbiHash = ""
    -- If you need these two, then you need to
    -- get the info from the UnitState
    , unitDepends = []
    , unitAbiDepends = []
    , unitImportDirs = []
    , unitLibraries = [] -- XXX
    , unitExtDepLibsSys = []
    , unitExtDepLibsGhc = []
    , unitLibraryDirs   = []
    , unitLibraryDynDirs = []
    , unitExtDepFrameworks = []
    , unitExtDepFrameworkDirs = []
    , unitLinkerOptions = []
    , unitCcOptions = []
    , unitIncludes = []
    , unitIncludeDirs = []
    , unitHaddockInterfaces = []
    , unitHaddockHTMLs = []
    , unitExposedModules = [] -- XXX
    , unitHiddenModules = []
    , unitIsIndefinite  = False
    , unitIsExposed  = True
    , unitIsTrusted  = True
    }

modifyUnitState :: (UnitState -> UnitState) -> HscEnv -> HscEnv
modifyUnitState f env = env
    { hsc_dflags = let dflags = hsc_dflags env in dflags
        { unitState = f (unitState dflags)
        }
    }

addUnit :: UnitInfo -> HscEnv -> HscEnv
addUnit unitInfo@GenericUnitInfo{..} = modifyUnitState $ \us -> us
    { packageNameMap = M.insert unitPackageName (Indefinite unitId Nothing) $ packageNameMap us
    , unitInfoMap = M.insert unitId unitInfo $ unitInfoMap us
    }

registerModule :: (GhcMonad m) => ModIface -> ModDetails -> m ()
registerModule iface details = modifySession $ extendHpt . addModule
  where
    mod_info = HomeModInfo iface details Nothing

    mod = mi_module iface
    modOrig = ModOrigin (Just True) [] [] True

    addModule = modifyUnitState $ \us -> us
        { moduleNameProvidersMap = M.insert (moduleName mod) (M.singleton mod modOrig) $ moduleNameProvidersMap us
        }

    extendHpt env
        | isHomeUnit = env{ hsc_HPT = hpt, hsc_type_env_var = Nothing }
        | otherwise = env
      where
        hpt = addToHpt (hsc_HPT env) (moduleName mod) mod_info
        isHomeUnit = toUnitId (moduleUnit mod) == homeUnitId (hsc_dflags env)

registerToEps :: (GhcMonad m) => ModIface -> ModDetails -> m ()
registerToEps iface details@ModDetails{..} = do
    env <- getSession
    liftIO $ runIOEnv (Env env '\0' () ()) $ updateEps_ extendEps
  where
    mod = mi_module iface

    extendEps :: ExternalPackageState -> ExternalPackageState
    extendEps eps = eps
        { eps_PIT = extendModuleEnv (eps_PIT eps) mod iface
        , eps_PTE = plusTypeEnv (eps_PTE eps) md_types
        , eps_rule_base = extendRuleBaseList (eps_rule_base eps) md_rules
        , eps_complete_matches = extendCompleteMatchMap (eps_complete_matches eps) md_complete_sigs
        , eps_inst_env = extendInstEnvList (eps_inst_env eps) md_insts
        , eps_fam_inst_env = extendFamInstEnvList (eps_fam_inst_env eps) md_fam_insts
        , eps_ann_env = extendAnnEnvList (eps_ann_env eps) md_anns
        , eps_mod_fam_inst_env =
                let fam_inst_env = extendFamInstEnvList emptyFamInstEnv md_fam_insts
                in extendModuleEnv (eps_mod_fam_inst_env eps) mod fam_inst_env
        , eps_stats = addEpsInStats (eps_stats eps) (length $ typeEnvElts md_types) (length md_insts) (length md_rules)
        }
