// 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.submit;

import static com.google.common.base.Preconditions.checkState;
import static com.google.gerrit.server.project.ProjectCache.noSuchProject;
import static java.util.Objects.requireNonNull;

import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Maps;
import com.google.gerrit.entities.BranchNameKey;
import com.google.gerrit.entities.Project;
import com.google.gerrit.entities.RefNames;
import com.google.gerrit.exceptions.StorageException;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.change.NotifyResolver;
import com.google.gerrit.server.git.CodeReviewCommit;
import com.google.gerrit.server.git.CodeReviewCommit.CodeReviewRevWalk;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.git.MergeTip;
import com.google.gerrit.server.git.validators.OnSubmitValidators;
import com.google.gerrit.server.project.NoSuchProjectException;
import com.google.gerrit.server.project.ProjectCache;
import com.google.gerrit.server.project.ProjectState;
import com.google.gerrit.server.update.BatchUpdate;
import com.google.inject.Inject;
import java.io.IOException;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import org.eclipse.jgit.errors.RepositoryNotFoundException;
import org.eclipse.jgit.lib.ObjectInserter;
import org.eclipse.jgit.lib.ObjectReader;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevFlag;
import org.eclipse.jgit.revwalk.RevSort;

/**
 * This is a helper class for MergeOp and not intended for general use.
 *
 * <p>Some database backends require to open a repository just once within a transaction of a
 * submission, this caches open repositories to satisfy that requirement.
 */
public class MergeOpRepoManager implements AutoCloseable {
  public class OpenRepo {
    final Repository repo;
    final CodeReviewRevWalk rw;
    final RevFlag canMergeFlag;
    final ObjectInserter ins;

    final ProjectState project;
    BatchUpdate update;

    private final ObjectReader reader;
    private final Map<BranchNameKey, OpenBranch> branches;

    private OpenRepo(Repository repo, ProjectState project) {
      this.repo = repo;
      this.project = project;
      ins = repo.newObjectInserter();
      reader = ins.newReader();
      rw = CodeReviewCommit.newRevWalk(reader);
      rw.sort(RevSort.TOPO);
      rw.sort(RevSort.COMMIT_TIME_DESC, true);
      rw.setRetainBody(false);
      canMergeFlag = rw.newFlag("CAN_MERGE");
      rw.retainOnReset(canMergeFlag);

      branches = Maps.newHashMapWithExpectedSize(1);
    }

    OpenBranch getBranch(BranchNameKey branch) throws IntegrationConflictException {
      OpenBranch ob = branches.get(branch);
      if (ob == null) {
        ob = new OpenBranch(this, branch);
        branches.put(branch, ob);
      }
      return ob;
    }

    public Repository getRepo() {
      return repo;
    }

    Project.NameKey getProjectName() {
      return project.getNameKey();
    }

    public CodeReviewRevWalk getCodeReviewRevWalk() {
      return rw;
    }

    public BatchUpdate getUpdate() {
      checkState(caller != null, "call setContext before getUpdate");
      if (update == null) {
        update =
            batchUpdateFactory
                .create(getProjectName(), caller, ts)
                .setRepository(repo, rw, ins)
                .setNotify(notify)
                .setOnSubmitValidators(onSubmitValidatorsFactory.create());
      }
      return update;
    }

    // We want to reuse the open repo BUT not the BatchUpdate (because they are already executed)
    public void resetExecutedUpdates() {
      if (update != null && update.isExecuted()) {
        update.close();
        update = null;
      }
    }

    private void close() {
      if (update != null) {
        update.close();
      }
      rw.close();
      reader.close();
      ins.close();
      repo.close();
    }
  }

  public static class OpenBranch {
    final CodeReviewCommit oldTip;
    MergeTip mergeTip;

    OpenBranch(OpenRepo or, BranchNameKey name) throws IntegrationConflictException {
      try {
        Ref ref = or.getRepo().exactRef(name.branch());
        if (ref != null) {
          oldTip = or.rw.parseCommit(ref.getObjectId());
        } else if (Objects.equals(or.repo.getFullBranch(), name.branch())
            || Objects.equals(RefNames.REFS_CONFIG, name.branch())) {
          oldTip = null;
        } else {
          throw new IntegrationConflictException(
              "The destination branch " + name + " does not exist anymore.");
        }
      } catch (IOException e) {
        throw new StorageException("Cannot open branch " + name, e);
      }
    }
  }

  private final Map<Project.NameKey, OpenRepo> openRepos;
  private final BatchUpdate.Factory batchUpdateFactory;
  private final OnSubmitValidators.Factory onSubmitValidatorsFactory;
  private final GitRepositoryManager repoManager;
  private final ProjectCache projectCache;

  private Timestamp ts;
  private IdentifiedUser caller;
  private NotifyResolver.Result notify;

  @Inject
  MergeOpRepoManager(
      GitRepositoryManager repoManager,
      ProjectCache projectCache,
      BatchUpdate.Factory batchUpdateFactory,
      OnSubmitValidators.Factory onSubmitValidatorsFactory) {
    this.repoManager = repoManager;
    this.projectCache = projectCache;
    this.batchUpdateFactory = batchUpdateFactory;
    this.onSubmitValidatorsFactory = onSubmitValidatorsFactory;

    openRepos = new HashMap<>();
  }

  public void setContext(Timestamp ts, IdentifiedUser caller, NotifyResolver.Result notify) {
    this.ts = requireNonNull(ts);
    this.caller = requireNonNull(caller);
    this.notify = requireNonNull(notify);
  }

  public OpenRepo getRepo(Project.NameKey project) throws NoSuchProjectException, IOException {
    if (openRepos.containsKey(project)) {
      return openRepos.get(project);
    }

    ProjectState projectState = projectCache.get(project).orElseThrow(noSuchProject(project));
    try {
      OpenRepo or = new OpenRepo(repoManager.openRepository(project), projectState);
      openRepos.put(project, or);
      return or;
    } catch (RepositoryNotFoundException e) {
      throw new NoSuchProjectException(project, e);
    }
  }

  public List<BatchUpdate> batchUpdates(Collection<Project.NameKey> projects)
      throws NoSuchProjectException, IOException {
    List<BatchUpdate> updates = new ArrayList<>(projects.size());
    for (Project.NameKey project : projects) {
      updates.add(getRepo(project).getUpdate().setNotify(notify).setRefLogMessage("merged"));
    }
    return updates;
  }

  public void resetUpdates(ImmutableSet<Project.NameKey> projects)
      throws NoSuchProjectException, IOException {
    for (Project.NameKey project : projects) {
      getRepo(project).resetExecutedUpdates();
    }
  }

  @Override
  public void close() {
    for (OpenRepo repo : openRepos.values()) {
      repo.close();
    }
    openRepos.clear();
  }
}
