import Lean.Environment
import Std.Tactic.OpenPrivate
import Lean.Data.Lsp.Communication

open Lean

inductive LoadingKind := | finalizeExtensions | loadConstants
  deriving ToJson

structure LoadingParams : Type where
  counter : Nat
  kind : LoadingKind
  deriving ToJson

-- Code adapted from `Lean/Environment.lean`

partial def importModulesCore' (imports : Array Import) : ImportStateM Unit := do
  for i in imports do
    if i.runtimeOnly || (← get).moduleNameSet.contains i.module then
      continue
    modify fun s => { s with moduleNameSet := s.moduleNameSet.insert i.module }
    let mFile ← findOLean i.module
    unless (← mFile.pathExists) do
      throw <| IO.userError s!"object file '{mFile}' of module {i.module} does not exist"
    let (mod, region) ← readModuleData mFile
    importModulesCore' mod.imports
    modify fun s => { s with
      moduleData  := s.moduleData.push mod
      regions     := s.regions.push region
      moduleNames := s.moduleNames.push i.module
    }

open private mkInitialExtensionStates Environment.mk setImportedEntries finalizePersistentExtensions
  ensureExtensionsArraySize from Lean.Environment

private partial def finalizePersistentExtensions' (env : Environment) (mods : Array ModuleData) (opts : Options) : IO Environment := do
  loop 0 env
where
  loop (i : Nat) (env : Environment) : IO Environment := do
    (← IO.getStdout).writeLspNotification {
      method := "$/game/loading",
      param := {counter := i, kind := .finalizeExtensions : LoadingParams} }
    -- Recall that the size of the array stored `persistentEnvExtensionRef` may increase when we import user-defined environment extensions.
    let pExtDescrs ← persistentEnvExtensionsRef.get
    if i < pExtDescrs.size then
      let extDescr := pExtDescrs[i]!
      let s := extDescr.toEnvExtension.getState env
      let prevSize := (← persistentEnvExtensionsRef.get).size
      let prevAttrSize ← getNumBuiltinAttributes
      let newState ← extDescr.addImportedFn s.importedEntries { env := env, opts := opts }
      let mut env := extDescr.toEnvExtension.setState env { s with state := newState }
      env ← ensureExtensionsArraySize env
      if (← persistentEnvExtensionsRef.get).size > prevSize || (← getNumBuiltinAttributes) > prevAttrSize then
        -- This branch is executed when `pExtDescrs[i]` is the extension associated with the `init` attribute, and
        -- a user-defined persistent extension is imported.
        -- Thus, we invoke `setImportedEntries` to update the array `importedEntries` with the entries for the new extensions.
        env ← setImportedEntries env mods prevSize
        -- See comment at `updateEnvAttributesRef`
        env ← updateEnvAttributes env
      loop (i + 1) env
    else
      return env

def finalizeImport' (s : ImportState) (imports : Array Import) (opts : Options) (trustLevel : UInt32 := 0) : IO Environment := do
  let numConsts := s.moduleData.foldl (init := 0) fun numConsts mod =>
    numConsts + mod.constants.size + mod.extraConstNames.size
  let mut const2ModIdx : HashMap Name ModuleIdx := mkHashMap (capacity := numConsts)
  let mut constantMap : HashMap Name ConstantInfo := mkHashMap (capacity := numConsts)
  for h:modIdx in [0:s.moduleData.size] do
    if modIdx % 100 = 0 then
      let percentage := modIdx * 100 / s.moduleData.size
      (← IO.getStdout).writeLspNotification {
        method := "$/game/loading",
        param := {counter := percentage, kind := .loadConstants : LoadingParams} }
    let mod := s.moduleData[modIdx]'h.upper
    for cname in mod.constNames, cinfo in mod.constants do
      match constantMap.insert' cname cinfo with
      | (constantMap', replaced) =>
        constantMap := constantMap'
        if replaced then
          throwAlreadyImported s const2ModIdx modIdx cname
      const2ModIdx := const2ModIdx.insert cname modIdx
    for cname in mod.extraConstNames do
      const2ModIdx := const2ModIdx.insert cname modIdx
  let constants : ConstMap := SMap.fromHashMap constantMap false
  let exts ← mkInitialExtensionStates
  let env : Environment := Environment.mk
    (const2ModIdx    := const2ModIdx)
    (constants       := constants)
    (extraConstNames := {})
    (extensions      := exts)
    (header          := {
      quotInit     := !imports.isEmpty -- We assume `core.lean` initializes quotient module
      trustLevel   := trustLevel
      imports      := imports
      regions      := s.regions
      moduleNames  := s.moduleNames
      moduleData   := s.moduleData
    })

  let env ← setImportedEntries env s.moduleData
  finalizePersistentExtensions' env s.moduleData opts

def importModules' (imports : Array Import) : IO Environment := do
  withImporting do
    let (_, s) ← importModulesCore' imports |>.run
    let env ← finalizeImport' s imports {} 0
    return env
