// Copyright 2000-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.intellij.codeInsight.completion.impl;

import com.intellij.codeInsight.CodeInsightSettings;
import com.intellij.codeInsight.completion.CompletionUtil;
import com.intellij.codeInsight.completion.PrefixMatcher;
import com.intellij.codeInsight.lookup.LookupElement;
import consulo.disposer.Disposable;
import consulo.disposer.Disposer;
import com.intellij.openapi.util.TextRange;
import com.intellij.openapi.util.registry.Registry;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.psi.codeStyle.MinusculeMatcher;
import com.intellij.psi.codeStyle.NameUtil;
import com.intellij.util.containers.FList;
import com.intellij.util.text.CharArrayUtil;
import javax.annotation.Nonnull;

import org.jetbrains.annotations.TestOnly;

import javax.annotation.Nullable;

/**
 * @author peter
 */
public class CamelHumpMatcher extends PrefixMatcher {
  private final MinusculeMatcher myMatcher;
  private final MinusculeMatcher myCaseInsensitiveMatcher;
  private final boolean myCaseSensitive;
  private static boolean ourForceStartMatching;
  private final boolean myTypoTolerant;


  public CamelHumpMatcher(@Nonnull final String prefix) {
    this(prefix, true);
  }

  public CamelHumpMatcher(String prefix, boolean caseSensitive) {
    this(prefix, caseSensitive, false);
  }

  CamelHumpMatcher(String prefix, boolean caseSensitive, boolean typoTolerant) {
    super(prefix);
    myCaseSensitive = caseSensitive;
    myTypoTolerant = typoTolerant;
    myMatcher = createMatcher(myCaseSensitive);
    myCaseInsensitiveMatcher = createMatcher(false);
  }

  @Override
  public boolean isStartMatch(String name) {
    return myMatcher.isStartMatch(name);
  }

  @Override
  public boolean isStartMatch(LookupElement element) {
    for (String s : CompletionUtil.iterateLookupStrings(element)) {
      FList<TextRange> ranges = myCaseInsensitiveMatcher.matchingFragments(s);
      if (ranges == null) continue;
      if (ranges.isEmpty() || skipUnderscores(s) >= ranges.get(0).getStartOffset()) {
        return true;
      }
    }

    return false;
  }

  boolean isTypoTolerant() {
    return myTypoTolerant;
  }

  private static int skipUnderscores(@Nonnull String name) {
    return CharArrayUtil.shiftForward(name, 0, "_");
  }

  @Override
  public boolean prefixMatches(@Nonnull final String name) {
    if (name.startsWith("_") && CodeInsightSettings.getInstance().COMPLETION_CASE_SENSITIVE == CodeInsightSettings.FIRST_LETTER && firstLetterCaseDiffers(name)) {
      return false;
    }

    return myMatcher.matches(name);
  }

  private boolean firstLetterCaseDiffers(String name) {
    int nameFirst = skipUnderscores(name);
    int prefixFirst = skipUnderscores(myPrefix);
    return nameFirst < name.length() && prefixFirst < myPrefix.length() && caseDiffers(name.charAt(nameFirst), myPrefix.charAt(prefixFirst));
  }

  private static boolean caseDiffers(char c1, char c2) {
    return Character.isLowerCase(c1) != Character.isLowerCase(c2) || Character.isUpperCase(c1) != Character.isUpperCase(c2);
  }

  @Override
  public boolean prefixMatches(@Nonnull final LookupElement element) {
    return prefixMatchersInternal(element, !element.isCaseSensitive());
  }

  private boolean prefixMatchersInternal(final LookupElement element, final boolean itemCaseInsensitive) {
    for (final String name : element.getAllLookupStrings()) {
      if (itemCaseInsensitive && StringUtil.startsWithIgnoreCase(name, myPrefix) || prefixMatches(name)) {
        return true;
      }
      if (itemCaseInsensitive && CodeInsightSettings.ALL != CodeInsightSettings.getInstance().COMPLETION_CASE_SENSITIVE) {
        if (myCaseInsensitiveMatcher.matches(name)) {
          return true;
        }
      }
    }
    return false;
  }

  @Override
  @Nonnull
  public PrefixMatcher cloneWithPrefix(@Nonnull final String prefix) {
    if (prefix.equals(myPrefix)) {
      return this;
    }

    return new CamelHumpMatcher(prefix, myCaseSensitive, myTypoTolerant);
  }

  private MinusculeMatcher createMatcher(final boolean caseSensitive) {
    String prefix = applyMiddleMatching(myPrefix);

    NameUtil.MatcherBuilder builder = NameUtil.buildMatcher(prefix);
    if (caseSensitive) {
      int setting = CodeInsightSettings.getInstance().COMPLETION_CASE_SENSITIVE;
      if (setting == CodeInsightSettings.FIRST_LETTER) {
        builder = builder.withCaseSensitivity(NameUtil.MatchingCaseSensitivity.FIRST_LETTER);
      }
      else if (setting == CodeInsightSettings.ALL) {
        builder = builder.withCaseSensitivity(NameUtil.MatchingCaseSensitivity.ALL);
      }
    }
    if (myTypoTolerant) {
      builder = builder.typoTolerant();
    }
    return builder.build();
  }

  public static String applyMiddleMatching(String prefix) {
    if (Registry.is("ide.completion.middle.matching") && !prefix.isEmpty() && !ourForceStartMatching) {
      return "*" + StringUtil.replace(prefix, ".", ". ").trim();
    }
    return prefix;
  }

  @Override
  public String toString() {
    return myPrefix;
  }

  /**
   * @deprecated In an ideal world, all tests would use the same settings as production, i.e. middle matching.
   * If you see a usage of this method which can be easily removed (i.e. it's easy to make a test pass without it
   * by modifying test expectations slightly), please do it
   */
  @TestOnly
  @Deprecated
  public static void forceStartMatching(Disposable parent) {
    ourForceStartMatching = true;
    Disposer.register(parent, new Disposable() {
      @Override
      public void dispose() {
        //noinspection AssignmentToStaticFieldFromInstanceMethod
        ourForceStartMatching = false;
      }
    });
  }

  @Override
  public int matchingDegree(String string) {
    return matchingDegree(string, matchingFragments(string));
  }

  @Nullable
  public FList<TextRange> matchingFragments(String string) {
    return myMatcher.matchingFragments(string);
  }

  public int matchingDegree(String string, @Nullable FList<? extends TextRange> fragments) {
    int underscoreEnd = skipUnderscores(string);
    if (underscoreEnd > 0) {
      FList<TextRange> ciRanges = myCaseInsensitiveMatcher.matchingFragments(string);
      if (ciRanges != null && !ciRanges.isEmpty()) {
        int matchStart = ciRanges.get(0).getStartOffset();
        if (matchStart > 0 && matchStart <= underscoreEnd) {
          return myCaseInsensitiveMatcher.matchingDegree(string.substring(matchStart), true) - 1;
        }
      }
    }

    return myMatcher.matchingDegree(string, true, fragments);
  }
}
