/*
 * Copyright 2003-2011 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 jetbrains.mps.reloading.ClassBytesProvider.ClassBytes;
import jetbrains.mps.util.NameUtil;
import jetbrains.mps.util.ProtectionDomainUtil;
import jetbrains.mps.util.iterable.IterableEnumeration;
import org.apache.log4j.LogManager;
import org.apache.log4j.Logger;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import sun.misc.CompoundEnumeration;

import java.io.IOException;
import java.net.URL;
import java.security.ProtectionDomain;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Enumeration;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

/**
 * MPS implementation of <code>java.lang.ClassLoader</code> which uses non-standard way of class loading delegation.
 * Its methods #loadClass, #findResources are called by JVM during JVM class loading process and also
 * by an explicit user call of #getClass and #getOwnClass methods in {@link ReloadableModule} and
 * in {@link ClassLoaderManager} instance (old deprecated way).
 * Note that these methods yield additional error information in the case of failure.
 * Users of class loading API are supposed to process it on their own behalf.
 *
 * @see jetbrains.mps.classloading.ModuleIsNotLoadableException
 * @see jetbrains.mps.classloading.ModuleClassNotFoundException
 *
 * @author apyshkin
 */
public final class ModuleClassLoader extends ClassLoader {
  private static final Logger LOG = LogManager.getLogger(ModuleClassLoader.class);
  private static final ClassLoader BOOTSTRAP_CLASSLOADER = Object.class.getClassLoader();

  private final ModuleClassLoaderSupport mySupport;
  // null values are not allowed => using <code>Optional</code>
  private final ConcurrentMap<String, Optional<Class<?>>> myClasses = new ConcurrentHashMap<>();

  private volatile Collection<ClassLoader> myDependenciesClassLoaders;
  private boolean myDisposed;
  private final Object myPackageLock = new Object();

  /**
   * MPS has a cyclic delegation classloading model (module A.a1 triggers class B.b which in turn triggers the loading of
   * the class A.a2 in the case when A depends on B and vice versa; the implicit class loading is triggered in the #defineClass invocation
   * in the {@link #loadFromSelf(String)} method).
   *
   * Thus according to jls we declare it as a parallel capable.
   * Without this registration the threading model of the MPS classloading is flawed.
   * @since 3.4
   */
  static {
    if (!registerAsParallelCapable()) {
      LOG.error("Was not able to register the MPS class loader as parallel capable: one might encounter a deadlock", new Throwable());
    }
  }

  public boolean isDisposed() {
    return myDisposed;
  }

  private void checkNotDisposed() throws ModuleClassLoaderIsDisposedException {
    if (isDisposed()) {
      throw new ModuleClassLoaderIsDisposedException(String.format("ClassLoader of the module '%s' is disposed and not operable!", getModule()), getModule());
    }
  }

  public ModuleClassLoader(ModuleClassLoaderSupport support) {
    super(support.getModule().getRootClassLoader());
    mySupport = support;
  }

  @NotNull
  public Class<?> loadOwnClass(String name) throws ClassNotFoundException {
    Class<?> aClass = loadClass(name, false, true);
    if (aClass == null) {
      throw new ModuleClassNotFoundException(getModule());
    }
    return aClass;
  }

  ReloadableModule getModule() {
    return mySupport.getModule();
  }

  @Override
  protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException{
    return loadClass(name, resolve, false);
  }

  private Class<?> loadClass(String fqName, boolean resolve, boolean onlyFromSelf) throws ClassNotFoundException {
    checkNotDisposed();

    Class<?> aClass = getClassFromCache(fqName);
    if (aClass != null) return aClass;

    if (fqName.startsWith("java.")) {
      return Class.forName(fqName, false, BOOTSTRAP_CLASSLOADER);
    }

    aClass = loadFromSelf(fqName);
    if (aClass != null) {
      return aClass;
    }
    try {
      if (!onlyFromSelf) {
        aClass = loadFromDeps(fqName);
      }
    } finally {
      aClass = recordClass(fqName, aClass);
    }
    if (aClass == null) throw createCLNFException(fqName);

    if (resolve) resolveClass(aClass);
    return aClass;
  }

  /**
   * @return new class if there was no same class already defined
   *         or an old class if there is another definition already recorded into the map.
   */
  private Class<?> recordClass(String fqName, Class<?> aClass) {
    Optional<Class<?>> previousValue =  myClasses.putIfAbsent(fqName, Optional.ofNullable(aClass));
    if (previousValue != null) { // class has been already defined in a concurrent thread
      if (previousValue.isPresent()) {
        aClass = previousValue.get();
      } else {
        aClass = null;
      }
    }
    return aClass;
  }

  private ModuleClassNotFoundException createCLNFException(String name) {
    ReloadableModule module = mySupport.getModule();
    return new ModuleClassNotFoundException(module,
                                            String.format("Unable to load class: '%s' using ModuleClassLoader of the '%s' module", name,
                                                          module.getModuleName()));
  }

  /**
   * @return null if there is no name in cache
   * @throws ClassNotFoundException if class has been found already and it was null
   */
  private Class<?> getClassFromCache(String name) throws ClassNotFoundException {
    Optional<Class<?>> optionalClass = myClasses.get(name);
    if (optionalClass == null) {
      return null;
    }
    if (!optionalClass.isPresent()) {
      throw createCLNFException(name);
    }
    return optionalClass.get();
  }

  private Class<?> loadFromSelf(String fqName) throws ClassNotFoundException {
    synchronized (getClassLoadingLock(fqName)) {
      Class<?> aClass = getClassFromCache(fqName);
      if (aClass != null) {
        return aClass;
      }
      ClassBytes classBytes = mySupport.findClassBytes(fqName);
      if (classBytes != null) {
        String pack = NameUtil.namespaceFromLongName(fqName);
        synchronized (myPackageLock) {
          if (getPackage(pack) == null) {
            definePackage(pack, null, null, null, null, null, null, null);
          }
        }
        ProtectionDomain newProtectionDomain = ProtectionDomainUtil.loadedClassDomain(classBytes.getPath());
        byte[] bytes = classBytes.getBytes();
        aClass = defineClass(fqName, bytes, 0, bytes.length, newProtectionDomain);
        return recordClass(fqName, aClass);
      }
    }
    return null;
  }

  private Class<?> loadFromDeps(String name) throws ClassNotFoundException {
    Collection<? extends ClassLoader> dependencyClassLoaders = getDependencyClassLoaders();

    // loading from ModuleClassLoaders firstly; it's faster, we can tell right here if we can find class there.
    for (ClassLoader depCL : dependencyClassLoaders) {
      if (depCL instanceof ModuleClassLoader) {

        ModuleClassLoader depCL1 = (ModuleClassLoader) depCL;
        if (depCL1.mySupport.canFindClass(name)) {
          // here it will certainly load with class loader depCL
          return depCL1.loadFromSelf(name);
        }
      }
    }

    for (ClassLoader depCL : dependencyClassLoaders) {
      if (!(depCL instanceof ModuleClassLoader)) {
        try {
          return Class.forName(name, false, depCL);
        } catch (ClassNotFoundException ignored) {
        }
      }
    }

    if (dependencyClassLoaders.contains(getParent())) {
      return null;
    } else {
      return loadFromParent(name);
    }
  }

  private Class<?> loadFromParent(String name) throws ClassNotFoundException {
    ClassLoader parent = getParent();
    if (parent == null) return null;
    return parent.loadClass(name);
  }

  @Nullable
  @Override
  public URL getResource(@NonNls String name) {
    URL ownResource = findResource(name);
    if (ownResource == null) {
      return getParent().getResource(name);
    }
    return ownResource;
  }

  @NotNull
  @Override
  public Enumeration<URL> getResources(@NonNls String name) throws IOException {
    Enumeration<URL> ownResources = findResources(name);
    Enumeration<URL> parentResources = getParent().getResources(name);
    //noinspection unchecked
    return new CompoundEnumeration<>(new Enumeration[]{ownResources, parentResources});
  }

  @Override
  protected URL findResource(String name) {
    checkNotDisposed();
    List<ClassLoader> classLoadersToCheck = new ArrayList<ClassLoader>();
    classLoadersToCheck.add(this);
    classLoadersToCheck.addAll(getDependencyClassLoaders());
    for (ClassLoader dep : classLoadersToCheck) {
      if (dep instanceof ModuleClassLoader) {
        URL res;
        try {
          res = ((ModuleClassLoader) dep).mySupport.findResource(name);
        } catch (ModuleIsNotLoadableException e) {
          throw new RuntimeException(e);
        }
        if (res != null) return res;
      }
    }

    return null;
  }

  @Override
  protected Enumeration<URL> findResources(String name) throws IOException {
    checkNotDisposed();
    List<ClassLoader> classLoadersToCheck = new ArrayList<ClassLoader>();
    classLoadersToCheck.add(this);
    classLoadersToCheck.addAll(getDependencyClassLoaders());
    List<URL> result = new ArrayList<>();
    for (ClassLoader dep : classLoadersToCheck) {
      if (dep instanceof ModuleClassLoader) {
        Enumeration<URL> resources;
        try {
          resources = ((ModuleClassLoader) dep).mySupport.findResources(name);
        } catch (ModuleIsNotLoadableException e) {
          throw new RuntimeException(e);
        }
        while (resources.hasMoreElements()) result.add(resources.nextElement());
      }
    }

    return new IterableEnumeration<>(result);
  }

  /**
   * Note: the actual dispose is called asynchronously in the EDT.
   * The motive is to allow a ClassLoading client to dispose asynchronously in the Event Dispatch Thread.
   */
  public void dispose() {
    myDisposed = true;
    myClasses.clear();
    if (myDependenciesClassLoaders != null) {
      myDependenciesClassLoaders.clear();
    }
  }

  /**
   * @return all dependencies [excluding itself]
   */
  private Collection<ClassLoader> getDependencyClassLoaders() {
    if (myDependenciesClassLoaders == null) {
      myDependenciesClassLoaders = mySupport.getCompileDependencies();
    }
    return myDependenciesClassLoaders;
  }

  public String toString() {
    return String.format("%s ModuleClassLoader %s", mySupport.getModule(), myDisposed ? "[DISPOSED]" : "");
  }

  public static class ModuleClassLoaderIsDisposedException extends IllegalStateException {
    private final ReloadableModule myModule;

    private ModuleClassLoaderIsDisposedException(String msg, @NotNull ReloadableModule module) {
      super(msg);
      myModule = module;
    }

    public ReloadableModule getModule() {
      return myModule;
    }
  }
}
