package jycessing;

import java.io.File;
import java.io.IOException;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;

import jycessing.mode.PythonMode;

import org.python.core.Py;
import org.python.core.PyObject;
import org.python.core.PyStringMap;
import org.python.google.common.base.Joiner;
import org.python.util.InteractiveConsole;

/**
 * {@link LibraryImporter} adds the add_library function to processing.py.
 * 
 * <p>add_library causes the given Processing library (all of its jar files and
 * library subdirectories potentially containing native code) to be added to the
 * Jython sys.path, and generates import statements bringing all top-level
 * classes available in the main library jar file into the sketch's namespace.
 * 
 * @author feinberg
 */
class LibraryImporter {
  private static void log(final String msg) {
    if (PythonMode.VERBOSE) {
      System.err.println(LibraryImporter.class.getSimpleName() + ": " + msg);
    }
  }

  private final List<File> libdirs;
  private final InteractiveConsole interp;
  private final Set<String> loadedLibs = new HashSet<>();

  private boolean didAddLibraries = false;

  public LibraryImporter(final List<File> libdirs, final InteractiveConsole interp) {
    this.libdirs = libdirs;
    this.interp = interp;
  }

  public void initialize() {
    final PyStringMap builtins = (PyStringMap)interp.getSystemState().getBuiltins();
    builtins.__setitem__("add_library", new PyObject() {
      @Override
      public PyObject __call__(final PyObject[] args, final String[] kws) {
        log("Adding library " + args[0].asString());
        addLibrary(args[0].asString());
        return Py.None;
      }
    });
  }

  private void appendToSysPath(final File file) {
    final String appendStatement =
        String.format("sys.path.append(\"%s\")\n", file.getAbsolutePath());
    log(appendStatement);
    interp.exec(appendStatement);
    if (file.isDirectory()) {
      for (final File f : file.listFiles()) {
        if (f.isDirectory() || f.getName().endsWith(".jar")) {
          appendToSysPath(f);
        }
      }
    }
  }

  protected void addLibrary(final String libName) {
    didAddLibraries = true;

    if (loadedLibs.contains(libName)) {
      return;
    }
    loadedLibs.add(libName);

    File libNameDir = null;
    for (final File libDir : libdirs) {
      libNameDir = new File(String.format("%s/%s", libDir.getAbsolutePath(), libName));
      if (libNameDir.exists()) {
        break;
      }
    }
    if (libNameDir == null || !libNameDir.exists()) {
      interp.exec("raise Exception('This sketch requires the \"" + libName + "\" library.')");
    }
    final File libClassDir = new File(libNameDir, "library");

    appendToSysPath(libClassDir);

    final String jarPath = String.format("%s/%s.jar", libClassDir.getAbsolutePath(), libName);

    try (final ZipFile file = new ZipFile(jarPath)) {
      final Enumeration<? extends ZipEntry> entries = file.entries();
      while (entries.hasMoreElements()) {
        final ZipEntry entry = entries.nextElement();
        if (entry.isDirectory()) {
          continue;
        }
        final String name = entry.getName();
        if (!name.endsWith(".class")) {
          log("Rejecting non-class " + name);
          continue;
        }
        final int slash = name.lastIndexOf('/');
        if (slash == -1) {
          log("Rejecting " + name);
          continue;
        }
        if (name.contains("$")) {
          log("Rejecting " + name);
          continue;
        }
        final String[] path = name.split("/");
        final String className = path[path.length - 1].replace(".class", "");
        final String packageName =
            Joiner.on(".").join(Arrays.asList(path).subList(0, path.length - 1));

        final String importStatement = String.format("from %s import %s", packageName, className);
        log(importStatement);
        interp.exec(importStatement);
      }
    } catch (final IOException e) {
      throw new RuntimeException("While trying to add " + libName + " library:", e);
    }
  }

  public boolean didAddLibraries() {
    return didAddLibraries;
  }
}

@@ -58,24 +58,201 @@
 OException;%0A
+import java.lang.reflect.InvocationTargetException;%0Aimport java.lang.reflect.Method;%0Aimport java.net.MalformedURLException;%0Aimport java.net.URL;%0Aimport java.net.URLClassLoader;%0A
 import java.
@@ -2036,24 +2036,52 @@
 ile file) %7B%0A
+    addToClassLoader(file);%0A
     final St
@@ -2433,24 +2433,831 @@
 %0A    %7D%0A  %7D%0A%0A
+  private void addToClassLoader(final File jar) %7B%0A    try %7B%0A      final URL url = jar.toURI().toURL();%0A      final URLClassLoader ucl = (URLClassLoader)ClassLoader.getSystemClassLoader();%0A      // Linear search for url. It's ok for this to be slow.%0A      for (final URL existing : ucl.getURLs()) %7B%0A        if (existing.equals(url)) %7B%0A          return;%0A        %7D%0A      %7D%0A      log(%22Appending %22 + url + %22 to the system classloader.%22);%0A      final Method addUrl = URLClassLoader.class.getDeclaredMethod(%22addURL%22, URL.class);%0A      addUrl.setAccessible(true);%0A      addUrl.invoke(ucl, url);%0A    %7D catch (NoSuchMethodException %7C SecurityException %7C IllegalAccessException%0A        %7C IllegalArgumentException %7C InvocationTargetException %7C MalformedURLException e) %7B%0A      throw new RuntimeException(e);%0A    %7D%0A  %7D%0A%0A
   protected 
@@ -3977,24 +3977,287 @@
 , libName);%0A
+    try %7B%0A      addPublicClassesFromJar(jarPath);%0A    %7D catch (final IOException e) %7B%0A      throw new RuntimeException(%22While trying to add %22 + libName + %22 library:%22, e);%0A    %7D%0A  %7D%0A%0A  private void addPublicClassesFromJar(final String jarPath) throws IOException %7B
 %0A    try (fi
@@ -5355,129 +5355,8 @@
   %7D%0A
-    %7D catch (final IOException e) %7B%0A      throw new RuntimeException(%22While trying to add %22 + libName + %22 library:%22, e);%0A
     

Add libraries to classloader dynamically at runtime.