import cliutils, docopt, os, ospaths, osproc, sequtils, strutils, terminal

type
  OutputBuffer = ref object
    outBuf*, errBuf*: seq[string]

let env = loadEnv()
var outputHandler: HandleProcMsgCB = nil
var verbose = false
var successRepos: seq[string] = @[]
var failRepos: seq[tuple[name, err: string]] = @[]

proc makeBufferLogger(b: OutputBuffer): HandleProcMsgCB =
  result = proc(outMsg, errMsg: TaintedString, cmd: string): void {.closure.} =
    let prefix = if cmd != nil: cmd & ": " else: ""
    if outMsg != nil: b.outBuf.add(prefix & outMsg)
    if errMsg != nil: b.errBuf.add(prefix & errMsg)

proc clear(buf: OutputBuffer): void =
  buf.outBuf = @[]
  buf.errBuf = @[]

proc `$`(buf: OutputBuffer): string =
  buf.outBuf.join("\n") & "\n" & buf.errBuf.join("\n")

proc git(repoDir: string, args: openArray[string]): bool =
  if verbose:
    stdout.setForegroundColor(fgBlue, false)
    stdout.writeLine("> git " & toSeq(args.items).mapIt("'" & it & "'").join(" "))
    stdout.resetAttributes()

  return exec("git", repoDir, args, env, {poUsePath}, outputHandler) == 0

proc isGitRepo(dir: string): bool =
  if not existsDir(dir): return false
  return git(dir, ["rev-parse", "--git-dir"])

proc findGitRepos(rootDir: string): seq[string] =
  return toSeq(rootDir.walkDir())
    .filterIt(it.kind == pcDir and isGitRepo(it.path))
    .mapIt(it.path.expandFilename)

proc writeColoredOutput(b: OutputBuffer): void =
  stdout.writeLine(b.outBuf.join("\n"))
  stdout.setForegroundColor(fgRed, false)
  stdout.writeLine(b.errBuf.join("\n"))
  stdout.resetAttributes()

proc writeErrMsg(msg: string): void =
  stdout.setForegroundColor(fgRed, false)
  stdout.writeLine(msg)
  stdout.resetAttributes()

when isMainModule:
  try:
    let doc = """
Usage:
  git_pull_all [options] [<root>...]

Options:

  -h --help             Print this usage information and exit.
  -v --verbose          Enable verbose logging.
  -V --version          Print version
  -r --remote <remote>  Pull from <remote> (defaults to "origin")
  -b --branch <branch>  Pull the <branch> branch (defaults to "master")
  -o --log-out <outlog> Log command output to <outfile>
  -e --log-err <errlog> Log error output to <errfile>
"""

    let args = docopt(doc, version = "git_pull_all 0.2.1")

    let rootDirs: seq[string] =
      if args["<root>"]: args["<root>"].mapIt(it)
      else: @["."]

    let remote =
      if args["--remote"]: $args["<remote>"]
      else: "origin"

    let branch =
      if args["--branch"]: $args["<branch>"]
      else: "master"

    # Get a list of repos -> absolute directory paths.
    #let repos = rootDirs.map(proc (dir: string): openArray[string] =
    let repos = rootDirs.mapIt(findGitRepos(it)).concat()

    let cmdOutput = OutputBuffer(outBuf: @[], errBuf: @[])
    outputHandler = makeBufferLogger(cmdOutput)

    if args["--verbose"]:
      verbose = true
      outputHandler = combineProcMsgHandlers(outputHandler,
        makeProcMsgHandler(stdout, stderr))

    var outLog, errLog: File = nil
    if args["--log-out"]: outLog = open($args["<outlog>"], fmRead)
    if args["--log-err"]: errLog = open($args["<errlog>"], fmRead)

    if outLog != nil or errLog != nil:
      outputHandler = combineProcMsgHandlers(outputHandler,
        makeProcMsgHandler(outLog, errLog))

    # Foreach repo:
    for repoDir in repos:
      let repoName = repoDir.extractFilename

      if verbose: stdout.writeLine("")
      stdout.setForegroundColor(fgCyan, true)
      stdout.write("Pulling ")
      stdout.setForegroundColor(fgYellow, false)
      stdout.write(repoName & "... ")
      stdout.resetAttributes()
      if verbose: stdout.writeLine("")

      let failRepo = proc(reason: string): void =
        failRepos.add((repoName, reason))
        writeErrMsg(reason)

      var pullOutput: seq[string]

      # Is the a bare repo clean?
      cmdOutput.clear()
      if not git(repoDir, ["status"]):

        # pull --ff-only
        if not git(repoDir, ["pull", "--ff-only", remote, branch]):
          failRepo("unable to ffwd branch")
          continue

        pullOutput = cmdOutput.outBuf

      # Not bare
      else:
        # Not clean? Try to stash the changes.
        var stashed = false
        if cmdOutput.outBuf.join("\n").find("working tree clean") < 0 and
           cmdOutput.outBuf.join("\n").find("working directory clean") < 0:
          cmdOutput.clear()
          if not git(repoDir, ["stash", "save"]):
            failRepo("error trying to stash uncommitted changes")
            continue
          else: stashed = true

        # Are we on the correct branch?
        cmdOutput.clear()
        if not git(repoDir, ["branch"]):
          failRepo("could not get current branch")
          continue

        let branches = cmdOutput.outBuf.filterIt(it.find("* ") > 0)
        let currentBranch =
          if branches.len == 1: branches[0][7..^1]
          else: nil

        # not on correct branch, switch branch
        if currentBranch != branch:
          if not git(repoDir, ["checkout", branch]):
            failRepo("could not check out " & branch & " branch")
            continue

        # pull --ff-only
        if not git(repoDir, ["pull", "--ff-only", remote, branch]):
          failRepo("unable to ffwd branch")
          continue

        pullOutput = cmdOutput.outBuf

        # restore original branch
        if currentBranch != branch:
          if not git(repoDir, ["checkout", currentBranch]):
            stdout.setForegroundColor(fgWhite, true)
            stdout.writeLine("WARNING: unable to checkout original branch (" & currentBranch & ")")
            stdout.resetAttributes()

        # restore stashed changes
        if stashed:
          if not git(repoDir, ["stash", "pop"]):
            stdout.setForegroundColor(fgWhite, true)
            stdout.writeLine("WARNING: unable to pop stashed changes")
            stdout.resetAttributes()

      if pullOutput.anyIt(it.find("Already up-to-date") > 0):
        stdout.setForegroundColor(fgBlack, true)
        stdout.writeLine("already up-to-date")
      else:
        stdout.setForegroundColor(fgGreen, true)
        stdout.writeLine("UPDATED")
      stdout.resetAttributes()
      successRepos.add(repoName)

  except:
    stderr.writeLine "git_pull_all: " & getCurrentExceptionMsg()