/*
 * 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.idea.core.editor;

import com.intellij.openapi.fileEditor.FileEditor;
import com.intellij.openapi.fileEditor.FileEditorManager;
import com.intellij.openapi.fileEditor.NavigatableFileEditor;
import com.intellij.openapi.fileEditor.OpenFileDescriptor;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.pom.Navigatable;
import jetbrains.mps.ide.editor.MPSFileNodeEditor;
import jetbrains.mps.ide.project.ProjectHelper;
import jetbrains.mps.ide.vfs.VirtualFileUtils;
import jetbrains.mps.idea.core.psi.impl.MPSPsiModel;
import jetbrains.mps.idea.core.psi.impl.MPSPsiNode;
import jetbrains.mps.idea.core.psi.impl.MPSPsiNodeBase;
import jetbrains.mps.idea.core.psi.impl.MPSPsiProvider;
import jetbrains.mps.smodel.ModelAccessHelper;
import jetbrains.mps.smodel.SModelFileTracker;
import jetbrains.mps.util.Computable;
import jetbrains.mps.nodefs.MPSNodeVirtualFile;
import jetbrains.mps.nodefs.NodeVirtualFileSystem;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.mps.openapi.model.SModel;
import org.jetbrains.mps.openapi.model.SNode;
import org.jetbrains.mps.openapi.model.SNodeReference;
import org.jetbrains.mps.openapi.module.SRepository;

/**
 * This editor is a work-around for the problem that idea navigates to PSI element (e.g. in goto super method)
 * in such a way: PsiFile.getVirtualFile() -> open editor for it -> navigate to the psi element's offset
 * In case of a single-file model we're asked to open the editor for the model file.
 * We know which root node to open the moment idea navigates to this editor via navigateTo().
 * We could internally use NodeEditor like MPSFileNodeEditor does, but we do a more hacky thing instead:
 * we open another MPSFileNodeEditor for the right MPSNodeVirtualFile and close this artificial editor.
 * The reason: if we do our work inside this editor then idea keeps thinking that it's an editor for
 * the model file (.mps) and
 * 1) will not open another editor for another root inside the same model
 * 2) shows the wrong name for the editor: modelname.mps
 * 3) opens a separate editor for the root node if the user opens it from, say, project pane
 *
 * The right solution seems to be: our MPSNodeFileSystem should be NewFileSystem and our virtual files should be
 * NewVirtualFile, then a lot of problems will be gone and there will be no need for hacks like this. Because
 * we will be able to return our node virtual files anywhere and idea will not think they're bad.
 *
 * Created by danilla on 20/11/15.
 */
public class ModelFileToRootDispatchingEditor extends MPSFileNodeEditor implements NavigatableFileEditor {
  private Project myProject;
  private VirtualFile myModelFile;

  ModelFileToRootDispatchingEditor(Project project, VirtualFile virtualFile) {
    super(ProjectHelper.fromIdeaProject(project), null);
    myProject = project;
    myModelFile = virtualFile;
  }

  @Override
  public boolean canNavigateTo(@NotNull final Navigatable navigatable) {
    if (!(navigatable instanceof OpenFileDescriptor)) {
      return false;
    }
    final int offset = ((OpenFileDescriptor) navigatable).getOffset();
    if (offset < 0) {
      return false;
    }
    final SRepository repository = ProjectHelper.fromIdeaProject(myProject).getRepository();
    return new ModelAccessHelper(repository.getModelAccess()).runReadAction(new Computable<Boolean>() {
      @Override
      public Boolean compute() {
        return iKnowThisNode(offset, repository);
      }
    });
  }

  @Override
  public void navigateTo(@NotNull final Navigatable navigatable) {
    assert navigatable instanceof OpenFileDescriptor;
    final int offset = ((OpenFileDescriptor) navigatable).getOffset();
    if (offset == -1) {
      return;
    }
    final SRepository repository = ProjectHelper.fromIdeaProject(myProject).getRepository();
    repository.getModelAccess().runReadAction(new Runnable() {
      @Override
      public void run() {
        SNodeReference nodeRef = getNode(offset, repository);
        SNode node = nodeRef.resolve(repository);
        if (node == null) {
          return;
        }
        SNode root = node.getContainingRoot();
        MPSNodeVirtualFile nodeFile = NodeVirtualFileSystem.getInstance().getFileFor(repository, root);
        // opening or finding the editor for the right root node
        MPSFileNodeEditor editor = getEditorForRoot(nodeFile);
        // selecting the node inside the root which is designated by the given 'offset'
        editor.getNodeEditor().showNode(node, true);

        // now closing this artificial editor
        FileEditorManager.getInstance(myProject).closeFile(myModelFile);
      }
    });
  }

  @Override
  public void selectNotify() {
    // guard from NPE in super.selectNotify()
  }

  @Override
  public void deselectNotify() {
    // guard from NPE in super.deselectNotify()
  }

  private boolean iKnowThisNode(int offset, SRepository repository) {
    return getNode(offset, repository) != null;
  }

  private SNodeReference getNode(int offset, SRepository repository) {
    SModel model = SModelFileTracker.getInstance(repository).findModel(VirtualFileUtils.toIFile(myModelFile));
    if (model == null) {
      return null;
    }
    MPSPsiModel psiModel = MPSPsiProvider.getInstance(myProject).getPsi(model);
    if (psiModel == null) {
      return null;
    }
    MPSPsiNodeBase node = psiModel.findNodeByPosition(offset);
    if (!(node instanceof MPSPsiNode)) {
      // we've been asked to navigate by an offset that corresponds not to a real node in the model
      // but some auxilary psi node (like MPSPsiParameterList) that has no counterpart in the model
      return null;
    }
    return ((MPSPsiNode) node).getSNodeReference();
  }

  @NotNull
  private MPSFileNodeEditor getEditorForRoot(MPSNodeVirtualFile nodeFile) {
    // finding currently open editor, if any
//    FileEditor[] editors = FileEditorManager.getInstance(myProject).getEditors(nodeFile);
//    MPSFileNodeEditor result = null;
//    for (FileEditor editor: editors) {
//      if (editor instanceof MPSFileNodeEditor) {
//        result = (MPSFileNodeEditor) editor;
//      }
//    }
//    if (result != null) {
//      return result;
//    }
    // creating a new editor
    FileEditor[] editors = FileEditorManager.getInstance(myProject).openFile(nodeFile, true);
    MPSFileNodeEditor result = null;
    for (FileEditor editor: editors) {
      if (editor instanceof MPSFileNodeEditor) {
        result = (MPSFileNodeEditor) editor;
      }
    }
    assert result != null : "Couldn't create MPSFileNodeEditor";
    return result;
  }
}
