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

import com.intellij.ide.util.gotoByName.ChooseByNamePopup;
import com.intellij.ide.util.gotoByName.ChooseByNamePopupComponent;
import com.intellij.openapi.actionSystem.ShortcutSet;
import com.intellij.openapi.application.ModalityState;
import com.intellij.openapi.application.TransactionGuard;
import jetbrains.mps.FilteredGlobalScope;
import jetbrains.mps.ide.project.ProjectHelper;
import jetbrains.mps.scope.ConditionalScope;
import jetbrains.mps.smodel.SModelStereotype;
import jetbrains.mps.smodel.undo.DefaultCommand;
import jetbrains.mps.smodel.undo.NodeBasedCommand;
import jetbrains.mps.util.Callback;
import jetbrains.mps.util.NotCondition;
import jetbrains.mps.workbench.choose.ChooseByNameData;
import jetbrains.mps.workbench.choose.ModelScopeIterable;
import jetbrains.mps.workbench.choose.ModelsPresentation;
import jetbrains.mps.workbench.choose.NavigationTargetPresentation;
import jetbrains.mps.workbench.choose.NavigationTargetScopeIterable;
import jetbrains.mps.workbench.goTo.ui.MpsPopupFactory;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
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.module.SRepository;
import org.jetbrains.mps.openapi.module.SearchScope;
import org.jetbrains.mps.openapi.persistence.NavigationParticipant.NavigationTarget;
import org.jetbrains.mps.util.Condition;

import java.awt.Frame;

/**
 * Facility to interoperate with user to add a new model dependency to a model.
 * Responsible to collect user input and to update model internals.
 *
 * @author Artem Tikhomirov
 * @since 3.4
 */
public class ModelImportHelper {
  private final MPSProject myProject;
  private ShortcutSet myShortcut;
  private String myInitialText;
  private SNode myContextNode;

  public ModelImportHelper(@NotNull MPSProject project) {
    myProject = project;
  }

  /**
   * Override keyboard shortcut for model pick dialog to switch between global and package scope (e.g. to match that of invoking action).
   * <p/>
   *
   * @return <code>this</code> for convenience
   */
  public ModelImportHelper setShortcut(@Nullable ShortcutSet shortcut) {
    myShortcut = shortcut;
    return this;
  }

  /**
   * @param initialText Text to start selection dialog with
   * @return <code>this</code> for convenience
   */
  public ModelImportHelper setInitialText(@Nullable String initialText) {
    myInitialText = initialText;
    return this;
  }

  /**
   * @param contextNode will be used to execute {@link jetbrains.mps.smodel.undo.NodeBasedCommand}
   *                    with this node used as a contextual
   * @return <code>this</code> for convenience
   */
  public ModelImportHelper setContextNode(SNode contextNode) {
    myContextNode = contextNode;
    return this;
  }

  /**
   * Ask user to select a model and import it
   *
   * @param model model to add import to
   */
  public void addImport(@NotNull SModel model) {
    // FIXME identical condition in GoToModel_Action and in GoToModelPlatformAction
    Condition<SModel> cond = new Condition<SModel>() {
      @Override
      public boolean met(SModel modelDescriptor) {
        boolean rightStereotype = SModelStereotype.isUserModel(modelDescriptor)
                                  || SModelStereotype.isStubModel(modelDescriptor);
        boolean hasModule = modelDescriptor.getModule() != null;
        return rightStereotype && hasModule;
      }
    };
    ConditionalScope localScope = new ConditionalScope(myProject.getScope(), null, cond);
    ConditionalScope globalScope = new ConditionalScope(new FilteredGlobalScope(), null, cond);
    SRepository repo = myProject.getRepository();
    ChooseByNameData<SModelReference> gotoData = new ChooseByNameData<>(new ModelsPresentation(repo));
    gotoData.derivePrompts("model").setPrompts("Import model:", gotoData.getNotFoundMessage(), gotoData.getNotInMessage());
    gotoData.setScope(new ModelScopeIterable(localScope, repo), new ModelScopeIterable(globalScope, repo));

    ChooseByNamePopup popup = MpsPopupFactory.createPackagePopup(myProject, gotoData, myInitialText);
    if (myShortcut != null) {
      popup.setCheckBoxShortcut(myShortcut);
    }

    popup.invoke(new AddImportCallback(myProject, model, myContextNode) {
      @Override
      public void elementChosen(Object element) {
        if (element instanceof SModelReference) {
          runImportCommand((SModelReference) element);
        }
      }
    }, ModalityState.current(), false);

  }

  public void addImportByRoot(@NotNull SModel model, final Callback<String> importedRootCallback) {
    ChooseByNameData<NavigationTarget> gotoData = new ChooseByNameData<>(new NavigationTargetPresentation());
    gotoData.derivePrompts("node").setPrompts("Import model that contains root:", gotoData.getNotFoundMessage(), gotoData.getNotInMessage());
    gotoData.setCheckBoxName("Include stub and non-project models");
    ConditionalScope localScope = new ConditionalScope(myProject.getScope(), null, NotCondition.negate(SModelStereotype::isStubModel));
    SearchScope globalScope = GlobalScope.getInstance();
    final SRepository repo = myProject.getRepository();
    gotoData.setScope(new NavigationTargetScopeIterable(localScope, repo), new NavigationTargetScopeIterable(globalScope, repo));

    ChooseByNamePopup popup = MpsPopupFactory.createNodePopup(myProject.getProject(), gotoData, myInitialText, null);
    if (myShortcut != null) {
      popup.setCheckBoxShortcut(myShortcut);
    }

    popup.invoke(new AddImportCallback<String>(myProject, model, myContextNode) {
      @Override
      public void elementChosen(Object element) {
        if (element instanceof NavigationTarget) {
          NavigationTarget object = (NavigationTarget) element;
          runImportCommand(object.getNodeReference().getModelReference(), object.getPresentation());
        }
      }

      @Override
      public void executeCallback(String... parameters) {
        importedRootCallback.call(parameters[0]);
      }
    }, ModalityState.current(), false);
  }

  // Callback.elementChosen shall populate myModelToImport
  private static abstract class AddImportCallback<T> extends ChooseByNamePopupComponent.Callback {
    private final jetbrains.mps.project.Project myProject;
    private final SModel myModel;
    private final SNode myContextNode;

    public AddImportCallback(jetbrains.mps.project.Project mpsProject, SModel model, SNode contextNode) {
      myProject = mpsProject;
      myModel = model;
      myContextNode = contextNode;
    }

    /*package*/ Frame getFrame() {
      return ProjectHelper.toMainFrame(myProject);
    }

    protected void runImportCommand(final SModelReference modelToImport, T... callbackParameters) {
      TransactionGuard.getInstance().submitTransactionAndWait(() -> {
        // Have to extract preparation of ModelImporter outside command,
        // because ModelImporter#confirmModuleChanges require Swing, which is not allowed inside command.
        final ModelImporter modelImporter = new ModelImporter(myModel);
        myProject.getModelAccess().runReadAction(() -> modelImporter.prepare(modelToImport));
        final boolean confirmed = !modelImporter.affectsModuleDependencies() || modelImporter.confirmModuleChanges(getFrame());

        Runnable command;
        if (myContextNode != null) {
          command = new NodeBasedCommand(myContextNode) {
            @Override
            public void run() {
              if (confirmed) {
                modelImporter.execute();
              }
              executeCallback(callbackParameters);
            }
          };
        } else {
          command = new DefaultCommand() {
            @Override
            public void run() {
              if (confirmed) {
                modelImporter.execute();
              }
              executeCallback(callbackParameters);
            }
          };
        }
        myProject.getModelAccess().executeCommand(command);
      });
    }

    public void executeCallback(T... parameters) {
    }
  }
}
