/*
 * 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.psi.impl;

import com.intellij.ide.impl.ProjectViewSelectInTarget;
import com.intellij.ide.projectView.impl.ProjectViewPane;
import com.intellij.lang.FileASTNode;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.util.Ref;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.PsiDirectory;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.psi.PsiFileSystemItem;
import com.intellij.psi.PsiManager;
import com.intellij.psi.impl.PsiManagerEx;
import com.intellij.psi.impl.PsiManagerImpl;
import com.intellij.psi.impl.file.PsiDirectoryImpl;
import com.intellij.psi.impl.file.impl.FileManager;
import com.intellij.psi.search.PsiElementProcessor;
import com.intellij.util.ArrayUtil;
import com.intellij.util.IncorrectOperationException;
import com.intellij.util.containers.BidirectionalMap;
import jetbrains.mps.extapi.persistence.FileDataSource;
import jetbrains.mps.icons.MPSIcons.Nodes;
import jetbrains.mps.ide.icons.GlobalIconManager;
import jetbrains.mps.ide.vfs.VirtualFileUtils;
import jetbrains.mps.idea.core.MPSBundle;
import jetbrains.mps.nodefs.NodeVirtualFileSystem;
import jetbrains.mps.persistence.FilePerRootDataSource;
import jetbrains.mps.project.MPSExtentions;
import jetbrains.mps.smodel.DynamicReference;
import jetbrains.mps.smodel.ModelAccessHelper;
import jetbrains.mps.smodel.StaticReference;
import jetbrains.mps.util.JavaNameUtil;
import jetbrains.mps.vfs.IFile;
import org.apache.log4j.Logger;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.mps.openapi.language.SAbstractConcept;
import org.jetbrains.mps.openapi.language.SAbstractLink;
import org.jetbrains.mps.openapi.language.SProperty;
import org.jetbrains.mps.openapi.model.SModel;
import org.jetbrains.mps.openapi.model.SModelReference;
import org.jetbrains.mps.openapi.model.SNode;
import org.jetbrains.mps.openapi.model.SNodeId;
import org.jetbrains.mps.openapi.model.SReference;
import org.jetbrains.mps.openapi.module.SRepository;
import org.jetbrains.mps.openapi.persistence.DataSource;

import javax.swing.Icon;
import java.io.IOException;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Queue;

/**
 * evgeny, 1/25/13
 */
public class MPSPsiModel extends MPSPsiNodeBase implements PsiDirectory {

  private static final Logger LOG = Logger.getLogger(MPSPsiModel.class);

  private static final PsiDirectory[] EMPTY_PSI_DIRECTORIES = new PsiDirectory[0];
  private final SModelReference myModelReference;
  private final Map<SNodeId, MPSPsiNode> myNodes = new HashMap<>();
  private final BidirectionalMap<MPSPsiNodeBase, Integer> myNodesOrder = new BidirectionalMap<>();
  private VirtualFile mySourceVirtualFile;
  private PsiDirectoryImpl myPsiDirectory;

  public MPSPsiModel(SModelReference reference, PsiManager manager) {
    super(manager);
    this.myModelReference = reference;
  }

  @Override
  public boolean equals(Object obj) {
    /* TODO: remove after fix in platform:
    This override fixes check for equality of SmartPointerElementInfo and MPSPsiModel
    in SmartPsiElementPointerImpl.createElementInfo() */
    if (obj instanceof PsiDirectory && !(obj instanceof MPSPsiNodeBase)) {
      return this.getVirtualFile().equals(((PsiDirectory) obj).getVirtualFile());
    }
    return super.equals(obj);
  }

  @Override
  protected Icon getBaseIcon() {
    final GlobalIconManager globalIconManager = ApplicationManager.getApplication().getComponent(GlobalIconManager.class);
    if (globalIconManager == null) {
      return Nodes.Model;
    }
    return new ModelAccessHelper(getProjectRepository()).runReadAction(() -> globalIconManager.getIconFor(myModelReference.resolve(getProjectRepository())));
  }

  @Nullable
  @Override
  public Icon getIcon(int flags) {
    return getBaseIcon();
  }

  @Nullable
  @Override
  protected Icon getElementIcon(@IconFlags int flags) {
    return getBaseIcon();
  }

  @NotNull
  @Override
  public String getName() {
    return JavaNameUtil.shortName(getQualifiedName());
  }

  public SModelReference getSModelReference() {
    return myModelReference;
  }

  @Override
  public boolean isValid() {
    if (myPsiDirectory == null || !(myPsiDirectory.isValid())) return false;
    final SRepository repository = getProjectRepository();
    final Ref<Boolean> result = new Ref<>(false);

    repository.getModelAccess().runReadAction(() -> {
      SModel model = myModelReference.resolve(repository);
      result.set(model != null);
    });

    return result.get();
  }

  @Override
  public boolean isPhysical() {
    // See SmartPsiElementPointerImpl.doCreateElementInfo() and LOG.error() in createElementInfo() in the same class
    // We implement PsiDirectory but don't want DirElementInfo to be created for us, because when it's restored
    // it creates not an MPSPsiModel obviously but a PsiDirectoryFactory.getInstance().findDirectory() which happens
    // to be PsiDirectoryImpl or something. Hence, LOG.error() is called.
    // It affects project pane tests.
    //
    // Currently waiting for the change element instanceof PsiDirectory && element.isPhysical() to happen in idea code
    return false;
  }


  @Override
  public String toString() {
    return "Model:" + myModelReference.toString();
  }

  public MPSPsiNode[] getRootNodes() {
    return getRootNodesOfType(MPSPsiNode.class);
  }

  public <T extends PsiElement> T[] getRootNodesOfType(Class<T> aClass) {
    PsiElement[] surrogateRoots = getChildren();
    List<T> result = new ArrayList<>();
    for (PsiElement r : surrogateRoots) {
      assert r instanceof MPSPsiRootNode; // a layer between model and real root
      for (PsiElement c : r.getChildren()) {
        if (aClass.isInstance(c)) {
          result.add((T) c);
        }
      }
    }
    return ArrayUtil.toObjectArray(result, aClass);
  }

  /* PsiFile */

  @Override
  public void checkSetName(String name) throws IncorrectOperationException {
    throw new IncorrectOperationException(MPSBundle.message("mps.psi.model.message.not.implemented"));
  }

  @NotNull
  @Override
  public VirtualFile getVirtualFile() {
    SRepository repository = getProjectRepository();
    NodeVirtualFileSystem fs = NodeVirtualFileSystem.getInstance();
    return new ModelAccessHelper(repository).runReadAction(() -> fs.getFileFor(repository, myModelReference));
  }

  @Override
  public boolean processChildren(PsiElementProcessor<PsiFileSystemItem> processor) {
    return false;
  }

  @Override
  public boolean isDirectory() {
    return true;
  }

  @Nullable
  @Override
  public PsiDirectory getParent() {
    return null;
  }

  @NotNull
  @Override
  public PsiDirectory[] getSubdirectories() {
    return EMPTY_PSI_DIRECTORIES;  //To change body of implemented methods use File | Settings | File Templates.
  }

  @NotNull
  @Override
  public PsiFile[] getFiles() {
    return getChildren(PsiFile.class);
  }

  @Nullable
  @Override
  public PsiDirectory findSubdirectory(@NotNull String name) {
    return null;
  }

  @Nullable
  @Override
  public PsiFile findFile(@NotNull @NonNls String name) {
    return null;
  }

  @Override
  public final boolean isWritable() {
    return myPsiDirectory != null && myPsiDirectory.isWritable();
  }

  @NotNull
  @Override
  public PsiDirectory createSubdirectory(@NotNull String name) throws IncorrectOperationException {
    if (myPsiDirectory == null)
      throw new IncorrectOperationException(MPSBundle.message("mps.psi.model.message.null.parent"));
    return myPsiDirectory.createSubdirectory(name);
  }

  @Override
  public void checkCreateSubdirectory(@NotNull String name) throws IncorrectOperationException {
    if (myPsiDirectory == null)
      throw new IncorrectOperationException(MPSBundle.message("mps.psi.model.message.null.parent"));
    myPsiDirectory.checkCreateSubdirectory(name);
  }

  @NotNull
  @Override
  public PsiFile createFile(@NotNull @NonNls String name) throws IncorrectOperationException {
    if (myPsiDirectory == null)
      throw new IncorrectOperationException(MPSBundle.message("mps.psi.model.message.null.parent"));
    return myPsiDirectory.createFile(name);
  }

  @NotNull
  @Override
  public PsiFile copyFileFrom(@NotNull String newName, @NotNull PsiFile originalFile) throws IncorrectOperationException {
    throw new IncorrectOperationException(MPSBundle.message("mps.psi.model.message.not.implemented"));
  }

  @Override
  public void checkCreateFile(@NotNull String name) throws IncorrectOperationException {
    if (myPsiDirectory == null)
      throw new IncorrectOperationException(MPSBundle.message("mps.psi.model.message.null.parent"));
    myPsiDirectory.checkCreateFile(name);
  }

  @Override
  public PsiElement add(@NotNull PsiElement element) throws IncorrectOperationException {
    if (myPsiDirectory == null)
      throw new IncorrectOperationException(MPSBundle.message("mps.psi.model.message.null.parent"));
    return myPsiDirectory.add(element);
  }

  @Override
  public void checkAdd(@NotNull PsiElement element) throws IncorrectOperationException {
    if (myPsiDirectory == null)
      throw new IncorrectOperationException(MPSBundle.message("mps.psi.model.message.null.parent"));
    myPsiDirectory.checkAdd(element);
  }

  @Override
  public FileASTNode getNode() {
    return null;
  }

  @NotNull
  @Override
  public PsiElement setName(@NonNls @NotNull String name) throws IncorrectOperationException {
    throw new IncorrectOperationException(MPSBundle.message("mps.psi.model.message.not.implemented"));
  }

  @Nullable
  @Override
  public PsiDirectory getParentDirectory() {
    VirtualFile parentFile = getSourceVirtualFile().getParent();
    if (parentFile == null) return null;
    return myManager.findDirectory(parentFile);
  }

  @Override
  public boolean canNavigateToSource() {
    return false;
  }

  @Override
  public boolean canNavigate() {
    return true;
  }

  @Override
  public void navigate(boolean requestFocus) {
    ProjectViewSelectInTarget.select(getProject(), this, ProjectViewPane.ID, null, getSourceVirtualFile(), requestFocus);
  }

  public String getQualifiedName() {
    return myModelReference.getModelName();
  }

  public VirtualFile getSourceVirtualFile() {
    if (mySourceVirtualFile == null) {
      final SRepository repo = getProjectRepository();
      repo.getModelAccess().runReadAction(() -> {
        SModel model = myModelReference.resolve(repo);
        // fixme when DefaultModelRoot.createModel() doesn't do register() anymore, it shouldn't be needed
        // happens in tests for creating file-per-root persisted model in a directory, when no languages is chosen
        // i.e. creation of model is cancelled
        if (model == null) {
          return;
        }
        DataSource source = model.getSource();
        if (source instanceof FileDataSource) {
          mySourceVirtualFile = VirtualFileUtils.getProjectVirtualFile(((FileDataSource) source).getFile());
        } else if (source instanceof FilePerRootDataSource) {
          // todo remove knowledge about particular PerRoot persistence, should be more generic
          mySourceVirtualFile = VirtualFileUtils.getProjectVirtualFile(((FilePerRootDataSource) source).getFolder()).findChild(MPSExtentions.DOT_MODEL_HEADER);
        }
      });
    }
    return mySourceVirtualFile;
  }

  @Override
  public PsiFile getContainingFile() {
    // if it's singe-file model then return that file
    final SRepository repository = getProjectRepository();
    return new ModelAccessHelper(repository.getModelAccess()).runReadAction(() -> {
      SModel model = myModelReference.resolve(repository);
      if (model.getSource() instanceof FileDataSource) {
        IFile iModelFile = ((FileDataSource) model.getSource()).getFile();
        VirtualFile vModelFile = VirtualFileUtils.getProjectVirtualFile(iModelFile);
        if (vModelFile == null) {
          // extra check due to MPS-21363
          LOG.warn(String.format(MPSBundle.message("mps.psi.model.warning.containing.file"), iModelFile.toPath().toString()));
          return null;
        }
        return PsiManager.getInstance(getProject()).findFile(vModelFile);
      } else {
        return null;
      }
    });
  }

  /* package */

  boolean isRoot(MPSPsiNode psiNode) {
    return psiNode.getParent() instanceof MPSPsiRootNode;
  }

  MPSPsiNode reload(SNodeId sNodeId) {
    SRepository repository = getProjectRepository();
    repository.getModelAccess().checkWriteAccess();
    MPSPsiNode mpsPsiNode = lookupNode(sNodeId);
    if (mpsPsiNode == null) {
      return null;
    }

    SNode sNode = mpsPsiNode.getSNodeReference().resolve(repository);
    MPSPsiNode replacement = convert(sNode);

    if (isRoot(mpsPsiNode)) {
      MPSPsiRootNode rootNode = (MPSPsiRootNode) mpsPsiNode.getParent();
      assert rootNode.getContainingModel().equals(this);

      MPSPsiRootNode replacementRoot;
      if (sNode.getContainingRoot() == sNode && sNode.getModel().getSource() instanceof FilePerRootDataSource) {
        final String name = extractName(sNode);
        final VirtualFile virtualFile = VirtualFileUtils.getProjectVirtualFile(((FilePerRootDataSource) sNode.getModel().getSource()).getFile(name + MPSExtentions.DOT_MODEL_ROOT));
        replacementRoot = new MPSPsiRootNode(sNode.getNodeId(), name, this, getManager(), virtualFile);
      } else {
        replacementRoot = new MPSPsiRootNode(sNode.getNodeId(), extractName(sNode), this, getManager());
      }
      replaceChild(rootNode, replacementRoot);
      ((PsiManagerEx) getManager()).getFileManager().setViewProvider(rootNode.getVirtualFile(), null);
      replacementRoot.addChildLast(replacement);
    } else {
      ((MPSPsiNodeBase) mpsPsiNode.getParent()).replaceChild(mpsPsiNode, replacement);
    }

    enumerateNodes();

    return replacement;
  }

  public void reloadAll() {
    final SRepository repository = getProjectRepository();
    repository.getModelAccess().checkReadAccess();
    SModel sModel = myModelReference.resolve(repository);
    for (SNode root : sModel.getRootNodes()) {
      MPSPsiNode mpsPsiNode = lookupNode(root.getNodeId());
      if (mpsPsiNode == null) continue;
      drop(mpsPsiNode);
    }
    myNodes.clear();
    reload(sModel);
  }

  private PsiFile tryReuseRootPsiFile(VirtualFile vfile) {
    // It's important that we only try to take cached psiFile. Otherwise we could end up creating psiFile
    // for this virtual file, which would require psiModel, but we're in the process of reloading it.
    FileManager fileManager = ((PsiManagerEx) PsiManagerEx.getInstance(getProject())).getFileManager();
    return fileManager.getCachedPsiFile(vfile);
  }

  void reload(SModel model) {
    clearChildren();
    for (SNode root : model.getRootNodes()) {
      String rootName;
      rootName = extractName(root);
      MPSPsiRootNode rootNode;
      if (model.getSource() instanceof FilePerRootDataSource) {
        final IFile iFile = ((FilePerRootDataSource) model.getSource()).getFile(rootName + MPSExtentions.DOT_MODEL_ROOT);
        VirtualFile virtualFile = VirtualFileUtils.getProjectVirtualFile(iFile);
        if (virtualFile == null) virtualFile = VirtualFileUtils.getVirtualFile(iFile.toPath().toString());
        PsiFile psiFile = virtualFile != null ? tryReuseRootPsiFile(virtualFile) : null;
        rootNode = psiFile != null && psiFile instanceof MPSPsiRootNode
          ? (MPSPsiRootNode) psiFile :
          new MPSPsiRootNode(root.getNodeId(), rootName, this, getManager(), virtualFile);

      } else {
        rootNode = new MPSPsiRootNode(root.getNodeId(), rootName, this, getManager());
      }

      addChildLast(rootNode);
      if (rootNode.getChildren().length == 0)
        rootNode.addChildLast(convert(root));
      else {
        rootNode.updateChildren();
        fillNodes(rootNode);
      }
    }

    enumerateNodes();

    getSourceVirtualFile();
    if (mySourceVirtualFile == null || mySourceVirtualFile.getParent() == null)
      myPsiDirectory = null;
    else
      myPsiDirectory = new PsiDirectoryImpl((PsiManagerImpl) myManager, getSourceVirtualFile().getParent());
      /*MPSModuleRepository.getInstance().getModelAccess().runReadAction(new Runnable() {
        @Override
        public void run() {
          mySourceVirtualFile = ModelUtil.getFileByModel(myModelReference.resolve(MPSModuleRepository.getInstance()));
        }
      });*/

  }

  private void fillNodes(MPSPsiRootNode rootNode) {
    Queue<MPSPsiNode> psiNodes = new LinkedList<>();
    for (PsiElement element : rootNode.getChildren()) {
      if (element instanceof MPSPsiNode)
        psiNodes.add((MPSPsiNode) element);
    }
    while (!psiNodes.isEmpty()) {
      MPSPsiNode mpsPsiNode = psiNodes.poll();
      myNodes.put(mpsPsiNode.getId(), mpsPsiNode);
      for (PsiElement element : mpsPsiNode.getChildren()) {
        if (element instanceof MPSPsiNode)
          psiNodes.add((MPSPsiNode) element);
      }
    }
  }

  private MPSPsiNode convert(SNode node) {
    MPSPsiNode psiNode = MPSPsiProvider.getInstance(getProject()).create(node.getNodeId(), node.getConcept(), node.getContainmentLink() == null ? null : node.getContainmentLink().getName());
    myNodes.put(node.getNodeId(), psiNode);

    // properties
    for (SProperty property : node.getProperties()) {
      psiNode.setProperty(property.getName(), node.getProperty(property));
    }

    // refs
    for (SReference ref : node.getReferences()) {
      SAbstractLink link = ref.getLink();
      SAbstractConcept linkTargetConcept = null;
      if (link != null) {
        linkTargetConcept = link.getTargetConcept();
      }
      MPSPsiRef psiRef = null;
      if (ref instanceof StaticReference) {
        psiRef = MPSPsiProvider.getInstance(getProject()).createReferenceNode(ref.getLink().getName(), linkTargetConcept, ref.getTargetSModelReference(), ref.getTargetNodeId());
      } else if (ref instanceof DynamicReference) {
        psiRef = MPSPsiProvider.getInstance(getProject()).createReferenceNode(ref.getLink().getName(), linkTargetConcept, ((DynamicReference) ref).getResolveInfo());
      }
      if (psiRef != null) {
        psiNode.addChild(null, psiRef);
      }
    }

    // children
    for (SNode root : node.getChildren()) {
      MPSPsiNode psiChild = convert(root);
      MPSPsiNodeBase artificialParent = psiNode.getParentFor(psiChild);
      MPSPsiNodeBase wouldBeParent = artificialParent == null ? psiNode : artificialParent;
      wouldBeParent.addChildLast(psiChild);
    }
    return psiNode;
  }

  private void drop(MPSPsiNode psiNode) {
    myNodes.remove(psiNode.getId());

    for (MPSPsiNodeBase node : psiNode.children()) {
      if (node instanceof MPSPsiNode) {
        drop((MPSPsiNode) node);
      }
    }
  }

  MPSPsiNode lookupNode(SNodeId nodeId) {
    return myNodes.get(nodeId);
  }

  Integer getNodePosition(MPSPsiNodeBase node) {
    return myNodesOrder.get(node);
  }

  public MPSPsiNodeBase findNodeByPosition(int pos) {
    List<MPSPsiNodeBase> nodes = myNodesOrder.getKeysByValue(pos);
    if (nodes == null) {
      return null;
    }
    assert nodes.size() <= 1 : "Non-unique mapping from psi nodes to their positions in the tree";
    return nodes.isEmpty() ? null : nodes.get(0);
  }

  private String extractName(SNode sNode) {
    return sNode.getName() != null && !sNode.getName().isEmpty() ? sNode.getName() : sNode.getNodeId().toString();
  }

  // todo replace with depth-first; depth is never very big in real models, and memory consumption will be less,
  // also the order will be more natural
  private void enumerateNodes() {
    myNodesOrder.clear();
    Deque<MPSPsiNodeBase> stack = new ArrayDeque<>();
    stack.add(this);

    int k = 0;

    while (!stack.isEmpty()) {
      MPSPsiNodeBase curr = stack.pop();

      // remembering position
      myNodesOrder.put(curr, k++);

      PsiElement[] children = curr.getChildren();
      for (int i = children.length - 1; i >= 0; i--) {
        PsiElement c = children[i];
        if (c instanceof MPSPsiNodeBase) {
          stack.push((MPSPsiNodeBase) c);
        }
      }

    }
  }

  @Override
  public void checkDelete() throws IncorrectOperationException {
  }

  @Override
  public void delete() throws IncorrectOperationException {
    // todo unregister model
    try {
      getSourceVirtualFile().delete(getManager());
    } catch (IOException e) {
      throw new IncorrectOperationException(e.toString(), e);
    }
  }
}
