// Copyright 2000-2024 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.

package org.jetbrains.idea.svn.history;

import com.intellij.openapi.actionSystem.DefaultActionGroup;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.progress.ProcessCanceledException;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.util.registry.Registry;
import com.intellij.openapi.vcs.*;
import com.intellij.openapi.vcs.changes.committed.DecoratorManager;
import com.intellij.openapi.vcs.changes.committed.VcsCommittedListsZipper;
import com.intellij.openapi.vcs.changes.committed.VcsCommittedViewAuxiliary;
import com.intellij.openapi.vcs.history.VcsRevisionNumber;
import com.intellij.openapi.vcs.versionBrowser.ChangeBrowserSettings;
import com.intellij.openapi.vcs.versionBrowser.ChangesBrowserSettingsEditor;
import com.intellij.openapi.vcs.versionBrowser.CommittedChangeList;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.util.AsynchConsumer;
import com.intellij.util.PairConsumer;
import com.intellij.util.ThrowableConsumer;
import com.intellij.util.messages.MessageBusConnection;
import com.intellij.vcsUtil.VcsUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.idea.svn.SvnUtil;
import org.jetbrains.idea.svn.SvnVcs;
import org.jetbrains.idea.svn.api.Depth;
import org.jetbrains.idea.svn.api.Revision;
import org.jetbrains.idea.svn.api.Target;
import org.jetbrains.idea.svn.api.Url;
import org.jetbrains.idea.svn.branchConfig.ConfigureBranchesAction;
import org.jetbrains.idea.svn.commandLine.SvnBindException;
import org.jetbrains.idea.svn.info.Info;
import org.jetbrains.idea.svn.status.StatusType;

import java.io.DataInput;
import java.io.DataOutput;
import java.io.File;
import java.io.IOException;
import java.util.*;

import static com.intellij.openapi.application.ApplicationManager.getApplication;
import static com.intellij.openapi.progress.ProgressManager.progress;
import static com.intellij.openapi.progress.ProgressManager.progress2;
import static com.intellij.openapi.vcs.changes.committed.VcsConfigurationChangeListener.BRANCHES_CHANGED_RESPONSE;
import static java.util.Collections.singletonList;
import static org.jetbrains.idea.svn.SvnBundle.message;

public class SvnCommittedChangesProvider implements CachingCommittedChangesProvider<SvnChangeList, ChangeBrowserSettings> {

  private static final Logger LOG = Logger.getInstance(SvnCommittedChangesProvider.class);

  private final @NotNull SvnVcs myVcs;
  private final @NotNull MessageBusConnection myConnection;
  private MergeInfoUpdatesListener myMergeInfoUpdatesListener;
  private final @NotNull SvnCommittedListsZipper myZipper;

  public static final int VERSION_WITH_COPY_PATHS_ADDED = 2;
  public static final int VERSION_WITH_REPLACED_PATHS = 3;

  public SvnCommittedChangesProvider(@NotNull SvnVcs vcs) {
    myVcs = vcs;
    myZipper = new SvnCommittedListsZipper(myVcs);

    myConnection = myVcs.getProject().getMessageBus().connect();
    myConnection.subscribe(BRANCHES_CHANGED_RESPONSE,
                           (project, vcsRoot, cachedList) -> getApplication().invokeLater(() -> {
                             cachedList.stream().filter(SvnChangeList.class::isInstance).map(SvnChangeList.class::cast)
                               .filter(list -> vcsRoot == null || vcsRoot.equals(list.getVcsRoot()))
                               .forEach(SvnChangeList::forceReloadCachedInfo);
                           }, project.getDisposed()));
  }

  @Override
  public @NotNull ChangesBrowserSettingsEditor<ChangeBrowserSettings> createFilterUI(boolean showDateFilter) {
    return new SvnVersionFilterComponent(showDateFilter);
  }

  @Override
  public @Nullable RepositoryLocation getLocationFor(@NotNull FilePath root) {
    Info info = myVcs.getInfo(root.getIOFile());

    return info != null && info.getUrl() != null ? new SvnRepositoryLocation(info.getUrl(), info.getRepositoryRootUrl(), root) : null;
  }

  @Override
  public @NotNull VcsCommittedListsZipper getZipper() {
    return myZipper;
  }

  @Override
  public void loadCommittedChanges(@NotNull ChangeBrowserSettings settings,
                                   @NotNull RepositoryLocation location,
                                   int maxCount,
                                   @NotNull AsynchConsumer<? super CommittedChangeList> consumer) throws VcsException {
    try {
      SvnRepositoryLocation svnLocation = (SvnRepositoryLocation)location;
      Url repositoryRoot = getRepositoryRoot(svnLocation);
      ChangeBrowserSettings.Filter filter = settings.createFilter();
      ThrowableConsumer<LogEntry, SvnBindException> resultConsumer = logEntry -> {
        SvnChangeList list = new SvnChangeList(myVcs, svnLocation, logEntry, repositoryRoot);
        if (filter.accepts(list)) {
          consumer.consume(list);
        }
      };
      Target target = Target.on(svnLocation.toSvnUrl(), createBeforeRevision(settings));

      getCommittedChangesImpl(settings, target, maxCount, resultConsumer, false, true);
    }
    finally {
      consumer.finished();
    }
  }

  @Override
  public @NotNull List<SvnChangeList> getCommittedChanges(@NotNull ChangeBrowserSettings settings,
                                                          @NotNull RepositoryLocation location,
                                                          int maxCount) throws VcsException {
    SvnRepositoryLocation svnLocation = (SvnRepositoryLocation)location;
    List<SvnChangeList> result = new ArrayList<>();
    Url repositoryRoot = getRepositoryRoot(svnLocation);
    ThrowableConsumer<LogEntry, SvnBindException> resultConsumer =
      logEntry -> result.add(new SvnChangeList(myVcs, svnLocation, logEntry, repositoryRoot));
    Target target = Target.on(svnLocation.toSvnUrl(), createBeforeRevision(settings));

    getCommittedChangesImpl(settings, target, maxCount, resultConsumer, false, true);
    settings.filterChanges(result);
    return result;
  }

  public void getCommittedChangesWithMergedRevisons(@NotNull ChangeBrowserSettings settings,
                                                    @NotNull RepositoryLocation location,
                                                    int maxCount,
                                                    @NotNull PairConsumer<SvnChangeList, LogHierarchyNode> finalConsumer)
    throws VcsException {
    SvnRepositoryLocation svnLocation = (SvnRepositoryLocation)location;
    Url repositoryRoot = getRepositoryRoot(svnLocation);
    MergeSourceHierarchyBuilder builder = new MergeSourceHierarchyBuilder(
      node -> finalConsumer.consume(new SvnChangeList(myVcs, svnLocation, node.getMe(), repositoryRoot), node));
    SvnMergeSourceTracker mergeSourceTracker = new SvnMergeSourceTracker(builder);

    getCommittedChangesImpl(settings, Target.on(svnLocation.toSvnUrl()), maxCount, mergeSourceTracker, true, false);

    builder.finish();
  }

  private @NotNull Url getRepositoryRoot(@NotNull SvnRepositoryLocation svnLocation) throws VcsException {
    // TODO: Additionally SvnRepositoryLocation could possibly be refactored to always contain FilePath (or similar local item)
    // TODO: So here we could get repository url without performing remote svn command

    Url url = svnLocation.toSvnUrl();
    Url rootUrl = SvnUtil.getRepositoryRoot(myVcs, url);

    if (rootUrl == null) {
      throw new SvnBindException(message("error.can.not.find.repository.root.for.url", url.toDecodedString()));
    }

    return rootUrl;
  }

  private void getCommittedChangesImpl(@NotNull ChangeBrowserSettings settings,
                                       @NotNull Target target,
                                       int maxCount,
                                       @NotNull ThrowableConsumer<LogEntry, SvnBindException> resultConsumer,
                                       boolean includeMergedRevisions,
                                       boolean filterOutByDate) throws VcsException {
    progress(message("progress.text.changes.collecting.changes"),
             message("progress.text2.changes.establishing.connection", target.getPath()));

    String author = settings.getUserFilter();
    Revision revisionBefore = createBeforeRevision(settings);
    Revision revisionAfter = createAfterRevision(settings);

    myVcs.getFactory(target).createHistoryClient()
      .doLog(target, revisionBefore, revisionAfter, settings.STOP_ON_COPY, true, includeMergedRevisions, maxCount, null,
             createLogHandler(resultConsumer, filterOutByDate, author));
  }

  private static @NotNull Revision createBeforeRevision(@NotNull ChangeBrowserSettings settings) {
    return createRevision(settings.getDateBeforeFilter(), settings.getChangeBeforeFilter(), Revision.HEAD);
  }

  private static @NotNull Revision createAfterRevision(@NotNull ChangeBrowserSettings settings) {
    return createRevision(settings.getDateAfterFilter(), settings.getChangeAfterFilter(), Revision.of(1));
  }

  private static @NotNull Revision createRevision(@Nullable Date date, @Nullable Long change, @NotNull Revision defaultValue) {
    Revision result;

    if (date != null) {
      result = Revision.of(date);
    }
    else if (change != null) {
      result = Revision.of(change.longValue());
    }
    else {
      result = defaultValue;
    }

    return result;
  }

  private @NotNull LogEntryConsumer createLogHandler(@NotNull ThrowableConsumer<LogEntry, SvnBindException> resultConsumer,
                                                     boolean filterOutByDate,
                                                     @Nullable String author) {
    return logEntry -> {
      if (myVcs.getProject().isDisposed()) throw new ProcessCanceledException();

      if (logEntry != LogEntry.EMPTY) {
        progress2(message("progress.text2.processing.revision", logEntry.getRevision()));
      }
      if (filterOutByDate && logEntry.getDate() == null) {
        // do not add lists without info - this situation is possible for lists where there are paths that user has no rights to observe
        return;
      }
      if (author == null || author.equalsIgnoreCase(logEntry.getAuthor())) {
        resultConsumer.consume(logEntry);
      }
    };
  }

  @Override
  public ChangeListColumn @NotNull [] getColumns() {
    return new ChangeListColumn[]{
      new ChangeListColumn.ChangeListNumberColumn(message("revision.title")),
      ChangeListColumn.NAME, ChangeListColumn.DATE, ChangeListColumn.DESCRIPTION
    };
  }

  private void refreshMergeInfo(@NotNull RootsAndBranches action) {
    if (myMergeInfoUpdatesListener == null) {
      myMergeInfoUpdatesListener = new MergeInfoUpdatesListener(myVcs.getProject(), myConnection);
    }
    myMergeInfoUpdatesListener.addPanel(action);
  }

  @Override
  public @NotNull VcsCommittedViewAuxiliary createActions(@NotNull DecoratorManager manager, @Nullable RepositoryLocation location) {
    RootsAndBranches rootsAndBranches = new RootsAndBranches(myVcs, manager, location);
    refreshMergeInfo(rootsAndBranches);

    DefaultActionGroup popup = DefaultActionGroup.createPopupGroup(() -> myVcs.getDisplayName());
    popup.add(rootsAndBranches.getIntegrateAction());
    popup.add(rootsAndBranches.getUndoIntegrateAction());
    popup.add(new ConfigureBranchesAction());

    ShowHideMergePanelAction action = new ShowHideMergePanelAction(manager, rootsAndBranches.getStrategy());

    return new VcsCommittedViewAuxiliary(singletonList(popup), () -> {
      if (myMergeInfoUpdatesListener != null) {
        myMergeInfoUpdatesListener.removePanel(rootsAndBranches);
        rootsAndBranches.dispose();
      }
    }, singletonList(action));
  }

  @Override
  public int getUnlimitedCountValue() {
    return 0;
  }

  @Override
  public @Nullable Pair<SvnChangeList, FilePath> getOneList(@NotNull VirtualFile file, @NotNull VcsRevisionNumber number) throws VcsException {
    return new SingleCommittedListProvider(myVcs, file, number).run();
  }

  @Override
  public @NotNull RepositoryLocation getForNonLocal(@NotNull VirtualFile file) {
    return new SvnRepositoryLocation(FileUtil.toSystemIndependentName(file.getPresentableUrl()));
  }

  @Override
  public int getFormatVersion() {
    return VERSION_WITH_REPLACED_PATHS;
  }

  @Override
  public void writeChangeList(@NotNull DataOutput dataStream, @NotNull SvnChangeList list) throws IOException {
    list.writeToStream(dataStream);
  }

  @Override
  public @NotNull SvnChangeList readChangeList(@NotNull RepositoryLocation location, @NotNull DataInput stream) throws IOException {
    int version = getFormatVersion();
    return new SvnChangeList(myVcs, (SvnRepositoryLocation)location, stream, VERSION_WITH_COPY_PATHS_ADDED <= version,
                             VERSION_WITH_REPLACED_PATHS <= version);
  }

  @Override
  public @Nullable Collection<FilePath> getIncomingFiles(@NotNull RepositoryLocation location) throws VcsException {
    FilePath root = null;

    if (Registry.is("svn.use.incoming.optimization")) {
      root = ((SvnRepositoryLocation)location).getRoot();

      if (root == null) {
        LOG.info("Working copy root is not provided for repository location " + location);
      }
    }

    return root != null ? getIncomingFiles(root) : null;
  }

  private @NotNull Collection<FilePath> getIncomingFiles(@NotNull FilePath root) throws SvnBindException {
    // TODO: "svn diff -r BASE:HEAD --xml --summarize" command is also suitable here and outputs only necessary changed files,
    // TODO: while "svn status -u" also outputs other files which could be not modified on server. But for svn 1.7 "--xml --summarize"
    // TODO: could only be used with url targets - so we could not use "svn diff" here now for all cases (we could not use url with
    // TODO: concrete revision as there could be mixed revision working copy).

    Set<FilePath> result = new HashSet<>();
    File rootFile = root.getIOFile();

    myVcs.getFactory(rootFile).createStatusClient()
      .doStatus(rootFile, Depth.INFINITY, true, false, false, false, status -> {
        File file = status.getFile();
        boolean changedOnServer = isNotNone(status.getRemoteItemStatus()) || isNotNone(status.getRemotePropertyStatus());

        if (changedOnServer) {
          result.add(VcsUtil.getFilePath(file));
        }
      });

    return result;
  }

  private static boolean isNotNone(@Nullable StatusType status) {
    return status != null && !StatusType.STATUS_NONE.equals(status);
  }

  @Override
  public String getChangelistTitle() {
    return message("changes.browser.revision.term");
  }

  @Override
  public boolean refreshIncomingWithCommitted() {
    return true;
  }

  public void deactivate() {
    myConnection.disconnect();
  }
}
