// Copyright (C) 2011 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.git.meta;

import com.google.gerrit.common.Nullable;
import com.google.gerrit.entities.Project;
import com.google.gerrit.server.GerritPersonIdent;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
import com.google.inject.assistedinject.Assisted;
import java.io.IOException;
import org.eclipse.jgit.errors.RepositoryNotFoundException;
import org.eclipse.jgit.lib.BatchRefUpdate;
import org.eclipse.jgit.lib.CommitBuilder;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.RefUpdate;
import org.eclipse.jgit.lib.Repository;

/** Helps with the updating of a {@link VersionedMetaData}. */
public class MetaDataUpdate implements AutoCloseable {
  @Singleton
  public static class User {
    private final InternalFactory factory;
    private final GitRepositoryManager mgr;
    private final Provider<PersonIdent> serverIdentProvider;
    private final Provider<IdentifiedUser> identifiedUser;

    @Inject
    User(
        InternalFactory factory,
        GitRepositoryManager mgr,
        @GerritPersonIdent Provider<PersonIdent> serverIdentProvider,
        Provider<IdentifiedUser> identifiedUser) {
      this.factory = factory;
      this.mgr = mgr;
      this.serverIdentProvider = serverIdentProvider;
      this.identifiedUser = identifiedUser;
    }

    public PersonIdent getUserPersonIdent() {
      return createPersonIdent(identifiedUser.get());
    }

    public MetaDataUpdate create(Project.NameKey name)
        throws RepositoryNotFoundException, IOException {
      return create(name, identifiedUser.get());
    }

    public MetaDataUpdate create(Project.NameKey name, IdentifiedUser user)
        throws RepositoryNotFoundException, IOException {
      return create(name, user, null);
    }

    /**
     * Create an update using an existing batch ref update.
     *
     * <p>This allows batching together updates to multiple metadata refs. For making multiple
     * commits to a single metadata ref, see {@link VersionedMetaData#openUpdate(MetaDataUpdate)}.
     *
     * @param name project name.
     * @param user user for the update.
     * @param batch batch update to use; the caller is responsible for committing the update.
     */
    public MetaDataUpdate create(Project.NameKey name, IdentifiedUser user, BatchRefUpdate batch)
        throws RepositoryNotFoundException, IOException {
      Repository repo = mgr.openRepository(name);
      MetaDataUpdate md = create(name, repo, user, batch);
      md.setCloseRepository(true);
      return md;
    }

    /**
     * Create an update using an existing batch ref update.
     *
     * <p>This allows batching together updates to multiple metadata refs. For making multiple
     * commits to a single metadata ref, see {@link VersionedMetaData#openUpdate(MetaDataUpdate)}.
     *
     * <p>Important: Create a new MetaDataUpdate instance for each update:
     *
     * <pre>
     * <code>
     *   try (Repository repo = repoMgr.openRepository(allUsersName);
     *       RevWalk rw = new RevWalk(repo)) {
     *     BatchRefUpdate batchUpdate = repo.getRefDatabase().newBatchUpdate();
     *     // WRONG: create the MetaDataUpdate instance here and reuse it for
     *     //        all updates in the loop
     *     for{@code (Map.Entry<Account.Id, DiffPreferencesInfo> e : diffPrefsFromDb)} {
     *       // CORRECT: create a new MetaDataUpdate instance for each update
     *       try (MetaDataUpdate md =
     *           metaDataUpdateFactory.create(allUsersName, batchUpdate)) {
     *         md.setMessage("Import diff preferences from reviewdb\n");
     *         VersionedAccountPreferences vPrefs =
     *             VersionedAccountPreferences.forUser(e.getKey());
     *         storeSection(vPrefs.getConfig(), UserConfigSections.DIFF, null,
     *             e.getValue(), DiffPreferencesInfo.defaults());
     *         vPrefs.commit(md);
     *       } catch (ConfigInvalidException e) {
     *         // TODO handle exception
     *       }
     *     }
     *     batchUpdate.execute(rw, NullProgressMonitor.INSTANCE);
     *   }
     * </code>
     * </pre>
     *
     * @param name project name.
     * @param repository the repository to update; the caller is responsible for closing the
     *     repository.
     * @param user user for the update.
     * @param batch batch update to use; the caller is responsible for committing the update.
     */
    public MetaDataUpdate create(
        Project.NameKey name, Repository repository, IdentifiedUser user, BatchRefUpdate batch) {
      MetaDataUpdate md = factory.create(name, repository, batch);
      md.getCommitBuilder().setCommitter(serverIdentProvider.get());
      md.setAuthor(user);
      return md;
    }

    private PersonIdent createPersonIdent(IdentifiedUser user) {
      PersonIdent serverIdent = serverIdentProvider.get();
      return user.newCommitterIdent(serverIdent.getWhen(), serverIdent.getTimeZone());
    }
  }

  @Singleton
  public static class Server {
    private final InternalFactory factory;
    private final GitRepositoryManager mgr;
    private final Provider<PersonIdent> serverIdentProvider;

    @Inject
    Server(
        InternalFactory factory,
        GitRepositoryManager mgr,
        @GerritPersonIdent Provider<PersonIdent> serverIdentProvider) {
      this.factory = factory;
      this.mgr = mgr;
      this.serverIdentProvider = serverIdentProvider;
    }

    public MetaDataUpdate create(Project.NameKey name)
        throws RepositoryNotFoundException, IOException {
      return create(name, null);
    }

    /** @see User#create(Project.NameKey, IdentifiedUser, BatchRefUpdate) */
    public MetaDataUpdate create(Project.NameKey name, BatchRefUpdate batch)
        throws RepositoryNotFoundException, IOException {
      Repository repo = mgr.openRepository(name);
      MetaDataUpdate md = factory.create(name, repo, batch);
      md.setCloseRepository(true);
      PersonIdent serverIdent = serverIdentProvider.get();
      md.getCommitBuilder().setAuthor(serverIdent);
      md.getCommitBuilder().setCommitter(serverIdent);
      return md;
    }
  }

  public interface InternalFactory {
    MetaDataUpdate create(
        @Assisted Project.NameKey projectName,
        @Assisted Repository repository,
        @Assisted @Nullable BatchRefUpdate batch);
  }

  private final GitReferenceUpdated gitRefUpdated;
  private final Project.NameKey projectName;
  private final Repository repository;
  private final BatchRefUpdate batch;
  private final CommitBuilder commit;
  private boolean allowEmpty;
  private boolean insertChangeId;
  private boolean closeRepository;
  private IdentifiedUser author;

  @Inject
  public MetaDataUpdate(
      GitReferenceUpdated gitRefUpdated,
      @Assisted Project.NameKey projectName,
      @Assisted Repository repository,
      @Assisted @Nullable BatchRefUpdate batch) {
    this.gitRefUpdated = gitRefUpdated;
    this.projectName = projectName;
    this.repository = repository;
    this.batch = batch;
    this.commit = new CommitBuilder();
  }

  public MetaDataUpdate(
      GitReferenceUpdated gitRefUpdated, Project.NameKey projectName, Repository repository) {
    this(gitRefUpdated, projectName, repository, null);
  }

  /** Set the commit message used when committing the update. */
  public void setMessage(String message) {
    getCommitBuilder().setMessage(message);
  }

  public void setAuthor(IdentifiedUser author) {
    this.author = author;
    getCommitBuilder()
        .setAuthor(
            author.newCommitterIdent(
                getCommitBuilder().getCommitter().getWhen(),
                getCommitBuilder().getCommitter().getTimeZone()));
  }

  public void setAllowEmpty(boolean allowEmpty) {
    this.allowEmpty = allowEmpty;
  }

  public void setInsertChangeId(boolean insertChangeId) {
    this.insertChangeId = insertChangeId;
  }

  public void setCloseRepository(boolean closeRepository) {
    this.closeRepository = closeRepository;
  }

  /** @return batch in which to run the update, or {@code null} for no batch. */
  BatchRefUpdate getBatch() {
    return batch;
  }

  /** Close the cached Repository handle. */
  @Override
  public void close() {
    if (closeRepository) {
      getRepository().close();
    }
  }

  public Project.NameKey getProjectName() {
    return projectName;
  }

  public Repository getRepository() {
    return repository;
  }

  boolean allowEmpty() {
    return allowEmpty;
  }

  boolean insertChangeId() {
    return insertChangeId;
  }

  public CommitBuilder getCommitBuilder() {
    return commit;
  }

  protected void fireGitRefUpdatedEvent(RefUpdate ru) {
    gitRefUpdated.fire(projectName, ru, author == null ? null : author.state());
  }
}
