/*
 * Copyright 2003-2015 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.classloading;

import jetbrains.mps.module.ReloadableModule;
import org.apache.log4j.LogManager;
import org.apache.log4j.Logger;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.mps.openapi.module.SModule;
import org.jetbrains.mps.openapi.module.SModuleReference;
import org.jetbrains.mps.openapi.module.SRepository;
import org.jetbrains.mps.openapi.module.SRepositoryListener;
import org.jetbrains.mps.openapi.module.SRepositoryListenerBase;
import org.jetbrains.mps.openapi.util.ProgressMonitor;

import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Set;

/**
 * This class stores a map SModuleReference->ModuleClassLoader
 *
 * Note: the actual dispose of ModuleClassLoaders happen asynchronously in the EDT
 * @see MPSClassLoadersRegistry#flushDisposeQueue()
 *
 * @see ClassLoaderManager#myLoadableCondition
 */
public class ClassLoadersHolder {
  private static final Logger LOG = LogManager.getLogger(ClassLoadersHolder.class);

  private static final List<String> INTERNAL_EXCLUDES = Arrays.asList("jetbrains.mps.samples.xmlPersistence", "TestBehaviorReflective");

  private final MPSClassLoadersRegistry myCLRegistry;
  private final SRepositoryListener myRepositoryListener = new SRepositoryListenerBase() {
    @Override
    public void moduleAdded(@NotNull SModule module) {
      checkPluginIsValid(module);
    }

    private void checkPluginIsValid(@NotNull SModule module) {
      CustomClassLoadingFacet customClassLoadingFacet = module.getFacet(CustomClassLoadingFacet.class);
      if (customClassLoadingFacet != null) {
        if (!customClassLoadingFacet.isValid() && !INTERNAL_EXCLUDES.contains(module.getModuleName())) {
          LOG.warn("Facet of the module " + module + " is not valid --" +
              " possibly the provided idea plugin (in the properties dialog/idea plugin facet tab) cannot be found among the bundled plugins");
        }
      }
    }
  };
  private final SRepository myRepository;

  public ClassLoadersHolder(SRepository repository, ModulesWatcher modulesWatcher, EDTDispatcher dispatcher) {
    myRepository = repository;
    myCLRegistry = new MPSClassLoadersRegistry(this, modulesWatcher, repository, dispatcher);
  }

  public void init() {
    myRepository.addRepositoryListener(myRepositoryListener);
  }

  public void dispose() {
    myCLRegistry.dispose();
    myRepository.removeRepositoryListener(myRepositoryListener);
  }

  @Nullable
  public ClassLoader getClassLoader(ReloadableModule module) {
    try {
      return getModuleClassLoader(module);
    } catch (ClassLoaderNotFoundException ignored) {
      // do nothing, there is no MPS ModuleClassLoader for this module
    }

    try {
      return getNonReloadableClassLoader(module);
    } catch (ClassLoaderNotFoundException ignored) {
      // do nothing, there is no IDEA ClassLoader for this module
    }

    return null;
  }

  @Nullable
  private ClassLoader getNonReloadableClassLoader(SModule module) throws ClassLoaderNotFoundException {
    CustomClassLoadingFacet customClassLoadingFacet = module.getFacet(CustomClassLoadingFacet.class);
    if (customClassLoadingFacet != null) {
      if (customClassLoadingFacet.isValid()) {
        return customClassLoadingFacet.getClassLoader();
      } else {
        return null;
      }
    }
    throw new ClassLoaderNotFoundException();
  }

  @Nullable
  private ClassLoader getModuleClassLoader(ReloadableModule module) throws ClassLoaderNotFoundException {
    return myCLRegistry.getModuleClassLoader(module);
  }

  /**
   * @return {@link ClassLoadingProgress} for the module. See the documentation of
   * {@link ClassLoadingProgress} for the description of states and a typical lifecycle of module in a repository.
   */
  @NotNull
  public ClassLoadingProgress getClassLoadingProgress(SModuleReference mRef) {
    return myCLRegistry.getClassLoadingProgress(mRef);
  }

  public void scheduleClassLoaderDisposeInEDT() {
    LOG.debug("Scheduling ModuleClassLoader disposal");
    myCLRegistry.flushDisposeQueue();
  }

  /**
   * @param toUnload for these modules ModuleClassLoaders were disposed
   * @return modules which changed their ClassLoadingProgress from LAZY_LOADED or LOADED to UNLOADED.
   */
  public Set<SModuleReference> doUnloadModules(Set<SModuleReference> toUnload) {
    return myCLRegistry.doUnloadModules(toUnload);
  }

  /**
   * @param toLoadLazy for these modules only notifications {@link MPSClassesListener#afterClassesLoaded} were sent,
   *                   so for {@link MPSClassesListener} clients these modules appear to be loaded.
   *                   No actual loading is performed for these modules.
   * @return modules which changed their ClassLoadingProgress from UNLOADED to LAZY_LOADED.
   */
  public Set<ReloadableModule> onLazyLoaded(Set<ReloadableModule> toLoadLazy) {
    return myCLRegistry.onLazyLoaded(toLoadLazy);
  }

  /**
   * @param toLoad for these modules ModuleClassLoaders were actually created
   */
  public void doLoadModules(Set<ReloadableModule> toLoad) {
    myCLRegistry.doLoadModules(toLoad);
  }

  public void setDispatcher(@NotNull EDTDispatcher dispatcher) {
    myCLRegistry.setDispatcher(dispatcher);
  }

  /**
   * Class loading progress of each MPS-loadable module.
   *
   * Module lifecycle:
   * At first the module is UNLOADED. It comes to repository and a call of {@link ClassLoaderManager#preLoadModules(Iterable, org.jetbrains.mps.openapi.util.ProgressMonitor)} happens.
   * Then we check whether the module's dependencies are valid to load (and some other conditions). If everything is okay then we send
   * broadcast notification to the clients of {@link jetbrains.mps.classloading.MPSClassesListener}.
   * The state of module is changed to LAZY_LOADED at that moment.
   * When the classes of module are requested [through #getClass(), #getOwnClass(), #getClassLoader()] methods,
   * the actual ClassLoader construction happens and then the module is marked as LOADED.
   * LAZY_LOADED state could not be skipped.
   *
   * When #reloadModules happens, module ClassLoader's are unloaded and then preLoaded (!) again. [back to lazy state again]
   *
   * So the state diagram looks like this:
   * UNLOADED -> LAZY_LOADED -> LOADED
   * LAZY_LOADED -> UNLOADED
   * LOADED -> UNLOADED
   */
  public enum ClassLoadingProgress {
    /**
     * Class loading has not been initiated yet. [Implies there is no such module in the repository].
     * Note: this enum value is not stored in corresponding map for the sake of simplicity.
     */
    UNLOADED,
    /**
     * The notifications for {@link MPSClassesListener} clients were sent. No actual class loading happened,
     * This module was only marked to load.
     */
    LAZY_LOADED,
    /**
     * ModuleClassLoader's are created.
     */
    LOADED
  }

  static class ClassLoaderNotFoundException extends Exception {}
}
