/*
 *  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.ByteArrayInputStream;
import java.io.DataInputStream;
import java.io.IOException;
import java.io.InputStream;
import org.jikesrvm.VM;
import org.jikesrvm.Constants;
import org.jikesrvm.Properties;

/**
 * Manufacture type descriptions as needed by the running virtual machine. <p>
 */
public class RVMClassLoader implements Constants, ClassLoaderConstants {

  private static final boolean DBG_APP_CL = false;

  private static ClassLoader appCL;

  /**
   * Set list of places to be searched for application classes and resources.
   * Do NOT set the java.class.path property; it is probably too early in
   * the VM's booting cycle to set properties.
   *
   * @param classpath path specification in standard "classpath" format
   */
  public static void stashApplicationRepositories(String classpath) {
    if (DBG_APP_CL) {
      VM.sysWriteln("RVMClassLoader.stashApplicationRepositories: " + "applicationRepositories = ", classpath);
    }
    /* If mis-initialized, trash it. */
    if (appCL != null && !classpath.equals(applicationRepositories)) {
      appCL = null;
      if (DBG_APP_CL) {
        VM.sysWriteln("RVMClassLoader.stashApplicationRepositories: Wiping out my remembered appCL.");
      }
    }
    applicationRepositories = classpath;
  }

  /**
   * Are Java 1.4 style assertions enabled?
   */
  private static boolean assertionsEnabled = false;
  /**
   * String describing packages and classes to enable assertions on (of the form ":<packagename>...|:<classname>")
   */
  private static String[] enabledAssertionStrings;
  /**
   * String describing packages and classes to disable assertions on (of the form ":<packagename>...|:<classname>")
   */
  private static String[] disabledAssertionStrings;

  /**
   * Remember the given enable assertions string
   * @param arg String of the form ":<packagename>...|:<classname>"
   */
  public static void stashEnableAssertionArg(String arg) {
    assertionsEnabled = true;
    enabledAssertionStrings = arg.split(":");
    if (enabledAssertionStrings != null) {
      if ((enabledAssertionStrings.length == 0) ||
          (enabledAssertionStrings.length == 1 && enabledAssertionStrings[0].equals(""))) {
        // force enabled assertion strings to null when no arguments are passed with -ea
        enabledAssertionStrings = null;
      }
    }
  }

  /**
   * Remember the given disable assertions string
   * @param arg String of the form ":<packagename>...|:<classname>"
   */
  public static void stashDisableAssertionArg(String arg) {
    if (arg == null || arg.equals("")) {
      assertionsEnabled = false;
    } else {
      disabledAssertionStrings = arg.split(":");
    }
  }

  /**
   * Calculate the desired assertion status for a freshly loaded class
   * @param klass to check against command line argument
   * @return whether assertions should be enabled on class
   */
  static boolean getDesiredAssertionStatus(RVMClass klass) {
    if (!assertionsEnabled) {
      // trivial - no assertions are enabled
      return false;
    } else {
      if (enabledAssertionStrings == null && disabledAssertionStrings == null) {
        // assertions enabled unconditionally
        return true;
      } else {
        // search command line arguments to see if assertions are enabled
        boolean result = false;
        if(enabledAssertionStrings != null) {
          for (String s : enabledAssertionStrings) {
            if (s.equals(klass.getTypeRef().getName().classNameFromDescriptor()) ||
                klass.getPackageName().startsWith(s)) {
              result = true;
              break;
            }
          }
        }
        if (disabledAssertionStrings != null) {
          for (String s : disabledAssertionStrings) {
            if (s.equals(klass.getTypeRef().getName().classNameFromDescriptor()) ||
                klass.getPackageName().startsWith(s)) {
              result = false;
              break;
            }
          }
        }
        return result;
      }
    }
  }

  /**
   * Set list of places to be searched for application classes and resources.
   * @param classpath path specification in standard "classpath" format
   *
   * Our Jikes RVM classloader can not handle having the class path reset
   * after it's been set up.   Unfortunately, it is stashed by classes in
   * GNU Classpath.
   */
  public static void setApplicationRepositories(String classpath) {
    System.setProperty("java.class.path", classpath);
    stashApplicationRepositories(classpath);
    if (DBG_APP_CL) {
      VM.sysWriteln("RVMClassLoader.setApplicationRepositories: applicationRepositories = ", applicationRepositories);
    }
  }

  /**
   * Get list of places currently being searched for application
   * classes and resources.
   * @return names of directories, .zip files, and .jar files
   */
  public static String getApplicationRepositories() {
    return applicationRepositories;
  }

  /** Are we getting the application CL?  Access is synchronized via the
   *  Class object.  Probably not necessary, but doesn't hurt, or shouldn't.
   *  Used for sanity checks. */

  private static int gettingAppCL = 0;

  /** Is the application class loader ready for use?  Don't leak it out until
   * it is! */
  private static boolean appCLReady;

  public static void declareApplicationClassLoaderIsReady() {
    appCLReady = true;
  }

  public static ClassLoader getApplicationClassLoader() {
    if (!VM.runningVM) {
      return null;
    }              /* trick the boot image writer with null,
                                   which it will use when initializing
                                   java.lang.ClassLoader$StaticData */

    /* Lie, until we are really ready for someone to actually try
     * to use this class loader to load classes and resources.
     */
    if (!appCLReady) {
      return BootstrapClassLoader.getBootstrapClassLoader();
    }

    if (appCL != null) {
      return appCL;
    }

    // Sanity Checks:
    //    synchronized (this) {
    if (gettingAppCL > 0 || DBG_APP_CL) {
      VM.sysWriteln("JikesRVM: RVMClassLoader.getApplicationClassLoader(): ",
                    gettingAppCL > 0 ? "Recursively " : "",
                    "invoked with ",
                    gettingAppCL,
                    " previous instances pending");
    }
    if (gettingAppCL > 0) {
      VM.sysFail(
          "JikesRVM: While we are setting up the application class loader, some class required that selfsame application class loader.  This is a chicken-and-egg problem; see a Jikes RVM Guru.");
    }
    ++gettingAppCL;

    String r = getApplicationRepositories();

    if (Properties.verboseBoot >= 1 || DBG_APP_CL) {
      VM.sysWriteln("RVMClassLoader.getApplicationClassLoader(): " +
                    "Initializing Application ClassLoader, with" +
                    " repositories: `", r, "'...");
    }

    appCL = new ApplicationClassLoader(r);

    if (Properties.verboseBoot >= 1 || DBG_APP_CL) {
      VM.sysWriteln("RVMClassLoader.getApplicationClassLoader(): ...initialized Application classloader, to ",
                    appCL.toString());
    }
    --gettingAppCL;
    //    }
    return appCL;
  }

  //----------------//
  // implementation //
  //----------------//

  //
  private static String applicationRepositories;

  // Names of special methods.
  //
  /** "<clinit>" */
  public static final Atom StandardClassInitializerMethodName = Atom.findOrCreateAsciiAtom("<clinit>");
  /** "()V" */
  public static final Atom StandardClassInitializerMethodDescriptor = Atom.findOrCreateAsciiAtom("()V");

  /** "<init>" */
  public static final Atom StandardObjectInitializerMethodName = Atom.findOrCreateAsciiAtom("<init>");
  /** "()V" */
  public static final Atom StandardObjectInitializerMethodDescriptor = Atom.findOrCreateAsciiAtom("()V");
  /** "this" */
  public static final Atom StandardObjectInitializerHelperMethodName = Atom.findOrCreateAsciiAtom("this");

  /** "finalize" */
  public static final Atom StandardObjectFinalizerMethodName = Atom.findOrCreateAsciiAtom("finalize");
  /** "()V" */
  public static final Atom StandardObjectFinalizerMethodDescriptor = Atom.findOrCreateAsciiAtom("()V");

  // Names of .class file attributes.
  //
  /** "Code" */
  static final Atom codeAttributeName = Atom.findOrCreateAsciiAtom("Code");
  /** "ConstantValue" */
  static final Atom constantValueAttributeName = Atom.findOrCreateAsciiAtom("ConstantValue");
  /** "LineNumberTable" */
  static final Atom lineNumberTableAttributeName = Atom.findOrCreateAsciiAtom("LineNumberTable");
  /** "Exceptions" */
  static final Atom exceptionsAttributeName = Atom.findOrCreateAsciiAtom("Exceptions");
  /** "SourceFile" */
  static final Atom sourceFileAttributeName = Atom.findOrCreateAsciiAtom("SourceFile");
  /** "LocalVariableTable" */
  static final Atom localVariableTableAttributeName = Atom.findOrCreateAsciiAtom("LocalVariableTable");
  /** "Deprecated" */
  static final Atom deprecatedAttributeName = Atom.findOrCreateAsciiAtom("Deprecated");
  /** "InnerClasses" */
  static final Atom innerClassesAttributeName = Atom.findOrCreateAsciiAtom("InnerClasses");
  /** "Synthetic" */
  static final Atom syntheticAttributeName = Atom.findOrCreateAsciiAtom("Synthetic");
  /** "EnclosingMethod" */
  static final Atom enclosingMethodAttributeName = Atom.findOrCreateAsciiAtom("EnclosingMethod");
  /** "Signature" */
  static final Atom signatureAttributeName = Atom.findOrCreateAsciiAtom("Signature");
  /** "RuntimeVisibleAnnotations" */
  static final Atom runtimeVisibleAnnotationsAttributeName =
      Atom.findOrCreateAsciiAtom("RuntimeVisibleAnnotations");
  /** "RuntimeInvisibleAnnotations" */
  static final Atom runtimeInvisibleAnnotationsAttributeName =
      Atom.findOrCreateAsciiAtom("RuntimeInvisibleAnnotations");
  /** "RuntimeVisibleParameterAnnotations" */
  static final Atom runtimeVisibleParameterAnnotationsAttributeName =
      Atom.findOrCreateAsciiAtom("RuntimeVisibleParameterAnnotations");
  /** "RuntimeInvisibleParameterAnnotations" */
  static final Atom runtimeInvisibleParameterAnnotationsAttributeName =
      Atom.findOrCreateAsciiAtom("RuntimeInvisibleParameterAnnotations");
  /** "AnnotationDefault" */
  static final Atom annotationDefaultAttributeName = Atom.findOrCreateAsciiAtom("AnnotationDefault");

  /** Initialize at boot time.
   */
  public static void boot() {
    appCL = null;
  }

  /**
   * Initialize for boot image writing
   */
  public static void init(String bootstrapClasspath) {
    // specify where the VM's core classes and resources live
    //
    applicationRepositories = "."; // Carried over.
    BootstrapClassLoader.boot(bootstrapClasspath);
  }

  public static RVMType defineClassInternal(String className, byte[] classRep, int offset, int length,
                                            ClassLoader classloader) throws ClassFormatError {
    return defineClassInternal(className, new ByteArrayInputStream(classRep, offset, length), classloader);
  }

  public static RVMType defineClassInternal(String className, InputStream is, ClassLoader classloader)
      throws ClassFormatError {
    TypeReference tRef;
    if (className == null) {
      // NUTS: Our caller hasn't bothered to tell us what this class is supposed
      //       to be called, so we must read the input stream and discover it overselves
      //       before we actually can create the RVMClass instance.
      try {
        is.mark(is.available());
        tRef = getClassTypeRef(new DataInputStream(is), classloader);
        is.reset();
      } catch (IOException e) {
        ClassFormatError cfe = new ClassFormatError(e.getMessage());
        cfe.initCause(e);
        throw cfe;
      }
    } else {
      Atom classDescriptor = Atom.findOrCreateAsciiAtom(className.replace('.', '/')).descriptorFromClassName();
      tRef = TypeReference.findOrCreate(classloader, classDescriptor);
    }

    try {
      if (VM.VerifyAssertions) VM._assert(tRef.isClassType());
      if (VM.TraceClassLoading && VM.runningVM) {
        VM.sysWriteln("loading \"" + tRef.getName() + "\" with " + classloader);
      }
      RVMClass ans = RVMClass.readClass(tRef, new DataInputStream(is));
      tRef.setType(ans);
      return ans;
    } catch (IOException e) {
      ClassFormatError cfe = new ClassFormatError(e.getMessage());
      cfe.initCause(e);
      throw cfe;
    }
  }

  // Shamelessly cloned & owned from RVMClass constructor....
  private static TypeReference getClassTypeRef(DataInputStream input, ClassLoader cl)
      throws IOException, ClassFormatError {
    int magic = input.readInt();
    if (magic != 0xCAFEBABE) {
      throw new ClassFormatError("bad magic number " + Integer.toHexString(magic));
    }

    // Drop class file version number on floor. RVMClass constructor will do the check later.
    input.readUnsignedShort(); // minor ID
    input.readUnsignedShort(); // major ID

    //
    // pass 1: read constant pool
    //
    int[] constantPool = new int[input.readUnsignedShort()];
    byte[] tmpTags = new byte[constantPool.length];

    // note: slot 0 is unused
    for (int i = 1; i < constantPool.length; i++) {
      tmpTags[i] = input.readByte();
      switch (tmpTags[i]) {
        case TAG_UTF: {
          byte[] utf = new byte[input.readUnsignedShort()];
          input.readFully(utf);
          constantPool[i] = Atom.findOrCreateUtf8Atom(utf).getId();
          break;
        }

        case TAG_UNUSED:
          break;

        case TAG_INT:
        case TAG_FLOAT:
        case TAG_FIELDREF:
        case TAG_METHODREF:
        case TAG_INTERFACE_METHODREF:
        case TAG_MEMBERNAME_AND_DESCRIPTOR:
          input.readInt(); // drop on floor
          break;

        case TAG_LONG:
        case TAG_DOUBLE:
          i++;
          input.readLong(); // drop on floor
          break;

        case TAG_TYPEREF:
          constantPool[i] = input.readUnsignedShort();
          break;

        case TAG_STRING:
          input.readUnsignedShort(); // drop on floor
          break;

        default:
          throw new ClassFormatError("bad constant pool entry: " + tmpTags[i]);
      }
    }

    //
    // pass 2: post-process type constant pool entries
    // (we must do this in a second pass because of forward references)
    //
    for (int i = 1; i < constantPool.length; i++) {
      switch (tmpTags[i]) {
        case TAG_LONG:
        case TAG_DOUBLE:
          ++i;
          break;

        case TAG_TYPEREF: { // in: utf index
          Atom typeName = Atom.getAtom(constantPool[constantPool[i]]);
          constantPool[i] = TypeReference.findOrCreate(cl, typeName.descriptorFromClassName()).getId();
          break;
        } // out: type reference id
      }
    }

    // drop modifiers on floor.
    input.readUnsignedShort();

    int myTypeIndex = input.readUnsignedShort();
    return TypeReference.getTypeRef(constantPool[myTypeIndex]);
  }
}
