/*
 * Copyright 2000-2016 JetBrains s.r.o.
 *
 * 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 git4idea.annotate;

import com.intellij.openapi.project.Project;
import com.intellij.openapi.vcs.FilePath;
import com.intellij.openapi.vcs.VcsException;
import com.intellij.openapi.vcs.VcsKey;
import com.intellij.openapi.vcs.annotate.FileAnnotation;
import com.intellij.openapi.vcs.annotate.LineAnnotationAspect;
import com.intellij.openapi.vcs.annotate.LineAnnotationAspectAdapter;
import com.intellij.openapi.vcs.annotate.ShowAllAffectedGenericAction;
import com.intellij.openapi.vcs.changes.ContentRevision;
import com.intellij.openapi.vcs.history.VcsFileRevision;
import com.intellij.openapi.vcs.history.VcsRevisionNumber;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.util.ObjectUtils;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.containers.hash.HashMap;
import com.intellij.util.text.DateFormatUtil;
import com.intellij.vcs.log.VcsUser;
import com.intellij.vcsUtil.VcsUtil;
import git4idea.GitContentRevision;
import git4idea.GitFileRevision;
import git4idea.GitRevisionNumber;
import git4idea.GitVcs;
import git4idea.i18n.GitBundle;
import gnu.trove.TObjectIntHashMap;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.util.*;

public class GitFileAnnotation extends FileAnnotation {
  private final Project myProject;
  @NotNull private final VirtualFile myFile;
  @NotNull private final GitVcs myVcs;
  @Nullable private final VcsRevisionNumber myBaseRevision;

  @NotNull private final List<LineInfo> myLines;
  @Nullable private List<VcsFileRevision> myRevisions;
  @Nullable private TObjectIntHashMap<VcsRevisionNumber> myRevisionMap;

  private final LineAnnotationAspect DATE_ASPECT = new GitAnnotationAspect(LineAnnotationAspect.DATE, true) {
    @Override
    public String doGetValue(LineInfo info) {
      return DateFormatUtil.formatPrettyDate(info.getDate());
    }
  };

  private final LineAnnotationAspect REVISION_ASPECT = new GitAnnotationAspect(LineAnnotationAspect.REVISION, false) {
    @Override
    protected String doGetValue(LineInfo lineInfo) {
      return String.valueOf(lineInfo.getRevisionNumber().getShortRev());
    }
  };

  private final LineAnnotationAspect AUTHOR_ASPECT = new GitAnnotationAspect(LineAnnotationAspect.AUTHOR, true) {
    @Override
    protected String doGetValue(LineInfo lineInfo) {
      return lineInfo.getAuthor();
    }
  };

  public GitFileAnnotation(@NotNull Project project,
                           @NotNull VirtualFile file,
                           @Nullable VcsRevisionNumber revision,
                           @NotNull List<LineInfo> lines) {
    super(project);
    myProject = project;
    myFile = file;
    myVcs = ObjectUtils.assertNotNull(GitVcs.getInstance(myProject));
    myBaseRevision = revision;
    myLines = lines;
  }

  public GitFileAnnotation(@NotNull GitFileAnnotation annotation) {
    this(annotation.getProject(), annotation.getFile(), annotation.getCurrentRevision(), annotation.getLines());
  }

  @Override
  public void dispose() {
  }

  @Override
  public LineAnnotationAspect[] getAspects() {
    return new LineAnnotationAspect[]{REVISION_ASPECT, DATE_ASPECT, AUTHOR_ASPECT};
  }

  @Nullable
  @Override
  public String getAnnotatedContent() {
    try {
      ContentRevision revision = GitContentRevision.createRevision(myFile, myBaseRevision, myProject);
      return revision.getContent();
    }
    catch (VcsException e) {
      return null;
    }
  }

  @Override
  public List<VcsFileRevision> getRevisions() {
    return myRevisions;
  }

  public void setRevisions(@NotNull List<VcsFileRevision> revisions) {
    myRevisions = revisions;

    myRevisionMap = new TObjectIntHashMap<>();
    for (int i = 0; i < myRevisions.size(); i++) {
      myRevisionMap.put(myRevisions.get(i).getRevisionNumber(), i);
    }
  }

  @Override
  public int getLineCount() {
    return myLines.size();
  }

  @Nullable
  public LineInfo getLineInfo(int lineNumber) {
    if (lineNumberCheck(lineNumber)) return null;
    return myLines.get(lineNumber);
  }

  @Nullable
  @Override
  public String getToolTip(int lineNumber) {
    LineInfo lineInfo = getLineInfo(lineNumber);
    if (lineInfo == null) return null;

    GitRevisionNumber revisionNumber = lineInfo.getRevisionNumber();

    VcsFileRevision fileRevision = null;
    if (myRevisions != null && myRevisionMap != null &&
        myRevisionMap.contains(revisionNumber)) {
      fileRevision = myRevisions.get(myRevisionMap.get(revisionNumber));
    }

    String commitMessage = fileRevision != null ? fileRevision.getCommitMessage() : lineInfo.getSubject() + "\n...";
    return GitBundle.message("annotation.tool.tip", revisionNumber.asString(), lineInfo.getAuthor(),
                             DateFormatUtil.formatDateTime(lineInfo.getDate()), commitMessage);
  }

  @Nullable
  @Override
  public VcsRevisionNumber getLineRevisionNumber(int lineNumber) {
    LineInfo lineInfo = getLineInfo(lineNumber);
    return lineInfo != null ? lineInfo.getRevisionNumber() : null;
  }

  @Nullable
  @Override
  public Date getLineDate(int lineNumber) {
    LineInfo lineInfo = getLineInfo(lineNumber);
    return lineInfo != null ? lineInfo.getDate() : null;
  }

  private boolean lineNumberCheck(int lineNumber) {
    return myLines.size() <= lineNumber || lineNumber < 0;
  }

  @NotNull
  public List<LineInfo> getLines() {
    return myLines;
  }

  /**
   * Revision annotation aspect implementation
   */
  private abstract class GitAnnotationAspect extends LineAnnotationAspectAdapter {
    public GitAnnotationAspect(String id, boolean showByDefault) {
      super(id, showByDefault);
    }

    @Override
    public String getValue(int lineNumber) {
      if (lineNumberCheck(lineNumber)) {
        return "";
      }
      else {
        return doGetValue(myLines.get(lineNumber));
      }
    }

    protected abstract String doGetValue(LineInfo lineInfo);

    @Override
    protected void showAffectedPaths(int lineNum) {
      if (lineNum >= 0 && lineNum < myLines.size()) {
        LineInfo info = myLines.get(lineNum);
        ShowAllAffectedGenericAction.showSubmittedFiles(myProject, info.getRevisionNumber(), myFile, GitVcs.getKey());
      }
    }
  }

  static class LineInfo {
    @NotNull private final Project myProject;
    @NotNull private final GitRevisionNumber myRevision;
    @NotNull private final FilePath myFilePath;
    @Nullable private final GitRevisionNumber myPreviousRevision;
    @Nullable private final FilePath myPreviousFilePath;
    @NotNull private final Date myDate;
    @NotNull private final VcsUser myAuthor;
    @NotNull private final String mySubject;

    public LineInfo(@NotNull Project project,
                    @NotNull GitRevisionNumber revision,
                    @NotNull FilePath path,
                    @NotNull Date date,
                    @NotNull VcsUser author,
                    @NotNull String subject,
                    @Nullable GitRevisionNumber previousRevision,
                    @Nullable FilePath previousPath) {
      myProject = project;
      myRevision = revision;
      myFilePath = path;
      myPreviousRevision = previousRevision;
      myPreviousFilePath = previousPath;
      myDate = date;
      myAuthor = author;
      mySubject = subject;
    }

    @NotNull
    public GitRevisionNumber getRevisionNumber() {
      return myRevision;
    }

    @NotNull
    public FilePath getFilePath() {
      return myFilePath;
    }

    @NotNull
    public VcsFileRevision getFileRevision() {
      return new GitFileRevision(myProject, myFilePath, myRevision);
    }

    @Nullable
    public VcsFileRevision getPreviousFileRevision() {
      if (myPreviousRevision == null || myPreviousFilePath == null) return null;
      return new GitFileRevision(myProject, myPreviousFilePath, myPreviousRevision);
    }

    @NotNull
    public Date getDate() {
      return myDate;
    }

    @NotNull
    public String getAuthor() {
      return myAuthor.getName();
    }

    @NotNull
    public String getSubject() {
      return mySubject;
    }
  }

  @NotNull
  @Override
  public VirtualFile getFile() {
    return myFile;
  }

  @Nullable
  @Override
  public VcsRevisionNumber getCurrentRevision() {
    return myBaseRevision;
  }

  @Override
  public VcsKey getVcsKey() {
    return GitVcs.getKey();
  }

  @Override
  public boolean isBaseRevisionChanged(VcsRevisionNumber number) {
    final VcsRevisionNumber currentCurrentRevision = myVcs.getDiffProvider().getCurrentRevision(myFile);
    return myBaseRevision != null && ! myBaseRevision.equals(currentCurrentRevision);
  }


  @Nullable
  @Override
  public CurrentFileRevisionProvider getCurrentFileRevisionProvider() {
    return (lineNumber) -> {
      LineInfo lineInfo = getLineInfo(lineNumber);
      return lineInfo != null ? lineInfo.getFileRevision() : null;
    };
  }

  @Nullable
  @Override
  public PreviousFileRevisionProvider getPreviousFileRevisionProvider() {
    return new PreviousFileRevisionProvider() {
      @Nullable
      @Override
      public VcsFileRevision getPreviousRevision(int lineNumber) {
        LineInfo lineInfo = getLineInfo(lineNumber);
        if (lineInfo == null) return null;

        VcsFileRevision previousFileRevision = lineInfo.getPreviousFileRevision();
        if (previousFileRevision != null) return previousFileRevision;

        GitRevisionNumber revisionNumber = lineInfo.getRevisionNumber();
        if (myRevisions != null && myRevisionMap != null &&
            myRevisionMap.contains(revisionNumber)) {
          int index = myRevisionMap.get(revisionNumber);
          if (index + 1 < myRevisions.size()) {
            return myRevisions.get(index + 1);
          }
        }

        return null;
      }

      @Nullable
      @Override
      public VcsFileRevision getLastRevision() {
        if (myBaseRevision instanceof GitRevisionNumber) {
          return new GitFileRevision(myProject, VcsUtil.getFilePath(myFile), (GitRevisionNumber)myBaseRevision);
        }
        else {
          return ContainerUtil.getFirstItem(getRevisions());
        }
      }
    };
  }

  @Nullable
  @Override
  public AuthorsMappingProvider getAuthorsMappingProvider() {
    Map<VcsRevisionNumber, String> authorsMap = new HashMap<>();
    for (int i = 0; i < getLineCount(); i++) {
      LineInfo lineInfo = getLineInfo(i);
      if (lineInfo == null) continue;

      if (!authorsMap.containsKey(lineInfo.getRevisionNumber())) {
        authorsMap.put(lineInfo.getRevisionNumber(), lineInfo.getAuthor());
      }
    }

    return () -> authorsMap;
  }

  @Nullable
  @Override
  public RevisionsOrderProvider getRevisionsOrderProvider() {
    ContainerUtil.KeyOrderedMultiMap<Date, VcsRevisionNumber> dates = new ContainerUtil.KeyOrderedMultiMap<>();

    for (int i = 0; i < getLineCount(); i++) {
      LineInfo lineInfo = getLineInfo(i);
      if (lineInfo == null) continue;

      VcsRevisionNumber number = lineInfo.getRevisionNumber();
      Date date = lineInfo.getDate();

      dates.putValue(date, number);
    }

    List<List<VcsRevisionNumber>> orderedRevisions = new ArrayList<>();
    NavigableSet<Date> orderedDates = dates.navigableKeySet();
    for (Date date : orderedDates.descendingSet()) {
      Collection<VcsRevisionNumber> revisionNumbers = dates.get(date);
      orderedRevisions.add(new ArrayList<>(revisionNumbers));
    }

    return () -> orderedRevisions;
  }
}
