// Copyright (C) 2012 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.collect.ImmutableMap.toImmutableMap;
import static com.google.gerrit.server.project.ProjectCache.illegalState;
import static java.util.Objects.requireNonNull;

import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Sets;
import com.google.gerrit.entities.BranchNameKey;
import com.google.gerrit.entities.Change;
import com.google.gerrit.entities.SubmissionId;
import com.google.gerrit.extensions.api.changes.SubmitInput;
import com.google.gerrit.extensions.client.SubmitType;
import com.google.gerrit.extensions.config.FactoryModule;
import com.google.gerrit.server.ApprovalsUtil;
import com.google.gerrit.server.ChangeMessagesUtil;
import com.google.gerrit.server.GerritPersonIdent;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.PatchSetUtil;
import com.google.gerrit.server.account.AccountCache;
import com.google.gerrit.server.change.LabelNormalizer;
import com.google.gerrit.server.change.RebaseChangeOp;
import com.google.gerrit.server.change.SetPrivateOp;
import com.google.gerrit.server.change.TestSubmitInput;
import com.google.gerrit.server.extensions.events.ChangeMerged;
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.MergeUtil;
import com.google.gerrit.server.git.TagCache;
import com.google.gerrit.server.git.validators.OnSubmitValidators;
import com.google.gerrit.server.patch.PatchSetInfoFactory;
import com.google.gerrit.server.patch.SubmitWithStickyApprovalDiff;
import com.google.gerrit.server.project.ProjectCache;
import com.google.gerrit.server.project.ProjectConfig;
import com.google.gerrit.server.project.ProjectState;
import com.google.gerrit.server.query.change.InternalChangeQuery;
import com.google.gerrit.server.submit.MergeOp.CommitStatus;
import com.google.gerrit.server.update.BatchUpdate;
import com.google.inject.Inject;
import com.google.inject.Module;
import com.google.inject.Provider;
import com.google.inject.assistedinject.Assisted;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevFlag;

/**
 * Base class that submit strategies must extend.
 *
 * <p>A submit strategy for a certain {@link SubmitType} defines how the submitted commits should be
 * merged.
 */
public abstract class SubmitStrategy {
  public static Module module() {
    return new FactoryModule() {
      @Override
      protected void configure() {
        factory(SubmitStrategy.Arguments.Factory.class);
        factory(EmailMerge.Factory.class);
      }
    };
  }

  static class Arguments {
    interface Factory {
      Arguments create(
          SubmitType submitType,
          BranchNameKey destBranch,
          CommitStatus commitStatus,
          CodeReviewRevWalk rw,
          IdentifiedUser caller,
          MergeTip mergeTip,
          RevFlag canMergeFlag,
          Set<RevCommit> alreadyAccepted,
          Set<CodeReviewCommit> incoming,
          SubmissionId submissionId,
          SubmitInput submitInput,
          SubmoduleCommits submoduleCommits,
          SubscriptionGraph subscriptionGraph,
          boolean dryrun);
    }

    final AccountCache accountCache;
    final ApprovalsUtil approvalsUtil;
    final ChangeMerged changeMerged;
    final ChangeMessagesUtil cmUtil;
    final EmailMerge.Factory mergedSenderFactory;
    final GitRepositoryManager repoManager;
    final LabelNormalizer labelNormalizer;
    final PatchSetInfoFactory patchSetInfoFactory;
    final PatchSetUtil psUtil;
    final ProjectCache projectCache;
    final PersonIdent serverIdent;
    final RebaseChangeOp.Factory rebaseFactory;
    final OnSubmitValidators.Factory onSubmitValidatorsFactory;
    final TagCache tagCache;
    final Provider<InternalChangeQuery> queryProvider;
    final ProjectConfig.Factory projectConfigFactory;
    final SetPrivateOp.Factory setPrivateOpFactory;
    final SubmitWithStickyApprovalDiff submitWithStickyApprovalDiff;

    final BranchNameKey destBranch;
    final CodeReviewRevWalk rw;
    final CommitStatus commitStatus;
    final IdentifiedUser caller;
    final MergeTip mergeTip;
    final RevFlag canMergeFlag;
    final Set<RevCommit> alreadyAccepted;
    final SubmissionId submissionId;
    final SubmitType submitType;
    final SubmitInput submitInput;
    final SubscriptionGraph subscriptionGraph;
    final SubmoduleCommits submoduleCommits;

    final ProjectState project;
    final MergeSorter mergeSorter;
    final RebaseSorter rebaseSorter;
    final MergeUtil mergeUtil;
    final boolean dryrun;

    @Inject
    Arguments(
        AccountCache accountCache,
        ApprovalsUtil approvalsUtil,
        ChangeMerged changeMerged,
        ChangeMessagesUtil cmUtil,
        EmailMerge.Factory mergedSenderFactory,
        GitRepositoryManager repoManager,
        LabelNormalizer labelNormalizer,
        MergeUtil.Factory mergeUtilFactory,
        PatchSetInfoFactory patchSetInfoFactory,
        PatchSetUtil psUtil,
        @GerritPersonIdent PersonIdent serverIdent,
        ProjectCache projectCache,
        RebaseChangeOp.Factory rebaseFactory,
        OnSubmitValidators.Factory onSubmitValidatorsFactory,
        TagCache tagCache,
        Provider<InternalChangeQuery> queryProvider,
        ProjectConfig.Factory projectConfigFactory,
        SetPrivateOp.Factory setPrivateOpFactory,
        SubmitWithStickyApprovalDiff submitWithStickyApprovalDiff,
        @Assisted BranchNameKey destBranch,
        @Assisted CommitStatus commitStatus,
        @Assisted CodeReviewRevWalk rw,
        @Assisted IdentifiedUser caller,
        @Assisted MergeTip mergeTip,
        @Assisted RevFlag canMergeFlag,
        @Assisted Set<RevCommit> alreadyAccepted,
        @Assisted Set<CodeReviewCommit> incoming,
        @Assisted SubmissionId submissionId,
        @Assisted SubmitType submitType,
        @Assisted SubmitInput submitInput,
        @Assisted SubscriptionGraph subscriptionGraph,
        @Assisted SubmoduleCommits submoduleCommits,
        @Assisted boolean dryrun) {
      this.accountCache = accountCache;
      this.approvalsUtil = approvalsUtil;
      this.changeMerged = changeMerged;
      this.mergedSenderFactory = mergedSenderFactory;
      this.repoManager = repoManager;
      this.cmUtil = cmUtil;
      this.labelNormalizer = labelNormalizer;
      this.projectConfigFactory = projectConfigFactory;
      this.patchSetInfoFactory = patchSetInfoFactory;
      this.psUtil = psUtil;
      this.projectCache = projectCache;
      this.rebaseFactory = rebaseFactory;
      this.tagCache = tagCache;
      this.queryProvider = queryProvider;
      this.setPrivateOpFactory = setPrivateOpFactory;
      this.submitWithStickyApprovalDiff = submitWithStickyApprovalDiff;

      this.serverIdent = serverIdent;
      this.destBranch = destBranch;
      this.commitStatus = commitStatus;
      this.rw = rw;
      this.caller = caller;
      this.mergeTip = mergeTip;
      this.canMergeFlag = canMergeFlag;
      this.alreadyAccepted = alreadyAccepted;
      this.submissionId = submissionId;
      this.submitType = submitType;
      this.submitInput = submitInput;
      this.submoduleCommits = submoduleCommits;
      this.subscriptionGraph = subscriptionGraph;
      this.dryrun = dryrun;

      this.project =
          projectCache.get(destBranch.project()).orElseThrow(illegalState(destBranch.project()));
      this.mergeSorter =
          new MergeSorter(caller, rw, alreadyAccepted, canMergeFlag, queryProvider, incoming);
      this.rebaseSorter =
          new RebaseSorter(
              caller,
              rw,
              mergeTip.getInitialTip(),
              alreadyAccepted,
              canMergeFlag,
              queryProvider,
              incoming);
      this.mergeUtil = mergeUtilFactory.create(project);
      this.onSubmitValidatorsFactory = onSubmitValidatorsFactory;
    }
  }

  final Arguments args;

  private final Set<SubmitStrategyOp> submitStrategyOps;

  SubmitStrategy(Arguments args) {
    this.args = requireNonNull(args);
    this.submitStrategyOps = new HashSet<>();
  }

  /**
   * Returns the updated changed after this submit strategy has been executed.
   *
   * @return the updated changes after this submit strategy has been executed
   */
  public ImmutableMap<Change.Id, Change> getUpdatedChanges() {
    return submitStrategyOps.stream()
        .map(SubmitStrategyOp::getUpdatedChange)
        .filter(Optional::isPresent)
        .map(Optional::get)
        .collect(toImmutableMap(c -> c.getId(), c -> c));
  }

  /**
   * Add operations to a batch update that execute this submit strategy.
   *
   * <p>Guarantees exactly one op is added to the update for each change in the input set.
   *
   * @param bu batch update to add operations to.
   * @param toMerge the set of submitted commits that should be merged using this submit strategy.
   *     Implementations are responsible for ordering of commits, and will not modify the input in
   *     place.
   */
  public final void addOps(BatchUpdate bu, Set<CodeReviewCommit> toMerge) {
    List<SubmitStrategyOp> ops = buildOps(toMerge);
    Set<CodeReviewCommit> added = Sets.newHashSetWithExpectedSize(ops.size());

    for (SubmitStrategyOp op : ops) {
      added.add(op.getCommit());
    }

    // First add ops for any implicitly merged changes.
    List<CodeReviewCommit> difference = new ArrayList<>(Sets.difference(toMerge, added));
    Collections.reverse(difference);
    for (CodeReviewCommit c : difference) {
      Change.Id id = c.change().getId();
      bu.addOp(id, args.setPrivateOpFactory.create(false, null));
      ImplicitIntegrateOp implicitIntegrateOp = new ImplicitIntegrateOp(args, c);
      bu.addOp(id, implicitIntegrateOp);
      maybeAddTestHelperOp(bu, id);
      this.submitStrategyOps.add(implicitIntegrateOp);
    }

    // Then ops for explicitly merged changes
    for (SubmitStrategyOp op : ops) {
      bu.addOp(op.getId(), args.setPrivateOpFactory.create(false, null));
      bu.addOp(op.getId(), op);
      maybeAddTestHelperOp(bu, op.getId());
      this.submitStrategyOps.add(op);
    }
  }

  private void maybeAddTestHelperOp(BatchUpdate bu, Change.Id changeId) {
    if (args.submitInput instanceof TestSubmitInput) {
      bu.addOp(changeId, new TestHelperOp(changeId, args));
    }
  }

  protected abstract List<SubmitStrategyOp> buildOps(Collection<CodeReviewCommit> toMerge);
}
