/*
 * Copyright 2000-2009 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.psi.impl.search;

import com.intellij.lang.Language;
import com.intellij.lang.LanguageParserDefinitions;
import com.intellij.lang.ParserDefinition;
import com.intellij.lexer.Lexer;
import com.intellij.openapi.extensions.Extensions;
import com.intellij.openapi.fileTypes.FileType;
import com.intellij.openapi.fileTypes.SyntaxHighlighter;
import com.intellij.openapi.fileTypes.SyntaxHighlighterFactory;
import com.intellij.openapi.fileTypes.impl.CustomSyntaxTableFileType;
import com.intellij.openapi.progress.ProgressManager;
import com.intellij.openapi.util.TextRange;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.*;
import com.intellij.psi.impl.cache.CacheUtil;
import com.intellij.psi.impl.cache.TodoCacheManager;
import com.intellij.psi.search.IndexPattern;
import com.intellij.psi.search.IndexPatternOccurrence;
import com.intellij.psi.search.IndexPatternProvider;
import com.intellij.psi.search.searches.IndexPatternSearch;
import com.intellij.psi.tree.IElementType;
import com.intellij.psi.tree.TokenSet;
import com.intellij.util.Processor;
import com.intellij.util.QueryExecutor;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.text.CharSequenceSubSequence;
import gnu.trove.TIntArrayList;
import org.jetbrains.annotations.NotNull;

import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * @author yole
 */
public class IndexPatternSearcher implements QueryExecutor<IndexPatternOccurrence, IndexPatternSearch.SearchParameters> {
  @Override
  public boolean execute(@NotNull final IndexPatternSearch.SearchParameters queryParameters,
                         @NotNull final Processor<IndexPatternOccurrence> consumer) {
    final PsiFile file = queryParameters.getFile();
    VirtualFile virtualFile = file.getVirtualFile();
    if (file instanceof PsiBinaryFile || file instanceof PsiCompiledElement || virtualFile == null) {
      return true;
    }

    final TodoCacheManager cacheManager = TodoCacheManager.SERVICE.getInstance(file.getProject());
    final IndexPatternProvider patternProvider = queryParameters.getPatternProvider();
    int count = patternProvider != null
                ? cacheManager.getTodoCount(virtualFile, patternProvider)
                : cacheManager.getTodoCount(virtualFile, queryParameters.getPattern());
    return count == 0 || executeImpl(queryParameters, consumer);
  }

  protected static boolean executeImpl(IndexPatternSearch.SearchParameters queryParameters,
                                       Processor<IndexPatternOccurrence> consumer) {
    final IndexPatternProvider patternProvider = queryParameters.getPatternProvider();
    final PsiFile file = queryParameters.getFile();
    TIntArrayList commentStarts = new TIntArrayList();
    TIntArrayList commentEnds = new TIntArrayList();

    final CharSequence chars = file.getViewProvider().getContents();
    findCommentTokenRanges(file, chars, queryParameters.getRange(), commentStarts, commentEnds);
    TIntArrayList occurrences = new TIntArrayList(1);
    IndexPattern[] patterns = patternProvider != null ? patternProvider.getIndexPatterns() : null;

    for (int i = 0; i < commentStarts.size(); i++) {
      int commentStart = commentStarts.get(i);
      int commentEnd = commentEnds.get(i);
      occurrences.resetQuick();

      if (patternProvider != null) {
        for (int j = patterns.length - 1; j >=0; --j) {
          if (!collectPatternMatches(patterns[j], chars, commentStart, commentEnd, file, queryParameters.getRange(), consumer, occurrences)) {
            return false;
          }
        }
      }
      else {
        if (!collectPatternMatches(queryParameters.getPattern(), chars, commentStart, commentEnd, file, queryParameters.getRange(),
                                   consumer, occurrences)) {
          return false;
        }
      }
    }

    return true;
  }


  private static final TokenSet COMMENT_TOKENS =
    TokenSet.create(CustomHighlighterTokenType.LINE_COMMENT, CustomHighlighterTokenType.MULTI_LINE_COMMENT);

  private static void findCommentTokenRanges(final PsiFile file,
                                             final CharSequence chars,
                                             final TextRange range,
                                             final TIntArrayList commentStarts,
                                             final TIntArrayList commentEnds) {
    if (file instanceof PsiPlainTextFile) {
      FileType fType = file.getFileType();
      if (fType instanceof CustomSyntaxTableFileType) {
        Lexer lexer = SyntaxHighlighterFactory.getSyntaxHighlighter(fType, file.getProject(), file.getVirtualFile()).getHighlightingLexer();
        findComments(lexer, chars, range, COMMENT_TOKENS, commentStarts, commentEnds, null);
      }
      else {
        commentStarts.add(0);
        commentEnds.add(file.getTextLength());
      }
    }
    else {
      final FileViewProvider viewProvider = file.getViewProvider();
      final Set<Language> relevantLanguages = viewProvider.getLanguages();
      for (Language lang : relevantLanguages) {
        final TIntArrayList commentStartsList = new TIntArrayList();
        final TIntArrayList commentEndsList = new TIntArrayList();

        final SyntaxHighlighter syntaxHighlighter =
          SyntaxHighlighterFactory.getSyntaxHighlighter(lang, file.getProject(), file.getVirtualFile());
        Lexer lexer = syntaxHighlighter.getHighlightingLexer();
        TokenSet commentTokens = null;
        IndexPatternBuilder builderForFile = null;
        for (IndexPatternBuilder builder : Extensions.getExtensions(IndexPatternBuilder.EP_NAME)) {
          Lexer lexerFromBuilder = builder.getIndexingLexer(file);
          if (lexerFromBuilder != null) {
            lexer = lexerFromBuilder;
            commentTokens = builder.getCommentTokenSet(file);
            builderForFile = builder;
          }
        }
        if (builderForFile == null) {
          final ParserDefinition parserDefinition = LanguageParserDefinitions.INSTANCE.forLanguage(lang);
          if (parserDefinition != null) {
            commentTokens = parserDefinition.getCommentTokens();
          }
        }

        if (commentTokens != null) {
          findComments(lexer, chars, range, commentTokens, commentStartsList, commentEndsList, builderForFile);
          mergeCommentLists(commentStarts, commentEnds, commentStartsList, commentEndsList);
        }
      }
    }
  }

  private static void mergeCommentLists(TIntArrayList commentStarts,
                                        TIntArrayList commentEnds,
                                        TIntArrayList commentStartsList,
                                        TIntArrayList commentEndsList) {
    if (commentStarts.isEmpty() && commentEnds.isEmpty()) {
      commentStarts.add(commentStartsList.toNativeArray());
      commentEnds.add(commentEndsList.toNativeArray());
      return;
    }

    ContainerUtil.mergeSortedArrays(commentStarts, commentEnds, commentStartsList, commentEndsList);
  }

  private static void findComments(final Lexer lexer,
                                   final CharSequence chars,
                                   final TextRange range,
                                   final TokenSet commentTokens,
                                   final TIntArrayList commentStarts,
                                   final TIntArrayList commentEnds,
                                   final IndexPatternBuilder builderForFile) {
    for (lexer.start(chars); ; lexer.advance()) {
      IElementType tokenType = lexer.getTokenType();
      if (tokenType == null) break;

      if (range != null) {
        if (lexer.getTokenEnd() <= range.getStartOffset()) continue;
        if (lexer.getTokenStart() >= range.getEndOffset()) break;
      }

      boolean isComment = commentTokens.contains(tokenType) || CacheUtil.isInComments(tokenType);

      if (isComment) {
        final int startDelta = builderForFile != null ? builderForFile.getCommentStartDelta(lexer.getTokenType()) : 0;
        final int endDelta = builderForFile != null ? builderForFile.getCommentEndDelta(lexer.getTokenType()) : 0;

        int start = lexer.getTokenStart() + startDelta;
        int end = lexer.getTokenEnd() - endDelta;
        assert start <= end : "Invalid comment range: " +
                              new TextRange(start, end) +
                              "; lexer token range=" +
                              new TextRange(lexer.getTokenStart(), lexer.getTokenEnd()) +
                              "; delta=" +
                              new TextRange(startDelta, endDelta) +
                              "; lexer=" +
                              lexer +
                              "; builder=" +
                              builderForFile +
                              "; chars length:" +
                              chars.length();
        assert end <= chars.length() : "Invalid comment end: " +
                                       new TextRange(start, end) +
                                       "; lexer token range=" +
                                       new TextRange(lexer.getTokenStart(), lexer.getTokenEnd()) +
                                       "; delta=" +
                                       new TextRange(startDelta, endDelta) +
                                       "; lexer=" +
                                       lexer +
                                       "; builder=" +
                                       builderForFile +
                                       "; chars length:" +
                                       chars.length();
        commentStarts.add(start);
        commentEnds.add(end);
      }
    }
  }

  private static boolean collectPatternMatches(IndexPattern indexPattern,
                                               CharSequence chars,
                                               int commentStart,
                                               int commentEnd,
                                               PsiFile file,
                                               TextRange range,
                                               Processor<IndexPatternOccurrence> consumer,
                                               TIntArrayList matches
                                               ) {
    Pattern pattern = indexPattern.getPattern();
    if (pattern != null) {
      ProgressManager.checkCanceled();

      CharSequence input = new CharSequenceSubSequence(chars, commentStart, commentEnd);
      Matcher matcher = pattern.matcher(input);
      while (true) {
        //long time1 = System.currentTimeMillis();
        boolean found = matcher.find();
        //long time2 = System.currentTimeMillis();
        //System.out.println("scanned text of length " + (lexer.getTokenEnd() - lexer.getTokenStart() + " in " + (time2 - time1) + " ms"));

        if (!found) break;
        int start = matcher.start() + commentStart;
        int end = matcher.end() + commentStart;
        if (start != end) {
          if ((range == null || range.getStartOffset() <= start && end <= range.getEndOffset()) && matches.indexOf(start) == -1) {
            matches.add(start);
            if (!consumer.process(new IndexPatternOccurrenceImpl(file, start, end, indexPattern))) {
              return false;
            }
          }
        }

        ProgressManager.checkCanceled();
      }
    }
    return true;
  }
}
