package gitbucket.core.util

import org.apache.commons.io.FileUtils
import org.eclipse.jgit.api.Git
import org.eclipse.jgit.dircache.DirCache
import org.eclipse.jgit.lib._
import org.eclipse.jgit.revwalk._
import org.eclipse.jgit.treewalk._
import org.eclipse.jgit.merge._
import org.eclipse.jgit.errors._

import java.nio.file._
import java.io.File
import scala.util.Using

object GitSpecUtil {

  def withTestFolder[U](f: File => U): U = {
    val folder = new File(System.getProperty("java.io.tmpdir"), "test-" + System.nanoTime)
    if (!folder.mkdirs()) {
      throw new java.io.IOException("can't create folder " + folder.getAbsolutePath)
    }
    try {
      f(folder)
    } finally {
      FileUtils.deleteQuietly(folder)
    }
  }

  def withTestRepository[U](f: Git => U): U =
    withTestFolder(folder => Using.resource(Git.open(createTestRepository(folder)))(f))

  def createTestRepository(dir: File): File = {
    RepositoryCache.clear()
    FileUtils.deleteQuietly(dir)
    Files.createDirectories(dir.toPath())
    JGitUtil.initRepository(dir)
    dir
  }

  def createFile(
    git: Git,
    branch: String,
    name: String,
    content: String,
    authorName: String = "dummy",
    authorEmail: String = "dummy@example.com",
    message: String = "test commit"
  ): ObjectId = {
    val builder = DirCache.newInCore.builder()
    val inserter = git.getRepository.newObjectInserter()
    val headId = git.getRepository.resolve(branch + "^{commit}")
    if (headId != null) {
      JGitUtil.processTree(git, headId) { (path, tree) =>
        if (name != path) {
          builder.add(JGitUtil.createDirCacheEntry(path, tree.getEntryFileMode, tree.getEntryObjectId))
        }
      }
    }
    builder.add(
      JGitUtil.createDirCacheEntry(
        name,
        FileMode.REGULAR_FILE,
        inserter.insert(Constants.OBJ_BLOB, content.getBytes("UTF-8"))
      )
    )
    builder.finish()
    val commitId = JGitUtil.createNewCommit(
      git,
      inserter,
      headId,
      builder.getDirCache.writeTree(inserter),
      branch,
      authorName,
      authorEmail,
      message
    )
    inserter.flush()
    inserter.close()
    commitId
  }

  def getFile(git: Git, branch: String, path: String) = {
    val revCommit = JGitUtil.getRevCommitFromId(git, git.getRepository.resolve(branch))
    val objectId = Using.resource(new TreeWalk(git.getRepository)) { walk =>
      walk.addTree(revCommit.getTree)
      walk.setRecursive(true)
      @scala.annotation.tailrec
      def _getPathObjectId: ObjectId = walk.next match {
        case true if (walk.getPathString == path) => walk.getObjectId(0)
        case true                                 => _getPathObjectId
        case false                                => throw new Exception(s"not found ${branch} / ${path}")
      }
      _getPathObjectId
    }
    JGitUtil.getContentInfo(git, path, objectId)
  }

  def mergeAndCommit(git: Git, into: String, branch: String, message: String = null): Unit = {
    val repository = git.getRepository
    val merger = MergeStrategy.RECURSIVE.newMerger(repository, true)
    val mergeBaseTip = repository.resolve(into)
    val mergeTip = repository.resolve(branch)
    val conflicted = try {
      !merger.merge(mergeBaseTip, mergeTip)
    } catch {
      case e: NoMergeBaseException => true
    }
    if (conflicted) {
      throw new RuntimeException("conflict!")
    }
    val mergeTipCommit = Using.resource(new RevWalk(repository))(_.parseCommit(mergeTip))
    val committer = mergeTipCommit.getCommitterIdent
    // creates merge commit
    val mergeCommit = new CommitBuilder()
    mergeCommit.setTreeId(merger.getResultTreeId)
    mergeCommit.setParentIds(Array[ObjectId](mergeBaseTip, mergeTip): _*)
    mergeCommit.setAuthor(committer)
    mergeCommit.setCommitter(committer)
    mergeCommit.setMessage(message)
    // insertObject and got mergeCommit Object Id
    val inserter = repository.newObjectInserter
    val mergeCommitId = inserter.insert(mergeCommit)
    inserter.flush()
    inserter.close()
    // update refs
    val refUpdate = repository.updateRef(into)
    refUpdate.setNewObjectId(mergeCommitId)
    refUpdate.setForceUpdate(true)
    refUpdate.setRefLogIdent(committer)
    refUpdate.update()
  }
}
