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

import com.intellij.openapi.actionSystem.AnAction;
import com.intellij.openapi.actionSystem.AnActionEvent;
import com.intellij.openapi.actionSystem.DataKey;
import com.intellij.openapi.actionSystem.LangDataKeys;
import com.intellij.openapi.actionSystem.PlatformDataKeys;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.fileEditor.FileEditorManager;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.util.Pair;
import com.intellij.openapi.util.Ref;
import com.intellij.openapi.vfs.LocalFileSystem;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.openapi.vfs.VirtualFileManager;
import jetbrains.mps.icons.MPSIcons.Nodes;
import jetbrains.mps.ide.actions.MPSCommonDataKeys;
import jetbrains.mps.ide.icons.GlobalIconManager;
import jetbrains.mps.ide.icons.IdeIcons;
import jetbrains.mps.ide.project.ProjectHelper;
import jetbrains.mps.idea.core.MPSBundle;
import jetbrains.mps.idea.core.project.module.ModuleMPSSupport;
import jetbrains.mps.idea.core.ui.CreateFromTemplateDialog;
import jetbrains.mps.nodefs.NodeVirtualFileSystem;
import jetbrains.mps.smodel.SModelOperations;
import jetbrains.mps.smodel.SNodeUtil;
import jetbrains.mps.smodel.action.NodeFactoryManager;
import jetbrains.mps.smodel.constraints.ModelConstraints;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.mps.openapi.language.SAbstractConcept;
import org.jetbrains.mps.openapi.language.SLanguage;
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.SNodeAccessUtil;
import org.jetbrains.mps.openapi.model.SNodeReference;

import javax.swing.Icon;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;

public class NewRootAction extends AnAction {
  private EditableSModel myModel;
  private Project myProject;
  private Map<String, SAbstractConcept> myConceptFqNameToNodePointerMap = new LinkedHashMap<>();

  public NewRootAction() {
    super(MPSBundle.message("new.root.action"), null, IdeIcons.DEFAULT_ROOT_ICON);
  }

  @Override
  public void actionPerformed(AnActionEvent e) {
    Interaction interaction = ApplicationManager.getApplication().isUnitTestMode() ?
      e.getData(HEADLESS_INTERACTION) : new UiInteraction();

    assert interaction != null;

    Pair<String, SAbstractConcept> choice = interaction.choose(myConceptFqNameToNodePointerMap);
    if (choice == null) {
      // cancelled
      return;
    }

    String name = choice.first;
    SAbstractConcept concept = choice.second;

    Ref<SNodeReference> createdNode = new Ref<>();

    ProjectHelper.getModelAccess(myProject).executeCommand(() -> {
      SNode newNode = NodeFactoryManager.createNode(concept, null, null, myModel);
      SNodeAccessUtil.setProperty(newNode, SNodeUtil.property_INamedConcept_name, name);
      myModel.addRootNode(newNode);
      myModel.save();

      createdNode.set(newNode.getReference());
    });

    if (!createdNode.isNull() && !ApplicationManager.getApplication().isUnitTestMode()) {
      FileEditorManager.getInstance(myProject).openFile(
        NodeVirtualFileSystem.getInstance().getFileFor(ProjectHelper.getProjectRepository(myProject), createdNode.get()), true);
    }
  }

  @Override
  public void update(AnActionEvent e) {
    updateFields(e);

    boolean isVisible = myProject != null && myModel != null;
    e.getPresentation().setVisible(isVisible);
    e.getPresentation().setEnabled(!myConceptFqNameToNodePointerMap.isEmpty());

    if (!isVisible) return;

    e.getPresentation().setText(MPSBundle.message("new.root.action"));
    e.getPresentation().setIcon(IdeIcons.DEFAULT_ROOT_ICON);
  }

  private void updateFields(AnActionEvent e) {
    // cleaning all local fields
    myModel = null;
    myConceptFqNameToNodePointerMap.clear();
    myProject = e.getData(PlatformDataKeys.PROJECT);

    final SModel model = MPSCommonDataKeys.CONTEXT_MODEL.getData(e.getDataContext());
    if (model != null && model.isReadOnly()) {
      return;
    }
    if (model instanceof EditableSModel) {
      myModel = (EditableSModel) model;
    }

    if (myProject == null || myModel == null) {
      return;
    }

    final jetbrains.mps.project.Project mpsProject = ProjectHelper.fromIdeaProject(myProject);
    if (mpsProject == null) {
      return;
    }

    final Module module = e.getData(LangDataKeys.MODULE);
    final VirtualFile[] vFiles = e.getData(PlatformDataKeys.VIRTUAL_FILE_ARRAY);
    if (module == null ||
      vFiles == null ||
      vFiles.length != 1) {
      return;
    }

    final ModuleMPSSupport mpsFacade = ModuleMPSSupport.getInstance();
    if (mpsFacade == null || !mpsFacade.isMPSEnabled(module)) {
      return;
    }

    String url = vFiles[0].getUrl();
    if (!LocalFileSystem.PROTOCOL.equals(VirtualFileManager.extractProtocol(url))) {
      return;
    }

    mpsProject.getModelAccess().runReadAction(() -> {
      Set<SLanguage> modelLanguages = SModelOperations.getAllLanguageImports(model);
      for (SLanguage language : modelLanguages) {
        for (SAbstractConcept concept : language.getConcepts()) {
          if (ModelConstraints.canBeRoot(concept, model)) {
            myConceptFqNameToNodePointerMap.put(concept.getQualifiedName(), concept);
          }
        }
      }
    });
  }

  private class UiInteraction implements Interaction {
    @Override
    public Pair<String, SAbstractConcept> choose(Map<String, SAbstractConcept> concepts) {
      MyCreateFromTemplateDialog dialog = new MyCreateFromTemplateDialog(myProject, concepts);
      dialog.setTitle(MPSBundle.message("create.new.root.dialog.title"));

      ProjectHelper.getModelAccess(myProject).runReadAction(() -> {
        for (Map.Entry<String, SAbstractConcept> entry : concepts.entrySet()) {
          String conceptFqName = entry.getKey();
          SAbstractConcept concept = entry.getValue();
          final GlobalIconManager globalIconManager = ApplicationManager.getApplication().getComponent(GlobalIconManager.class);
          final Icon conceptIcon = globalIconManager == null ? Nodes.RootNode : globalIconManager.getIconFor(concept);
          dialog.getKindCombo().addItem(concept.getConceptAlias(), conceptIcon, conceptFqName);
          dialog.setTemplateKindComponentsVisible(true);
        }
      });

      dialog.show();
      return dialog.getResult();
    }
  }

  private class MyCreateFromTemplateDialog extends CreateFromTemplateDialog {
    private Pair<String, SAbstractConcept> myResult = null;
    private Map<String, SAbstractConcept> myConcepts;

    protected MyCreateFromTemplateDialog(@NotNull Project project, @NotNull Map<String, SAbstractConcept> concepts) {
      super(project);
      myConcepts = concepts;
    }

    public Pair<String, SAbstractConcept> getResult() {
      return myResult;
    }

    @Override
    protected void doOKAction() {
      String name = getNameField().getText();
      SAbstractConcept concept = myConcepts.get(getKindCombo().getSelectedName());
      myResult = Pair.create(name, concept);
      super.doOKAction();
    }
  }

  public static final DataKey<Interaction> HEADLESS_INTERACTION = DataKey.create("newRootActionHeadlessInteraction");

  public interface Interaction {
    /**
     * @param concepts List of concepts that the dialog has to offer to new node creation
     * @return Name and concept (from the map param) for the node to create; or null if cancelled
     */
    Pair<String, SAbstractConcept> choose(Map<String, SAbstractConcept> concepts);
  }
}
