/-
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
-/
import Leanpkg.Manifest
import Leanpkg.Proc
import Leanpkg.Git

namespace Leanpkg

def Assignment := List (String × String)

namespace Assignment
def empty : Assignment := []

def contains (a : Assignment) (s : String) : Bool :=
  (a.lookup s).isSome

def insert (a : Assignment) (k v : String) : Assignment :=
  if a.contains k then a else (k, v) :: a

def fold {α} (i : α) (f : α → String → String → α) : Assignment → α :=
  List.foldl (fun a ⟨k, v⟩ => f a k v) i

end Assignment

abbrev Solver := StateT Assignment IO

def notYetAssigned (d : String) : Solver Bool := do
  ¬ (← get).contains d

def resolvedPath (d : String) : Solver String := do
  let some path ← pure ((← get).lookup d) | unreachable!
  path

-- TODO(gabriel): windows?
def resolveDir (absOrRel : String) (base : String) : String :=
  if absOrRel.front = '/' then
    absOrRel -- absolute
  else
    base ++ "/" ++ absOrRel

def materialize (relpath : String) (dep : Dependency) : Solver Unit :=
  match dep.src with
  | Source.path dir => do
    let depdir := resolveDir dir relpath
    IO.eprintln s!"{dep.name}: using local path {depdir}"
    modify (·.insert dep.name depdir)
  | Source.git url rev branch => do
    let depdir := "build/deps/" ++ dep.name
    let alreadyThere ← IO.isDir depdir
    if alreadyThere then
      IO.eprint s!"{dep.name}: trying to update {depdir} to revision {rev}"
      IO.eprintln (match branch with | none => "" | some branch => "@" ++ branch)
      let hash ← gitParseOriginRevision depdir rev
      let revEx ← gitRevisionExists depdir hash
      unless revEx do
        execCmd {cmd := "git", args := #["fetch"], cwd := depdir}
    else
      IO.eprintln s!"{dep.name}: cloning {url} to {depdir}"
      execCmd {cmd := "git", args := #["clone", url, depdir]}
    let hash ← gitParseOriginRevision depdir rev
    execCmd {cmd := "git", args := #["checkout", "--detach", hash], cwd := depdir}
    modify (·.insert dep.name depdir)

def solveDepsCore (relPath : String) (d : Manifest) : (maxDepth : Nat) → Solver Unit
  | 0 => throw <| IO.userError "maximum dependency resolution depth reached"
  | maxDepth + 1 => do
    let deps ← d.dependencies.filterM (notYetAssigned ·.name)
    deps.forM (materialize relPath)
    for dep in deps do
      let p ← resolvedPath dep.name
      let d' ← Manifest.fromFile $ p ++ "/" ++ "leanpkg.toml"
      unless d'.name = dep.name do
        throw <| IO.userError <| d.name ++ " (in " ++ relPath ++ ") depends on " ++ d'.name ++
          ", but resolved dependency has name " ++ dep.name ++ " (in " ++ p ++ ")"
      solveDepsCore p d' maxDepth

def solveDeps (d : Manifest) : IO Assignment := do
  let (_, assg) ← (solveDepsCore "." d 1024).run <| Assignment.empty.insert d.name "."
  assg

def constructPathCore (depname : String) (dirname : String) : IO String := do
  let path ← Manifest.effectivePath (← Manifest.fromFile $ dirname ++ "/" ++ leanpkgTomlFn)
  return dirname ++ "/" ++ path

def constructPath (assg : Assignment) : IO (List String) := do
  assg.reverse.mapM fun ⟨depname, dirname⟩ => constructPathCore depname dirname

end Leanpkg
