/*
 * Copyright 2000-2017 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.ide.ui;

import com.intellij.ide.IdeBundle;
import com.intellij.ide.SearchTopHitProvider;
import com.intellij.ide.ui.search.OptionDescription;
import com.intellij.openapi.Disposable;
import com.intellij.openapi.application.ApplicationBundle;
import com.intellij.openapi.application.PreloadingActivity;
import com.intellij.openapi.components.ComponentManager;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.keymap.KeyMapBundle;
import com.intellij.openapi.progress.ProgressIndicator;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.startup.StartupActivity;
import com.intellij.openapi.util.Disposer;
import com.intellij.openapi.util.Key;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.psi.codeStyle.MinusculeMatcher;
import com.intellij.psi.codeStyle.NameUtil;
import com.intellij.util.Consumer;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.util.Collection;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;

import static com.intellij.openapi.application.ApplicationManager.getApplication;
import static java.util.Collections.emptyList;

/**
 * @author Konstantin Bulenkov
 */
public abstract class OptionsTopHitProvider implements SearchTopHitProvider {
  private static final Logger LOG = Logger.getInstance(OptionsTopHitProvider.class);

  @NotNull
  public abstract Collection<OptionDescription> getOptions(@Nullable Project project);

  private Collection<OptionDescription> getCachedOptions(@Nullable Project project) {
    ComponentManager manager = project != null ? project : getApplication();
    if (manager == null || manager.isDisposed()) return emptyList();

    CachedOptions cache = manager.getUserData(CachedOptions.KEY);
    if (cache == null) cache = new CachedOptions(manager);

    return cache.map.computeIfAbsent(getClass(), type -> getOptions(project));
  }

  @Override
  public final void consumeTopHits(@NonNls String pattern, Consumer<Object> collector, Project project) {
    if (!pattern.startsWith("#")) return;
    pattern = pattern.substring(1);
    final List<String> parts = StringUtil.split(pattern, " ");

    if (parts.size() == 0) {
      return;
    }

    String id = parts.get(0);
    if (getId().startsWith(id) || pattern.startsWith(" ")) {
      if (pattern.startsWith(" ")) {
        pattern = pattern.trim();
      } else {
        pattern = pattern.substring(id.length()).trim().toLowerCase();
      }
      final MinusculeMatcher matcher = NameUtil.buildMatcher("*" + pattern, NameUtil.MatchingCaseSensitivity.NONE);
      for (OptionDescription option : getCachedOptions(project)) {
        if (matcher.matches(option.getOption())) {
          collector.consume(option);
        }
      }
    }
  }

  public abstract String getId();

  public boolean isEnabled(@Nullable Project project) {
    return true;
  }

  static String messageApp(String property) {
    return StringUtil.stripHtml(ApplicationBundle.message(property), false);
  }

  static String messageIde(String property) {
    return StringUtil.stripHtml(IdeBundle.message(property), false);
  }

  static String messageKeyMap(String property) {
    return StringUtil.stripHtml(KeyMapBundle.message(property), false);
  }

  /*
   * Marker interface for option provider containing only descriptors which are backed by toggle actions.
   * E.g. UiSettings.SHOW_STATUS_BAR is backed by View > Status Bar action.
   */
  @Deprecated
  public interface CoveredByToggleActions { // for search everywhere only
  }

  private static final class CachedOptions implements Disposable {
    private static final Key<CachedOptions> KEY = Key.create("cached top hits");
    private final ConcurrentHashMap<Class<?>, Collection<OptionDescription>> map = new ConcurrentHashMap<>();
    private final ComponentManager manager;

    private CachedOptions(ComponentManager manager) {
      this.manager = manager;
      Disposer.register(manager, this);
      manager.putUserData(KEY, this);
    }

    @Override
    public void dispose() {
      manager.putUserData(KEY, null);
      map.values().forEach(CachedOptions::dispose);
    }

    private static void dispose(Collection<OptionDescription> options) {
      if (options != null) options.forEach(CachedOptions::dispose);
    }

    private static void dispose(OptionDescription option) {
      if (option instanceof Disposable) Disposer.dispose((Disposable)option);
    }
  }

  public static final class Activity extends PreloadingActivity implements StartupActivity {
    @Override
    public void preload(@NotNull ProgressIndicator indicator) {
      cacheAll(indicator, null); // for application
    }

    @Override
    public void runActivity(@NotNull Project project) {
      if (!getApplication().isUnitTestMode()) cacheAll(null, project); // for given project
    }

    private static void cacheAll(@Nullable ProgressIndicator indicator, @Nullable Project project) {
      long time = System.currentTimeMillis();
      for (SearchTopHitProvider provider : SearchTopHitProvider.EP_NAME.getExtensions()) {
        if (indicator != null) indicator.checkCanceled();
        if (provider instanceof OptionsTopHitProvider) {
          OptionsTopHitProvider options = (OptionsTopHitProvider)provider;
          if (options.isEnabled(project)) options.getCachedOptions(project);
        }
      }
      LOG.debug((System.currentTimeMillis() - time) + " ms spent to cache options in " + (project == null ? "application" : "project"));
    }
  }
}

@@ -794,24 +794,77 @@
 Disposable;%0A
+import com.intellij.openapi.application.Application;%0A
 import com.i
@@ -5497,48 +5497,8 @@
     
- if (!getApplication().isUnitTestMode())
  cac
@@ -5658,48 +5658,120 @@
     
-long time = System.currentTimeMillis();%0A
+Application application = getApplication();%0A      if (application != null && !application.isUnitTestMode()) %7B%0A  
     
@@ -5869,66 +5869,114 @@
     
+  
 if (
-indicator != null) indicator.checkCanceled();%0A
+provider instanceof OptionsTopHitProvider) %7B%0A            application.invokeLater(() -%3E %7B%0A   
         
 if (
@@ -5975,53 +5975,59 @@
     
+   
 if (
-provider instanceof OptionsTopHitProvider
+indicator == null %7C%7C !indicator.isCanceled()
 ) %7B%0A
@@ -6036,16 +6036,22 @@
         
+   
+   
 OptionsT
@@ -6113,24 +6113,30 @@
 ;%0A          
+    
+  
 if (options.
@@ -6202,154 +6202,61 @@
     
+    
-%7D%0A
+  %7D%0A     
       
-%7D
+ %7D);
 %0A      
-LOG.debug((System.currentTimeMillis() - time) + %22 ms spent to cache options in %22 + (project == null ? %22application%22 : %22project%22));
+    %7D%0A        %7D%0A      %7D
 %0A   

load top hit providers on EDT