/*
 * Copyright 2003-2013 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.projectView;

import com.intellij.ide.DeleteProvider;
import com.intellij.ide.projectView.SelectableTreeStructureProvider;
import com.intellij.ide.projectView.ViewSettings;
import com.intellij.ide.projectView.impl.nodes.PsiDirectoryNode;
import com.intellij.ide.projectView.impl.nodes.PsiFileNode;
import com.intellij.ide.util.treeView.AbstractTreeNode;
import com.intellij.openapi.actionSystem.LangDataKeys;
import com.intellij.openapi.actionSystem.PlatformDataKeys;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.application.ModalityState;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.module.ModuleUtilCore;
import com.intellij.openapi.options.ex.SingleConfigurableEditor;
import com.intellij.openapi.project.DumbAware;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Ref;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.PsiElement;
import jetbrains.mps.extapi.persistence.FolderDataSource;
import jetbrains.mps.fileTypes.MPSFileTypeFactory;
import jetbrains.mps.ide.actions.MPSCommonDataKeys;
import jetbrains.mps.ide.project.ProjectHelper;
import jetbrains.mps.ide.ui.dialogs.properties.MPSPropertiesConfigurable;
import jetbrains.mps.ide.ui.dialogs.properties.ModelPropertiesConfigurable;
import jetbrains.mps.ide.vfs.VirtualFileUtils;
import jetbrains.mps.idea.core.MPSBundle;
import jetbrains.mps.idea.core.MPSDataKeys;
import jetbrains.mps.idea.core.projectView.edit.SNodeCutCopyProvider;
import jetbrains.mps.idea.core.projectView.edit.SNodeDeleteProvider;
import jetbrains.mps.idea.core.projectView.edit.SNodePasteProvider;
import jetbrains.mps.idea.core.projectView.edit.SingleFileModelDeleteProvider;
import jetbrains.mps.idea.core.psi.impl.MPSPsiModel;
import jetbrains.mps.idea.core.psi.impl.MPSPsiNodeBase;
import jetbrains.mps.idea.core.psi.impl.MPSPsiProvider;
import jetbrains.mps.idea.core.psi.impl.MPSPsiRealNode;
import jetbrains.mps.idea.core.psi.impl.MPSPsiRootNode;
import jetbrains.mps.smodel.SModelFileTracker;
import jetbrains.mps.util.ModelComputeRunnable;
import jetbrains.mps.vfs.FileSystem;
import jetbrains.mps.vfs.IFile;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.mps.openapi.model.EditableSModel;
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.SModule;
import org.jetbrains.mps.openapi.module.SRepository;
import org.jetbrains.mps.openapi.persistence.DataSource;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

/**
 * User: fyodor
 * Date: 2/27/13
 */
public class MPSTreeStructureProvider implements SelectableTreeStructureProvider, DumbAware {

  @Nullable
  @Override
  public PsiElement getTopLevelElement(PsiElement element) {
    return null;
  }

  @NotNull
  @Override
  public Collection<AbstractTreeNode> modify(@NotNull final AbstractTreeNode treeNode, @NotNull final Collection<AbstractTreeNode> children, final ViewSettings settings) {
    final Ref<Collection<AbstractTreeNode>> result = new Ref<>(children);

    SRepository repository = ProjectHelper.getProjectRepository(treeNode.getProject());
    // we're actually in EDT here, but we work with SModels, and various routines assert that we can read, thus read action
    repository.getModelAccess().runReadAction(new Runnable() {
      @Override
      public void run() {
        List<AbstractTreeNode> updatedChildren = null;
        final MPSPsiProvider mpsPsiProvider = MPSPsiProvider.getInstance(treeNode.getProject());

        // if current dir is data source from some model
        FolderDataSource currentDirectoryDataSource = null;

        if (treeNode instanceof PsiDirectoryNode) {
          // let's see if we have a model built from this dir, e.g. in per-root persistence
          SModel sModel = SModelFileTracker.getInstance(repository).findModel(VirtualFileUtils.toIFile(((PsiDirectoryNode) treeNode).getVirtualFile()));
          if (sModel != null) {
            // adding root nodes (removing their corresponding files' nodes from the tree is further below)
            List<MPSPsiElementTreeNode> rootsTreeNodes = new ArrayList<>();
            for (SNode root : sModel.getRootNodes()) {
              rootsTreeNodes.add(new MPSPsiElementTreeNode(treeNode.getProject(), (MPSPsiRootNode) mpsPsiProvider.getPsi(root).getContainingFile(), settings));
            }
            if (!rootsTreeNodes.isEmpty()) {
              updatedChildren = new ArrayList<>(children);
              updatedChildren.addAll(rootsTreeNodes);
            }

            DataSource dataSource = sModel.getSource();
            if (dataSource instanceof FolderDataSource) {
              // could be assert as currently SModelFileTracker only tracks FileDataSource and FolderDataSource
              currentDirectoryDataSource = (FolderDataSource) dataSource;
            }
          }
        } else if (treeNode instanceof MPSPsiModelTreeNode) {
          MPSPsiModel psiModel = ((MPSPsiModelTreeNode) treeNode).extractPsiFromValue();
          updatedChildren = new ArrayList<>();
          for (PsiElement psiElement : psiModel.getChildren()) {
            updatedChildren.add(new MPSPsiElementTreeNode(treeNode.getProject(), (MPSPsiRootNode) psiElement, settings));
          }
        }

        for (final AbstractTreeNode child : children) {
          if (child instanceof PsiFileNode) {
            VirtualFile vFile = ((PsiFileNode) child).getVirtualFile();
            if (vFile == null) {
              continue;
            }

            // check if it's a single file model
            final SModel sModel = SModelFileTracker.getInstance(repository).findModel(VirtualFileUtils.toIFile(vFile));
            if (sModel != null) {
              if (updatedChildren == null) updatedChildren = new ArrayList<>(children);
              int idx = updatedChildren.indexOf(child);
              updatedChildren.remove(idx);
              updatedChildren.add(idx, new MPSPsiModelTreeNode(treeNode.getProject(), mpsPsiProvider.getPsi(sModel), settings));
              continue;
            }

            if (currentDirectoryDataSource != null && currentDirectoryDataSource.isIncluded(VirtualFileUtils.toIFile(vFile))) {
              // it's a file that constitutes a FolderDataSource-backed model, remove it from the tree (root nodes are shown instead)
              if (updatedChildren == null) updatedChildren = new ArrayList<>(children);
              int idx = updatedChildren.indexOf(child);
              updatedChildren.remove(idx);
            }

          } else if (child instanceof PsiDirectoryNode) {
            // below code only attaches our action to the directory and makes it show added children - our root nodes

            final SModel perRootModel = SModelFileTracker.getInstance(repository).findModel(VirtualFileUtils.toIFile(((PsiDirectoryNode) child).getVirtualFile()));
            if (perRootModel != null) {
              if (updatedChildren == null) updatedChildren = new ArrayList<>(children);

              int idx = updatedChildren.indexOf(child);
              updatedChildren.remove(idx);
              updatedChildren.add(idx, new PsiDirectoryNode(treeNode.getProject(), ((PsiDirectoryNode) child).getValue(), settings) {
                @Override
                public boolean canNavigate() {
                  return true;
                }

                @Override
                public String getNavigateActionText(boolean focusEditor) {
                  return MPSBundle.message("open.model.properties.action");
                }

                @Override
                public void navigate(boolean requestFocus) {
                  MPSPropertiesConfigurable configurable = new ModelPropertiesConfigurable(perRootModel, ProjectHelper.fromIdeaProject(myProject), true);
                  final SingleConfigurableEditor dialog = new SingleConfigurableEditor(myProject, configurable);
                  configurable.setParentForCallBack(dialog);
                  ApplicationManager.getApplication().invokeLater(dialog::show, ModalityState.current());
                }
              });
            }
          }
        }

        if (updatedChildren != null) {
          result.set(updatedChildren);
        }
      }
    });

    return result.get();
  }

  @Nullable
  @Override
  public Object getData(Collection<AbstractTreeNode> selected, String dataName) {
    if (selected == null) {
      return null;
    }

    if (PlatformDataKeys.COPY_PROVIDER.is(dataName) || PlatformDataKeys.CUT_PROVIDER.is(dataName)) {
      return getProvider(selected, CUT_COPY_PROVIDER_FACTORY);
    }
    if (PlatformDataKeys.DELETE_ELEMENT_PROVIDER.is(dataName)) {
      DeleteProvider deleteModelProvider = getDeleteModelProvider(selected);
      if (deleteModelProvider != null) {
        return deleteModelProvider;
      }
      return getProvider(selected, DELETE_PROVIDER_FACTORY);
    }
    if (MPSDataKeys.MODEL_FILES.is(dataName)) {
      return getModelFiles(selected);
    }

    if (selected.size() != 1) {
      return null;
    }

    // Applicable only to single element selection
    AbstractTreeNode selectedNode = selected.iterator().next();

    if (PlatformDataKeys.VIRTUAL_FILE_ARRAY.is(dataName)) {
      return getModelFilesArray(selectedNode);
    }
    if (PlatformDataKeys.PASTE_PROVIDER.is(dataName)) {
      return getModelProvider(selectedNode, PASTE_PROVIDER_FACTORY);
    }
    if (MPSCommonDataKeys.NODE.is(dataName)) {
      return getNode(selectedNode);
    }
    if (MPSCommonDataKeys.CONTEXT_MODEL.is(dataName) || MPSCommonDataKeys.MODEL.is(dataName)) {
      return getModel(selectedNode);
    }
    if (MPSCommonDataKeys.CONTEXT_MODULE.is(dataName) || MPSCommonDataKeys.MODULE.is(dataName)) {
      return getModule(selectedNode);
    }
    if (LangDataKeys.MODULE.is(dataName)) {
      return getIdeaModule(selectedNode);
    }
    return null;
  }

  private <T> T getProvider(Collection<AbstractTreeNode> selected, ProviderFactory<T> createProvider) {
    if (selected.size() == 0) return null;

    List<SNodeReference> selectedNodePointers = new ArrayList<>();
    Project project = null;
    EditableSModel modelDescriptor = null;

    for (AbstractTreeNode treeNode : selected) {
      if (!(treeNode instanceof MPSPsiElementTreeNode)) return null; // only root nodes please

      MPSPsiRootNode mpsPsiNode = ((MPSPsiElementTreeNode) treeNode).getValue();
      if (!mpsPsiNode.isValid()) return null;

      selectedNodePointers.add(mpsPsiNode.getSNodeReference());

      if (project == null) {
        project = treeNode.getProject();
      } else if (project != treeNode.getProject()) {
        return null; // only same project
      }

      if (modelDescriptor == null) {
        modelDescriptor = getModel(mpsPsiNode);
      } else if (modelDescriptor != getModel(mpsPsiNode)) {
        return null; // only same model
      }

    }
    jetbrains.mps.project.Project mpsProject = ProjectHelper.fromIdeaProject(project);
    if (mpsProject == null || modelDescriptor == null) return null;

    return createProvider.create(selectedNodePointers, modelDescriptor, modelDescriptor, mpsProject);
  }

  private DeleteProvider getDeleteModelProvider(Collection<AbstractTreeNode> selected) {
    final List<MPSPsiModel> psiModels = new ArrayList<>();
    for (AbstractTreeNode treeNode : selected) {
      if (!(treeNode instanceof MPSPsiModelTreeNode)) {
        return null;
      }
      MPSPsiModel psiModel = ((MPSPsiModelTreeNode) treeNode).getModel();
      psiModels.add(psiModel);
    }

    return new SingleFileModelDeleteProvider(psiModels);
  }

  private <T> T getModelProvider(AbstractTreeNode treeNode, ModelProviderFactory<T> createProvider) {
    if (!(treeNode instanceof MPSPsiModelTreeNode)) return null; // only model

    MPSPsiModel psiModel = ((MPSPsiModelTreeNode) treeNode).getModel();
    if (psiModel == null || !psiModel.isValid()) return null;

    Project project = treeNode.getProject();
    EditableSModel modelDescriptor = getModel(treeNode);

    jetbrains.mps.project.Project mpsProject = ProjectHelper.fromIdeaProject(project);
    if (mpsProject == null || modelDescriptor == null) return null;

    return createProvider.create(modelDescriptor, modelDescriptor, mpsProject);
  }

  private Set<IFile> getModelFiles(Collection<AbstractTreeNode> selected) {
    Set<IFile> modelFiles = new HashSet<>();
    for (AbstractTreeNode nextTreeNode : selected) {
      IFile nextModelFile = getModelFile(nextTreeNode);
      if (nextModelFile != null) {
        modelFiles.add(nextModelFile);
      }
    }
    return modelFiles;
  }

  private IFile getModelFile(AbstractTreeNode treeNode) {
    if (treeNode instanceof MPSPsiModelTreeNode) {
      MPSPsiModelTreeNode fileNode = (MPSPsiModelTreeNode) treeNode;
      VirtualFile virtualFile = fileNode.getVirtualFile();
      if (virtualFile == null || virtualFile.getFileType() != MPSFileTypeFactory.MPS_FILE_TYPE && virtualFile.getFileType() != MPSFileTypeFactory.MPS_HEADER_FILE_TYPE) {
        return null;
      }
      return FileSystem.getInstance().getFile(virtualFile.getPath());

    } else if (treeNode instanceof PsiDirectoryNode) {
      VirtualFile virtualFile = ((PsiDirectoryNode) treeNode).getVirtualFile();
      if (virtualFile == null) {
        return null;
      }
      IFile ifile = FileSystem.getInstance().getFile(virtualFile.getPath());
      SModel model = SModelFileTracker.getInstance(ProjectHelper.getProjectRepository(treeNode.getProject())).findModel(ifile);
      if (model != null) return ifile;

    }
    return null;
  }

  private VirtualFile[] getModelFilesArray(AbstractTreeNode treeNode) {
    VirtualFile virtualFile = getVirtualFile(treeNode);
    if (virtualFile == null) return null;

    return new VirtualFile[]{virtualFile};
  }

  private VirtualFile getVirtualFile(AbstractTreeNode treeNode) {
    if (!(treeNode instanceof MPSPsiModelTreeNode)) {
      return null;
    }
    MPSPsiModelTreeNode modelTreeNode = (MPSPsiModelTreeNode) treeNode;
    VirtualFile modelVFile = modelTreeNode.getVirtualFile();
    if (modelVFile == null || (modelVFile.getFileType() != MPSFileTypeFactory.MPS_FILE_TYPE && modelVFile.getFileType() != MPSFileTypeFactory.MPS_HEADER_FILE_TYPE))
      return null;
    return modelVFile;
  }

  private SNode getNode(AbstractTreeNode treeNode) {
    if (!(treeNode instanceof MPSPsiElementTreeNode)) {
      return null;
    }
    MPSPsiNodeBase psiNode = ((MPSPsiElementTreeNode) treeNode).getValue();
    if (!(psiNode instanceof MPSPsiRealNode)) {
      return null;
    }
    final SNodeReference nodeRef = ((MPSPsiRealNode) psiNode).getSNodeReference();
    final SRepository repository = ProjectHelper.getProjectRepository(treeNode.getProject());
    // TODO remove read action from here once SModelFileTracker stops doing the same (creating read action if not already in one)
    return new ModelComputeRunnable<>(() -> nodeRef.resolve(repository)).runRead(repository.getModelAccess());
  }

  private Module getIdeaModule(AbstractTreeNode treeNode) {
    VirtualFile modelVFile = getVirtualFile(treeNode);
    if (modelVFile == null) return null;
    return ModuleUtilCore.findModuleForFile(modelVFile, treeNode.getProject());
  }

  private SModule getModule(AbstractTreeNode selectedNode) {
    EditableSModel contextModel = getContextModel(selectedNode);
    return contextModel != null ? contextModel.getModule() : null;
  }

  private EditableSModel getModel(AbstractTreeNode selectedNode) {
    SRepository repository = ProjectHelper.getProjectRepository(selectedNode.getProject());
    if (selectedNode instanceof MPSPsiElementTreeNode) {
      MPSPsiNodeBase value = ((MPSPsiElementTreeNode) selectedNode).getValue();
      return getModel(value);
    } else if (selectedNode instanceof MPSPsiModelTreeNode) {
      MPSPsiModel psiModel = ((MPSPsiModelTreeNode) selectedNode).getModel();
      SModel sModel = psiModel.getSModelReference().resolve(repository);
      return (EditableSModel) sModel;
    } else if (selectedNode instanceof PsiDirectoryNode) {
      SModel sModel = SModelFileTracker.getInstance(repository).findModel(VirtualFileUtils.toIFile(((PsiDirectoryNode) selectedNode).getVirtualFile()));
      if (sModel instanceof EditableSModel) {
        return (EditableSModel) sModel;
      }
    }
    return null;
  }

  private EditableSModel getContextModel(AbstractTreeNode selectedNode) {
    if (selectedNode instanceof MPSPsiElementTreeNode) {
      MPSPsiNodeBase value = ((MPSPsiElementTreeNode) selectedNode).getValue();
      return getModel(value);
    } else if (selectedNode instanceof MPSPsiModelTreeNode) {
      MPSPsiModel psiModel = ((MPSPsiModelTreeNode) selectedNode).getModel();
      SModel sModel = psiModel.getSModelReference().resolve(ProjectHelper.getProjectRepository(selectedNode.getProject()));
      return (EditableSModel) sModel;
    }
    return null;
  }

  private EditableSModel getModel(MPSPsiNodeBase mpsPsiNode) {
    MPSPsiModel containingModel = mpsPsiNode.getContainingModel();
    SModel sModel = containingModel.getSModelReference().resolve(ProjectHelper.getProjectRepository(mpsPsiNode.getProject()));
    return (EditableSModel) sModel;
  }

  private interface ProviderFactory<T> {
    T create(Collection<SNodeReference> selectedNodes, @NotNull EditableSModel modelDescriptor, SModel sModel, @NotNull jetbrains.mps.project.Project project);
  }

  private interface ModelProviderFactory<T> {
    T create(@NotNull EditableSModel modelDescriptor, SModel sModel, @NotNull jetbrains.mps.project.Project project);
  }

  private static final ProviderFactory<SNodeCutCopyProvider> CUT_COPY_PROVIDER_FACTORY =
    (selectedNodes, modelDescriptor, sModel, project) -> new SNodeCutCopyProvider(selectedNodes, modelDescriptor, project);

  private static final ProviderFactory<SNodeDeleteProvider> DELETE_PROVIDER_FACTORY =
    (selectedNodes, modelDescriptor, sModel, project) -> new SNodeDeleteProvider(selectedNodes, project);

  private static final ModelProviderFactory<SNodePasteProvider> PASTE_PROVIDER_FACTORY =
    (modelDescriptor, sModel, project) -> new SNodePasteProvider(sModel, project, modelDescriptor);

}
