/*
 * Copyright 2003-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 jetbrains.mps.ide.findusages.view;

import com.intellij.icons.AllIcons.Actions;
import com.intellij.icons.AllIcons.General;
import com.intellij.icons.AllIcons.Toolwindows;
import com.intellij.ide.actions.PinActiveTabAction;
import com.intellij.openapi.actionSystem.ActionManager;
import com.intellij.openapi.actionSystem.AnAction;
import com.intellij.openapi.actionSystem.AnActionEvent;
import com.intellij.openapi.actionSystem.DataContext;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.components.PersistentStateComponent;
import com.intellij.openapi.components.State;
import com.intellij.openapi.components.Storage;
import com.intellij.openapi.components.StoragePathMacros;
import com.intellij.openapi.progress.PerformInBackgroundOption;
import com.intellij.openapi.progress.ProgressIndicator;
import com.intellij.openapi.progress.Task.Backgroundable;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.startup.StartupManager;
import com.intellij.openapi.ui.MessageType;
import com.intellij.openapi.wm.ToolWindowAnchor;
import com.intellij.openapi.wm.ToolWindowManager;
import com.intellij.ui.content.Content;
import com.intellij.ui.content.ContentManager;
import jetbrains.mps.ide.ThreadUtils;
import jetbrains.mps.ide.actions.MPSActions;
import jetbrains.mps.ide.actions.MPSCommonDataKeys;
import jetbrains.mps.ide.findusages.CantLoadSomethingException;
import jetbrains.mps.ide.findusages.CantSaveSomethingException;
import jetbrains.mps.ide.findusages.model.IResultProvider;
import jetbrains.mps.ide.findusages.model.SearchQuery;
import jetbrains.mps.ide.findusages.model.SearchResult;
import jetbrains.mps.ide.findusages.model.SearchResults;
import jetbrains.mps.ide.findusages.view.UsagesView.RebuildAction;
import jetbrains.mps.ide.findusages.view.UsagesView.RerunAction;
import jetbrains.mps.ide.findusages.view.UsagesView.SearchTaskImpl;
import jetbrains.mps.ide.findusages.view.treeholder.tree.DataTreeChangesNotifier;
import jetbrains.mps.ide.project.ProjectHelper;
import jetbrains.mps.openapi.navigation.EditorNavigator;
import jetbrains.mps.progress.ProgressMonitorAdapter;
import jetbrains.mps.smodel.RepoListenerRegistrar;
import jetbrains.mps.util.annotation.ToRemove;
import org.jdom.Element;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.mps.openapi.model.SNode;
import org.jetbrains.mps.openapi.model.SNodeReference;
import org.jetbrains.mps.openapi.module.SRepository;

import javax.swing.Icon;
import javax.swing.JComponent;
import java.util.ArrayList;
import java.util.List;

@State(
    name = "UsagesViewTool",
    storages = @Storage(StoragePathMacros.WORKSPACE_FILE)
)
public class UsagesViewTool extends TabbedUsagesTool implements PersistentStateComponent<Element> {

  private static final String VERSION_NUMBER = "1";
  private static final String VERSION = "version";
  private static final String ID = "id";

  private static final String TAB = "tab";
  private static final String TABS = "tabs";

  private static final String DEFAULT_VIEW_OPTIONS = "default_view_options";
  private static final String TOOL_WINDOW_ID = "Usages";

  private List<UsageViewData> myUsageViewsData = new ArrayList<>();
  private jetbrains.mps.ide.findusages.view.treeholder.treeview.ViewOptions myDefaultViewOptions =
      new jetbrains.mps.ide.findusages.view.treeholder.treeview.ViewOptions();
  private final DataTreeChangesNotifier myChangeTracker = new DataTreeChangesNotifier();

  //----CONSTRUCT STUFF----

  public UsagesViewTool(Project project) {
    super(project, TOOL_WINDOW_ID, 3, Toolwindows.ToolWindowFind, ToolWindowAnchor.BOTTOM, true);
  }

  @Override
  protected UsagesView getUsagesView(int index) {
    return myUsageViewsData.get(index).myUsagesView;
  }

  private void register(UsageViewData viewData) {
    if (myUsageViewsData.isEmpty()) {
      new RepoListenerRegistrar(ProjectHelper.getProjectRepository(getProject()), myChangeTracker).attach();
    }
    myUsageViewsData.add(viewData);
  }

  @Override
  protected void onRemove(int index) {
    myUsageViewsData.remove(index);
    if (myUsageViewsData.isEmpty()) {
      new RepoListenerRegistrar(ProjectHelper.getProjectRepository(getProject()), myChangeTracker).detach();
    }
  }

  //----TOOL STUFF----

  public int getPriority() {
    return 0;
  }

  @Override
  protected boolean isInitiallyAvailable() {
    return true;
  }

  //---FIND USAGES STUFF----

  /**
   * Display usages in a tool window of a respective project, according to options supplied.
   */
  public static void showUsages(@NotNull Project project, @NotNull IResultProvider provider, @NotNull SearchQuery query, @NotNull UsageToolOptions options) {
    project.getComponent(UsagesViewTool.class).findUsages(provider, query, options);
  }

  /**
   * @deprecated Use {@link #showUsages(com.intellij.openapi.project.Project, jetbrains.mps.ide.findusages.model.IResultProvider, jetbrains.mps.ide.findusages.model.SearchQuery, UsageToolOptions)} instead
   */
  @Deprecated
  @ToRemove(version = 3.3)
  public void findUsages(IResultProvider provider, SearchQuery query, boolean isRerunnable, boolean showOne, boolean forceNewTab, String notFoundMsg) {
    findUsages(provider, query,
               new UsageToolOptions().allowRunAgain(isRerunnable).navigateIfSingle(!showOne).forceNewTab(forceNewTab).notFoundMessage(notFoundMsg));
  }

  private void findUsages(IResultProvider provider, final SearchQuery query, final UsageToolOptions options) {
    final SearchTaskImpl searchTask = new SearchTaskImpl(provider, query);
    ThreadUtils.runInUIThreadNoWait(new Runnable() {
      @Override
      public void run() {
        new Backgroundable(getProject(), "Searching", true, PerformInBackgroundOption.DEAF) {
          private SearchResults searchResults;

          @Override
          public void run(@NotNull final ProgressIndicator indicator) {
            searchResults = searchTask.execute(ProjectHelper.toMPSProject(getProject()).getModelAccess(), new ProgressMonitorAdapter(indicator));
          }

          @Override
          public void onSuccess() {
            showResults(searchTask, searchResults, options);
          }
        }.queue();
      }
    });
  }

  public void show(SearchResults results, String notFoundMsg) {
    ThreadUtils.assertEDT();
    showResults(null, results, new UsageToolOptions().navigateIfSingle(false).forceNewTab(true).allowRunAgain(false).notFoundMessage(notFoundMsg));
  }

  private void showResults(SearchTaskImpl searchTask, final SearchResults<?> searchResults, UsageToolOptions options) {
    final jetbrains.mps.project.Project mpsProject = ProjectHelper.toMPSProject(getProject());
    int resCount = searchResults.getSearchResults().size();
    if (resCount == 0) {
      final ToolWindowManager manager = ToolWindowManager.getInstance(getProject());
      manager.notifyByBalloon(TOOL_WINDOW_ID, MessageType.INFO, options.myNotFoundMessage, null, null);
      return;
    } else if (resCount == 1 && options.myNavigateIfSingle) {
      final SearchResult<?> searchResult = searchResults.getSearchResults().get(0);
      if (searchResult.getObject() instanceof SNode) {
        final SNode node = (SNode) searchResult.getObject();
        new EditorNavigator(mpsProject).shallFocus(true).selectIfChild().open(node.getReference());
        return;
      }
      // FALL THROUGH (single result we can't navigate to)
    }
    int index = getCurrentTabIndex();
    UsagesView usagesView = createUsageView(options.myRunAgain ? searchTask : null);
    UsageViewData usageViewData = new UsageViewData(usagesView, options.myRunAgain ? searchTask : null);
    usageViewData.setTransientView(options.myTransientView);
    register(usageViewData);

    usagesView.setContents(searchResults);

    Icon icon = usagesView.getIcon();
    String caption = usagesView.getCaption();
    JComponent component = usagesView.getComponent();
    Content content = addContent(component, caption, icon, true);
    getContentManager().setSelectedContent(content);

    if (!options.myForceNewTab) {
      closeLastUnpinnedTab(index);
    }
    openTool(true);
  }

  //---END FIND STUFF----

  private void read(Element element, jetbrains.mps.project.Project project) {
    Element versionXML = element.getChild(VERSION);
    if (versionXML == null) {
      return;
    }
    String version = versionXML.getAttribute(ID).getValue();
    if (!VERSION_NUMBER.equals(version)) {
      return;
    }

    Element tabsXML = element.getChild(TABS);
    if (tabsXML != null) {
      for (Element tabXML : tabsXML.getChildren()) {
        final UsageViewData usageViewData;
        try {
          usageViewData = UsageViewData.read(this, tabXML, project);
        } catch (CantLoadSomethingException e) {
          continue;
        }
        register(usageViewData);

        ApplicationManager.getApplication().invokeLater(() -> {
          final String caption = usageViewData.myUsagesView.getCaption();
          final Icon icon = usageViewData.myUsagesView.getIcon();
          addContent(usageViewData.myUsagesView.getComponent(), caption, icon, true);
        });
      }
    }

    Element defaultViewOptionsXML = element.getChild(DEFAULT_VIEW_OPTIONS);
    myDefaultViewOptions.read(defaultViewOptionsXML, project);

    ApplicationManager.getApplication().invokeLater(() -> {
      ContentManager cm = getContentManager();
      if (cm == null) {
        return;
      }
      if (cm.getContentCount() == 0) {
        makeUnavailableLater();
      }
    });
  }

  private void write(Element element, jetbrains.mps.project.Project project) {
    Element versionXML = new Element(VERSION);
    versionXML.setAttribute(ID, VERSION_NUMBER);
    element.addContent(versionXML);

    Element tabsXML = new Element(TABS);
    for (UsageViewData usageViewData : myUsageViewsData) {
      if (usageViewData.isTransientView()) {
        continue;
      }
      Element tabXML = new Element(TAB);
      try {
        usageViewData.write(tabXML, project);
        tabsXML.addContent(tabXML);
      } catch (CantSaveSomethingException e) {
        // ignore
      }
    }
    element.addContent(tabsXML);

    Element defaultViewOptionsXML = new Element(DEFAULT_VIEW_OPTIONS);
    myDefaultViewOptions.write(defaultViewOptionsXML, project);
    element.addContent(defaultViewOptionsXML);
  }

  @Override
  public Element getState() {
    final jetbrains.mps.project.Project mpsProject = ProjectHelper.toMPSProject(getProject());
    final Element state = new Element("state");
    mpsProject.getModelAccess().runReadAction(() -> write(state, mpsProject));
    return state;
  }

  @Override
  public void loadState(final Element state) {
    //startup manager is needed cause the contract is that you can't use read and write locks
    //on component load - it can cause a deadlock (MPS-2811) 
    StartupManager.getInstance(getProject()).runWhenProjectIsInitialized(() -> {
      if (getProject().isDisposed()) {
        return;
      }
      final jetbrains.mps.project.Project mpsProject = ProjectHelper.toMPSProject(getProject());
      mpsProject.getModelAccess().runReadAction(() -> read(state, mpsProject));
    });
  }

  private UsagesView createUsageView(@Nullable SearchTaskImpl searchTask) {
    jetbrains.mps.project.Project mpsProject = ProjectHelper.fromIdeaProject(getProject());
    final UsagesView view = new UsagesView(mpsProject, myDefaultViewOptions, myChangeTracker);
    ArrayList<AnAction> actions = new ArrayList<>();
    if (searchTask != null) {
      final RerunAction rerunAction = new RerunAction(view, "Run again");
      rerunAction.setRunOptions(searchTask);
      actions.add(rerunAction);
    }
    actions.add(new RebuildAction(view));
    actions.add(new AnAction("Close", "", Actions.Cancel) {
      @Override
      public void actionPerformed(@NotNull AnActionEvent e) {
        int i = 0;
        for (UsageViewData vd : myUsageViewsData) {
          if (vd.myUsagesView == view) {
            UsagesViewTool.this.closeTab(i);
            break;
          }
          i++;
        }
      }
    });
    actions.add(new PinActiveTabAction.TW());
    if (ActionManager.getInstance().getAction(MPSActions.FIND_USAGES_WITH_DIALOG_ACTION) != null && searchTask != null) {
      actions.add(new FindUsagesWithDialogAction(mpsProject.getRepository(), searchTask));
    }
    view.setActions(actions);
    return view;
  }

  /**
   * Tracks result presentation and optional task to re-populate the view.
   * Persists state
   */
  private static class UsageViewData {
    private static final String USAGE_VIEW = "usage_view";
    private static final String USAGE_VIEW_OPTIONS = "usage_view_options";

    public final UsagesView myUsagesView;
    public final SearchTaskImpl mySearchTask;
    private boolean myIsTransientView = false;
    // now it's not in use, but will be used to implement constructable finders
//    private FindUsagesOptions myOptions = new FindUsagesOptions();

    public UsageViewData(@NotNull UsagesView view, @Nullable SearchTaskImpl searchTask) {
      myUsagesView = view;
      mySearchTask = searchTask;
    }

    /*package*/void setTransientView(boolean isTransientView) {
      myIsTransientView = isTransientView;
    }

    /*package*/boolean isTransientView() {
      return myIsTransientView;
    }

    @NotNull
    public static UsageViewData read(UsagesViewTool tool, Element element, jetbrains.mps.project.Project project) throws CantLoadSomethingException {
      final SearchTaskImpl task = SearchTaskImpl.read(element, project);
      final UsagesView usageView = tool.createUsageView(task);
      Element usageViewXML = element.getChild(USAGE_VIEW);
      usageView.read(usageViewXML, project);

//      Element usageViewOptionsXML = element.getChild(USAGE_VIEW_OPTIONS);
//      myOptions = new FindUsagesOptions(usageViewOptionsXML, project);
      return new UsageViewData(usageView, task);
    }

    public void write(Element element, jetbrains.mps.project.Project project) throws CantSaveSomethingException {
      //this is to partially fix MPS-14671
      if (myUsagesView.getTreeComponent().getAllResultNodes().size() > 500) {
        throw new CantSaveSomethingException("usages view size too big to save");
      }


      if (mySearchTask != null) {
        mySearchTask.write(element, project);
      }
      Element usageViewXML = new Element(USAGE_VIEW);
      myUsagesView.write(usageViewXML, project);
      element.addContent(usageViewXML);

//      Element usageViewOptionsXML = new Element(USAGE_VIEW_OPTIONS);
//      myOptions.write(usageViewOptionsXML, project);
//      element.addContent(usageViewOptionsXML);
    }
  }

  private static class FindUsagesWithDialogAction extends AnAction {
    private final SRepository myRepository;
    private final SearchTaskImpl mySearchTask;

    public FindUsagesWithDialogAction(@NotNull SRepository repository, @NotNull SearchTaskImpl searchTask) {
      super("Settings...", "Show find usages settings dialog", General.ProjectSettings);
      myRepository = repository;
      mySearchTask = searchTask;
    }

    @Override
    public void update(AnActionEvent e) {
      e.getPresentation().setEnabled(ActionManager.getInstance().getAction(MPSActions.FIND_USAGES_WITH_DIALOG_ACTION) != null);
    }

    @Override
    public void actionPerformed(final AnActionEvent e) {
      if (!mySearchTask.canExecute()) {
        return;
      }
      if (!(mySearchTask.getSearchObject() instanceof SNodeReference)) {
        return; //object of an incompatible kind (see #getData() below)
      }
      final SNodeReference searchedNode = (SNodeReference) mySearchTask.getSearchObject();

      DataContext dataContext = new DataContext() {
        private final DataContext myDelegate = e.getDataContext();

        @Nullable
        @Override
        public Object getData(@NonNls String dataId) {
          if (MPSCommonDataKeys.CONTEXT_MODEL.is(dataId)) {
            SNode resolved = searchedNode.resolve(myRepository);
            return resolved == null ? null : resolved.getModel();
          }
          // if a caller asks for an SNode, I assume it has appropriate model read, otherwise what would be SNode for?
          if (MPSCommonDataKeys.NODE.is(dataId)) {
            return searchedNode.resolve(myRepository);
          }
          return myDelegate.getData(dataId);
        }
      };
      AnActionEvent event = new AnActionEvent(e.getInputEvent(), dataContext, e.getPlace(), e.getPresentation(), e.getActionManager(), e.getModifiers());

      AnAction action = ActionManager.getInstance().getAction(MPSActions.FIND_USAGES_WITH_DIALOG_ACTION);
      action.actionPerformed(event);
    }
  }
}
