// Copyright (C) 2016 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package com.google.gerrit.server.restapi.project;

import com.google.gerrit.exceptions.InvalidMergeStrategyException;
import com.google.gerrit.extensions.client.SubmitType;
import com.google.gerrit.extensions.common.MergeableInfo;
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
import com.google.gerrit.extensions.restapi.Response;
import com.google.gerrit.extensions.restapi.RestReadView;
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.git.InMemoryInserter;
import com.google.gerrit.server.git.MergeUtil;
import com.google.gerrit.server.project.BranchResource;
import com.google.inject.Inject;
import java.io.IOException;
import org.eclipse.jgit.errors.NoMergeBaseException;
import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.ObjectInserter;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.merge.Merger;
import org.eclipse.jgit.merge.ResolveMerger;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk;
import org.kohsuke.args4j.Option;

/** Check the mergeability at current branch for a git object references expression. */
public class CheckMergeability implements RestReadView<BranchResource> {
  private String source;
  private String strategy;
  private SubmitType submitType;

  @Option(
      name = "--source",
      metaVar = "COMMIT",
      usage =
          "the source reference to merge, which could be any git object "
              + "references expression, refer to "
              + "org.eclipse.jgit.lib.Repository#resolve(String)",
      required = true)
  public void setSource(String source) {
    this.source = source;
  }

  @Option(
      name = "--strategy",
      metaVar = "STRATEGY",
      usage = "name of the merge strategy, refer to org.eclipse.jgit.merge.MergeStrategy")
  public void setStrategy(String strategy) {
    this.strategy = strategy;
  }

  private final GitRepositoryManager gitManager;
  private final CommitsCollection commits;

  @Inject
  CheckMergeability(
      GitRepositoryManager gitManager, CommitsCollection commits, @GerritServerConfig Config cfg) {
    this.gitManager = gitManager;
    this.commits = commits;
    this.strategy = MergeUtil.getMergeStrategy(cfg).getName();
    this.submitType = cfg.getEnum("project", null, "submitType", SubmitType.MERGE_IF_NECESSARY);
  }

  @Override
  public Response<MergeableInfo> apply(BranchResource resource)
      throws IOException, BadRequestException, ResourceNotFoundException,
          ResourceConflictException {
    if (!(submitType.equals(SubmitType.MERGE_ALWAYS)
        || submitType.equals(SubmitType.MERGE_IF_NECESSARY))) {
      throw new BadRequestException("Submit type: " + submitType + " is not supported");
    }

    MergeableInfo result = new MergeableInfo();
    result.submitType = submitType;
    result.strategy = strategy;
    try (Repository git = gitManager.openRepository(resource.getNameKey());
        RevWalk rw = new RevWalk(git);
        ObjectInserter inserter = new InMemoryInserter(git)) {
      Merger m = MergeUtil.newMerger(inserter, git.getConfig(), strategy);

      Ref destRef = git.getRefDatabase().exactRef(resource.getRef());
      if (destRef == null) {
        throw new ResourceNotFoundException(resource.getRef());
      }

      RevCommit targetCommit = rw.parseCommit(destRef.getObjectId());

      RevCommit sourceCommit = null;
      try {
        sourceCommit = MergeUtil.resolveCommit(git, rw, source);
        if (!commits.canRead(resource.getProjectState(), git, sourceCommit)) {
          throw new BadRequestException("do not have read permission for: " + source);
        }
      } catch (BadRequestException e) {
        // Throw a unified exception for permission denied and unresolvable commits.
        throw new BadRequestException(
            "Error resolving: '"
                + source
                + "'. Do not have read permission, or failed to resolve to a commit.",
            e);
      }

      if (rw.isMergedInto(sourceCommit, targetCommit)) {
        result.mergeable = true;
        result.commitMerged = true;
        result.contentMerged = true;
        return Response.ok(result);
      }

      if (m.merge(false, targetCommit, sourceCommit)) {
        result.mergeable = true;
        result.commitMerged = false;
        result.contentMerged = m.getResultTreeId().equals(targetCommit.getTree());
      } else {
        result.mergeable = false;
        if (m instanceof ResolveMerger) {
          result.conflicts = ((ResolveMerger) m).getUnmergedPaths();
        }
      }
    } catch (InvalidMergeStrategyException e) {
      throw new BadRequestException(e.getMessage());
    } catch (NoMergeBaseException e) {
      // TODO(ekempin) Rather return MergeableInfo with mergeable = false. But then we need a new
      // field in MergeableInfo to carry the message to the client and the frontend needs to be
      // adapted to show the message to the user.
      throw new ResourceConflictException(
          String.format("Change cannot be merged: %s", e.getMessage()), e);
    }
    return Response.ok(result);
  }
}
