/*
 * Copyright 2000-2015 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 com.intellij.openapi.vcs.contentAnnotation;

import com.intellij.execution.filters.ExceptionInfoCache;
import com.intellij.execution.filters.ExceptionWorker;
import com.intellij.execution.filters.Filter;
import com.intellij.execution.filters.FilterMixin;
import com.intellij.openapi.application.ReadAction;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.diff.DiffColors;
import com.intellij.openapi.editor.DefaultLanguageHighlighterColors;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.editor.colors.EditorColorsManager;
import com.intellij.openapi.editor.colors.EditorColorsScheme;
import com.intellij.openapi.editor.markup.TextAttributes;
import com.intellij.openapi.fileEditor.FileDocumentManager;
import com.intellij.openapi.localVcs.UpToDateLineNumberProvider;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.TextRange;
import com.intellij.openapi.util.Trinity;
import com.intellij.openapi.vcs.FileStatus;
import com.intellij.openapi.vcs.changes.ChangeListManager;
import com.intellij.openapi.vcs.history.VcsRevisionNumber;
import com.intellij.openapi.vcs.impl.UpToDateLineNumberProviderImpl;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.*;
import com.intellij.psi.search.GlobalSearchScope;
import com.intellij.util.Consumer;
import com.intellij.util.SmartList;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.util.*;

public class VcsContentAnnotationExceptionFilter implements Filter, FilterMixin {
  private final Project myProject;
  private static final Logger LOG = Logger.getInstance("#com.intellij.openapi.vcs.contentAnnotation.VcsContentAnnotationExceptionFilter");
  private final VcsContentAnnotationSettings mySettings;
  private final Map<VirtualFile,VcsRevisionNumber> myRevNumbersCache;
  private final ExceptionInfoCache myCache;

  public VcsContentAnnotationExceptionFilter(@NotNull GlobalSearchScope scope) {
    myProject = scope.getProject();
    mySettings = VcsContentAnnotationSettings.getInstance(myProject);
    myRevNumbersCache = new HashMap<>();
    myCache = new ExceptionInfoCache(scope);
  }

  private static class MyAdditionalHighlight extends AdditionalHighlight {
    private MyAdditionalHighlight(int start, int end) {
      super(start, end);
    }

    @NotNull
    @Override
    public TextAttributes getTextAttributes(@Nullable TextAttributes source) {
      EditorColorsScheme globalScheme = EditorColorsManager.getInstance().getGlobalScheme();
      final TextAttributes changedColor = globalScheme.getAttributes(DiffColors.DIFF_MODIFIED);
      if (source == null) {
        TextAttributes attrs =
          globalScheme.getAttributes(DefaultLanguageHighlighterColors.CLASS_NAME).clone();
        attrs.setBackgroundColor(changedColor.getBackgroundColor());
        return attrs;
      }
      TextAttributes clone = source.clone();
      clone.setBackgroundColor(changedColor.getBackgroundColor());
      return clone;
    }
  }

  @Override
  public boolean shouldRunHeavy() {
    return mySettings.isShow();
  }

  @Override
  public void applyHeavyFilter(@NotNull final Document copiedFragment,
                               int startOffset,
                               int startLineNumber,
                               @NotNull Consumer<AdditionalHighlight> consumer) {
    VcsContentAnnotation vcsContentAnnotation = VcsContentAnnotationImpl.getInstance(myProject);
    final LocalChangesCorrector localChangesCorrector = new LocalChangesCorrector(myProject);
    Trinity<PsiClass, PsiFile, String> previousLineResult = null;

    for (int i = 0; i < copiedFragment.getLineCount(); i++) {
      final int lineStartOffset = copiedFragment.getLineStartOffset(i);
      final int lineEndOffset = copiedFragment.getLineEndOffset(i);
      final ExceptionWorker worker = new ExceptionWorker(myCache);
      final String lineText = copiedFragment.getText(new TextRange(lineStartOffset, lineEndOffset));
      if (ReadAction.compute(() -> worker.execute(lineText, lineEndOffset)) != null) {
        VirtualFile vf = worker.getFile().getVirtualFile();
        if (vf.getFileSystem().isReadOnly()) continue;

        VcsRevisionNumber recentChangeRevision = myRevNumbersCache.get(vf);
        if (recentChangeRevision == null) {
          recentChangeRevision = vcsContentAnnotation.fileRecentlyChanged(vf);
          if (recentChangeRevision == null) {
            myRevNumbersCache.put(vf, VcsRevisionNumber.NULL);
          } else {
            myRevNumbersCache.put(vf, recentChangeRevision);
          }
        }
        if (VcsRevisionNumber.NULL.equals(recentChangeRevision)) {
          recentChangeRevision = null;
        }

        FileStatus status = ChangeListManager.getInstance(myProject).getStatus(vf);
        boolean isFileChanged = FileStatus.NOT_CHANGED.equals(status) ||
                                FileStatus.UNKNOWN.equals(status) ||
                                FileStatus.IGNORED.equals(status);

        if (localChangesCorrector.isFileAlreadyIdentifiedAsChanged(vf) || isFileChanged || recentChangeRevision != null) {
          final Document document = getDocumentForFile(worker);
          if (document == null) return;

          int startFileOffset = worker.getInfo().fileLineRange.getStartOffset();
          int idx = lineText.indexOf(':', startFileOffset);
          int endIdx = idx == -1 ? worker.getInfo().fileLineRange.getEndOffset() : idx;
          consumer.consume(new MyAdditionalHighlight(startOffset + lineStartOffset + startFileOffset + 1, startOffset + lineStartOffset + endIdx));

          if (worker.getPsiClass() != null) {
            // also check method
            final List<TextRange> ranges = findMethodRange(worker, document, previousLineResult);
            if (ranges != null) {
              boolean methodChanged = false;
              for (TextRange range : ranges) {
                if (localChangesCorrector.isRangeChangedLocally(vf, document, range)) {
                  methodChanged = true;
                  break;
                }
                final TextRange correctedRange = localChangesCorrector.getCorrectedRange(vf, document, range);
                if (vcsContentAnnotation.intervalRecentlyChanged(vf, correctedRange, recentChangeRevision)) {
                  methodChanged = true;
                  break;
                }
              }
              if (methodChanged) {
                consumer.consume(new MyAdditionalHighlight(startOffset + lineStartOffset + worker.getInfo().methodNameRange.getStartOffset(),
                                                           startOffset + lineStartOffset + worker.getInfo().methodNameRange.getEndOffset()));
              }
            }
          }
        }
      }
      previousLineResult = worker.getResult() == null ? null :
                           new Trinity<>(worker.getPsiClass(), worker.getFile(), worker.getMethod());
    }
  }

  @NotNull
  @Override
  public String getUpdateMessage() {
    return "Checking recent changes...";
  }

  private static class LocalChangesCorrector {
    private final Map<VirtualFile, UpToDateLineNumberProvider> myRecentlyChanged;
    private final Project myProject;

    private LocalChangesCorrector(final Project project) {
      myProject = project;
      myRecentlyChanged = new HashMap<>();
    }

    public boolean isFileAlreadyIdentifiedAsChanged(final VirtualFile vf) {
      return myRecentlyChanged.containsKey(vf);
    }
    
    public boolean isRangeChangedLocally(final VirtualFile vf, final Document document, final TextRange range) {
      final UpToDateLineNumberProvider provider = getProvider(vf, document);
      return ReadAction.compute(() -> provider.isRangeChanged(range.getStartOffset(), range.getEndOffset()));
    }
    
    public TextRange getCorrectedRange(final VirtualFile vf, final Document document, final TextRange range) {
      final UpToDateLineNumberProvider provider = getProvider(vf, document);
      if (provider == null) return range;
      return ReadAction
        .compute(() -> new TextRange(provider.getLineNumber(range.getStartOffset()), provider.getLineNumber(range.getEndOffset())));
    }

    private UpToDateLineNumberProvider getProvider(VirtualFile vf, Document document) {
      UpToDateLineNumberProvider provider = myRecentlyChanged.get(vf);
      if (provider == null) {
        provider = new UpToDateLineNumberProviderImpl(document, myProject);
        myRecentlyChanged.put(vf, provider);
      }
      return provider;
    }
  }

  private static Document getDocumentForFile(final ExceptionWorker worker) {
    return ReadAction.compute(() -> {
      final Document document = FileDocumentManager.getInstance().getDocument(worker.getFile().getVirtualFile());
      if (document == null) {
        LOG.info("can not get document for file: " + worker.getFile().getVirtualFile());
        return null;
      }
      return document;
    });
  }

  // line numbers
  private static List<TextRange> findMethodRange(final ExceptionWorker worker,
                                                 final Document document,
                                                 final Trinity<PsiClass, PsiFile, String> previousLineResult) {
    return ReadAction.compute(() -> {
      List<TextRange> ranges = getTextRangeForMethod(worker, previousLineResult);
      if (ranges == null) return null;
      final List<TextRange> result = new ArrayList<>();
      for (TextRange range : ranges) {
        result.add(new TextRange(document.getLineNumber(range.getStartOffset()),
                                 document.getLineNumber(range.getEndOffset())));
      }
      return result;
    });
  }

  // null - check all
  @Nullable
  private static List<PsiMethod> selectMethod(final PsiMethod[] methods, final Trinity<PsiClass, PsiFile, String> previousLineResult) {
    if (previousLineResult == null || previousLineResult.getThird() == null) return null;

    final List<PsiMethod> result = new SmartList<>();
    for (final PsiMethod method : methods) {
      method.accept(new JavaRecursiveElementVisitor() {
        @Override
        public void visitCallExpression(PsiCallExpression callExpression) {
          final PsiMethod resolved = callExpression.resolveMethod();
          if (resolved != null) {
            if (resolved.getName().equals(previousLineResult.getThird())) {
              result.add(method);
            }
          }
        }
      });
    }

    return result;
  }

  private static List<TextRange> getTextRangeForMethod(final ExceptionWorker worker, Trinity<PsiClass, PsiFile, String> previousLineResult) {
    String method = worker.getMethod();
    PsiClass psiClass = worker.getPsiClass();
    PsiMethod[] methods;
    if (method.contains("<init>")) {
      // constructor
      methods = psiClass.getConstructors();
    } else if (method.contains("$")) {
      // access$100
      return null;
    } else {
      methods = psiClass.findMethodsByName(method, false);
    }
    if (methods.length > 0) {
      if (methods.length == 1) {
        final TextRange range = methods[0].getTextRange();
        return Collections.singletonList(range);
      } else {
        List<PsiMethod> selectedMethods = selectMethod(methods, previousLineResult);
        final List<PsiMethod> toIterate = selectedMethods == null ? Arrays.asList(methods) : selectedMethods;
        final List<TextRange> result = new ArrayList<>();
        for (PsiMethod psiMethod : toIterate) {
          result.add(psiMethod.getTextRange());
        }
        return result;
      }
    }
    return null;
  }

  @Override
  public Result applyFilter(String line, int entireLength) {
    return null;
  }
}
