import Lake.CLI.Main

/-!
A simple script that inserts `module` and `@[expose] public section` into un-modulized files and
bumps their imports to `public`.
-/

open Lean Parser.Module

def main (args : List String) : IO Unit := do
  initSearchPath (← findSysroot)
  let mods := args.toArray.map (·.toName)

  -- Determine default module(s) to run modulize on
  let defaultTargetModules : Array Name ← try
    let (elanInstall?, leanInstall?, lakeInstall?) ← Lake.findInstall?
    let config ← Lake.MonadError.runEIO <| Lake.mkLoadConfig { elanInstall?, leanInstall?, lakeInstall? }
    let some workspace ← Lake.loadWorkspace config |>.toBaseIO
      | throw <| IO.userError "failed to load Lake workspace"
    let defaultTargetModules := workspace.root.defaultTargets.flatMap fun target =>
      if let some lib := workspace.root.findLeanLib? target then
        lib.roots
      else if let some exe := workspace.root.findLeanExe? target then
        #[exe.config.root]
      else
        #[]
    pure defaultTargetModules
  catch _ =>
    pure #[]

  -- the list of root modules
  let mods := if mods.isEmpty then defaultTargetModules else mods
  -- Only submodules of `pkg` will be edited or have info reported on them
  let pkg := mods[0]!.components.head!

  -- Load all the modules
  let imps := mods.map ({ module := · })
  let env ← importModules imps {}

  let srcSearchPath ← getSrcSearchPath

  for mod in env.header.moduleNames do
    if !pkg.isPrefixOf mod then
      continue

    -- Parse the input file
    let some path ← srcSearchPath.findModuleWithExt "lean" mod
      | throw <| .userError "error: failed to find source file for {mod}"
    let mut text ← IO.FS.readFile path
    let inputCtx := Parser.mkInputContext text path.toString
    let (header, parserState, msgs) ← Parser.parseHeader inputCtx
    if !msgs.toList.isEmpty then -- skip this file if there are parse errors
      msgs.forM fun msg => msg.toString >>= IO.println
      throw <| .userError "parse errors in file"
    let `(header| $[module%$moduleTk?]? $imps:import*) := header
      | throw <| .userError s!"unexpected header syntax of {path}"
    if moduleTk?.isSome then
      continue

    let looksMeta := mod.components.any (· ∈ [`Tactic, `Linter])

    -- initial whitespace if empty header
    let startPos := header.raw.getPos? |>.getD parserState.pos

    -- insert section if any trailing text
    if header.raw.getTrailingTailPos?.all (· < text.endPos) then
      let insertPos := header.raw.getTailPos? |>.getD startPos  -- empty header
      let mut sec := if looksMeta then
        "public meta section"
      else
        "@[expose] public section"
      if !imps.isEmpty then
        sec := "\n\n" ++ sec
      if header.raw.getTailPos?.isNone then
        sec := sec ++ "\n\n"
      text := text.extract 0 insertPos ++ sec ++ text.extract insertPos text.endPos

    -- prepend each import with `public `
    for imp in imps.reverse do
      let insertPos := imp.raw.getPos?.get!
      let prfx := if looksMeta then "public meta " else "public "
      text := text.extract 0 insertPos ++ prfx ++ text.extract insertPos text.endPos

    -- insert `module` header
    let mut initText := text.extract 0 startPos
    if !initText.trim.isEmpty then
      -- If there is a header comment, preserve it and put `module` in the line after
      initText := initText.trimRight ++ "\n"
    text := initText ++ "module\n\n" ++ text.extract startPos text.endPos

    IO.FS.writeFile path text
