/*
 * Copyright 2000-2013 JetBrains s.r.o.
 * Copyright 2014-2014 AS3Boyan
 * Copyright 2014-2014 Elias Ku
 * Copyright 2017-2018 Eric Bishton
 *
 * 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 com.intellij.plugins.haxe.haxelib;

import com.intellij.compiler.ant.BuildProperties;
import com.intellij.ide.highlighter.XmlFileType;
import com.intellij.openapi.application.Application;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.module.Module;
import com.intellij.openapi.module.ModuleUtil;
import com.intellij.openapi.progress.PerformInBackgroundOption;
import com.intellij.openapi.progress.ProgressIndicator;
import com.intellij.openapi.progress.ProgressManager;
import com.intellij.openapi.progress.Task;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.projectRoots.Sdk;
import com.intellij.openapi.roots.*;
import com.intellij.openapi.roots.impl.libraries.ProjectLibraryTable;
import com.intellij.openapi.roots.libraries.Library;
import com.intellij.openapi.roots.libraries.LibraryTable;
import com.intellij.openapi.startup.StartupManager;
import com.intellij.openapi.util.Disposer;
import com.intellij.openapi.vfs.*;
import com.intellij.plugins.haxe.build.IdeaTarget;
import com.intellij.plugins.haxe.build.MethodWrapper;
import com.intellij.plugins.haxe.config.HaxeConfiguration;
import com.intellij.plugins.haxe.config.sdk.HaxeSdkType;
import com.intellij.plugins.haxe.hxml.HXMLFileType;
import com.intellij.plugins.haxe.hxml.model.HXMLProjectModel;
import com.intellij.plugins.haxe.ide.module.HaxeModuleSettings;
import com.intellij.plugins.haxe.ide.module.HaxeModuleType;
import com.intellij.plugins.haxe.nmml.NMMLFileType;
import com.intellij.plugins.haxe.util.HaxeDebugTimeLog;
import com.intellij.plugins.haxe.util.HaxeDebugUtil;
import com.intellij.plugins.haxe.util.HaxeFileUtil;
import com.intellij.plugins.haxe.util.Lambda;
import com.intellij.psi.PsiFile;
import com.intellij.psi.PsiManager;
import com.intellij.psi.xml.XmlFile;
import org.apache.log4j.Level;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.io.LocalFileFinder;

import java.io.File;
import java.util.*;
import java.util.concurrent.ConcurrentLinkedQueue;

/**
 * Manages Haxe library class paths across projects.
 *
 * This class is intended to keep the class paths up to date as the projects
 * and module settings change.  It encapsulates reading classpaths from the
 * various types of Haxe project definitions (OpenFL, NME, etc.) and adding
 * them to module settings so that the paths available at runtime are also
 * available when writing the code.
 *
 *
 * Implementation Note: We might need to track each module separately, in a
 * list attached to the project.  We'd need that in case we can update the
 * project fast enough that all of the modules haven't been opened yet.
 * However, this is unlikely, since the first project update run takes place
 * after the project has finished initializing, and the open notifications
 * come during initialization.
 *
 */
public class HaxelibProjectUpdater {

  static Logger LOG = Logger.getInstance("#com.intellij.plugins.haxe.haxelib.HaxelibManager");

  {
    LOG.setLevel(Level.DEBUG);
  }

  /**
   * Set this to run in the foreground for speed testing.
   * It overrides myRunInForeground.  The UI is blocked with no updates.
   */
  private static final boolean myTestInForeground = false;
  /**
   * Set this true to put up a modal dialog and run in the foreground thread
   * (locking up the UI.)
   * Set it false to run in a background thread.  Progress is updated in the
   * status bar and the UI is usable.
   */
  private static final boolean myRunInForeground = false;

  public static final HaxelibProjectUpdater INSTANCE = new HaxelibProjectUpdater();

  private ProjectUpdateQueue myQueue = null;
  private ProjectMap myProjects = null;

  private HaxelibProjectUpdater() {
    myQueue = new ProjectUpdateQueue();
    myProjects = new ProjectMap();
  }

  @NotNull
  static HaxelibProjectUpdater getInstance() {
    return INSTANCE;
  }

  /**
   * Adds a new project to the manager.  This is normally called in response to a
   * ModuleComponent.openProject() notification.  Multiple modules referencing
   * the same project cause a counter to be incremented.
   *
   * @param project that is being opened.
   */
  public void openProject(@NotNull Project project) {
    ProjectTracker tracker = myProjects.add(project);
    tracker.setDirty(true);
    myQueue.add(tracker);
  }

  /**
   * Close and possibly remove a project, if the reference count has been exhausted.
   *
   * @param project to close
   * @return whether the close has been delayed because an update is in process.
   */
  public boolean closeProject(Project project) {
    boolean delayed = false;
    boolean removed = false;

    ProjectTracker tracker = myProjects.get(project);
    removed = myProjects.remove(project);
    if (removed) {
      myQueue.remove(tracker);
      if (tracker.equals(myQueue.getUpdatingProject())) {
        delayed = true;
      }
    }
    return delayed;
  }

  /**
   * Retrieve the HaxelibLibraryCacheManager for a given module/project.
   * <p>
   * Convenience function that doesn't quite match the purpose of this class,
   * but we haven't made the CacheManager a singleton -- and we really can't
   * unless we move the notion of a project into it.
   *
   * @param module that we need the HaxelibLibraryCacheManager for.
   * @return the appropriate HaxelibLibraryCacheManager.
   */
  @Nullable
  public HaxelibLibraryCacheManager getLibraryCacheManager(@NotNull Module module) {
    return getLibraryCacheManager(module.getProject());
  }

  /**
   * Retrieve the HaxelibLibraryCacheManager for a given module/project.
   * <p>
   * Convenience function that doesn't quite match the purpose of this class,
   * but we haven't made the CacheManager a singleton -- and we really can't
   * unless we move the notion of a project into it.
   *
   * @param project that we need the HaxelibLibraryCacheManager for.
   * @return the appropriate HaxelibLibraryCacheManager.
   */
  @Nullable
  public HaxelibLibraryCacheManager getLibraryCacheManager(@NotNull Project project) {
    ProjectTracker tracker = myProjects.get(project);
    return null == tracker ? null : tracker.getSdkManager();
  }

  /**
   * Retrieve the library cache for a given module.
   * <p>
   * Convenience function that doesn't quite match the purpose of this class,
   * but we haven't made the CacheManager a singleton -- and we really can't
   * unless we move the notion of a project into it.
   *
   * @param module
   * @return the HaxelibLibraryCache for the module, if found; null, otherwise.
   */
  @Nullable
  public static HaxelibLibraryCache getLibraryCache(@NotNull Module module) {
    HaxelibLibraryCacheManager mgr = HaxelibProjectUpdater.getInstance().getLibraryCacheManager(module);
    return mgr != null ? mgr.getLibraryManager(module) : null;
  }

  /**
   * Retrieve the library cache for a given module.
   * <p>
   * Convenience function that doesn't quite match the purpose of this class,
   * but we haven't made the CacheManager a singleton -- and we really can't
   * unless we move the notion of a project into it.
   *
   * @param project
   * @return the HaxelibLibraryCache for the project, if found; null, otherwise.
   */
  @Nullable
  public static HaxelibLibraryCache getLibraryCache(@NotNull Project project) {
    HaxelibLibraryCacheManager mgr = HaxelibProjectUpdater.getInstance().getLibraryCacheManager(project);
    Sdk projectSdk = HaxelibSdkUtils.lookupSdk(project);
    return (mgr != null && projectSdk != null) ? mgr.getLibraryCache(projectSdk) : null;
  }

  /**
   * Resolve the classpath/library entries for a module.  Determines which
   * libraries to add and remove from the module.  Only libraries that have
   * previously been added may be removed, if they have become redundant
   * or otherwise specified.
   *
   * @param tracker      for the project being updated.
   * @param module       being updated.
   * @param externalLibs potential new libraries that must be available
   *                     to the module when this routine finishes.  These are
   *                     typically specified in the Haxe project files. (e.g. -lib)
   */
  private void resolveModuleLibraries(ProjectTracker tracker, Module module, HaxeLibraryList externalLibs) {
    HaxeLibraryList toAdd;
    HaxeLibraryList toRemove;

    toAdd = new HaxeLibraryList(module);
    toRemove = new HaxeLibraryList(module);
    Sdk moduleSdk = ModuleRootManager.getInstance(module).getSdk();
    if (null == moduleSdk) {
      LOG.debug("No SDK for module " + module.getName() + ".  Not syncing haxelibs.");
      return; // Nothing to do if there is no SDK.
    }
    syncLibraryLists(moduleSdk,
                     HaxelibUtil.getModuleLibraries(module),
                     externalLibs,
        /*modifies*/ toAdd,
        /*modifies*/ toRemove);

    updateModule(tracker, module, toRemove, toAdd);
  }

  /**
   * Find an IDEA library matching a HaxeLibraryReference.
   *
   * @param iter the table iterator.
   * @param ref the library to look for.
   * @return the Library, if found; null, otherwise.
   */
  @Nullable
  private Library lookupLibrary(@NotNull Iterator<Library> iter, @NotNull HaxeLibraryReference ref) {
    Library found = null;
    while (null == found && iter.hasNext()) {
      Library toTest = iter.next();
      if (ref.matchesIdeaLib(toTest)) {
        found = toTest;
      }
    }
    return found;
  }

  /**
   * Find an IDEA library in a project's LibraryTable matching a HaxeLibraryReference.
   *
   * @param table - the LibraryTable to look in.
   * @param ref - the library to find.
   * @return the Library, if found; null, otherwise.
   */
  @Nullable
  private Library lookupProjectLibrary(@NotNull LibraryTable table, @NotNull HaxeLibraryReference ref) {
    return lookupLibrary(table.getLibraryIterator(), ref);
  }

  /**
   * Find an IDEA library in a module's LibraryTable (actually, its ModifiableModel)
   * matching a HaxeLibraryReference.
   *
   * @param table - the LibraryTable to look in.
   * @param ref - the library to find.
   * @return the Library, if found; null, otherwise.
   */
  @Nullable
  private Library lookupModelLibrary(@NotNull LibraryTable.ModifiableModel table, @NotNull HaxeLibraryReference ref) {
    return lookupLibrary(table.getLibraryIterator(), ref);
  }

  /**
   * Remove libraries from a library table.
   *
   * @param toRemove - The list of libraries to remove.
   * @param libraryTableModel - The (modifiable model of the) table to remove them from.
   * @param timeLog - Debugging time log.
   */
  private void removeLibraries(@NotNull final HaxeLibraryList toRemove,
                               @NotNull final LibraryTable.ModifiableModel libraryTableModel,
                               @NotNull final HaxeDebugTimeLog timeLog) {
    timeLog.stamp("Removing libraries.");
    toRemove.iterate(new HaxeLibraryList.Lambda() {
      @Override
      public boolean processEntry(HaxeLibraryReference entry) {
        Library library = lookupModelLibrary(libraryTableModel, entry);
        if (null != library) {
          // Why use this?: ModuleHelper.removeDependency(rootManager, library);
          libraryTableModel.removeLibrary(library);
          timeLog.stamp("Removed library " + library.getName());
        }
        else {
          LOG.warn(
            "Internal inconsistency: library to remove was not found: " +
            entry.getName());
        }
        return true;
      }
    });
  }

  /**
   * Add libraries to a library table.
   *
   * @param toAdd - List of libraries to add.
   * @param projectLibraries - libraries that can be referenced instead of created.
   * @param projectTable - the library table that projectLibraries belong to.
   * @param moduleModel - the module we are adding libraries to
   * @param libraryTableModel - the library table for that module
   * @param timeLog - Debugging info and timer.
   */
  private void addLibraries(@NotNull final HaxeLibraryList toAdd,
                            @NotNull final HaxeLibraryList projectLibraries,
                            @NotNull final LibraryTable projectTable,
                            @NotNull final ModifiableRootModel moduleModel,
                            @NotNull final LibraryTable.ModifiableModel libraryTableModel,
                            @NotNull final HaxeDebugTimeLog timeLog) {
    timeLog.stamp("Locating libraries and adding dependencies.");
    toAdd.iterate(new HaxeLibraryList.Lambda() {
      @Override
      public boolean processEntry(HaxeLibraryReference entry) {
        Library moduleLibrary = lookupModelLibrary(libraryTableModel, entry);

        // If the lib is in the project list, then we just add a reference to it.
        if (projectLibraries.contains(entry)) {
          if (null != moduleLibrary) {
            LOG.warn("Internal inconsistency: attempting to add library that is already in the module.");
          } else {
            Library projectLibrary = lookupProjectLibrary(projectTable, entry);
            if (null == projectLibrary) {
              // EMB: Not writing recovery code because this really shouldn't happen.
              LOG.warn("Internal inconsistency: Could not find project library when it should exist.");
            } else {
              LibraryOrderEntry libraryOrderEntry = moduleModel.addLibraryEntry(projectLibrary);
              libraryOrderEntry.setExported(false);
              libraryOrderEntry.setScope(DependencyScope.PROVIDED);
              timeLog.stamp("Added module-level reference to project lib " + projectLibrary.getName());
            }
          }
        }
        else {  // Not in the project, so add it to the module.

          if (moduleLibrary == null) {
            Library.ModifiableModel libraryModelToDispose = null;
            try {
              final Library newLibrary = libraryTableModel.createLibrary(entry.getPresentableName());
              moduleLibrary = newLibrary;

              final Library.ModifiableModel libraryModifiableModel = newLibrary.getModifiableModel();
              libraryModelToDispose = libraryModifiableModel;

              HaxeLibrary entryLibrary = entry.getLibrary();
              HaxeClasspath classpath = entryLibrary != null ? entryLibrary.getClasspathEntries() : null;
              if (null != classpath) {
                classpath.iterate(new HaxeClasspath.Lambda() {
                  @Override
                  public boolean processEntry(HaxeClasspathEntry cp) {
                    String url = HaxeFileUtil.fixUrl(cp.getUrl());
                    VirtualFile directory = VirtualFileManager.getInstance().findFileByUrl(url);
                    if (null == directory) {
                      timeLog.stamp("Skipping classpath for " + newLibrary.getName() + ", no directory entry for " + url);
                    }
                    else {
                      libraryModifiableModel.addRoot(directory, OrderRootType.CLASSES);
                      libraryModifiableModel.addRoot(directory, OrderRootType.SOURCES);
                    }
                    return true;
                  }
                });
              }

              LibraryOrderEntry libraryOrderEntry = moduleModel.findLibraryOrderEntry(newLibrary);
              libraryOrderEntry.setExported(false);
              libraryOrderEntry.setScope(DependencyScope.PROVIDED);

              libraryModifiableModel.commit();
              libraryModelToDispose = null; // So we don't dispose of it now that we've committed.
              timeLog.stamp("Added library " + newLibrary.getName());
            }
            finally {
              if (null != libraryModelToDispose) {
                timeLog.stamp("Failure to create library " + moduleLibrary.getName());
                Disposer.dispose(libraryModelToDispose);
              }
            }
          }
          else {
            LOG.warn("Internal inconsistency: library to add was already in the module's library table.");
          }
        }
        return true;
      }
    });
  }

  /**
   * Ensure that all entries in the given list are managed.
   *
   * @param list - List of entries to check.
   * @param msg - Message to display on failure.
   */
  private void assertEntriesAreManaged(HaxeLibraryList list, final String msg) {
    if (null != list) {
      list.iterate(new HaxeLibraryList.Lambda() {
        @Override
        public boolean processEntry(HaxeLibraryReference entry) {
          LOG.assertTrue(entry.isManaged(), msg);
          return true;
        }
      });
    }
  }

  /**
   * Workhorse routine for resolveModuleLibraries.  This does the actual
   * update of the module.  It will block until all of the running events
   * on the AWT thread have completed, and then this will run on that thread.
   *
   * @param module   to update.
   * @param toRemove libraries that need to be removed from the module.
   * @param toAdd    libraries that need to be added to the module.
   */
  private void updateModule(final ProjectTracker tracker,
                            final Module module,
                            final HaxeLibraryList toRemove,
                            final HaxeLibraryList toAdd) {
    if ((null == toRemove || toRemove.isEmpty()) && (null == toAdd || toAdd.isEmpty())) {
      return;
    }

    // Some internal error checking.
    assertEntriesAreManaged(toRemove, "Attempting to automatically remove a library that was not marked as managed.");
    assertEntriesAreManaged(toAdd, "Attempting to automatically add a library that is not marked as managed.");

    final HaxeDebugTimeLog timeLog = new HaxeDebugTimeLog("Write action:");
    timeLog.stamp("Queueing write action...");

    doWriteAction(new Runnable() {
      @Override
      public void run() {
        timeLog.stamp("<-- Time elapsed waiting for write access on the AWT thread.");
        timeLog.stamp("Begin: Updating module libraries for " + module.getName());

        // Figure out the list of project libraries that we should reference, if we can.
        HaxeLibraryList projectLibraries = ModuleRootManager.getInstance(module).isSdkInherited()
                                         ? getProjectLibraryList(tracker)
                                         : new HaxeLibraryList(module);
        final LibraryTable projectTable = ProjectLibraryTable.getInstance(tracker.getProject());

        timeLog.stamp("<-- Time elapsed retrieving project libraries.");

        ModifiableRootModel moduleRootModel = null;
        LibraryTable.ModifiableModel libraryTableModel = null;
        try {
          moduleRootModel = ModuleRootManager.getInstance(module).getModifiableModel();
          libraryTableModel = moduleRootModel.getModuleLibraryTable().getModifiableModel();

          // Remove unused packed "haxelib|<lib_name>" libraries from the module and project library.
          if (null != toRemove) {
            removeLibraries(toRemove, libraryTableModel, timeLog);
          }

          // Add new dependencies to modules.
          if (null != toAdd) {
            addLibraries(toAdd, projectLibraries, projectTable, moduleRootModel, libraryTableModel, timeLog);
          }

          timeLog.stamp("Committing changes to module libraries");
          libraryTableModel.commit();
          libraryTableModel = null;
          moduleRootModel.commit();
          moduleRootModel = null;
        }
        finally {
          if (null != moduleRootModel || null != libraryTableModel)
            timeLog.stamp("Failure to update module libraries");
          if (null != libraryTableModel)
            if (IdeaTarget.IS_VERSION_15_COMPATIBLE) {
              // libraryTableModel.dispose() in IDEA 15+; not a disposable in earlier versions.
              new MethodWrapper<Void>(libraryTableModel.getClass(), "dispose").invoke(libraryTableModel);
            }
          if (null != moduleRootModel)
            moduleRootModel.dispose();
        }
        timeLog.stamp("Finished: Updating module libraries");
      }
    });

    timeLog.print();
  }

  /**
   * The guts of syncModuleClasspaths, separated so that it can be run as
   * either a foreground or background task.
   *
   * @param tracker for the project being updated.
   * @param module  being updated.
   * @param timeLog where to log timing results
   */
  private void syncOneModule(@NotNull final ProjectTracker tracker, @NotNull Module module, @NotNull HaxeDebugTimeLog timeLog) {

    Project project = tracker.getProject();
    HaxeLibraryList haxelibExternalItems = new HaxeLibraryList(module);
    HaxelibLibraryCache libManager = tracker.getSdkManager().getLibraryManager(module);
    HaxeModuleSettings settings = HaxeModuleSettings.getInstance(module);

    // If the module says not to keep libs synched, then don't.
    if (!settings.isKeepSynchronizedWithProjectFile()) {
      timeLog.stamp("Module " + module.getName() + " is set to not synchronize dependencies.");
      return;
    }

    switch (settings.getBuildConfiguration()) {
      case NMML:
        timeLog.stamp("Start loading haxelibs from NMML file.");
        HaxeLibrary nme = libManager.getLibrary("nme", HaxelibSemVer.ANY_VERSION);
        if (null != nme) {
          haxelibExternalItems.add(nme.createReference(HaxelibSemVer.ANY_VERSION));
        }
        else {
          // TODO: Figure out how to communicate this to the user.
          LOG.warn("Required library 'nme' is not known to haxelib.");
        }

        // TODO: Pull libs off of the command line, too.

        String nmmlPath = settings.getNmmlPath();
        if (nmmlPath != null && !nmmlPath.isEmpty()) {
          VirtualFile file = LocalFileFinder.findFile(nmmlPath);

          if (file != null && file.getFileType().equals(NMMLFileType.INSTANCE)) {
            VirtualFileManager.getInstance().syncRefresh();
            PsiFile psiFile = PsiManager.getInstance(project).findFile(file);

            if (psiFile != null && psiFile instanceof XmlFile) {
              haxelibExternalItems.addAll(HaxelibUtil.getHaxelibsFromXmlFile((XmlFile)psiFile, libManager));
            }
          }
        }
        timeLog.stamp("Finished loading haxelibs from NMML file.");

        // TODO: Load classpaths from the NMML file, CL, and ensure that they are included in the module sources.

        break;

      case OPENFL:
        timeLog.stamp("Start loading haxelibs from openFL configuration file.");
        HaxeLibrary openfl = libManager.getLibrary("openfl", HaxelibSemVer.ANY_VERSION);
        if (null != openfl) {
          haxelibExternalItems.add(openfl.createReference(HaxelibSemVer.ANY_VERSION)); // No specific version.
        }
        else {
          // TODO: Figure out how to report this to the user.
          LOG.warn("Required library 'openfl' is not known to haxelib.");
        }

        // TODO: Pull libs off of the command line, too.

        String openFLXmlPath = settings.getOpenFLPath();
        if (openFLXmlPath != null && !openFLXmlPath.isEmpty()) {
          VirtualFile file = LocalFileFinder.findFile(openFLXmlPath);

          if (file != null && file.getFileType().equals(XmlFileType.INSTANCE)) {
            PsiFile psiFile = PsiManager.getInstance(project).findFile(file);

            if (psiFile != null && psiFile instanceof XmlFile) {
              haxelibExternalItems.addAll(HaxelibUtil.getHaxelibsFromXmlFile((XmlFile)psiFile, libManager));
            }
          }
        }
        else {
          // XXX: EMB - Not sure of the validity of using this path if xml lib isn't specified.

          File dir = BuildProperties.getProjectBaseDir(project);
          List<String> projectClasspaths =
            HaxelibClasspathUtils.getProjectDisplayInformation(project, dir, "openfl",
                                                               HaxelibSdkUtils.lookupSdk(module));

          for (String classpath : projectClasspaths) {
            VirtualFile file = LocalFileFinder.findFile(classpath);
            if (file != null) {
              HaxeLibrary lib = libManager.getLibraryByPath(file.getPath());
              if (null != lib) {
                haxelibExternalItems.add(lib.createReference());
              }
              else {
                // TODO: Figure out how to communicate this to the user.
                LOG.warn("Library referenced by openFL configuration is not known to haxelib " + classpath);
              }
            }
          }
        }
        haxelibExternalItems.debugDump("haxelibExternalItems for module " + module.getName());
        timeLog.stamp("Finished loading haxelibs from openfl file.");

        // TODO: Add classpaths from the xml file and the CL to the module sources.

        break;

      case HXML:
        timeLog.stamp("Start loading haxelibs from HXML file.");
        String hxmlPath = settings.getHxmlPath();

        // TODO: Walk the command line looking for libs, too.

        if (hxmlPath != null && !hxmlPath.isEmpty()) {
          VirtualFile file = LocalFileFinder.findFile(hxmlPath);

          if (file != null && file.getFileType().equals(HXMLFileType.INSTANCE)) {
            PsiFile psiFile = PsiManager.getInstance(project).findFile(file);
            if (psiFile != null) {
              HXMLProjectModel hxml = new HXMLProjectModel(psiFile);
              // TODO: Needs to walk all of the children and load referenced .hxml files in place. Should probably be added to the model.
              List<String> libs = hxml.getLibraries();
              if (libs != null) {
                for (String lib : libs) {
                  HaxeLibraryReference reference = HaxeLibraryReference.create(module, lib);
                  if (null != reference.getLibrary()) {
                    haxelibExternalItems.add(reference);
                  }
                  else {
                    LOG.warn("Library referenced by HXML configuration is not known to haxelib.");
                  }
                }
              }
            }
          }
        }
        timeLog.stamp("Finish loading haxelibs from HXML file.");

        // TODO: Add classpaths from the HXML file and CL to module sources

        break;

      case CUSTOM:
        timeLog.stamp("Start loading haxelibs from properties.");

        // TODO: Grab the command line?? Run it through the algorithm for USE_HXML.

        timeLog.stamp("Finish loading haxelibs from properties.");
        break;

      default:
        throw new HaxeDebugUtil.InvalidCaseException(settings.getBuildConfiguration());
    }

    // We can't just remove all of the project classpaths from the module's
    // library list here because we need to remove any managed classpaths that
    // are no longer valid in the modules.  We can't do that if we don't have
    // the list of valid ones.  :/
    timeLog.stamp("Adding libraries to module.");
    resolveModuleLibraries(tracker, module, haxelibExternalItems);
    timeLog.stamp("Finished adding libraries to module.");
  }


  private void syncModuleClasspaths(final ProjectTracker tracker) {
    final HaxeDebugTimeLog timeLog = HaxeDebugTimeLog.startNew("syncModuleClasspaths");

    final Project project = tracker.getProject();

    //LOG.debug("Scanning project " + project.getName());
    timeLog.stamp("Scanning project " + project.getName());

    Collection<Module> modules = ModuleUtil.getModulesOfType(project, HaxeModuleType.getInstance());
    int i = 0;
    final int count = modules.size();
    for (final Module module : modules) {

      final int num = ++i;

      //LOG.debug("Scanning module " + (++i) + " of " + count + ": " + module.getName());
      timeLog.stamp("\nScanning module " + (num) + " of " + count + ": " + module.getName());

      if (myTestInForeground) {
        syncOneModule(tracker, module, timeLog);
      }
      else {
        // Running inside of a read action lets the UI run, and messes with the timing.
        doReadAction(new Runnable() {
          @Override
          public void run() {
            syncOneModule(tracker, module, timeLog);
          }
        });
      }
    }
    timeLog.stamp("Completed.");
    timeLog.print();
  }


  private void synchronizeClasspaths(@NotNull ProjectTracker tracker) {

    //
    // Either of these commented-out sections will cause indexing to not be attempted
    // while the haxelibs are synchronizing.  However, they also hide the fact that
    // haxelibs are updating by not allowing the haxelib progress dialog to start.
    // Instead, it looks like indexing restarts over and over and over (which it
    // kinda does).
    //

    //DumbServiceImpl dumbService = DumbServiceImpl.getInstance(tracker.getProject());
    //dumbService.queueTask(new DumbModeTask() {
    //  @Override
    //  public void performInDumbMode(@NotNull ProgressIndicator indicator) {
    //    syncProjectClasspath(tracker);
    //    syncModuleClasspaths(tracker);
    //  }
    //});

    //TransactionGuard tg = TransactionGuard.getInstance();
    //tg.submitTransactionAndWait(new Runnable() {
    //  @Override
    //  public void run() {
    //    syncProjectClasspath(tracker);
    //    syncModuleClasspaths(tracker);
    //  }
    //});

    syncProjectClasspath(tracker);
    syncModuleClasspaths(tracker);
  }

  /**
   * Retrieves the project's libraries, either from the cache if available,
   * or from the project's library table.
   *
   * @param tracker for the project being updated.
   * @return a HaxeClasspath representing the libraries specified at the project level.
   */
  @NotNull
  private HaxeLibraryList getProjectLibraryList(@NotNull ProjectTracker tracker) {
    ProjectLibraryCache cache = tracker.getCache();
    HaxeLibraryList projectLibraries;
    HaxeConfiguration buildConfig = HaxeConfiguration.CUSTOM; // Only properties available.

    if (cache.isListSetFor(buildConfig)) {
      projectLibraries = cache.getListFor(buildConfig);
    }
    else {
      projectLibraries = HaxelibUtil.getProjectLibraries(tracker.getProject(), false, false);
      cache.setListFor(buildConfig, projectLibraries);
    }
    return projectLibraries;
  }

  /**
   * Collect all dependencies for libraries in list.
   *
   * @param list of libraries to collect dependencies for.
   * @return a list of libraries that list depends upon.  May include entries
   * from list itself, if there are cross-dependencies.
   */
  @NotNull
  private HaxeLibraryList collectDependencies(@NotNull HaxeLibraryList list) {
    final HaxeLibraryList dependencies = new HaxeLibraryList(list.getOwner());
    list.iterate(new HaxeLibraryList.Lambda() {
      @Override
      public boolean processEntry(HaxeLibraryReference entry) {
        if (entry.isAvailable()) {
          dependencies.addAll(entry.getLibrary().collectDependents());
        }
        return true;
      }
    });
    return dependencies;
  }

  /**
   * Mark all entries in a list as managed...
   *
   * @param list
   */
  @NotNull
  private void markAsManaged(@NotNull HaxeLibraryList list) {
    list.iterate(new HaxeLibraryList.Lambda() {
      @Override
      public boolean processEntry(HaxeLibraryReference entry) {
        entry.markAsManagedEntry();
        return true;
      }
    });
  }

  /**
   * Find libraries with roots that exist in the classpath.
   *
   * @param libs libs to match to classpaths.
   * @param classpath classpath to match.
   * @return a list of libraries whose directory entries match entries from the classpath.
   */
  @NotNull
  private HaxeLibraryList findLibsMatchingClasspath(@NotNull final HaxeLibraryList libs, @NotNull final HaxeClasspath classpath) {
    final HaxeLibraryList matchingLibs = new HaxeLibraryList(libs.getOwner());
    libs.iterate(new HaxeLibraryList.Lambda() {
      @Override
      public boolean processEntry(HaxeLibraryReference entry) {
        HaxeLibrary lib = entry.getLibrary();
        if (null != lib && classpath.contains(lib.getLibraryRoot()))
          matchingLibs.add(entry);
        return true;
      }
    });
    return matchingLibs;
  }

  /**
   * Synchronize library lists to figure out which dependencies need to be added or removed.
   *
   * @param currentList the list of libraries currently defined in the module/project/sdk.
   * @param externallyRequired the list of libraries required by project settings. (e.g. openfl, nme)
   * @param newLibrariesToAdd the resultant list of libraries to add to currentList
   * @param oldLibrariesToRemove the resultant list of libraries to remove from currentList.
   */
  @NotNull
  private void syncLibraryLists(@NotNull Sdk sdk,
                                @NotNull HaxeLibraryList currentList,
                                @NotNull HaxeLibraryList externallyRequired,
                   /*modifies*/ @NotNull HaxeLibraryList newLibrariesToAdd,
                   /*modifies*/ @NotNull HaxeLibraryList oldLibrariesToRemove) {

    final HaxeLibraryList currentManagedEntries = new HaxeLibraryList(sdk);
    final HaxeLibraryList currentUnmanagedEntries = new HaxeLibraryList(sdk);
    currentList.iterate(new HaxeLibraryList.Lambda() {
      @Override
      public boolean processEntry(HaxeLibraryReference entry) {
        if (entry.isManaged()) { // (e.g. starts with "haxelib|"
          currentManagedEntries.add(entry);
        }  else {
          currentUnmanagedEntries.add(entry);
        }
        return true;
      }
    });

    HaxeLibraryList dependencies = new HaxeLibraryList(collectDependencies(externallyRequired));
    dependencies.addAll(collectDependencies(currentUnmanagedEntries));

    // We want to remove all managed entries that we don't need any more.
    HaxeLibraryList toRemove = currentManagedEntries;

    // We want to add all externally required entries
    HaxeLibraryList toAdd = new HaxeLibraryList(sdk);
    toAdd.addAll(externallyRequired);
    toAdd.addAll(dependencies);
    toAdd.removeAll(currentUnmanagedEntries);
    // At this point, we could remove libs that exist via alternative names (find by classpath).
    // However, they can't be used with the compiler anyway, so the better part of valor
    // is to include them by the name that haxelib knows, even if it technically duplicates
    // the entry.
    markAsManaged(toAdd);

    // Anything that is in both lists, we don't want to touch.
    newLibrariesToAdd.addAll(toAdd);
    newLibrariesToAdd.removeAll(toRemove);

    oldLibrariesToRemove.addAll(toRemove);
    oldLibrariesToRemove.removeAll(toAdd);
  }


  /**
   * Removes old unneeded libraries and adds new dependencies to the project classpath.
   * Queues an update to the Project.
   *
   * @param tracker for the project being updated.
   */
  @NotNull
  private void syncProjectClasspath(@NotNull ProjectTracker tracker) {
    Sdk sdk = HaxelibSdkUtils.lookupSdk(tracker.getProject());
    boolean isHaxeSDK = sdk.getSdkType().equals(HaxeSdkType.getInstance());

    if (!isHaxeSDK) {
      return;
    }

    HaxeDebugTimeLog timeLog = new HaxeDebugTimeLog("syncProjectClasspath");
    timeLog.stamp("Start synchronizing project " + tracker.getProject().getName());

    HaxeLibraryList toAdd = new HaxeLibraryList(sdk);
    HaxeLibraryList toRemove = new HaxeLibraryList(sdk);
    syncLibraryLists(sdk,
                     HaxelibUtil.getProjectLibraries(tracker.getProject(),false, false),
                     new HaxeLibraryList(sdk),
        /*modifies*/ toAdd,
        /*modifies*/ toRemove );

    if (!toAdd.isEmpty() && !toRemove.isEmpty()) {
      timeLog.stamp("Add/Remove calculations finished.  Queuing write task.");
      updateProject(tracker, toRemove, toAdd);
    }

    timeLog.stamp("Finished synchronizing.");
    timeLog.print();

    // And update the cache.
    tracker.getCache().setPropertiesList(HaxelibUtil.getProjectLibraries(tracker.getProject(), false, false));
  }


  /**
   * Workhorse routine for syncProjectClasspath.  This does the actual update of the
   * project.  It will block until all of the running events on the AWT thread have
   * completed, and then this will run on that thread.
   *
   * @param tracker for the project to update.
   * @param toRemove libraries that need to be removed from the project.
   * @param toAdd libraries that need to be added to the project.
   */
  private void updateProject(@NotNull final ProjectTracker tracker,
                             final @Nullable HaxeLibraryList toRemove,
                             final @Nullable HaxeLibraryList toAdd) {
    if (null == toRemove && null == toAdd) {
      return;
    }
    if (null != toRemove) {
      toRemove.iterate(new HaxeLibraryList.Lambda() {
        @Override
        public boolean processEntry(HaxeLibraryReference entry) {
          LOG.assertTrue(entry.isManaged(), "Attempting to automatically remove a library that was not marked as managed.");
          return true;
        }
      });
    }
    if (null != toAdd) {
      toAdd.iterate(new HaxeLibraryList.Lambda() {
        @Override
        public boolean processEntry(HaxeLibraryReference entry) {
          LOG.assertTrue(entry.isManaged(), "Attempting to automatically add a library that is not marked as managed.");
          return true;
        }
      });
    }

    doWriteAction(new Runnable() {
      @Override
      public void run() {
        final HaxeDebugTimeLog timeLog = new HaxeDebugTimeLog("Write action:");
        timeLog.stamp("Begin: Updating project libraries");

        LibraryTable projectTable = ProjectLibraryTable.getInstance(tracker.getProject());
        final LibraryTable.ModifiableModel projectModifiableModel = projectTable.getModifiableModel();

        // Remove unused packed "haxelib|<lib_name>" libraries from the module and project library.
        if (null != toRemove) {
          timeLog.stamp("Removing unneeded haxelib libraries.");
          toRemove.iterate(new HaxeLibraryList.Lambda() {
            @Override
            public boolean processEntry(HaxeLibraryReference entry) {
              Library library = projectModifiableModel.getLibraryByName(
                entry.getName());
              LOG.assertTrue(null != library, "Library " + entry.getName() + " was not found in the project and will not be removed.");
              if (null != library) {
                projectModifiableModel.removeLibrary(library);
                timeLog.stamp("Removed library " + entry.getName());
              }
              else {
                timeLog.stamp(
                  "Library to remove was not found: " + entry.getName());
              }
              return true;
            }
          });
        }

        // Add new dependencies to modules.
        if (null != toAdd) {
          timeLog.stamp("Adding haxelib dependencies.");
          toAdd.iterate(new HaxeLibraryList.Lambda() {
            @Override
            public boolean processEntry(HaxeLibraryReference newItem) {
              Library libraryByName = projectModifiableModel.getLibraryByName(newItem.getName());
              if (libraryByName == null) {
                assert newItem.isAvailable(); // Should have been removed if unavailable.
                libraryByName = projectModifiableModel.createLibrary(newItem.getName());  // TODO: Presentable Name??
                Library.ModifiableModel libraryModifiableModel = libraryByName.getModifiableModel();
                libraryModifiableModel.addRoot(newItem.getLibrary().getSourceRoot().getUrl(), OrderRootType.CLASSES);
                libraryModifiableModel.addRoot(newItem.getLibrary().getSourceRoot().getUrl(), OrderRootType.SOURCES);
                libraryModifiableModel.commit();

                timeLog.stamp("Added library " + libraryByName.getName());
              }
              return true;
            }
          });
        }

        timeLog.stamp("Committing project changes.");
        projectModifiableModel.commit();
        timeLog.stamp("Finished: Updating project Libraries");
        timeLog.print();
      }
    });
  }

  /**
   * Cause a synchronous write action to be run on the AWT thread.
   *
   * @param action action to run.
   */
  private static void doWriteAction(final Runnable action) {
    final Application application = ApplicationManager.getApplication();
    application.invokeAndWait(new Runnable() {
      @Override
      public void run() {
        application.runWriteAction(action);
      }
    }, application.getDefaultModalityState());
  }

  /**
   * Cause a synchronous read action to be run.  Blocks if a write action is
   * currently running in the AWT thread.  Also blocks write actions from
   * occuring while this is being run.  So don't let tasks take too long, or
   * the UI gets choppy.
   *
   * @param action action to run.
   */
  private static void doReadAction(final Runnable action) {
    final Application application = ApplicationManager.getApplication();
    application.invokeAndWait(new Runnable() {
      @Override
      public void run() {
        application.runReadAction(action);
      }
    }, application.getDefaultModalityState());
  }

  /**
   *  Cache for project library lists.
   */
  final private class ProjectLibraryCache {

    private Sdk sdk;
    private HaxeLibraryList nmmlList;
    private HaxeLibraryList openFLList;
    private HaxeLibraryList hxmlList;
    private HaxeLibraryList propertiesList;
    private boolean nmmlIsSet;
    private boolean openFLIsSet;
    private boolean hxmlIsSet;
    private boolean propertiesIsSet;

    public ProjectLibraryCache(@NotNull Sdk sdk) {
      this.sdk = sdk;
      clear();
    }

    public void clear() {
      setNmmlList(new HaxeLibraryList(sdk));
      setOpenFLList(new HaxeLibraryList(sdk));
      setHxmlList(new HaxeLibraryList(sdk));
      setPropertiesList(new HaxeLibraryList(sdk));

      // Reset the 'set' bits.
      nmmlIsSet = openFLIsSet = hxmlIsSet = propertiesIsSet = false;
    }

    public boolean isListSetFor(HaxeConfiguration buildConfig) {
      switch(buildConfig) {
        case NMML:
          return nmmlIsSet;
        case OPENFL:
          return openFLIsSet;
        case HXML:
          return hxmlIsSet;
        case CUSTOM:
          return propertiesIsSet;
      }
      return false;
    }

    @NotNull
    public HaxeLibraryList getListFor(HaxeConfiguration buildConfig) {
      switch(buildConfig) {
        case NMML:
          return getNmmlList();
        case OPENFL:
          return getOpenFLList();
        case HXML:
          return getHxmlList();
        case CUSTOM:
          return getPropertiesList();
      }
      return new HaxeLibraryList(sdk);
    }

    public void setListFor(HaxeConfiguration buildConfig, HaxeLibraryList list) {
      switch(buildConfig) {
        case NMML:
          setNmmlList(list);
        case OPENFL:
          setOpenFLList(list);
        case HXML:
          setHxmlList(list);
        case CUSTOM:
          setPropertiesList(list);
      }
    }


    @NotNull
    public HaxeLibraryList getNmmlList() {
  return nmmlList != null ? nmmlList : new HaxeLibraryList(sdk);
    }

    @NotNull
    public HaxeLibraryList getOpenFLList() {
      return openFLList != null ? openFLList : new HaxeLibraryList(sdk);
    }

    @NotNull
    public HaxeLibraryList getHxmlList() {
      return hxmlList != null ? hxmlList : new HaxeLibraryList(sdk);
    }

    @NotNull
    public HaxeLibraryList getPropertiesList() {
      return propertiesList != null ? propertiesList : new HaxeLibraryList(sdk);
    }

    public void setNmmlList(HaxeLibraryList nmmlList)  {
      this.nmmlList = nmmlList;
      nmmlIsSet = true;
    }

    public void setOpenFLList(HaxeLibraryList openFLList) {
      this.openFLList = openFLList;
      openFLIsSet = true;
    }

    public void setHxmlList(HaxeLibraryList hxmlList) {
      this.hxmlList = hxmlList;
      hxmlIsSet = true;
    }

    public void setPropertiesList(HaxeLibraryList propertiesList) {
      this.propertiesList = propertiesList;
      propertiesIsSet = true;
    }
  }



  /**
   * Tracks the state of a project for updating class paths.
   */
  public final class ProjectTracker {
    final Project myProject;
    boolean myIsDirty;
    boolean myIsUpdating;
    ProjectLibraryCache myCache;
    HaxelibLibraryCacheManager mySdkManager;

    // TODO: Determine if we need to track whether the project is still open.

    /**
     * Typically, a project gets open and closed events for all of the modules it
     * contains.  We don't want to destroy or de-queue ProjectTrackers until all
     * of the modules have been destroyed.
     */
    int myReferenceCount;


    public ProjectTracker(Project project) {
      myProject = project;
      myIsDirty = true;
      myIsUpdating = false;
      myReferenceCount = 0;
      myCache = new ProjectLibraryCache(HaxelibSdkUtils.lookupSdk(project));
      mySdkManager = new HaxelibLibraryCacheManager();
    }

    /**
     * Get the settings cache.
     */
    @NotNull
    public ProjectLibraryCache getCache() {
      return myCache;
    }

    /**
     * Get the library classpath cache.
     */
    @NotNull
    public HaxelibLibraryCacheManager getSdkManager() {
      return mySdkManager;
    }

    /**
     * Tell whether this project is dirty (needs updating).
     *
     * @return true if the project needs updating.
     */
    public boolean isDirty() {
      boolean ret = false;
      synchronized (this) {
        ret = myIsDirty;
      }
      return ret;
    }

    /**
     * Set/clear the dirty state.  Marking the project dirty clears the cache.
     *
     * @param newState the new state to set.
     * @return the state prior to setting it.
     */
    public boolean setDirty(boolean newState) {
      boolean ret = false;
      synchronized (this) {
        ret = myIsDirty;
        myIsDirty = newState;
        if (myIsDirty) {
          // XXX: May need something more sophisicated than just clearing it.
          //      It could be that a module is getting changed, but not
          //      the project.  In that case, we would want to detect whether
          //      the project settings really changed, and act accordingly.
          myCache.clear();
        }
      }
      return ret;
    }

    /**
     * Tell whether this project is currently updating.
     *
     * @return true if the project is currently running an update.
     */
    public boolean isUpdating() {
      boolean ret = false;

      synchronized(this) {
        ret = myIsUpdating;
      }

      return ret;
    }

    /**
     * Set/clear the 'updating' state.
     *
     * @param newState the new state to set.
     * @return the state prior to setting it.
     */
    public boolean setUpdating(boolean newState) {
      boolean ret = false;
      synchronized(this) {
        ret = myIsUpdating;
        myIsUpdating = newState;
      }
      return ret;
    }

    /**
     * Increase the reference count.
     */
    public void addReference() {
      synchronized(this) {
        myReferenceCount++;
      }
    }

    /**
     * Decrease the reference count.
     *
     * @return the number of references still attached to the object.
     */
    public int removeReference() {
      int refs;
      synchronized(this) {
        refs = --myReferenceCount;
      }
      // XXX: Maybe we don't need an assertion for this.
      LOG.assertTrue(refs >= 0);

      return refs;
    }

    /**
     * Get the project we are tracking.  Note that the project may
     * not still be open at the moment that this is retrieved.
     */
    @NotNull
    public Project getProject() {
      return myProject;
    }

    @NotNull
    public String toString() {
      return "ProjectTracker:" + myProject.getName();
    }

    public boolean equalsName(@Nullable ProjectTracker tracker) {
      if (null == tracker) {
        return false;
      }
      return myProject.getName().equals(tracker.getProject().getName());
    }
  } // end class ProjectTracker


  /**
   * Tracks all of the projects that are currently open.  (As opposed to those
   * that are bing queued for update, which the ProjectUpdateQueue does.)
   * ProjectTrackers are shared between this map and the queue.
   */
  public final class ProjectMap {

    // Hashtable is already synchronized, so we don't have to manage that ourselves.
    final Hashtable<String, ProjectTracker> myMap;


    public ProjectMap() {
      myMap = new Hashtable<String, ProjectTracker>();
    }

    /**
     * Adds a new project to the map, if it doesn't exist there already.
     *
     * @param project An open project to be tracked.
     *
     * @return a new ProjectTracker for the project added, or the existing
     *         ProjectTracker for an existing project.
     */
    @NotNull
    public ProjectTracker add(@NotNull Project project) {
      ProjectTracker tracker;

      synchronized (this) {
        tracker = myMap.get(project.getName());
        if (null == tracker) {
          tracker = new ProjectTracker(project);
          myMap.put(project.getName(), tracker);
        }

        tracker.addReference();
      }
      return tracker;
    }

    public boolean remove(@NotNull Project project) {
      boolean removed = false;
      synchronized(this) {
        ProjectTracker tracker = myMap.get(project.getName());
        if (null != tracker) {
          int refs = tracker.removeReference();
          if (refs == 0) {
            removed = null != myMap.remove(project.getName());
          }
        }
      }
      return removed;
    }

    @Nullable
    public ProjectTracker get(@NotNull Project project) {
      ProjectTracker tracker;
      synchronized(this) {
        tracker = myMap.get(project.getName());
      }
      return tracker;
    }

    public boolean iterate(@NotNull Lambda<ProjectTracker> lambda) {
      synchronized(this) {
         for(ProjectTracker tracker : myMap.values()) {
           if (!lambda.process(tracker)) {
             return false;
           }
         }
      }
      return true;
    }
  }


  /**
   * A FIFO queue for projects that need updating.  Projects are tracked
   * through the ProjectTracker class. When a project placed is in this queue,
   * it is marked dirty.  When the project is being updated, it's marked
   * as updating.  The currently updating project can be retrieved with
   * getUpdatingProject().
   */
  final class ProjectUpdateQueue {

    final Object updateSyncToken;
    ConcurrentLinkedQueue<ProjectTracker> queue;
    ProjectTracker updatingProject = null;

    public ProjectUpdateQueue() {
      queue = new ConcurrentLinkedQueue<ProjectTracker>();
      updateSyncToken = new Object();
      updatingProject = null;
    }

    /**
     * Determine whether there are any projects awaiting updating.
     *
     * The queue may be empty even though a project is currently updating.
     *
     * @return whether there are any projects waiting to be updated.
     */
    public boolean isEmpty() {
      return queue.isEmpty();
    }

    /**
     * Adds a new project to the update queue.  If the project already
     * exists in the queue (as described by equals()) then it will not
     * be added.
     *
     * @param tracker for the project that needs to be updated.
     * @return true if the project was added to the update queue.
     */
    public boolean add(@NotNull ProjectTracker tracker) {
      boolean ret = false;
      // XXX: Something seems wrong about skipping the currently updating project.
      //      What if a project change happens while the project is already running?
      //      Should we cancel and restart instead?  And, if so, should we have a
      //      short delay to ensure that all identical messages are already queued?
      if (!tracker.equalsName(getUpdatingProject())) {
        if (queue.isEmpty() || !queue.contains(tracker)) {
          ret = queue.add(tracker);
          if (null == getUpdatingProject()) {
            queueNextProject();
          }
        }
      }
      return ret;
    }

    /**
     * Remove a project from the update queue.
     *
     * Projects that are currently updating are not canceled or removed, but will
     * be as soon as they are finished.
     *
     * @param tracker project to remove
     * @return true if the project was removed; false otherwise, or if it wasn't queued.
     */
    public boolean remove(@NotNull ProjectTracker tracker) {
      boolean removed = false;

      synchronized(updateSyncToken) {
        if (queue.remove(tracker)) {
          tracker.setUpdating(false);
          // We haven't changed anything, so it's still dirty.
          removed = true;
        }
      }
      return removed;
    }

    /**
     * @return the project currently being updated, if any.
     */
    @Nullable
    public ProjectTracker getUpdatingProject() {
      ProjectTracker tracker;
      synchronized (updateSyncToken) {
        tracker = updatingProject;
      }
      return tracker;
    }

    /**
     * Puts the next project on the event queue.
     */
    private void queueNextProject() {
      synchronized (updateSyncToken) {
        LOG.assertTrue(null == updatingProject);

        // Get the next project from the queue. We're done if there's
        // nothing left.
        updatingProject = queue.poll();  // null if empty.
        if (updatingProject == null) return;

        LOG.assertTrue(updatingProject.isDirty());
        LOG.assertTrue(!updatingProject.isUpdating());

        updatingProject.setUpdating(true);
      }

      // Waiting for runWhenProjectIsInitialized() ensures that the project is
      // fully loaded and accessible.  Otherwise, we crash. ;)
      StartupManager.getInstance(updatingProject.getProject()).runWhenProjectIsInitialized(new Runnable() {
        public void run() {
          LOG.debug("Starting haxelib library sync...");
          runUpdate();
        }
      });

    }

    /**
     * Runs the update, either in the foreground or background, depending upon
     * the state of the myTestInForeground debug flag.
     */
    private void runUpdate() {
      final ProjectTracker tracker = getUpdatingProject();
      final Project project = tracker == null ? null : tracker.getProject();

      if (myTestInForeground) {
        doUpdateWork();
      } else if (myRunInForeground) {
        // TODO: Put this string in a resource bundle.
        ProgressManager.getInstance().run(new Task.Modal(project, "Synchronizing with haxelib libraries...", false) {
          @Override
          public void run(@NotNull ProgressIndicator indicator) {
            indicator.setIndeterminate(true);
            indicator.startNonCancelableSection();
            doUpdateWork();
            indicator.finishNonCancelableSection();
          }
        });
      } else {
        ApplicationManager.getApplication().invokeLater(new Runnable() {
          @Override
          public void run() {
            ProgressManager.getInstance().run(
              // TODO: Put this string in a resource bundle.
              new Task.Backgroundable(project, "Synchronizing with haxelib libraries...", false, PerformInBackgroundOption.ALWAYS_BACKGROUND) {
                @Override
                public void run(@NotNull ProgressIndicator indicator) {
                  doUpdateWork();
                }
              });
          }
        });
      }
    }

    /**
     * The basic bit of work that an update does.
     */
    private void doUpdateWork() {
      LOG.debug("Loading referenced libraries...");
      ProjectTracker tracker = getUpdatingProject();
      if (null == tracker) {
        LOG.warn("Attempt to load libraries, but no project queued for updating.");
        return;
      }
      synchronizeClasspaths(tracker);
      finishUpdate(tracker);
    }

    /**
     * Cleanup and queue the next in line, if any.
     *
     * @param up - the project that is finishing its update run.
     */
    private void finishUpdate(ProjectTracker up) {
      synchronized (updateSyncToken) {
        LOG.assertTrue(null != updatingProject);
        LOG.assertTrue(up.equals(updatingProject));

        updatingProject.setUpdating(false);
        updatingProject.setDirty(false);
        updatingProject = null;
      }
      queueNextProject();
    }
  } // end class projectUpdateQueue

}