/*
 *  This file is part of the Jikes RVM project (http://jikesrvm.org).
 *
 *  This file is licensed to You under the Common Public License (CPL);
 *  You may not use this file except in compliance with the License. You
 *  may obtain a copy of the License at
 *
 *      http://www.opensource.org/licenses/cpl1.0.php
 *
 *  See the COPYRIGHT.txt file distributed with this work for information
 *  regarding copyright ownership.
 */
package org.jikesrvm.classloader;

import java.io.DataInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.StringTokenizer;
import java.util.Vector;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import org.jikesrvm.VM;
import org.jikesrvm.runtime.Entrypoints;
import org.jikesrvm.util.ImmutableEntryHashMapRVM;

/**
 * Implements an object that functions as the bootstrap class loader.
 * This class is a Singleton pattern.
 */
public final class BootstrapClassLoader extends java.lang.ClassLoader {

  private final ImmutableEntryHashMapRVM<String, RVMType> loaded =
    new ImmutableEntryHashMapRVM<String, RVMType>();

  private static final boolean DBG = false;

  /** Places whence we load bootstrap .class files. */
  private static String bootstrapClasspath;

  /**
   * Set list of places to be searched for vm classes and resources.
   * @param bootstrapClasspath path specification in standard "classpath"
   *    format
   */
  public static void setBootstrapRepositories(String bootstrapClasspath) {
    BootstrapClassLoader.bootstrapClasspath = bootstrapClasspath;
  }

  /**
   * @return List of places to be searched for VM classes and resources,
   *      in standard "classpath" format
   */
  public static String getBootstrapRepositories() {
    return bootstrapClasspath;
  }

  /**
   * Initialize for execution.
   * @param bootstrapClasspath names of directories containing the bootstrap
   * .class files, and the names of any .zip/.jar files.
   * These are the ones that implement the VM and its
   * standard runtime libraries.  This may contain several names separated
   * with colons (':'), just
   * as a classpath may.   (<code>null</code> ==> use the values specified by
   * {@link #setBootstrapRepositories} when the boot image was created.  This
   * feature is not actually used, but may be helpful in avoiding trouble.)
   */
  public static void boot(String bootstrapClasspath) {
    if (bootstrapClasspath != null) {
      BootstrapClassLoader.bootstrapClasspath = bootstrapClasspath;
    }
    zipFileCache = new HashMap<String, ZipFile>();
    if (VM.runningVM) {
      try {
        /* Here, we have to replace the fields that aren't carried over from
         * boot image writing time to run time.
         * This would be the following, if the fields weren't final:
         *
         * bootstrapClassLoader.definedPackages    = new HashMap();
         */
        Entrypoints.classLoaderDefinedPackages.setObjectValueUnchecked(bootstrapClassLoader,
                                                                          new java.util.HashMap<String, Package>());
      } catch (Exception e) {
        VM.sysFail("Failed to setup bootstrap class loader");
      }
    }
  }

  /** Prevent other classes from constructing one. */
  private BootstrapClassLoader() {
    super(null);
  }

  /* Interface */
  private static final BootstrapClassLoader bootstrapClassLoader = new BootstrapClassLoader();

  public static BootstrapClassLoader getBootstrapClassLoader() {
    return bootstrapClassLoader;
  }

  /**
   * Backdoor for use by TypeReference.resolve when !VM.runningVM.
   * As of this writing, it is not used by any other classes.
   * @throws NoClassDefFoundError
   */
  synchronized RVMType loadVMClass(String className) throws NoClassDefFoundError {
    try {
      InputStream is = getResourceAsStream(className.replace('.', '/') + ".class");
      if (is == null) throw new NoClassDefFoundError(className);
      DataInputStream dataInputStream = new DataInputStream(is);
      RVMType type = null;
      try {
        // Debugging:
        // VM.sysWriteln("loadVMClass: trying to resolve className " + className);
        type = RVMClassLoader.defineClassInternal(className, dataInputStream, this);
        loaded.put(className, type);
      } finally {
        try {
          // Make sure the input stream is closed.
          dataInputStream.close();
        } catch (IOException e) { }
      }
      return type;
    } catch (NoClassDefFoundError e) {
      throw e;
    } catch (Throwable e) {
      // We didn't find the class, or it wasn't valid, etc.
      NoClassDefFoundError ncdf = new NoClassDefFoundError(className);
      ncdf.initCause(e);
      throw ncdf;
    }
  }

  public synchronized Class<?> loadClass(String className, boolean resolveClass) throws ClassNotFoundException {
    if (!VM.runningVM) {
      return super.loadClass(className, resolveClass);
    }
    if (className.startsWith("L") && className.endsWith(";")) {
      className = className.substring(1, className.length() - 2);
    }
    RVMType loadedType = loaded.get(className);
    Class<?> loadedClass;
    if (loadedType == null) {
      loadedClass = findClass(className);
    } else {
      loadedClass = loadedType.getClassForType();
    }
    if (resolveClass) {
      resolveClass(loadedClass);
    }
    return loadedClass;
  }

  /**
   * Search the bootstrap class loader's classpath for given class.
   *
   * @param className the name of the class to load
   * @return the class object, if it was found
   * @exception ClassNotFoundException if the class was not found, or was invalid
   */
  public Class<?> findClass(String className) throws ClassNotFoundException {
    if (!VM.runningVM) {
      return super.findClass(className);
    }
    if (className.startsWith("[")) {
      TypeReference typeRef =
          TypeReference.findOrCreate(this, Atom.findOrCreateAsciiAtom(className.replace('.', '/')));
      RVMType ans = typeRef.resolve();
      loaded.put(className, ans);
      return ans.getClassForType();
    } else {
      if (!VM.fullyBooted) {
        VM.sysWrite("Trying to load a class (");
        VM.sysWrite(className);
        VM.sysWrite(") too early in the booting process, before dynamic");
        VM.sysWriteln(" class loading is enabled; aborting.");
        VM.sysFail("Trying to load a class too early in the booting process");
      }
      // class types: try to find the class file
      try {
        if (className.startsWith("L") && className.endsWith(";")) {
          className = className.substring(1, className.length() - 2);
        }
        InputStream is = getResourceAsStream(className.replace('.', '/') + ".class");
        if (is == null) throw new ClassNotFoundException(className);
        DataInputStream dataInputStream = new DataInputStream(is);
        Class<?> cls = null;
        try {
          RVMType type = RVMClassLoader.defineClassInternal(className, dataInputStream, this);
          loaded.put(className, type);
          cls = type.getClassForType();
        } finally {
          try {
            // Make sure the input stream is closed.
            dataInputStream.close();
          } catch (IOException e) { }
        }
        return cls;
      } catch (ClassNotFoundException e) {
        throw e;
      } catch (Throwable e) {
        if (DBG) {
          VM.sysWrite("About to throw ClassNotFoundException(", className, ") because we got this Throwable:");
          e.printStackTrace();
        }
        // We didn't find the class, or it wasn't valid, etc.
        throw new ClassNotFoundException(className, e);
      }
    }
  }

  /** Keep this a static field, since it's looked at in
   *  {@link MemberReference#parse}. */
  public static final String myName = "BootstrapCL";

  public String toString() { return myName; }

  private static HashMap<String, ZipFile> zipFileCache;

  private interface Handler<T> {
    void process(ZipFile zf, ZipEntry ze) throws Exception;

    void process(File f) throws Exception;

    T getResult();
  }

  public InputStream getResourceAsStream(final String name) {
    Handler<InputStream> findStream = new Handler<InputStream>() {
      InputStream stream;

      public InputStream getResult() { return stream; }

      public void process(ZipFile zf, ZipEntry ze) throws Exception {
        stream = zf.getInputStream(ze);
      }

      public void process(File file) throws Exception {
        stream = new FileInputStream(file);
      }
    };

    return getResourceInternal(name, findStream, false);
  }

  public URL findResource(final String name) {
    Handler<URL> findURL = new Handler<URL>() {
      URL url;

      public URL getResult() { return url; }

      public void process(ZipFile zf, ZipEntry ze) throws Exception {
        url = new URL("jar", null, -1, "file:" + zf.getName() + "!/" + name);
      }

      public void process(File file) throws Exception {
        url = new URL("file", null, -1, file.getName());
      }
    };

    return getResourceInternal(name, findURL, false);
  }

  public Enumeration<URL> findResources(final String name) {
    Handler<Enumeration<URL>> findURL = new Handler<Enumeration<URL>>() {
      Vector<URL> urls;

      public Enumeration<URL> getResult() {
        if (urls == null) urls = new Vector<URL>();
        return urls.elements();
      }

      public void process(ZipFile zf, ZipEntry ze) throws Exception {
        if (urls == null) urls = new Vector<URL>();
        urls.addElement(new URL("jar", null, -1, "file:" + zf.getName() + "!/" + name));
      }

      public void process(File file) throws Exception {
        if (urls == null) urls = new Vector<URL>();
        urls.addElement(new URL("file", null, -1, file.getName()));
      }
    };

    return getResourceInternal(name, findURL, true);
  }

  private <T> T getResourceInternal(String name, Handler<T> h, boolean multiple) {
    if (name.startsWith(File.separator)) {
      name = name.substring(File.separator.length());
    }

    StringTokenizer tok = new StringTokenizer(getBootstrapRepositories(), File.pathSeparator);

    while (tok.hasMoreElements()) {
      try {
        String path = tok.nextToken();
        if (path.endsWith(".jar") || path.endsWith(".zip")) {
          ZipFile zf = zipFileCache.get(path);
          if (zf == null) {
            zf = new ZipFile(path);
            if (zf == null) {
              continue;
            } else {
              zipFileCache.put(path, zf);
            }
          }

          ZipEntry ze = zf.getEntry(name);
          if (ze == null) continue;

          h.process(zf, ze);
          if (!multiple) return h.getResult();
        } else if (path.endsWith(File.separator)) {
          File file = new File(path + name);
          if (file.exists()) {
            h.process(file);
            if (!multiple) return h.getResult();
          }
        } else {
          File file = new File(path + File.separator + name);
          if (file.exists()) {
            h.process(file);
            if (!multiple) return h.getResult();
          }
        }
      } catch (Exception e) {
      }
    }

    return (multiple) ? h.getResult() : null;
  }
}
