/-
Copyright (c) 2017 Microsoft Corporation. All rights reserved.
Released under Apache 2.0 license as described in the file LICENSE.
Authors: Gabriel Ebner, Sebastian Ullrich, Mac Malone
-/
import Lean.Data.Name
import Lean.Elab.Import
import Lake.Target
import Lake.BuildModule
import Lake.Resolve
import Lake.Package

open System
open Lean hiding SearchPath

namespace Lake

-- # Build Target

abbrev PackageTarget := BuildTarget Package

abbrev ActivePackageTarget i := ActiveBuildTarget (Package × NameMap (ActiveBuildTarget i))

namespace ActivePackageTarget

def package (self : ActivePackageTarget i) :=
  self.info.1

def withOnlyPackageInfo (self : ActivePackageTarget i) : ActiveBuildTarget Package :=
  self.withInfo self.package

def moduleTargetMap (self : ActivePackageTarget i) : NameMap (ActiveBuildTarget i) :=
  self.info.2

def moduleTargets (self : ActivePackageTarget i) : Array (Name × ActiveBuildTarget i) :=
  self.moduleTargetMap.fold (fun arr k v => arr.push (k, v)) #[]

end ActivePackageTarget

/-- Returns the `oleanDir`s of the given package targets in reverse order. -/
def packageTargetsToOleanDirs (targets : Array (ActivePackageTarget i)) : List FilePath :=
  targets.map (·.package.oleanDir) |>.foldl (flip List.cons) []

-- # Build Packages

/--
  Builds the package's `extraDepTarget` and combines it with
  the given package targets to create a single active opaque target.
-/
def Package.buildDepTargetWith
(depTargets : Array (ActiveBuildTarget i)) (self : Package) : BuildM ActiveOpaqueTarget := do
  let extraDepTarget ← self.extraDepTarget.run
  let depTarget ← ActiveTarget.collectOpaqueArray depTargets
  extraDepTarget.mixOpaqueAsync depTarget

/--
  Build an active package target after
  resolving the package's dependencies and recursively building them.

  To build each package, run the given `build` function on it,
  which takes the package and its building dependencies as arguments.
-/
def recBuildPackageWithDeps
[Monad m] [MonadLiftT BuildM m] [MonadStore Name (Array (ActivePackageTarget i)) m]
[Inhabited i] (build : Array (ActivePackageTarget i) → Package → BuildM (ActivePackageTarget i))
: RecBuild Package (Array (ActivePackageTarget i)) m := fun pkg buildPkg => do
  let mut depTargets := #[]
  for dep in pkg.dependencies do
    let targets? ← fetch? dep.name
    let targets ← do
      if let some targets := targets? then
        pure targets
      else
        let depPkg ← liftM (m := BuildM) <| resolveDep pkg dep
        buildPkg depPkg
    depTargets := depTargets ++ targets
  let pkgTarget ← build depTargets pkg
  depTargets.push pkgTarget

/--
  Build the  package along with its dependencies
  by using the given `build` function recursively.
-/
def Package.buildRec (self : Package) [Inhabited i]
(build : Array (ActivePackageTarget i) → Package → BuildM (ActivePackageTarget i))
: BuildM (ActivePackageTarget i) := do
  let recBuild := recBuildPackageWithDeps build
  let targets ← failOnBuildCycle <| ← RBTopT.run' <|
    buildRBTop (cmp := Name.quickCmp) recBuild Package.name self
  let target ← ActiveTarget.collectOpaqueArray targets
  target.withInfo targets.back.info

/--
  Build an `Array` of `Package`s along with their dependencies
  by using the given `build` function recursively.
-/
def buildPackageArrayWithDeps (pkgs : Array Package) [Inhabited i]
(build : Array (ActivePackageTarget i) → Package → BuildM (ActivePackageTarget i))
: BuildM (Array (ActivePackageTarget i)) := do
  failOnBuildCycle <| ← RBTopT.run' <| pkgs.concatMapM fun pkg =>
    buildRBTop (cmp := Name.quickCmp) (recBuildPackageWithDeps build) Package.name pkg

/-- Build the dependencies of a package (as targets) using the given `build` function. -/
def Package.buildDepTargets [Inhabited i]
(build :  Array (ActivePackageTarget i) → Package → BuildM (ActivePackageTarget i)) (self : Package)
: BuildM (Array (ActivePackageTarget i)) := do
  buildPackageArrayWithDeps (← self.resolveDirectDeps) build

/--
  Build an active target for the package by
  running the given `RecModuleTargetBuild` on the given root module.
-/
def Package.buildModuleDAGTarget [Inhabited i] (mod : Name)
(buildMod : RecModuleTargetBuild i) (self : Package) : BuildM (ActivePackageTarget i) := do
  let (targetE, targetMap) ← RBTopT.run do
    buildRBTop buildMod id mod
  let target ← failOnBuildCycle targetE
  return target.withInfo ⟨self, targetMap⟩

/--
  Build an active target for the package by
  running the given `RecModuleTargetBuild` on the given root modules.
-/
def Package.buildModulesDAGTarget [Inhabited i] (mods : Array Name)
(buildMod : RecModuleTargetBuild i) (self : Package) : BuildM (ActivePackageTarget i) := do
  let (targetsE, targetMap) ← RBTopT.run do
    mods.mapM fun mod => buildRBTop buildMod id mod
  let targets ← failOnBuildCycle targetsE
  let target ← ActiveTarget.collectOpaqueArray targets
  return target.withInfo ⟨self, targetMap⟩

/--
  Build an active target for the package by
  running the given `RecModuleTargetBuild` on each of its top-level modules
  (i.e., those specified in its `libGlobs` configuration).
-/
def Package.buildTarget [Inhabited i]
(buildMod : RecModuleTargetBuild i) (self : Package) : BuildM (ActivePackageTarget i) := do
  self.buildModulesDAGTarget (← self.getModuleArray) buildMod

-- # Build Package Modules

def Package.buildOleanTargetWithDepTargets
(depTargets : Array (ActivePackageTarget x)) (self : Package)
: BuildM (ActivePackageTarget FilePath) := do
  let depTarget ← self.buildDepTargetWith depTargets
  let moreOleanDirs := packageTargetsToOleanDirs depTargets
  self.buildTarget <| self.recBuildModuleOleanTargetWithLocalImports moreOleanDirs depTarget

/-- Build the `.olean` and files of package and its dependencies' modules. -/
def Package.buildOleanTarget (self : Package) : BuildM (ActivePackageTarget FilePath) := do
  self.buildRec buildOleanTargetWithDepTargets

def Package.buildOleanAndCTargetWithDepTargets
(depTargets : Array (ActivePackageTarget x)) (self : Package)
: BuildM (ActivePackageTarget ActiveOleanAndCTargets) := do
  let depTarget ← self.buildDepTargetWith depTargets
  let moreOleanDirs := packageTargetsToOleanDirs depTargets
  self.buildTarget <| self.recBuildModuleOleanAndCTargetWithLocalImports moreOleanDirs depTarget

/-- Build the `.olean` and `.c` files of package and its dependencies' modules. -/
def Package.buildOleanAndCTarget (self : Package) : BuildM (ActivePackageTarget ActiveOleanAndCTargets) := do
  self.buildRec buildOleanAndCTargetWithDepTargets

def Package.buildDepOleans (self : Package) : BuildM PUnit := do
  let targets ← self.buildDepTargets buildOleanTargetWithDepTargets
  targets.forM fun target => discard <| target.materialize

def Package.oleanTarget (self : Package) : PackageTarget :=
  Target.mk self do pure (← self.buildOleanTarget).task

def Package.build (self : Package) : BuildM PUnit := do
  discard (← self.buildOleanTarget).materialize

-- # Build Specific Modules of the Package

def Package.moduleOleanTarget (mod : Name) (self : Package) : FileTarget :=
  Target.mk (self.modToOlean mod) do
    let depTargets ← self.buildDepTargets buildOleanTargetWithDepTargets
    let depTarget ← self.buildDepTargetWith depTargets
    let moreOleanDirs := packageTargetsToOleanDirs depTargets
    let build := self.recBuildModuleOleanTargetWithLocalImports moreOleanDirs depTarget
    return (← buildModule mod build).task

def Package.moduleOleanAndCTarget (mod : Name) (self : Package) : OleanAndCTarget :=
  Target.mk ⟨self.modToOlean mod, self.modToC mod⟩ do
    let depTargets ← self.buildDepTargets buildOleanAndCTargetWithDepTargets
    let depTarget ← self.buildDepTargetWith depTargets
    let moreOleanDirs := packageTargetsToOleanDirs depTargets
    let build := self.recBuildModuleOleanAndCTargetWithLocalImports moreOleanDirs depTarget
    return (← buildModule mod build).task

-- # Build Imports

/-- Pick the local imports of the package from a list of import strings. -/
def Package.filterLocalImports
(imports : List String) (self : Package) : Array Name := do
  let mut localImports := #[]
  for imp in imports do
    let impName := imp.toName
    if self.isLocalModule impName then
      localImports := localImports.push impName
  return localImports

/--
  Build the package's dependencies and a list of imports,
  returning the list of packages built.

  Builds only module `.olean` files if the default package facet is
  just `oleans`. Otherwise, builds both `.olean` and `.c` files.
-/
def Package.buildImportsAndDeps
(imports : List String := []) (self : Package) : BuildM (List Package) := do
  -- resolve and build deps
  let depTargets ←
    if self.defaultFacet == PackageFacet.oleans then
      let depTargets ← self.buildDepTargets buildOleanTargetWithDepTargets
      depTargets.map (·.withOnlyPackageInfo)
    else
      let depTargets ← self.buildDepTargets buildOleanAndCTargetWithDepTargets
      depTargets.map (·.withOnlyPackageInfo)
  let depTarget ← self.buildDepTargetWith depTargets
  let depPkgs := depTargets.map (·.info) |>.foldl (flip List.cons) []
  if imports.isEmpty then
    -- wait for deps to finish building
    discard depTarget.materialize
  else
     -- build local imports from list
    let moreOleanDirs := depPkgs.map (·.oleanDir)
    let localImports := self.filterLocalImports imports
    if self.defaultFacet == PackageFacet.oleans then
      let build := self.recBuildModuleOleanTargetWithLocalImports moreOleanDirs depTarget
      let targets ← buildModules localImports build
      targets.forM (discard ·.materialize)
    else
      let build := self.recBuildModuleOleanAndCTargetWithLocalImports moreOleanDirs depTarget
      let targets ← buildModules localImports build
      targets.forM (discard ·.materialize)
  return self :: depPkgs
