package jetbrains.mps.tool.environment;

/*Generated by MPS */

import jetbrains.mps.annotations.GeneratedClass;
import jetbrains.mps.logging.Logger;
import org.jetbrains.annotations.NotNull;
import com.intellij.util.PlatformUtils;
import jetbrains.mps.ide.MPSCoreComponents;
import jetbrains.mps.util.annotation.Hack;
import java.util.StringJoiner;
import java.io.File;
import jetbrains.mps.tool.common.PluginData;
import java.util.Set;
import jetbrains.mps.internal.collections.runtime.SetSequence;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.application.PathManager;
import java.awt.GraphicsEnvironment;
import com.intellij.testFramework.TestApplicationManager;
import jetbrains.mps.project.Project;
import jetbrains.mps.RuntimeFlags;
import com.intellij.openapi.Disposable;
import com.intellij.openapi.vfs.newvfs.impl.VfsRootAccess;
import jetbrains.mps.project.MPSProject;
import com.intellij.openapi.util.Disposer;
import com.intellij.openapi.fileEditor.ex.FileEditorManagerEx;
import com.intellij.openapi.project.ex.ProjectManagerEx;
import com.intellij.openapi.application.ModalityState;
import com.intellij.openapi.application.Application;
import jetbrains.mps.project.ProjectManager;
import java.util.List;
import java.util.ArrayList;
import com.intellij.testFramework.PlatformTestUtil;
import com.intellij.openapi.vfs.impl.jar.JarFileSystemImpl;
import jetbrains.mps.library.LibraryInitializer;
import java.util.Collections;
import jetbrains.mps.util.FileUtil;
import jetbrains.mps.util.Reference;
import java.util.concurrent.TimeUnit;
import jetbrains.mps.vfs.refresh.CachingFileSystem;
import jetbrains.mps.ide.vfs.IdeaFileSystem;
import jetbrains.mps.vfs.refresh.DefaultCachingContext;
import jetbrains.mps.core.platform.Platform;
import com.intellij.ide.startup.StartupManagerEx;
import kotlinx.coroutines.future.FutureKt;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.ExecutionException;

/**
 * TODO: fix dispose methods
 */
@GeneratedClass(node = "r:1d4e7c57-c144-4228-9dec-8180ddf9f0ee(jetbrains.mps.tool.environment)/7413225496542992859", model = "r:1d4e7c57-c144-4228-9dec-8180ddf9f0ee(jetbrains.mps.tool.environment)")
public final class IdeaEnvironment extends EnvironmentBase {
  private static final Logger LOG = Logger.getLogger(IdeaEnvironment.class);
  private static final String PLUGIN_PATH = "plugin.path";
  private static final String IDEA_LOAD_PLUGINS_ID = "idea.load.plugins.id";
  private static final String IDEA_USE_CORE_CL = "idea.force.use.core.classloader";

  private Object myIdeaApplication;

  static {
    // initialize before ij
    EnvironmentBase.initializeLog();
  }

  public IdeaEnvironment(@NotNull EnvironmentConfig config) {
    super(config);
    PlatformUtils.setDefaultPrefixForCE();
  }

  public void init() {
    if (LOG.isInfoLevel()) {
      LOG.info("Creating IDEA environment");
    }

    addRequiredPlugins(myConfig);
    createIdeaApplication();
    // fixme IJ must allow to use our own logging initialization
    // FIXME why do I care to initialize again once IDEA did its own log configuration?

    MPSCoreComponents coreComponents = getMPSCoreComponents();
    super.init(coreComponents.getPlatform());
  }

  /**
   * Currently it is needed in the tests from sources and ant tasks.
   * I suggest that it is too late to set the plugins here and
   * it is better for ant tasks and run configurations to initialize this properties earlier.
   * It is the property of the smth like `EntryPointConfig`, the one, that is used in MPSWorker (before new process is started).
   * Source tests might be able to entry the execution (and creating a new java process) via the
   * same utility (in tool.common)
   * apyshkin
   */
  @Hack
  private void addRequiredPlugins(EnvironmentConfig config) {
    if (myConfig.areLoadingPluginsAutomatically()) {
      if (LOG.isInfoLevel()) {
        LOG.info("The environment config is set to automatic plugins discovery mode");
      }
      return;
    }

    setPluginPathProperty();
    setPluginIdsPropertyFromConfig(config);
  }

  @Hack
  private void setPluginPathProperty() {
    if (System.getProperty(PLUGIN_PATH) != null) {
      if (LOG.isWarningLevel()) {
        LOG.warning("The environment config plugin location settings are overridden by the system property " + System.getProperty(PLUGIN_PATH));
      }
      return;
    }
    StringJoiner pluginPathResult = new StringJoiner(File.pathSeparator);
    for (PluginData pd : myConfig.getPlugins()) {
      pluginPathResult.add(pd.path);
    }
    // IMPORTANT! "plugin.path" doesn't tell plugin's classpath, it points to location where to read plugin.xml from
    // I.e. for unit test mode, complete plugin's classpath has to be in global CP already,
    // PluginManagerCore.loadDescriptorsFromClassPath
    System.setProperty(PLUGIN_PATH, pluginPathResult.toString());
  }

  /**
   * after I talked to artem it seems that the property makes sense since we in fact want to
   * constrict the set of plugins when we are running the rcp based on mps (say mbeddr).
   * In that case they have a great number of bundled plugins in app_dir/plugins, but they want to
   * disable some of them from time to time.
   */
  @Hack
  private void setPluginIdsPropertyFromConfig(EnvironmentConfig config) {
    if (System.getProperty(IDEA_LOAD_PLUGINS_ID) != null) {
      if (LOG.isWarningLevel()) {
        LOG.warning("The environment config plugin settings settings are overridden by the system property " + System.getProperty(IDEA_LOAD_PLUGINS_ID));
      }
      return;
    }
    StringJoiner result = new StringJoiner(",");
    Set<PluginData> plugins = config.getPlugins();
    if (SetSequence.fromSet(plugins).isEmpty()) {
      return;
    }
    for (PluginData plugin : SetSequence.fromSet(plugins)) {
      assert plugin.id != null : "id should be specified for plugin " + plugin.path;
      result.add(plugin.id);
    }
    System.setProperty(IDEA_LOAD_PLUGINS_ID, result.toString());
  }

  private MPSCoreComponents getMPSCoreComponents() {
    return ApplicationManager.getApplication().getComponent(MPSCoreComponents.class);
  }

  private void createIdeaApplication() {
    if (LOG.isInfoLevel()) {
      LOG.info("Creating IdeaCmdApplication");
    }
    final String PERF_LOG_KEY = "idea.log.perf.stats";
    if (System.getProperty(PERF_LOG_KEY) == null) {
      // see StartUpPerformanceReporter.kt/logStats, which logs statistics when App.isInternal (our case). 
      // I doubt there's any reason for an end-user to see this statistics in a regular MPS scenario. Still, they could opt to do so with system property explicitly set.
      System.setProperty(PERF_LOG_KEY, Boolean.FALSE.toString());
    }
    System.setProperty("ide.new.project.model", Boolean.toString(false));
    System.setProperty("ide.open.readme.md.on.startup", Boolean.toString(false));
    if (System.getProperty(PathManager.PROPERTY_PATHS_SELECTOR) == null) {
      String val = "MPSCmdLine";
      if (myConfig.isTestMode()) {
        val += "Test";
      }
      System.setProperty(PathManager.PROPERTY_PATHS_SELECTOR, val);
    }
    String oldValue = System.getProperty(IDEA_USE_CORE_CL);
    if (oldValue == null) {
      // always true, resorting to the default behavior loading plugins from cp [apyshkin]
      System.setProperty(IDEA_USE_CORE_CL, Boolean.TRUE.toString());
    }
    if (myConfig.isTestMode()) {
      // Circumvent JBUIScale.computeSystemScaleFactor "Must be precomputed" error in tests
      System.setProperty("hidpi", Boolean.FALSE.toString());
      // Force GraphicsEnvironment to cache headless false state before TestApplicationManager resets it to true
      System.setProperty("java.awt.headless", Boolean.FALSE.toString());
      GraphicsEnvironment.isHeadless();
      System.setProperty("java.awt.headless", Boolean.TRUE.toString());
      myIdeaApplication = TestApplicationManager.getInstance();
    } else {
      myIdeaApplication = MPSHeadlessPlatformStarter.Holder.IT.createApp();
    }
  }

  @Override
  @NotNull
  public Project doOpenProject(@NotNull File projectFile) {
    boolean testMode = RuntimeFlags.isTestMode();
    Disposable disposable0 = null;
    if (testMode) {
      disposable0 = new Disposable() {
        @Override
        public void dispose() {
          // Explicit abstract class instead of lambda to avoid issues with WeakHashMap
        }
      };
      // for ant tests we run with the flag, which disables the checks
      VfsRootAccess.allowRootAccess(disposable0, projectFile.getAbsolutePath());
    }
    MPSProject openedProject = openProjectInIdeaEnvironment(projectFile);
    if (testMode) {
      Disposer.register(openedProject.getProject(), disposable0);
    }
    return openedProject;
  }

  @NotNull
  @Override
  public Project createEmptyProject() {
    checkInitialized();
    if (LOG.isInfoLevel()) {
      LOG.info("Creating an empty project");
    }
    File dummyProjectFile = createDummyProjectFile();
    Project dummyProject = openProject(dummyProjectFile);
    return dummyProject;
  }


  @Override
  public void closeProject(@NotNull Project project) {
    if (project instanceof MPSProject) {
      // For MPSProject as ProjectComponent for IDEA's Project, use platform means to close
      // the project.
      checkInitialized();
      final com.intellij.openapi.project.Project ideaProject = ((MPSProject) project).getProject();
      ApplicationManager.getApplication().invokeAndWait(() -> {
        FileEditorManagerEx.getInstanceEx(ideaProject).closeAllFiles();
        ProjectManagerEx.getInstanceEx().closeAndDispose(ideaProject);
      }, ModalityState.NON_MODAL);
    } else {
      super.closeProject(project);
    }
  }

  @Override
  public void doDispose() {
    final Application application = ApplicationManager.getApplication();
    application.invokeAndWait(new Runnable() {
      @Override
      public void run() {
        // XXX why PM from MPS, not from IDEA? We're in IDEA env, after all. Do we care
        //    about non-MPS projects?
        ProjectManager pm = getPlatform().findComponent(ProjectManager.class);
        List<Project> openedProjects = new ArrayList<Project>(pm.getOpenedProjects());
        for (final Project project : openedProjects) {
          if (project instanceof MPSProject) {
            // FIXME refactor do avoid code duplication with closeProject
            // Perhaps, shall use IDEA's Project manager here directly, and then check if any
            // MPSProject left open
            com.intellij.openapi.project.Project ideaProject = ((MPSProject) project).getProject();
            FileEditorManagerEx.getInstanceEx(ideaProject).closeAllFiles();
            ProjectManagerEx.getInstanceEx().closeAndDispose(ideaProject);
          } else {
            // no idea why there's a need to add write action for project dispose. isn't write-ready thread enough?
            project.dispose();
          }
        }
        PlatformTestUtil.dispatchAllEventsInIdeEventQueue();
        JarFileSystemImpl.cleanupForNextTest();
        application.runWriteAction(new Runnable() {
          public void run() {
            // for IdeaTestApplication case (myUnitTestMode == true) dispose() eventually clears DTA.ourInstance field
            if (myIdeaApplication instanceof Disposable) {
              Disposer.dispose((Disposable) myIdeaApplication);
            }
            myIdeaApplication = null;
          }
        });
      }
    }, ModalityState.nonModal());
  }

  @Override
  protected void initLibraries(@NotNull LibraryInitializer libInitializer) {
    if (SetSequence.fromSet(myConfig.getLibs()).isNotEmpty()) {
      LibraryContributorHelper helper = new LibraryContributorHelper();
      libInitializer.load(Collections.singletonList(helper.createLibContributorForLibs(myConfig.getLibs(), getRootClassLoader())));
    }
    // modules from IDEA plugins are loaded with regular platform component mechanism (ext points, PluginLibraryContributor and RepositoryInitializingComponent)
  }

  private File createDummyProjectFile() {
    File dummyProjDir = FileUtil.createTmpDir();
    File dotMps = new File(dummyProjDir, com.intellij.openapi.project.Project.DIRECTORY_STORE_FOLDER);
    dotMps.mkdir();
    dummyProjDir.deleteOnExit();
    return dummyProjDir;
  }

  @NotNull
  private MPSProject openProjectInIdeaEnvironment(File projectFile) {
    if (!(projectFile.exists())) {
      throw new ProjectDirectoryDoesNotExistException(projectFile.getAbsolutePath());
    }
    final ProjectManagerEx projectManager = ProjectManagerEx.getInstanceEx();
    final String filePath = projectFile.getAbsolutePath();

    final Reference<com.intellij.openapi.project.Project> project = new Reference<com.intellij.openapi.project.Project>();
    final Reference<Exception> exc = new Reference<Exception>();
    ApplicationManager.getApplication().invokeAndWait(new Runnable() {
      public void run() {
        try {
          if (LOG.isInfoLevel()) {
            LOG.info("Load and open the project with path '" + filePath + "'");
          }
          // fixme this is an IDE way of opening project
          project.set(projectManager.loadAndOpenProject(filePath));
          refreshProjectDir(project.get());
        } catch (Exception e) {
          exc.set(e);
        }
      }
    }, ModalityState.NON_MODAL);

    if (!(exc.isNull())) {
      throw new CouldNotLoadProjectException(String.format("ProjectManager could not load project from '%s'", projectFile.getAbsolutePath()), exc.get());
    }

    if (project.isNull()) {
      throw new ProjectCouldNotBeOpenedException(String.format("Could not open project (null in return) from the '%s'", projectFile.getAbsolutePath()), exc.get());
    }

    // does not seem applicable in test mode
    if (!(myConfig.isTestMode())) {
      final PostStartupActivitiesWaiter waiter = new PostStartupActivitiesWaiter(project.get());
      waiter.wait0(30, TimeUnit.SECONDS);
    }


    return project.get().getComponent(MPSProject.class);
  }

  private void refreshProjectDir(@NotNull com.intellij.openapi.project.Project project) {
    // calling sync refresh for FS in order to update all modules/models loaded from the project
    // if unit-test is executed with the "reuse caches" option.
    String basePath = project.getBasePath();
    if (basePath != null) {
      CachingFileSystem fs = ApplicationManager.getApplication().getComponent(IdeaFileSystem.class);
      fs.getFile(basePath).refresh(new DefaultCachingContext(true, true));
    }
  }

  /**
   * BIG GUN
   * Only use when absolutely necessary.
   * Please wrap an invocation of this method into a method which is clear what it waits for
   * For instance if you wait for the end of post-startup activities -- extract a new method
   * #waitForPostStartupActivitiesToEnd.
   * 
   * Otherwise it will be hard to perceive what exactly your code waits for
   */
  @Override
  public void flushAllEvents() {
    checkInitialized();
    ApplicationManager.getApplication().invokeAndWait(() -> PlatformTestUtil.dispatchAllEventsInIdeEventQueue(), ModalityState.NON_MODAL);
    // I expect invokeAndWait() above to pump all the pending model events
  }

  @Override
  public Platform getPlatform() {
    return getMPSCoreComponents().getPlatform();
  }

  private static final class PostStartupActivitiesWaiter {
    private final com.intellij.openapi.project.Project myProject;

    public PostStartupActivitiesWaiter(@NotNull com.intellij.openapi.project.Project project) {
      myProject = project;
    }

    private StartupManagerEx getStartupManager() {
      return StartupManagerEx.getInstanceEx(myProject);
    }

    public void wait0(long time, TimeUnit units) {
      try {
        FutureKt.asCompletableFuture(getStartupManager().getAllActivitiesPassedFuture()).get(time, units);
      } catch (InterruptedException | TimeoutException | ExecutionException e) {
        if (LOG.isErrorLevel()) {
          LOG.error("got while waiting for startup activities to finish", e);
        }
      }
      if (!(myProject.isDisposed()) && !(getStartupManager().postStartupActivityPassed())) {
        if (LOG.isErrorLevel()) {
          LOG.error("Could not wait until post-startup activities are finished", new IllegalStateException());
        }
      }
    }
  }

  private static final class CouldNotLoadProjectException extends EnvironmentSetupException {
    public CouldNotLoadProjectException(String message, Exception cause) {
      super(message, cause);
    }
  }

  private static final class ProjectCouldNotBeOpenedException extends EnvironmentSetupException {
    public ProjectCouldNotBeOpenedException(String message, Exception cause) {
      super(message, cause);
    }
  }

  private static final class ProjectDirectoryDoesNotExistException extends EnvironmentSetupException {
    public ProjectDirectoryDoesNotExistException(String projectPath) {
      super("Cannot find the project at '" + projectPath + "'");
    }
  }
}
