/-
Copyright (c) 2022 Mac Malone. All rights reserved.
Released under Apache 2.0 license as described in the file LICENSE.
Authors: Mac Malone, Gabriel Ebner
-/
import Lake.Util.EStateT
import Lake.Util.StoreInsts
import Lake.Config.Workspace
import Lake.Build.Topological
import Lake.Build.Module
import Lake.Build.Package
import Lake.Build.Library
import Lake.Load.Materialize
import Lake.Load.Package
import Lake.Load.Elab

open System Lean

namespace Lake

/-- Load the tagged `Dependency` definitions from a package configuration environment. -/
def loadDepsFromEnv (env : Environment) (opts : Options) : Except String (Array Dependency) := do
  (packageDepAttr.ext.getState env).mapM (evalConstCheck env opts Dependency ``Dependency)

def loadDepPackage (parentPkg : Package) (result : MaterializeResult)
(dep : Dependency) : LogIO Package := do
  let dir := result.pkgDir
  let configEnv ← elabConfigFile dir dep.options parentPkg.leanOpts (dir / defaultConfigFile)
  let config ← IO.ofExcept <| PackageConfig.loadFromEnv configEnv parentPkg.leanOpts
  return {
    dir, config, configEnv
    remoteUrl? := result.remoteUrl?
    gitTag? := result.gitTag?
    leanOpts := parentPkg.leanOpts
  }

def buildUpdatedManifest (ws : Workspace) : LogIO Manifest := do
  let res ← StateT.run (s := mkNameMap MaterializeResult) do
    EStateT.run' (mkNameMap Package) do
      buildAcyclic (·.name) ws.root fun pkg resolve => do
        let topLevel := pkg.name = ws.root.name
        let relPkgDir :=
          if let some {relPkgDir, ..} := ((← getThe (NameMap MaterializeResult)).find? pkg.name) then
            relPkgDir
          else
            "." -- topLevel
        unless topLevel do
          for entry in (← Manifest.loadOrEmpty pkg.manifestFile) do
            unless (← getThe (NameMap MaterializeResult)).contains entry.name do
              let entry := entry.inDirectory relPkgDir
              let result ← materializePackageEntry ws.dir ws.packagesDir entry
              modifyThe (NameMap MaterializeResult) (·.insert entry.name result)
        let deps ← IO.ofExcept <| loadDepsFromEnv pkg.configEnv pkg.leanOpts
        let deps ← deps.mapM fun dep => do
          if let some result := (← getThe (NameMap MaterializeResult)).find? dep.name then
            return (dep, result)
          else
            let depName := dep.name.toString (escape := false)
            let entry ← updateSource relPkgDir ws.packagesDir depName dep.src
            let result ← materializePackageEntry ws.dir ws.packagesDir entry
            modifyThe (NameMap MaterializeResult) (·.insert entry.name result)
            return (dep, result)
        let depPkgs ← deps.mapM fun (dep, result) => do
          if let .some pkg := (← getThe (NameMap Package)).find? dep.name then
            return pkg
          else
            let pkg ← loadDepPackage pkg result dep
            modifyThe (NameMap Package) (·.insert dep.name pkg)
            return pkg
        return {pkg with opaqueDeps := ← depPkgs.mapM (.mk <$> resolve ·)}
  match res with
  | (.ok _, results) =>
    let mut manifest : Manifest := {packagesDir? := ws.packagesDir}
    for (_, result) in results do
      manifest := manifest.insert result.manifestEntry
    return manifest
  | (.error cycle, _) =>
    let cycle := cycle.map (s!"  {·}")
    error s!"dependency cycle detected:\n{"\n".intercalate cycle}"

/--
Load a `Workspace` for a Lake package by elaborating its configuration file.
Does not resolve dependencies.
-/
def loadWorkspaceRoot (config : LoadConfig) : LogIO Workspace := do
  Lean.searchPathRef.set config.env.leanSearchPath
  let configEnv ← elabConfigFile config.rootDir config.configOpts config.leanOpts config.configFile
  let pkgConfig ← IO.ofExcept <| PackageConfig.loadFromEnv configEnv config.leanOpts
  let repo := GitRepo.mk config.rootDir
  let root := {
    configEnv, leanOpts := config.leanOpts
    dir := config.rootDir, config := pkgConfig
    remoteUrl? := ← repo.getFilteredRemoteUrl?
    gitTag? := ← repo.findTag?
  }
  return {
    root, lakeEnv := config.env
    moduleFacetConfigs := initModuleFacetConfigs
    packageFacetConfigs := initPackageFacetConfigs
    libraryFacetConfigs := initLibraryFacetConfigs
  }

/--
Finalize the workspace's root and its transitive dependencies
and add them to the workspace.
-/
def Workspace.finalize (ws : Workspace) : LogIO Workspace := do
  have : MonadStore Name Package (StateT Workspace LogIO) := {
    fetch? := fun name => return (← get).findPackage? name
    store := fun _ pkg => modify (·.addPackage pkg)
  }
  let (res, ws) ← EStateT.run ws do
    buildTop (·.name) ws.root fun pkg load => do
      let depPkgs ← pkg.deps.mapM load
      set <| ← IO.ofExcept <| (← get).addFacetsFromEnv pkg.configEnv pkg.leanOpts
      let pkg ← pkg.finalize depPkgs
      return pkg
  match res with
  | Except.ok root =>
    return {ws with root}
  | Except.error cycle => do
    let cycle := cycle.map (s!"  {·}")
    error <|
      s!"oops! dependency load cycle detected (this likely indicates a bug in Lake):\n" ++
      "\n".intercalate cycle

/--
Resolving a workspace's dependencies using a manifest,
downloading and/or updating them as necessary.
-/
def Workspace.materializeDeps (ws : Workspace) (manifest : Manifest) : LogIO Workspace := do
  if !manifest.isEmpty && manifest.packagesDir? != some ws.packagesDir then
    logWarning <|
      "manifest out of date: package directory changed, " ++
      "use `lake update` to update"
  let packagesDir := manifest.packagesDir?.getD ws.packagesDir
  let res ← EStateT.run' (mkNameMap Package) do
    buildAcyclic (·.name) ws.root fun pkg resolve => do
      let topLevel := pkg.name = ws.root.name
      let deps ← IO.ofExcept <| loadDepsFromEnv pkg.configEnv pkg.leanOpts
      if topLevel then
        for dep in deps do
          let warnOutOfDate (what : String) :=
            logWarning <|
              s!"manifest out of date: {what} of dependency {dep.name} changed, " ++
              "use `lake update` to update"
          if let .some entry := manifest.find? dep.name then
          match dep.src, entry with
          | .git url rev _, .git _ url' _ rev' _ =>
            if url ≠ url' then warnOutOfDate "git url"
            if rev ≠ rev' then warnOutOfDate "git revision"
          | .path .., .path .. => pure ()
          | _, _ => warnOutOfDate "source kind (git/path)"
      let depPkgs ← deps.mapM fun dep => do
        fetchOrCreate dep.name do
          let .some entry := manifest.find? dep.name
            | error <| s!"dependency {dep.name} of {pkg.name} not in manifest, " ++
              "use `lake update` to update"
          let result ← materializePackageEntry ws.dir packagesDir entry
          loadDepPackage pkg result dep
      return { pkg with opaqueDeps := ← depPkgs.mapM (.mk <$> resolve ·) }
  match res with
  | Except.ok root =>
    ({ws with root}).finalize
  | Except.error cycle =>
    let cycle := cycle.map (s!"  {·}")
    error s!"dependency cycle detected:\n{"\n".intercalate cycle}"

/--
Load a `Workspace` for a Lake package by
elaborating its configuration file and resolving its dependencies.
If `updateDeps` is true, updates the manifest before resolving dependencies.
-/
def loadWorkspace (config : LoadConfig) (updateDeps := false) : LogIO Workspace := do
  let ws ← loadWorkspaceRoot config
  let manifest ← do
    if updateDeps then
      let manifest ← buildUpdatedManifest ws
      manifest.saveToFile ws.manifestFile
      pure manifest
    else
      Manifest.loadOrEmpty ws.manifestFile
  ws.materializeDeps manifest

/-- Updates the manifest for a Lake package. -/
def updateManifest (config : LoadConfig) : LogIO Unit := do
  let ws ← loadWorkspaceRoot config
  let manifest ← buildUpdatedManifest ws
  manifest.saveToFile ws.manifestFile
