/*
 * 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.Toolwindows;
import com.intellij.ide.OccurenceNavigator;
import com.intellij.openapi.actionSystem.ActionGroup;
import com.intellij.openapi.actionSystem.ActionManager;
import com.intellij.openapi.actionSystem.ActionPlaces;
import com.intellij.openapi.actionSystem.ActionToolbar;
import com.intellij.openapi.actionSystem.AnAction;
import com.intellij.openapi.actionSystem.AnActionEvent;
import com.intellij.openapi.actionSystem.DataProvider;
import com.intellij.openapi.actionSystem.DefaultActionGroup;
import com.intellij.openapi.actionSystem.PlatformDataKeys;
import com.intellij.openapi.progress.ProgressIndicator;
import com.intellij.openapi.progress.ProgressManager;
import com.intellij.openapi.progress.Task.Modal;
import jetbrains.mps.generator.GenerationFacade;
import jetbrains.mps.ide.findusages.CantLoadSomethingException;
import jetbrains.mps.ide.findusages.CantSaveSomethingException;
import jetbrains.mps.ide.findusages.IExternalizeable;
import jetbrains.mps.ide.findusages.model.IResultProvider;
import jetbrains.mps.ide.findusages.model.SearchQuery;
import jetbrains.mps.ide.findusages.model.SearchResults;
import jetbrains.mps.ide.findusages.model.SearchTask;
import jetbrains.mps.ide.findusages.model.holders.IHolder;
import jetbrains.mps.ide.findusages.model.holders.VoidHolder;
import jetbrains.mps.ide.findusages.view.treeholder.tree.DataTreeChangesNotifier;
import jetbrains.mps.ide.findusages.view.treeholder.treeview.INodeRepresentator;
import jetbrains.mps.ide.findusages.view.treeholder.treeview.UsagesTreeComponent;
import jetbrains.mps.ide.findusages.view.treeholder.treeview.ViewOptions;
import jetbrains.mps.ide.make.DefaultMakeMessageHandler;
import jetbrains.mps.ide.project.ProjectHelper;
import jetbrains.mps.make.IMakeService;
import jetbrains.mps.make.MakeSession;
import jetbrains.mps.make.resources.IResource;
import jetbrains.mps.progress.ProgressMonitorAdapter;
import jetbrains.mps.project.Project;
import jetbrains.mps.smodel.ModelAccessHelper;
import jetbrains.mps.smodel.RepoListenerRegistrar;
import jetbrains.mps.smodel.resources.ModelsToResources;
import jetbrains.mps.util.Computable;
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.SModel;
import org.jetbrains.mps.openapi.model.SNodeReference;
import org.jetbrains.mps.openapi.module.ModelAccess;
import org.jetbrains.mps.openapi.util.ProgressMonitor;

import javax.swing.BorderFactory;
import javax.swing.Icon;
import javax.swing.JComponent;
import javax.swing.JPanel;
import javax.swing.SwingConstants;
import java.awt.BorderLayout;
import java.awt.Dimension;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Set;
import java.util.concurrent.atomic.AtomicReference;

public class UsagesView implements IExternalizeable {
  //read/write constants
  private static final String QUERY = "query";
  private static final String RESULT_PROVIDER = "result_provider";
  private static final String CLASS_NAME = "class_name";
  private static final String TREE_WRAPPER = "tree_wrapper";

  private Project myProject;

  //my components
  private JPanel myPanel;
  private UsagesTreeComponent myTreeComponent;
  private String myCaption = "Usages";
  private Icon myIcon = Toolwindows.ToolWindowFind;

  private DataTreeChangesNotifier myChangeTracker;
  private boolean myOwnChangeTracker = false; // true indicates this class shall manage repository listener (myChangeTracker)

  // note: this field is not restored from XML
  @Nullable
  private SearchResults myLastResults;

  public UsagesView(com.intellij.openapi.project.Project project, ViewOptions defaultOptions) {
    this(ProjectHelper.toMPSProject(project), defaultOptions);
  }

  public UsagesView(Project mpsProject, ViewOptions defaultOptions) {
    this(mpsProject, defaultOptions, new DataTreeChangesNotifier());
    myOwnChangeTracker = true;
    new RepoListenerRegistrar(mpsProject.getRepository(), myChangeTracker).attach();
  }

  public UsagesView(Project mpsProject, ViewOptions defaultOptions, DataTreeChangesNotifier changeTracker) {
    myProject = mpsProject;

    myTreeComponent = new UsagesTreeComponent(defaultOptions, myProject, changeTracker);
    myPanel = new RootPanel(myTreeComponent.getOccurenceNavigator());

    JPanel treeWrapperPanel = new JPanel(new BorderLayout());
    JPanel treeToolbarPanel = new JPanel(new BorderLayout());
    treeToolbarPanel.add(myTreeComponent.getViewToolbar(), BorderLayout.NORTH);
    treeWrapperPanel.add(treeToolbarPanel, BorderLayout.WEST);
    treeWrapperPanel.add(myTreeComponent, BorderLayout.CENTER);
    myPanel.add(treeWrapperPanel, BorderLayout.CENTER);

    myPanel.setMinimumSize(new Dimension());
    myChangeTracker = changeTracker;
  }

  public void dispose() {
    myTreeComponent.dispose();
    if (myOwnChangeTracker) {
      new RepoListenerRegistrar(myProject.getRepository(), myChangeTracker).detach();
    }
    myChangeTracker = null;
  }

  //----RUN STUFF----

  public void setContents(SearchResults results) {
    myLastResults = results;
    myTreeComponent.setContents(results);
  }

  public void setCustomNodeRepresentator(INodeRepresentator nodeRepresentator) {
    myTreeComponent.setCustomRepresentator(nodeRepresentator);
  }

  //----COMPONENT STUFF----

  public JComponent getComponent() {
    return myPanel;
  }

  public String getCaption() {
    return myCaption;
  }

  public void setCaption(String caption) {
    myCaption = caption;
  }

  public Icon getIcon() {
    return myIcon;
  }

  public void setIcon(Icon icon) {
    myIcon = icon;
  }

  public void setActions(AnAction... actions) {
    DefaultActionGroup ag = new DefaultActionGroup();
    ag.addAll(actions);
    ag.addAll(myTreeComponent.getActionsToolbar());
    myPanel.add(createActionsToolbar(ag, myTreeComponent), BorderLayout.WEST);
  }

  public void setActions(Collection<? extends AnAction> actions) {
    setActions(actions.toArray(new AnAction[actions.size()]));
  }

  public void close() {
  }

  //----RESULTS MANIPUALTION STUFF----

  public Set<SModel> getIncludedModels() {
    return myTreeComponent.getIncludedModels();
  }

  public Set<SModel> getAllModels() {
    return myTreeComponent.getAllModels();
  }

  public List<SNodeReference> getIncludedResultNodes() {
    return myTreeComponent.getIncludedResultNodes();
  }

  public List<SNodeReference> getAllResultNodes() {
    return myTreeComponent.getAllResultNodes();
  }

  @Nullable
  public SearchResults getSearchResults() {
    return myLastResults;
  }

  public UsagesTreeComponent getTreeComponent() {
    return myTreeComponent;
  }

  //----SAVE/LOAD STUFF----

  @Override
  public void read(Element element, Project project) throws CantLoadSomethingException {
    Element treeWrapperXML = element.getChild(TREE_WRAPPER);
    myTreeComponent.read(treeWrapperXML, project);
  }

  @Override
  public void write(Element element, Project project) throws CantSaveSomethingException {
    Element treeWrapperXML = new Element(TREE_WRAPPER);
    myTreeComponent.write(treeWrapperXML, project);
    element.addContent(treeWrapperXML);
  }

  private static class RootPanel extends JPanel implements OccurenceNavigator, DataProvider {
    private final OccurenceNavigator myOccurrenceNavigator;

    public RootPanel(@Nullable OccurenceNavigator occurrenceNavigator) {
      super(new BorderLayout());
      myOccurrenceNavigator = occurrenceNavigator;
    }

    @Override
    public boolean hasNextOccurence() {
      return myOccurrenceNavigator != null && myOccurrenceNavigator.hasNextOccurence();
    }

    @Override
    public boolean hasPreviousOccurence() {
      return myOccurrenceNavigator != null && myOccurrenceNavigator.hasPreviousOccurence();
    }

    @Override
    public OccurenceInfo goNextOccurence() {
      return myOccurrenceNavigator != null ? myOccurrenceNavigator.goNextOccurence() : null;
    }

    @Override
    public OccurenceInfo goPreviousOccurence() {
      return myOccurrenceNavigator != null ? myOccurrenceNavigator.goPreviousOccurence() : null;
    }

    @Override
    public String getNextOccurenceActionName() {
      return myOccurrenceNavigator != null ? myOccurrenceNavigator.getNextOccurenceActionName() : "";
    }

    @Override
    public String getPreviousOccurenceActionName() {
      return myOccurrenceNavigator != null ? myOccurrenceNavigator.getPreviousOccurenceActionName() : "";
    }

    @Nullable
    @Override
    public Object getData(@NonNls String dataId) {
      if (PlatformDataKeys.HELP_ID.is(dataId)) {
        return "ideaInterface.usagesView";
      }
      return null;
    }
  }

  private JPanel createActionsToolbar(ActionGroup ag, JComponent targetComponent) {
    JPanel rv = new JPanel();
    rv.setBorder(BorderFactory.createEmptyBorder(2, 1, 2, 1));
    ActionToolbar actionToolbar = ActionManager.getInstance().createActionToolbar(ActionPlaces.USAGE_VIEW_TOOLBAR, ag, false);
    actionToolbar.setTargetComponent(targetComponent);
    actionToolbar.setOrientation(SwingConstants.VERTICAL);
    rv.add(actionToolbar.getComponent());
    return rv;
  }

  protected final Project getProject() {
    return myProject;
  }

  public static class RerunAction extends AnAction {
    private final UsagesView myView;
    private SearchTask mySearchTask;
    private String myProgressText = "Searching";

    public RerunAction(UsagesView view, String text) {
      this(view, text, "", Actions.Rerun);
    }

    public RerunAction(UsagesView view, String text, String description, Icon icon) {
      super(text, description, icon);
      myView = view;
    }

    public void setProgressText(@NotNull String text) {
      myProgressText = text;
    }

    public void setRunOptions(IResultProvider resultProvider, SearchQuery searchQuery) {
      setRunOptions(new SearchTaskImpl(resultProvider, searchQuery));
    }

    public void setRunOptions(SearchTask searchTask) {
      mySearchTask = searchTask;
    }

    @Override
    public void update(@NotNull AnActionEvent e) {
      super.update(e);
      e.getPresentation().setEnabled(mySearchTask != null);
    }

    @Override
    public void actionPerformed(@NotNull AnActionEvent e) {
      assert mySearchTask != null;
      if (!mySearchTask.canExecute()) {
        return;
      }
      ProgressManager.getInstance().run(new Modal(ProjectHelper.toIdeaProject(myView.myProject), myProgressText, true) {
        private SearchResults mySearchResults;

        @Override
        public void run(@NotNull final ProgressIndicator indicator) {
          mySearchResults = mySearchTask.execute(myView.myProject.getModelAccess(), new ProgressMonitorAdapter(indicator));
        }

        @Override
        public void onSuccess() {
          if (mySearchResults != null) {
            mySearchResults.removeDuplicates();
            myView.setContents(mySearchResults);
          }
        }
      });
    }
  }

  public static final class SearchTaskImpl implements SearchTask, Runnable {
    private final IResultProvider myResultProvider;
    private final SearchQuery mySearchQuery;
    private SearchResults myLastResults;
    private ProgressMonitor myProgress;

    public SearchTaskImpl(@NotNull IResultProvider resultProvider, @NotNull SearchQuery searchQuery) {
      myResultProvider = resultProvider;
      mySearchQuery = searchQuery;
    }

    public boolean canExecute() {
      if (mySearchQuery.getScope() == null) {
        return false;
      }
      final IHolder holder = mySearchQuery.getObjectHolder();
      return !(holder instanceof VoidHolder) && holder.getObject() != null;
    }

    public Object getSearchObject() {
      final IHolder objectHolder = mySearchQuery.getObjectHolder();
      if (objectHolder instanceof VoidHolder) {
        return null;
      }
      return objectHolder.getObject();
    }

    public SearchResults execute(ModelAccess modelAccess, ProgressMonitor progressMonitor) {
      myProgress = progressMonitor;
      modelAccess.runReadAction(this);
      return getSearchResults();
    }

    @Override
    public void run() {
      myLastResults = myResultProvider.getResults(mySearchQuery, myProgress);
    }

    public SearchResults getSearchResults() {
      return myLastResults;
    }

    @Nullable
    public static SearchTaskImpl read(Element element, Project mpsProject) throws CantLoadSomethingException {
      Element resultProviderXML = element.getChild(RESULT_PROVIDER);
      if (resultProviderXML != null) {
        String className = resultProviderXML.getAttributeValue(CLASS_NAME);
        try {
          IResultProvider resultProvider = (IResultProvider) Class.forName(className).newInstance();
          resultProvider.read(resultProviderXML, mpsProject);
          Element queryXML = element.getChild(QUERY);
          SearchQuery searchQuery = new SearchQuery(queryXML, mpsProject);
          return new SearchTaskImpl(resultProvider, searchQuery);
        } catch (Throwable t) {
          throw new CantLoadSomethingException("Can't instantiate result provider: " + className, t);
        }
      }
      return null;
    }

    public void write(Element element, Project mpsProject) throws CantSaveSomethingException {
      Element resultProviderXML = new Element(RESULT_PROVIDER);
      resultProviderXML.setAttribute(CLASS_NAME, myResultProvider.getClass().getName());
      myResultProvider.write(resultProviderXML, mpsProject);
      element.addContent(resultProviderXML);

      Element queryXML = new Element(QUERY);
      mySearchQuery.write(queryXML, mpsProject);
      element.addContent(queryXML);
    }
  }

  public static class RebuildAction extends AnAction {
    private final AtomicReference<MakeSession> myMakeSession = new AtomicReference<MakeSession>();
    private final UsagesView myView;

    public RebuildAction(UsagesView view) {
      this(view, "Rebuild models", "", Actions.Compile);
    }

    public RebuildAction(UsagesView view, String text, String description, Icon icon) {
      super(text, description, icon);
      myView = view;
    }

    @Override
    public void update(@NotNull AnActionEvent e) {
      e.getPresentation().setEnabled(myMakeSession.get() == null && !IMakeService.INSTANCE.isSessionActive());
    }

    @Override
    public void actionPerformed(@NotNull AnActionEvent e) {
      final Project mpsProject = myView.myProject;
      Iterable<IResource> makeRes = new ModelAccessHelper(mpsProject.getModelAccess()).runReadAction(new Computable<Iterable<IResource>>() {
        @Override
        public Iterable<IResource> compute() {
          List<SModel> models = new ArrayList<SModel>();
          for (SModel modelDescriptor : myView.getIncludedModels()) {
            if (GenerationFacade.canGenerate(modelDescriptor)) {
              models.add(modelDescriptor);
            }
          }
          return new ModelsToResources(models).resources(false);
        }
      });

      if (myMakeSession.compareAndSet(null, new MakeSession(mpsProject, new DefaultMakeMessageHandler(mpsProject), false))) {
        try {
          if (IMakeService.INSTANCE.get().openNewSession(myMakeSession.get())) {
            IMakeService.INSTANCE.get().make(myMakeSession.get(), makeRes);
          }
        } finally {
          myMakeSession.set(null);
        }
      }
    }
  }
}
