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

import jetbrains.mps.classloading.ClassLoaderManager;
import jetbrains.mps.classloading.ModuleClassLoader;
import jetbrains.mps.classloading.ModuleClassNotFoundException;
import jetbrains.mps.classloading.ModuleIsNotLoadableException;
import jetbrains.mps.library.SLibrary;
import jetbrains.mps.project.AbstractModule;
import jetbrains.mps.smodel.MPSModuleOwner;
import jetbrains.mps.smodel.ModuleRepositoryFacade;
import jetbrains.mps.util.InternUtil;
import jetbrains.mps.vfs.IFile;
import jetbrains.mps.vfs.openapi.FileSystem;
import org.apache.log4j.LogManager;
import org.apache.log4j.Logger;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.util.List;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArrayList;

public class ReloadableModuleBase extends AbstractModule implements ReloadableModule {
  private final static Logger LOG = LogManager.getLogger(ReloadableModuleBase.class);
  private final ClassLoaderManager myManager = ClassLoaderManager.getInstance();
  private final List<SModuleDependenciesListener> myListeners = new CopyOnWriteArrayList<SModuleDependenciesListener>();

  protected ReloadableModuleBase() {
    super();
  }

  protected ReloadableModuleBase(@Nullable IFile file) {
    super(file);
  }

  protected ReloadableModuleBase(@NotNull FileSystem fileSystem) {
    super(fileSystem);
  }

  @NotNull
  @Override
  public Class<?> getClass(String classFqName) throws ClassNotFoundException, ModuleIsNotLoadableException {
    return getClass(classFqName, false);
  }

  @NotNull
  @Override
  public Class<?> getOwnClass(String classFqName) throws ClassNotFoundException, ModuleIsNotLoadableException {
    return getClass(classFqName, true);
  }

  @NotNull
  protected Class<?> getClass(String classFqName, boolean ownClassOnly) throws ClassNotFoundException, ModuleClassNotFoundException, ModuleIsNotLoadableException {
    ClassLoader classLoader = getClassLoader();
    if (classLoader == null) {
      throw new ModuleClassLoaderIsNullException(this);
    }
    String internClassName = InternUtil.intern(classFqName);
    if (ownClassOnly && classLoader instanceof ModuleClassLoader) {
      return ((ModuleClassLoader) classLoader).loadOwnClass(internClassName);
    }
    Class<?> aClass = classLoader.loadClass(internClassName);
    if (aClass == null) {
      throw new LoadedClassIsNullException(classLoader, internClassName);
    }
    return aClass;
  }

  @Nullable
  @Override
  public ClassLoader getClassLoader() {
    return myManager.getClassLoader(this);
  }

  @Override
  public ClassLoader getRootClassLoader() {
    getRepository().getModelAccess().checkReadAccess();
    Set<MPSModuleOwner> moduleOwners = ModuleRepositoryFacade.getInstance().getModuleOwners(this);
    for (MPSModuleOwner owner : moduleOwners) {
      if (owner instanceof SLibrary) {
        ClassLoader classLoader = ((SLibrary) owner).getPluginClassLoader();
        if (classLoader != null) {
          return classLoader;
        }
      }
    }
    return ReloadableModule.class.getClassLoader();
  }

  @Override
  public void reload() {
    if (!willLoad()) return;
    LOG.info("Reloading module " + this);
    myManager.reloadModule(this);
  }

  @Override
  public boolean willLoad() {
    return true;
  }

  @Override
  protected void dependenciesChanged() {
    super.dependenciesChanged();
    if (willLoad()) {
      fireDependenciesChanged();
    }
  }

  protected final void fireDependenciesChanged() {
    assertCanChange();

    for (SModuleDependenciesListener listener : myListeners) {
      listener.dependenciesChanged(this);
    }
  }

  // NOTE: for internal use
  public final void addDependenciesListener(SModuleDependenciesListener listener) {
    myListeners.add(listener);
  }

  // NOTE: for internal use
  public final void removeDependenciesListener(SModuleDependenciesListener listener) {
    myListeners.remove(listener);
  }

  // NOTE: for internal use
  // notifies about ANY changes in deps, used languages, etc.
  // designed specifically for the class loading client
  public interface SModuleDependenciesListener {
    void dependenciesChanged(@NotNull ReloadableModuleBase module);
  }
}
