// Copyright 2008-2010 Victor Iacoban
//
// 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 org.zmlx.hg4idea.provider;

import com.intellij.openapi.actionSystem.AnAction;
import com.intellij.openapi.actionSystem.AnActionEvent;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.ide.CopyPasteManager;
import com.intellij.openapi.project.DumbAwareAction;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Comparing;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.vcs.*;
import com.intellij.openapi.vcs.changes.Change;
import com.intellij.openapi.vcs.changes.ChangeList;
import com.intellij.openapi.vcs.changes.committed.DecoratorManager;
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.PlatformIcons;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.vcsUtil.VcsUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.zmlx.hg4idea.*;
import org.zmlx.hg4idea.command.HgLogCommand;
import org.zmlx.hg4idea.ui.HgVersionFilterComponent;
import org.zmlx.hg4idea.util.HgUtil;

import java.awt.datatransfer.StringSelection;
import java.io.File;
import java.text.SimpleDateFormat;
import java.util.*;

public class HgCommittedChangesProvider implements CommittedChangesProvider<CommittedChangeList, ChangeBrowserSettings> {

  private static final Logger LOG = Logger.getInstance(HgCommittedChangesProvider.class);
  private final Project project;
  private final HgVcs myVcs;

  public HgCommittedChangesProvider(Project project, HgVcs vcs) {
    this.project = project;
    myVcs = vcs;
  }

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

  @Override
  public @Nullable RepositoryLocation getLocationFor(@NotNull FilePath filePath) {
    VirtualFile repo = VcsUtil.getVcsRootFor(project, filePath);
    if (repo == null) {
      return null;
    }
    return new HgRepositoryLocation(repo.getUrl(), repo);
  }

  @Override
  public void loadCommittedChanges(ChangeBrowserSettings changeBrowserSettings,
                                   @NotNull RepositoryLocation repositoryLocation,
                                   int maxCount,
                                   @NotNull AsynchConsumer<? super CommittedChangeList> consumer) {
    try {
      List<CommittedChangeList> results = getCommittedChanges(changeBrowserSettings, repositoryLocation, maxCount);
      for (CommittedChangeList result : results) {
        consumer.consume(result);
      }
    }
    finally {
      consumer.finished();
    }
  }

  @Override
  public @NotNull List<CommittedChangeList> getCommittedChanges(ChangeBrowserSettings changeBrowserSettings,
                                                                @NotNull RepositoryLocation repositoryLocation,
                                                                int maxCount) {
    VirtualFile root = ((HgRepositoryLocation)repositoryLocation).getRoot();

    HgFile hgFile = new HgFile(root, VcsUtil.getFilePath(root.getPath()));

    List<CommittedChangeList> result = new LinkedList<>();
    HgLogCommand hgLogCommand = new HgLogCommand(project);
    hgLogCommand.setLogFile(false);
    List<String> args = null;
    if (changeBrowserSettings != null) {
      HgLogArgsBuilder argsBuilder = new HgLogArgsBuilder(changeBrowserSettings);
      args = argsBuilder.getLogArgs();
      if (args.isEmpty()) {
        maxCount = maxCount == 0 ? VcsConfiguration.getInstance(project).MAXIMUM_HISTORY_ROWS  : maxCount;
      }
    }
    final List<HgFileRevision> localRevisions;
    localRevisions = hgLogCommand.execute(hgFile, maxCount == 0 ? -1 : maxCount, true, args);
    Collections.reverse(localRevisions);

    for (HgFileRevision revision : localRevisions) {
      HgRevisionNumber vcsRevisionNumber = revision.getRevisionNumber();
      List<? extends HgRevisionNumber> parents = vcsRevisionNumber.getParents();

      HgRevisionNumber firstParent = parents.isEmpty() ? null : parents.get(0); // can have no parents if it is a root

      List<Change> changes = new ArrayList<>();
      for (String file : revision.getModifiedFiles()) {
        changes.add(createChange(root, file, firstParent, file, vcsRevisionNumber, FileStatus.MODIFIED));
      }
      for (String file : revision.getAddedFiles()) {
        changes.add(createChange(root, null, null, file, vcsRevisionNumber, FileStatus.ADDED));
      }
      for (String file : revision.getDeletedFiles()) {
        changes.add(createChange(root, file, firstParent, null, vcsRevisionNumber, FileStatus.DELETED));
      }
      for (Map.Entry<String, String> copiedFile : revision.getMovedFiles().entrySet()) {
        changes
          .add(createChange(root, copiedFile.getKey(), firstParent, copiedFile.getValue(), vcsRevisionNumber, HgChangeProvider.FileStatuses.RENAMED));
      }

      result.add(new HgCommittedChangeList(myVcs, vcsRevisionNumber, revision.getBranchName(), revision.getCommitMessage(),
                                           revision.getAuthor(), revision.getRevisionDate(), changes));
    }
    Collections.reverse(result);
    return result;
  }

  private Change createChange(VirtualFile root,
                              @Nullable String fileBefore,
                              @Nullable HgRevisionNumber revisionBefore,
                              @Nullable String fileAfter,
                              HgRevisionNumber revisionAfter,
                              FileStatus aStatus) {

    HgContentRevision beforeRevision =
      fileBefore == null ? null : HgContentRevision.create(project, new HgFile(root, new File(root.getPath(), fileBefore)), revisionBefore);
    HgContentRevision afterRevision =
      fileAfter == null ? null : HgContentRevision.create(project, new HgFile(root, new File(root.getPath(), fileAfter)), revisionAfter);
    return new Change(beforeRevision, afterRevision, aStatus);
  }

  @Override
  public ChangeListColumn @NotNull [] getColumns() {
    return new ChangeListColumn[]{BRANCH_COLUMN, ChangeListColumn.NUMBER, ChangeListColumn.DATE, ChangeListColumn.DESCRIPTION, ChangeListColumn.NAME};
  }

  @Override
  public @NotNull VcsCommittedViewAuxiliary createActions(@NotNull DecoratorManager manager, @Nullable RepositoryLocation location) {
    AnAction copyHashAction = new DumbAwareAction(
      HgBundle.messagePointer("action.DumbAware.HgCommittedChangesProvider.text.copy.hash"),
      HgBundle.messagePointer("action.DumbAware.HgCommittedChangesProvider.description.copy.hash.to.clipboard"),
      PlatformIcons.COPY_ICON) {
      @Override
      public void actionPerformed(@NotNull AnActionEvent e) {
        ChangeList[] changeLists = e.getData(VcsDataKeys.CHANGE_LISTS);
        if (changeLists != null && changeLists[0] instanceof HgCommittedChangeList) {
          HgRevisionNumber revisionNumber = ((HgCommittedChangeList)changeLists[0]).getRevisionNumber();
          CopyPasteManager.getInstance().setContents(new StringSelection(revisionNumber.getChangeset()));
        }
      }
    };
    return new VcsCommittedViewAuxiliary(Collections.singletonList(copyHashAction), () -> {
    }, Collections.singletonList(copyHashAction));
  }

  @Override
  public int getUnlimitedCountValue() {
    return -1;
  }

  @Override
  public @Nullable Pair<CommittedChangeList, FilePath> getOneList(VirtualFile file, @NotNull VcsRevisionNumber number) {
    final ChangeBrowserSettings settings = createDefaultSettings();
    settings.USE_CHANGE_AFTER_FILTER = true;
    settings.USE_CHANGE_BEFORE_FILTER = true;
    settings.CHANGE_AFTER = number.asString();
    settings.CHANGE_BEFORE = number.asString();
    // todo implement in proper way
    VirtualFile localVirtualFile = HgUtil.convertToLocalVirtualFile(file);
    if (localVirtualFile == null) {
      return null;
    }
    final FilePath filePath = VcsUtil.getFilePath(localVirtualFile);
    final CommittedChangeList list = getCommittedChangesForRevision(getLocationFor(filePath), number.asString());
    if (list != null) {
      return new Pair<>(list, filePath);
    }
    return null;
  }

  @Override
  public boolean supportsIncomingChanges() {
    return false;
  }

  public @Nullable CommittedChangeList getCommittedChangesForRevision(@Nullable RepositoryLocation repositoryLocation, String revision) {
    if (repositoryLocation == null) {
      return null;
    }
    VirtualFile root = ((HgRepositoryLocation)repositoryLocation).getRoot();
    HgFile hgFile = new HgFile(root, VcsUtil.getFilePath(root.getPath()));
    HgLogCommand hgLogCommand = new HgLogCommand(project);
    hgLogCommand.setLogFile(false);
    hgLogCommand.setFollowCopies(true);
    List<String> args = new ArrayList<>();
    args.add("--rev");
    args.add(revision);
    final List<HgFileRevision> revisions;
    revisions = hgLogCommand.execute(hgFile, 1, true, args);
    if (ContainerUtil.isEmpty(revisions)) {
      return null;
    }
    HgFileRevision localRevision = revisions.get(0);
    HgRevisionNumber vcsRevisionNumber = localRevision.getRevisionNumber();
    List<? extends HgRevisionNumber> parents = vcsRevisionNumber.getParents();
    HgRevisionNumber firstParent = parents.isEmpty() ? null : parents.get(0); // can have no parents if it is a root
    List<Change> changes = new ArrayList<>();
    for (String file : localRevision.getModifiedFiles()) {
      changes.add(createChange(root, file, firstParent, file, vcsRevisionNumber, FileStatus.MODIFIED));
    }
    for (String file : localRevision.getAddedFiles()) {
      changes.add(createChange(root, null, null, file, vcsRevisionNumber, FileStatus.ADDED));
    }
    for (String file : localRevision.getDeletedFiles()) {
      changes.add(createChange(root, file, firstParent, null, vcsRevisionNumber, FileStatus.DELETED));
    }
    for (Map.Entry<String, String> copiedFile : localRevision.getMovedFiles().entrySet()) {
      changes.add(createChange(root, copiedFile.getKey(), firstParent, copiedFile.getValue(), vcsRevisionNumber, HgChangeProvider.FileStatuses.RENAMED));
    }

    return new HgCommittedChangeList(myVcs, vcsRevisionNumber, localRevision.getBranchName(), localRevision.getCommitMessage(),
                                     localRevision.getAuthor(), localRevision.getRevisionDate(), changes);
  }

  private static final Comparator<HgCommittedChangeList> BRANCH_COLUMN_COMPARATOR =
    (o1, o2) -> Comparing.compare(o1.getBranch(), o2.getBranch());

  private static final ChangeListColumn<HgCommittedChangeList> BRANCH_COLUMN = new ChangeListColumn<>() {
    @Override
    public String getTitle() {
      return HgBundle.message("hg4idea.changelist.column.branch");
    }

    @Override
    public Object getValue(final HgCommittedChangeList changeList) {
      final String branch = changeList.getBranch();
      return branch.isEmpty() ? "default" : branch;
    }

    @Override
    public @NotNull Comparator<HgCommittedChangeList> getComparator() {
      return BRANCH_COLUMN_COMPARATOR;
    }
  };

  private static class HgLogArgsBuilder {

    private final @NotNull ChangeBrowserSettings myBrowserSettings;

    HgLogArgsBuilder(@NotNull ChangeBrowserSettings browserSettings) {
      myBrowserSettings = browserSettings;
    }

    @NotNull
    List<String> getLogArgs() {

      StringBuilder args = new StringBuilder();
      Date afterDate = myBrowserSettings.getDateAfter();
      Date beforeDate = myBrowserSettings.getDateBefore();
      Long afterFilter = myBrowserSettings.getChangeAfterFilter();
      Long beforeFilter = myBrowserSettings.getChangeBeforeFilter();

      final SimpleDateFormat dateFormatter = new SimpleDateFormat("yyyy-MM-dd HH:mm");

      if ((afterFilter != null) && (beforeFilter != null)) {
        args.append(afterFilter).append(":").append(beforeFilter);
      }
      else if (afterFilter != null) {
        args.append("tip:").append(afterFilter);
        if (afterFilter == 0) {
          LOG.debug("Get repository commits not from Log", new Throwable());
        }
      }
      else if (beforeFilter != null) {
        args.append("reverse(:").append(beforeFilter).append(")");
      }

      if (afterDate != null) {
        if (!args.isEmpty()) {
          args.append(" and ");
        }
        args.append("date('>").append(dateFormatter.format(afterDate)).append("')");
      }

      if (beforeDate != null) {
        if (!args.isEmpty()) {
          args.append(" and ");
        }

        args.append("date('<").append(dateFormatter.format(beforeDate)).append("')");
      }

      if (!args.isEmpty()) {
        List<String> logArgs = new ArrayList<>();
        logArgs.add("-r");
        logArgs.add(args.toString());
        return logArgs;
      }

      return Collections.emptyList();
    }
  }
}
