/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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 org.netbeans.modules.debugger.jpda.jdi;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.Writer;
import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;

/**
 * Generates wrapper methods for JDI calls.
 * Use "ant generate" to run this class and generate JDI wrapper classes.
 */
public class Generate {

    private static final String JDI_CLASSES_PATH = "com/sun/jdi";

    private static final Class[] RUNTIME_EXCEPTIONS = new Class[]
        { com.sun.jdi.ClassNotPreparedException.class, com.sun.jdi.InconsistentDebugInfoException.class,
          com.sun.jdi.InternalException.class, com.sun.jdi.InvalidStackFrameException.class,
          com.sun.jdi.NativeMethodException.class, com.sun.jdi.ObjectCollectedException.class,
          com.sun.jdi.VMCannotBeModifiedException.class, com.sun.jdi.VMDisconnectedException.class,
          com.sun.jdi.VMOutOfMemoryException.class,
          com.sun.jdi.request.DuplicateRequestException.class,
          com.sun.jdi.request.InvalidRequestStateException.class };

    private static final String PACKAGE = "org.netbeans.modules.debugger.jpda.jdi";

    private static final Map<Class, String> EXCEPTION_WRAPPERS = new LinkedHashMap<Class, String>();

    private static final Map<String/*class name*/, Map<String/*method*/, Set<Class/*exception*/>>> EXCEPTIONS_BY_METHODS = new LinkedHashMap<String, Map<String, Set<Class>>>();

    // Fake values can be returned if these exceptions are thrown:
    private static final Set<Class> SILENT_EXCEPTIONS = Collections.unmodifiableSet(new LinkedHashSet<Class>(Arrays.asList(new Class[] {
           com.sun.jdi.InternalException.class, com.sun.jdi.ObjectCollectedException.class, com.sun.jdi.VMDisconnectedException.class })));

    private static final Set<String> NOT_USED_CLASSES = Collections.unmodifiableSet(new HashSet<String>(Arrays.asList(new String[] {
            com.sun.jdi.Accessible.class.getName(), com.sun.jdi.Bootstrap.class.getName(),
            com.sun.jdi.ClassLoaderReference.class.getName(),
            com.sun.jdi.PathSearchingVirtualMachine.class.getName(),
            com.sun.jdi.VoidValue.class.getName(),
            // Connectors are used in API and UI modules.
            // Classes starting with "com.sun.jdi.connect" are not generated
            com.sun.jdi.event.EventIterator.class.getName(),
    })));
    
    private static final String METHODS_BY_JDK = "MethodsByJDK";

    private static String license = null;

    // Runtime exceptions:
    // VMDisconnectedException on all JDI calls
    // InternalException on all JDI calls
    // ObjectCollectedException on all calls on ObjectReference
    // Other exceptions on selected methods

    static {
        Map<String, Set<Class>> AllClassExceptions = new LinkedHashMap<String, Set<Class>>();
        AllClassExceptions.put("*", new LinkedHashSet<Class>(Arrays.asList(
                new Class[] { com.sun.jdi.InternalException.class,
                              com.sun.jdi.VMDisconnectedException.class })));
        EXCEPTIONS_BY_METHODS.put("*", AllClassExceptions);

        Map<String, Set<Class>> ReferenceTypeExceptions = new LinkedHashMap<String, Set<Class>>();
        ReferenceTypeExceptions.put("fields", Collections.singleton((Class) com.sun.jdi.ClassNotPreparedException.class));
        ReferenceTypeExceptions.put("visibleFields", Collections.singleton((Class) com.sun.jdi.ClassNotPreparedException.class));
        ReferenceTypeExceptions.put("allFields", Collections.singleton((Class) com.sun.jdi.ClassNotPreparedException.class));
        ReferenceTypeExceptions.put("fieldByName", Collections.singleton((Class) com.sun.jdi.ClassNotPreparedException.class));
        ReferenceTypeExceptions.put("methods", Collections.singleton((Class) com.sun.jdi.ClassNotPreparedException.class));
        ReferenceTypeExceptions.put("visibleMethods", Collections.singleton((Class) com.sun.jdi.ClassNotPreparedException.class));
        ReferenceTypeExceptions.put("allMethods", Collections.singleton((Class) com.sun.jdi.ClassNotPreparedException.class));
        ReferenceTypeExceptions.put("methodsByName", Collections.singleton((Class) com.sun.jdi.ClassNotPreparedException.class));
        ReferenceTypeExceptions.put("allLineLocations", Collections.singleton((Class) com.sun.jdi.ClassNotPreparedException.class));
        ReferenceTypeExceptions.put("locationsOfLine", Collections.singleton((Class) com.sun.jdi.ClassNotPreparedException.class));
        ReferenceTypeExceptions.put("classObject", Collections.singleton((Class) java.lang.UnsupportedOperationException.class));
        ReferenceTypeExceptions.put("*", Collections.singleton((Class) com.sun.jdi.ObjectCollectedException.class));
        EXCEPTIONS_BY_METHODS.put(com.sun.jdi.ReferenceType.class.getName(), ReferenceTypeExceptions);
        Map<String, Set<Class>> ClassTypeExceptions = new LinkedHashMap<String, Set<Class>>();
        ClassTypeExceptions.put("interfaces", Collections.singleton((Class) com.sun.jdi.ClassNotPreparedException.class));
        ClassTypeExceptions.put("allInterfaces", Collections.singleton((Class) com.sun.jdi.ClassNotPreparedException.class));
        ClassTypeExceptions.put("setValue", Collections.singleton((Class) com.sun.jdi.ClassNotPreparedException.class)); // JDWP protocol says that this can be thrown!
        ClassTypeExceptions.put("concreteMethodByName", Collections.singleton((Class) com.sun.jdi.ClassNotPreparedException.class));
        ClassTypeExceptions.put("invokeMethod", Collections.singleton((Class) com.sun.jdi.ObjectCollectedException.class));
        ClassTypeExceptions.put("newInstance", Collections.singleton((Class) com.sun.jdi.ObjectCollectedException.class));
        EXCEPTIONS_BY_METHODS.put(com.sun.jdi.ClassType.class.getName(), ClassTypeExceptions);
        Map<String, Set<Class>> InterfaceTypeExceptions = new LinkedHashMap<String, Set<Class>>();
        InterfaceTypeExceptions.put("superinterfaces", Collections.singleton((Class) com.sun.jdi.ClassNotPreparedException.class));
        EXCEPTIONS_BY_METHODS.put(com.sun.jdi.InterfaceType.class.getName(), InterfaceTypeExceptions);
        Map<String, Set<Class>> TypeExceptions = new LinkedHashMap<String, Set<Class>>();
        TypeExceptions.put("*", Collections.singleton((Class) com.sun.jdi.ObjectCollectedException.class));
        EXCEPTIONS_BY_METHODS.put(com.sun.jdi.Type.class.getName(), TypeExceptions);

        Map<String, Set<Class>> ObjectReferenceExceptions = new LinkedHashMap<String, Set<Class>>();
        ObjectReferenceExceptions.put("setValue", Collections.singleton((Class) java.lang.IllegalArgumentException.class));
        ObjectReferenceExceptions.put("disableCollection", Collections.singleton((Class) java.lang.UnsupportedOperationException.class));
        ObjectReferenceExceptions.put("enableCollection", Collections.singleton((Class) java.lang.UnsupportedOperationException.class));
        EXCEPTIONS_BY_METHODS.put(com.sun.jdi.ObjectReference.class.getName(), ObjectReferenceExceptions);

        Map<String, Set<Class>> FieldExceptions = new LinkedHashMap<String, Set<Class>>();
        FieldExceptions.put("type", Collections.singleton((Class) com.sun.jdi.ObjectCollectedException.class));
        EXCEPTIONS_BY_METHODS.put(com.sun.jdi.Field.class.getName(), FieldExceptions);

        Map<String, Set<Class>> ThreadReferenceExceptions = new LinkedHashMap<String, Set<Class>>();
        // IllegalThreadStateException is thrown through JDWPException when INVALID_THREAD is received from JDWP.
        ThreadReferenceExceptions.put("*", Collections.singleton((Class) IllegalThreadStateException.class));
        ThreadReferenceExceptions.put("popFrames", new LinkedHashSet<Class>(Arrays.asList(
                new Class[] { com.sun.jdi.NativeMethodException.class,
                              com.sun.jdi.InvalidStackFrameException.class })));
        ThreadReferenceExceptions.put("forceEarlyReturn", new LinkedHashSet<Class>(Arrays.asList(
                new Class[] { com.sun.jdi.NativeMethodException.class,
                              com.sun.jdi.InvalidStackFrameException.class })));
        ThreadReferenceExceptions.put("frameCount", new LinkedHashSet<Class>(Arrays.asList(
                new Class[] { com.sun.jdi.InvalidStackFrameException.class })));// #233070
        ThreadReferenceExceptions.put("threadGroup", new LinkedHashSet<Class>(Arrays.asList(
                new Class[] { com.sun.jdi.VMOutOfMemoryException.class })));
        EXCEPTIONS_BY_METHODS.put(com.sun.jdi.ThreadReference.class.getName(), ThreadReferenceExceptions);

        Map<String, Set<Class>> StackFrameExceptions = new LinkedHashMap<String, Set<Class>>();
        StackFrameExceptions.put("*", Collections.singleton((Class) com.sun.jdi.InvalidStackFrameException.class));
        StackFrameExceptions.put("setValue", Collections.singleton((Class) com.sun.jdi.ObjectCollectedException.class));
        StackFrameExceptions.put("visibleVariableByName", Collections.singleton((Class) com.sun.jdi.NativeMethodException.class));
        StackFrameExceptions.put("visibleVariables", Collections.singleton((Class) com.sun.jdi.NativeMethodException.class));
        EXCEPTIONS_BY_METHODS.put(com.sun.jdi.StackFrame.class.getName(), StackFrameExceptions);

        Map<String, Set<Class>> MonitorInfoExceptions = new LinkedHashMap<String, Set<Class>>();
        MonitorInfoExceptions.put("*", Collections.singleton((Class) com.sun.jdi.InvalidStackFrameException.class));
        EXCEPTIONS_BY_METHODS.put("com.sun.jdi.MonitorInfo", MonitorInfoExceptions);

        Map<String, Set<Class>> VirtualMachineExceptions = new LinkedHashMap<String, Set<Class>>();
        // UnsupportedOperationException can be thrown on J2ME:
        VirtualMachineExceptions.put("mirrorOf(java.lang.String)", Collections.singleton((Class) java.lang.UnsupportedOperationException.class));
        EXCEPTIONS_BY_METHODS.put(com.sun.jdi.VirtualMachine.class.getName(), VirtualMachineExceptions);

        Map<String, Set<Class>> EventRequestExceptions = new LinkedHashMap<String, Set<Class>>();
        // ObjectCollectedException can be thrown
        EventRequestExceptions.put("disable", new LinkedHashSet<Class>(Arrays.asList(
                new Class [] { com.sun.jdi.ObjectCollectedException.class,
                               com.sun.jdi.request.InvalidRequestStateException.class })));
        EventRequestExceptions.put("enable", new LinkedHashSet<Class>(Arrays.asList(
                new Class [] { com.sun.jdi.ObjectCollectedException.class,
                               com.sun.jdi.request.InvalidRequestStateException.class })));
        EventRequestExceptions.put("setEnabled", new LinkedHashSet<Class>(Arrays.asList(
                new Class [] { com.sun.jdi.ObjectCollectedException.class,
                               com.sun.jdi.request.InvalidRequestStateException.class })));
        EXCEPTIONS_BY_METHODS.put(com.sun.jdi.request.EventRequest.class.getName(), EventRequestExceptions);
        Map<String, Set<Class>> EventRequestManagerExceptions = new LinkedHashMap<String, Set<Class>>();
        EventRequestManagerExceptions.put("deleteEventRequest", Collections.singleton((Class)
                com.sun.jdi.request.InvalidRequestStateException.class));
        EventRequestManagerExceptions.put("deleteEventRequests", Collections.singleton((Class)
                com.sun.jdi.request.InvalidRequestStateException.class));
        EXCEPTIONS_BY_METHODS.put(com.sun.jdi.request.EventRequestManager.class.getName(), EventRequestManagerExceptions);

        Map<String, Set<Class>> EventSetExceptions = new LinkedHashMap<String, Set<Class>>();
        // IllegalThreadStateException is thrown through JDWPException when INVALID_THREAD is received from JDWP.
        // ObjectCollectedException can be thrown when the thread is collected.
        EventSetExceptions.put("resume", new LinkedHashSet<Class>(Arrays.asList(
                new Class [] { IllegalThreadStateException.class,
                               com.sun.jdi.ObjectCollectedException.class })));
        EXCEPTIONS_BY_METHODS.put(com.sun.jdi.event.EventSet.class.getName(), EventSetExceptions);
    }


    private static String readLicense() throws IOException {
        StringBuilder sb = new StringBuilder();
        File f = new File(System.getProperty("user.dir"));
        f = new File(f, "gensrc/"+Generate.class.getName().replace('.', '/')+".java");
        BufferedReader r = new BufferedReader(new FileReader(f));
        while (true) {
            String line = r.readLine();
            sb.append(line);
            sb.append(System.getProperty("line.separator"));
            if (line.trim().indexOf("*/") >= 0) {
                break;
            }
        }
        r.close();
        return sb.toString();
    }

    private static String getLicense() {
        if (license == null) {
            try {
                license = readLicense();
            } catch (IOException ioex) {
                ioex.printStackTrace();
            }
        }
        return license;
    }

    private static String generateWrapperException(File dir, Class jdiException) throws IOException {
        String name = jdiException.getSimpleName();
        String cName = name + "Wrapper";
        String eName = jdiException.getName();
        File cf = new File(dir, cName+".java");
        Writer w = new BufferedWriter(new FileWriter(cf));
        w.write(getLicense());
        w.write("\npackage "+PACKAGE+";\n\n");
        w.write("/**\n * Wrapper for "+name+" JDI exception.\n * The calling code must count with this exception being thrown.\n */\n");
        w.write("public final class "+cName+" extends Exception {\n");
        w.write("\n    public "+cName+"("+eName+" ex) {\n");
        w.write("        super(ex);\n");
        w.write("    }\n\n");
        w.write("    @Override\n");
        w.write("    public "+eName+" getCause() {\n");
        w.write("        return ("+eName+") super.getCause();\n");
        w.write("    }\n\n");
        w.write("}\n");
        w.close();
        return PACKAGE+"."+cName;
    }

    private static List<Class> getJDIClasses() throws IOException {
        List<Class> classes = new ArrayList<Class>();
        URL resource = ClassLoader.getSystemClassLoader().getResource("com/sun/jdi/VirtualMachine.class");
        String jarFileName = resource.getFile();
        jarFileName = jarFileName.substring("file:".length(), jarFileName.indexOf('!'));
        ZipFile jar = new ZipFile(jarFileName);
        Enumeration<? extends ZipEntry> classEntries = jar.entries();
        while (classEntries.hasMoreElements()) {
            ZipEntry classEntry = classEntries.nextElement();
            String name = classEntry.getName();
            if (name.startsWith(JDI_CLASSES_PATH) && name.endsWith(".class")) {
                String className = name.substring(0, name.length() - ".class".length()).replace('/', '.');
                //className = className.replace('$', '.');
                //System.err.println("Have class from JAR: '"+className+"'");
                Class c;
                try {
                    c = Class.forName(className);
                } catch (ClassNotFoundException ex) {
                    Logger.getLogger(Generate.class.getName()).log(Level.SEVERE, null, ex);
                    continue;
                }
                if (Throwable.class.isAssignableFrom(c)) {
                    continue;
                }
                classes.add(c);
            }
        }
        Collections.sort(classes, new Comparator<Class>() {
            public int compare(Class o1, Class o2) {
                return o1.getName().compareTo(o2.getName());
            }
        });
        return classes;
    }

    private static void generate(File dir) throws IOException {
        List<Class> classes = getJDIClasses();
        for (Map<String, Set<Class>> map : EXCEPTIONS_BY_METHODS.values()) {
            for (Set<Class> set : map.values()) {
                for (Class ex : set) {
                    if (!EXCEPTION_WRAPPERS.containsKey(ex)) {
                        EXCEPTION_WRAPPERS.put(ex, generateWrapperException(dir, ex));
                    }
                }
            }
        }
        EXCEPTION_WRAPPERS.put(com.sun.jdi.ObjectCollectedException.class, generateWrapperException(dir, com.sun.jdi.ObjectCollectedException.class));

        // Add classes and methods that are in JDK 1.6 and higher versions and generate reflection calls
        String jdkVersion = System.getProperty("java.version");
        System.err.println("jdkVersion = "+jdkVersion);
        File rootResource;
        try {
            rootResource = new File(Generate.class.getResource("").toURI());
        } catch (Exception ex) {
            IOException ioex = new IOException();
            ioex.initCause(ex);
            throw ioex;
        }

        File jdkLogFile = new File(rootResource, METHODS_BY_JDK+jdkVersion);
        Writer log = new BufferedWriter(new FileWriter(jdkLogFile));
        // Information about all JDI methods of the current JDK version are stored
        // in the following form:
        // <class name>:<wrapper class simple name> [isObjectReference]
        //  <method name>(<generic parameter types>):<generic return type>:<default return value> throws <exception classes>

        Map<String, List<String>> higherVersionMethods = getHigherVersionMethods(rootResource, jdkVersion);
        int[] higherVersionMethodIndexes = new int[higherVersionMethods.size()];

        Set<String> higherVersionClasses = new HashSet<String>();
        int[] indexes = new int[higherVersionMethods.size()];
        for (Class c : classes) {
            if (c.getDeclaredMethods().length == 0) {
                continue;
            }
            int ii = 0;
            for (Iterator<String> it = higherVersionMethods.keySet().iterator(); it.hasNext(); ii++) {
                String version = it.next();
                List<String> methodsLog = higherVersionMethods.get(version);
                int i;
                for (i = indexes[ii]; i < methodsLog.size(); i++) {
                    String loggedClass = methodsLog.get(i);
                    if (loggedClass.startsWith(" ")) continue;
                    int colonIndex = loggedClass.indexOf(':');
                    if (!loggedClass.substring(0, colonIndex).equals(c.getName())) {
                        higherVersionClasses.add(loggedClass.substring(0, colonIndex).replace('$', '.'));
                    } else {
                        i++;
                        break;
                    }
                }
                indexes[ii] = i;
            }
        }
        System.out.println("\nHigher version classes: "+higherVersionClasses+"\n");
        
        for (Class c : classes) {
            String name = c.getSimpleName();
            String className = c.getName().replace('$', '.');
            String classPackage = c.getPackage().getName();
            String cName;
            Class enclosingClass = c.getEnclosingClass();
            if (enclosingClass != null) {
                cName = enclosingClass.getSimpleName() + name + "Wrapper";
                name = enclosingClass.getSimpleName() + "." + name;
            } else {
                cName = name + "Wrapper";
            }
            Method[] methods = c.getDeclaredMethods();
            if (methods.length == 0) {
                System.err.println("Class: "+c.getName());
                System.err.println(" - ignored, have no methods.");
                continue;
            }
            writeHigherVersionClasses(dir, c, higherVersionMethods, higherVersionMethodIndexes, higherVersionClasses);
            System.err.println("Class: "+c.getName());
            Arrays.sort(methods, new Comparator<Method>() {
                public int compare(Method m1, Method m2) {
                    int c = m1.getName().compareTo(m2.getName());
                    if (c == 0) {
                        StringBuilder p1 = new StringBuilder();
                        for (Class pt : m1.getParameterTypes()) {
                            p1.append(pt.getName());
                            p1.append(" ");
                        }
                        StringBuilder p2 = new StringBuilder();
                        for (Class pt : m2.getParameterTypes()) {
                            p2.append(pt.getName());
                            p2.append(" ");
                        }
                        c = p1.toString().compareTo(p2.toString());
                    }
                    return c;
                }
            });
            log.write(c.getName()+":"+cName+"\n");
            Writer w;
            if (NOT_USED_CLASSES.contains(c.getName()) || c.getName().startsWith("com.sun.jdi.connect")) {
                w = null;
            } else {
                w = writeClassHeader(dir, name, classPackage, cName, null);
            }
            for (Method m : methods) {
                writeHigherVersionMethods(w, m, className, higherVersionMethods, higherVersionMethodIndexes, higherVersionClasses);
                String mName = m.getName();
                Type[] paramTypes = m.getGenericParameterTypes();
                String rType = translateType(m.getGenericReturnType());
                String defaultReturn = getDefaultReturn(m.getReturnType());
                Class[] exceptionTypes = m.getExceptionTypes();
                
                System.err.println("  Method: "+mName);
                logMethod(log, mName, paramTypes, exceptionTypes, rType, defaultReturn);
                if (w != null) {
                    if (defaultReturn != null) {
                        writeMethod(w, c, className, mName, mName+"0", paramTypes, exceptionTypes, rType, defaultReturn);
                    }
                    writeMethod(w, c, className, mName, mName, paramTypes, exceptionTypes, rType, null);
                }
            }
            writeHigherVersionMethods(w, null, className, higherVersionMethods, higherVersionMethodIndexes, higherVersionClasses);
            if (w != null) {
                w.write("}\n");
                w.close();
            }
        }
        writeHigherVersionClasses(dir, null, higherVersionMethods, higherVersionMethodIndexes, higherVersionClasses);
        log.close();
    }

    private static Writer writeClassHeader(File dir, String name, String classPackage, String cName, String jdkVersion) throws IOException {
        String classDir = classPackage.replace('.', '/').substring(JDI_CLASSES_PATH.length());
        if (classDir.startsWith("/")) classDir = classDir.substring(1);
        File cDir = (classDir.length() == 0) ? dir : new File(dir, classDir);
        cDir.mkdirs();
        File cf = new File(cDir, cName+".java");
        Writer w = new BufferedWriter(new FileWriter(cf));
        String cPackage = classPackage.substring(JDI_CLASSES_PATH.length());
        w.write(getLicense());
        w.write("\npackage "+PACKAGE+cPackage+";\n\n");
        w.write("// DO NOT MODIFY THIS CODE, GENERATED AUTOMATICALLY\n");
        w.write("// Generated by "+Generate.class.getName()+" class located in 'gensrc' folder,\n// perform the desired modifications there and re-generate by \"ant generate\".\n\n");
        w.write("/**\n * Wrapper for "+name+" JDI class");
        if (jdkVersion != null) {
            w.write(" from JDK ");
            w.write(jdkVersion);
        }
        w.write(".\n * Use methods of this class instead of direct calls on JDI objects.\n"+
                " * These methods assure that exceptions thrown from JDI calls are handled appropriately.\n"+
                " *\n * @author Martin Entlicher\n */\n");
        w.write("public final class "+cName+" {\n");
        w.write("\n    private "+cName+"() {}\n\n");
        return w;
    }

    private static void logMethod(Writer log, String mName,
                                  Type[] paramTypes, Class[] exceptionTypes,
                                  String rType, String defaultReturn) throws IOException {
        log.write(" "+mName+"(");
        for (int i = 0; i < paramTypes.length; i++) {
            String paramType = translateType(paramTypes[i]);
            if (i > 0) log.write(", ");
            log.write(paramType);
        }
        log.write("):"+rType+":"+((defaultReturn != null) ? defaultReturn : ""));
        if (exceptionTypes.length > 0) {
            log.write(" throws ");
            for (int i = 0; i < exceptionTypes.length; i++) {
                if (i > 0) log.write(", ");
                log.write(exceptionTypes[i].getName());
            }
        }
        log.write("\n");
    }

    private static void writeMethod(Writer w, Class c, String className,
                                    String mName, String mGenName, Type[] paramTypes, Class[] exceptionTypes,
                                    String rType, String defaultReturn) throws IOException {
        w.write("    // DO NOT MODIFY THIS CODE, GENERATED AUTOMATICALLY\n");
        w.write("    public static "+rType+" "+mGenName+"("+className+" a");
        String[] paramNames = new String[paramTypes.length];
        StringBuilder paramTypesList = new StringBuilder("(");
        for (int i = 0; i < paramTypes.length; i++) {
            //Class t = paramTypes[i];
            paramNames[i] = Character.toString((char) ('a'+(i+1)));
            String paramType = translateType(paramTypes[i]);
            w.write(", "+paramType+" "+paramNames[i]);
            if (i > 0) paramTypesList.append(", ");
            paramTypesList.append(paramType);
        }
        w.write(")");
        paramTypesList.append(")");
        String mNameWithParamTypes = mName + paramTypesList.toString();

        // Add wrappers of JDI runtime exceptions...
        Set<Class> thrownExceptions = new LinkedHashSet<Class>();
        thrownExceptions.addAll(EXCEPTIONS_BY_METHODS.get("*").get("*"));
        if (com.sun.jdi.Value.class.isAssignableFrom(c) &&
            !com.sun.jdi.PrimitiveValue.class.isAssignableFrom(c)) {

            thrownExceptions.add(com.sun.jdi.ObjectCollectedException.class);
        }
        Map<String, Set<Class>> excByMethods = EXCEPTIONS_BY_METHODS.get(c.getName());
        if (excByMethods != null) {
            Set<Class> excs = excByMethods.get("*");
            if (excs != null) {
                thrownExceptions.addAll(excs);
            }
            excs = excByMethods.get(mNameWithParamTypes);
            if (excs == null) {
                excs = excByMethods.get(mName);
            }
            if (excs != null) {
                thrownExceptions.addAll(excs);
            }
        }

        Set<Class> caughtExceptions = new LinkedHashSet<Class>(thrownExceptions);
        if (defaultReturn != null) thrownExceptions.removeAll(SILENT_EXCEPTIONS);


        if (exceptionTypes.length > 0 || thrownExceptions.size() > 0) {
            w.write(" throws ");
            for (int i = 0; i < exceptionTypes.length; i++) {
                if (i > 0) w.write(", ");
                w.write(exceptionTypes[i].getName());
            }
            if (exceptionTypes.length > 0 && thrownExceptions.size() > 0) {
                w.write(", ");
            }
            int i = 0;
            for (Iterator it = thrownExceptions.iterator(); it.hasNext(); i++) {
                if (i > 0) w.write(", ");
                w.write(EXCEPTION_WRAPPERS.get(it.next()));
            }
        }
        w.write(" {\n");
        String preCode = methodPreCode(className, mName);
        if (preCode != null) {
            w.write(preCode);
        }
        w.write("        if (org.netbeans.modules.debugger.jpda.JDIExceptionReporter.isLoggable()) {\n");
        w.write("            org.netbeans.modules.debugger.jpda.JDIExceptionReporter.logCallStart(\n"+
                "                    \""+className+"\",\n"+
                "                    \""+mName+"\",\n"+
                "                    \"JDI CALL: "+className+"({0})."+mName+"(");
        for (int i = 0; i < paramNames.length; i++) {
            if (i > 0) {
                w.write(", ");
            }
            w.write("{"+Integer.toString(i+1)+"}");
        }
        w.write(")\",\n"+
                "                    new Object[] {a");
        for (int i = 0; i < paramNames.length; i++) {
            w.write(", ");
            w.write(paramNames[i]);
        }
        w.write("});\n");
        w.write("        }\n"); // if
        
        boolean isVoidReturn = "void".equals(rType);
        if (!isVoidReturn) {
            w.write("        Object retValue = null;\n");
        }

        w.write("        try {\n");

        StringBuffer exec = new StringBuffer();
        if (!isVoidReturn) {
            exec.append("            ").append(rType).append(" ret;\n");
            exec.append("            ret = ");
        } else {
            exec.append("            ");
        }
        exec.append("a."+mName+"(");
        for (int i = 0; i < paramNames.length; i++) {
            if (i > 0) {
                exec.append(", ");
            }
            exec.append(paramNames[i]);
        }
        exec.append(");\n");
        if (!isVoidReturn) {
            exec.append("            retValue = ret;\n");
            exec.append("            return ret;\n");
        }
        w.write(methodImpl(className, mName, exec.toString()));

        w.write("        }");
        /*// First re-throw the checked exceptions:
        for (int i = 0; i < exceptionTypes.length; i++) {
            w.write(exceptionTypes[i].getName());
            w.write(" ex) {\n");
            w.write("            throw ex;\n");
            w.write("        } catch (");
        }*/
        for (Iterator<Class> it = caughtExceptions.iterator(); it.hasNext(); ) {
            Class cex = it.next();
            w.write(" catch (");
            w.write(cex.getName());
            w.write(" ex) {\n");
            if (!isVoidReturn) {
                w.write("            retValue = ex;\n");
            }
            if (com.sun.jdi.InternalException.class.equals(cex)) {
                w.write(reportOfJDIException(className, mName, "ex"));
            }
            if (com.sun.jdi.VMDisconnectedException.class.equals(cex)) {
                w.write(disposeVMOnVMDisconnectedException(className, mName));
            }
            if (defaultReturn != null && SILENT_EXCEPTIONS.contains(cex)) {
                w.write("            return "+defaultReturn+";\n");
            } else {
                w.write("            throw new "+EXCEPTION_WRAPPERS.get(cex)+"(ex);\n");
            }
            w.write("        }");
        }
        if (!isVoidReturn) {
            for (int i = 0; i < exceptionTypes.length; i++) {
                w.write(" catch (");
                w.write(exceptionTypes[i].getName());
                w.write(" ex) {\n");
                w.write("            retValue = ex;\n");
                w.write("            throw ex;\n        }");
            }
            w.write(" catch (Error err) {\n");
            w.write("            retValue = err;\n");
            w.write("            throw err;\n");
            w.write("        } catch (RuntimeException rex) {\n");
            w.write("            retValue = rex;\n");
            w.write("            throw rex;\n        }");

        }
        w.write(" finally {\n");
        w.write("            if (org.netbeans.modules.debugger.jpda.JDIExceptionReporter.isLoggable()) {\n");
        w.write("                org.netbeans.modules.debugger.jpda.JDIExceptionReporter.logCallEnd(\n"+
                "                        \""+className+"\",\n"+
                "                        \""+mName+"\",\n"+
                (isVoidReturn ?
                "                        org.netbeans.modules.debugger.jpda.JDIExceptionReporter.RET_VOID);\n" :
                "                        retValue);\n"));
        //w.write("                logger.log(java.util.logging.Level.FINER, \"          returned after {0} ns\", (t2 - t1));\n");
        w.write("            }\n");
        w.write("        }\n");
        w.write("    }\n\n");
    }

    public static void writeHigherVersionClasses(File dir, Class c,
                                                 Map<String, List<String>> higherVersionMethods,
                                                 int[] higherVersionMethodIndexes,
                                                 Set<String> higherVersionClasses) throws IOException {
        int versionIndex = 0;
        for (Iterator<String> it = higherVersionMethods.keySet().iterator(); it.hasNext(); versionIndex++) {
            String version = it.next();
            List<String> methodsLog = higherVersionMethods.get(version);
            while (higherVersionMethodIndexes[versionIndex] < methodsLog.size()) {
                String loggedClass = methodsLog.get(higherVersionMethodIndexes[versionIndex]++);
                int colonIndex = loggedClass.indexOf(':');
                if (c == null || !loggedClass.substring(0, colonIndex).equals(c.getName())) {
                    String loggedClassBinaryName = loggedClass.substring(0, colonIndex);
                    int lastDotIndex = loggedClassBinaryName.lastIndexOf('.');
                    String loggedClassPackage = loggedClassBinaryName.substring(0, lastDotIndex);
                    String loggedClassName = loggedClassBinaryName.replace('$', '.');
                    String loggedName = loggedClassName.substring(loggedClassName.lastIndexOf('.') + 1);
                    System.err.println("Have class: "+loggedName+" from JDK "+version);
                    Writer w;
                    if (NOT_USED_CLASSES.contains(loggedClassBinaryName) || loggedClassBinaryName.startsWith("com.sun.jdi.connect")) {
                        w = null;
                    } else {
                        w = writeClassHeader(dir, loggedName, loggedClassPackage, loggedClass.substring(colonIndex + 1), version);
                    }
                    int i;
                    for (i = higherVersionMethodIndexes[versionIndex]; i < methodsLog.size(); i++) {
                        String method = methodsLog.get(i);
                        if (!method.startsWith(" ")) {
                            break;
                        }
                        if (w != null) {
                            writeHigherVersionMethod(w, loggedClassName, method, version, higherVersionClasses);
                        }
                    }
                    higherVersionMethodIndexes[versionIndex] = i;
                    if (w != null) {
                        w.write("}\n");
                        w.close();
                    }
                } else {
                    break;
                }
            }
        }
    }

    private static void writeHigherVersionMethods(Writer w, Method m,
                                                  String className,
                                                  Map<String, List<String>> higherVersionMethods,
                                                  int[] higherVersionMethodIndexes,
                                                  Set<String> higherVersionClasses) throws IOException {
        //System.err.println("writeHigherVersionMethods("+m+")");
        int versionIndex = 0;
        for (Iterator<String> it = higherVersionMethods.keySet().iterator(); it.hasNext(); versionIndex++) {
            String version = it.next();
            List<String> methodsLog = higherVersionMethods.get(version);
            while (higherVersionMethodIndexes[versionIndex] < methodsLog.size()) {
                String loggedMethod = methodsLog.get(higherVersionMethodIndexes[versionIndex]);
                if (loggedMethod.startsWith(" ")) {
                    higherVersionMethodIndexes[versionIndex]++;
                    if (w != null && (m == null || !isLoggedMethod(m, loggedMethod))) {
                        //System.out.println(" Method "+loggedMethod.substring(1, loggedMethod.indexOf("):") + 1)+" from JDK "+version);
                        writeHigherVersionMethod(w, className, loggedMethod, version, higherVersionClasses);
                    } else {
                        break;
                    }
                } else {
                    break;
                }
            };
        }
    }

    private static boolean isLoggedMethod(Method m, String loggedMethod) {
        String mName = m.getName();
        Type[] paramTypes = m.getGenericParameterTypes();
        StringBuilder mLog = new StringBuilder(" ");
        mLog.append(mName);
        mLog.append("(");
        for (int i = 0; i < paramTypes.length; i++) {
            String paramType = translateType(paramTypes[i]);
            if (i > 0) mLog.append(", ");
            mLog.append(paramType);
        }
        mLog.append("):");
        return loggedMethod.startsWith(mLog.toString());
    }


    private static void writeHigherVersionMethod(Writer w, String className,
                                                 String methodLine, String jdkVersion,
                                                 Set<String> higherVersionClasses) throws IOException {
        methodLine = methodLine.trim();
        System.err.println("  Method: "+methodLine+" from JDK "+jdkVersion);
        int index = methodLine.indexOf('(');
        String mName = methodLine.substring(0, index);
        List<String> paramTypes = new ArrayList<String>();
        index++;
        int index2 = index;
        char c;
        while ((c = methodLine.charAt(index2)) != ')') {
            if (c == ',') {
                paramTypes.add(substituteHigherClasses(methodLine.substring(index, index2), higherVersionClasses));
                index = index2 + 2; // ", "
                index2 = index;
            } else if (c == '<') {
                index2 = findPair(methodLine, index2 + 1, '<', '>');
            } else {
                index2++;
            }
            //System.err.println("  c = "+c+", index = "+index+", index2 = "+index2);
        }
        if (index2 > index) {
            paramTypes.add(substituteHigherClasses(methodLine.substring(index, index2), higherVersionClasses));
        }
        String mNameWithParamTypes = methodLine.substring(0, index2 + 1);
        index = index2 + 2; // "):"
        index2 = methodLine.indexOf(":", index);
        String rType = methodLine.substring(index, index2);
        rType = substituteHigherClasses(rType, higherVersionClasses);
        index = index2 + 1; // ":"
        index2 = methodLine.indexOf(" throws ", index);
        String defaultReturn;
        List<String> exceptionTypes;
        if (index2 < 0) {
            defaultReturn = methodLine.substring(index).trim();
            exceptionTypes = Collections.emptyList();
        } else {
            defaultReturn = methodLine.substring(index, index2).trim();
            index = index2 + " throws ".length();
            exceptionTypes = new ArrayList<String>();
            for (index2 = index; index2 < methodLine.length(); index2++) {
                if (methodLine.charAt(index2) == ',') {
                    exceptionTypes.add(methodLine.substring(index, index2).trim());
                    index = index2 + 2; // ", "
                    index2 = index;
                }
            }
            exceptionTypes.add(methodLine.substring(index).trim());
        }
        if (defaultReturn.length() == 0) {
            defaultReturn = null;
        }

        Set<Class> thrownExceptions = new LinkedHashSet<Class>();
        thrownExceptions.addAll(EXCEPTIONS_BY_METHODS.get("*").get("*"));
        //if (com.sun.jdi.ObjectReference.class.isAssignableFrom(c)) {
        //    thrownExceptions.add(com.sun.jdi.ObjectCollectedException.class);
        //}
        Map<String, Set<Class>> excByMethods = EXCEPTIONS_BY_METHODS.get(className);
        if (excByMethods != null) {
            Set<Class> excs = excByMethods.get("*");
            if (excs != null) {
                thrownExceptions.addAll(excs);
            }
            excs = excByMethods.get(mNameWithParamTypes);
            if (excs == null) {
                excs = excByMethods.get(mName);
            }
            if (excs != null) {
                thrownExceptions.addAll(excs);
            }
        }

        className = substituteHigherClasses(className, higherVersionClasses);

        Set<Class> caughtExceptions = new LinkedHashSet<Class>(thrownExceptions);

        writeHigherVersionMethod(w, className, mName, mName, paramTypes, exceptionTypes, thrownExceptions, caughtExceptions, rType, null, jdkVersion);
        if (defaultReturn != null) {
            thrownExceptions.removeAll(SILENT_EXCEPTIONS);
            writeHigherVersionMethod(w, className, mName, mName+"0", paramTypes, exceptionTypes, thrownExceptions, caughtExceptions, rType, defaultReturn, jdkVersion);
        }

    }

    private static void writeHigherVersionMethod(Writer w, String className,
                                                 String mName, String mGenName,
                                                 List<String> paramTypes, List<String> exceptionTypes,
                                                 Set<Class> thrownExceptions, Set<Class> caughtExceptions,
                                                 String rType, String defaultReturn,
                                                 String jdkVersion) throws IOException {
        w.write("    // DO NOT MODIFY THIS CODE, GENERATED AUTOMATICALLY\n");
        w.write("    /** Wrapper for method "+mName+" from JDK "+jdkVersion+". */\n");
        w.write("    public static "+rType+" "+mGenName+"("+className+" a");
        String[] paramNames = new String[paramTypes.size()];
        for (int i = 0; i < paramTypes.size(); i++) {
            paramNames[i] = Character.toString((char) ('a'+(i+1)));
            String paramType = paramTypes.get(i);
            w.write(", "+paramType+" "+paramNames[i]);
        }
        w.write(")");

        if (exceptionTypes.size() > 0 || thrownExceptions.size() > 0) {
            w.write(" throws ");
            for (int i = 0; i < exceptionTypes.size(); i++) {
                if (i > 0) w.write(", ");
                w.write(exceptionTypes.get(i));
            }
            if (exceptionTypes.size() > 0 && thrownExceptions.size() > 0) {
                w.write(", ");
            }
            int i = 0;
            for (Iterator it = thrownExceptions.iterator(); it.hasNext(); i++) {
                if (i > 0) w.write(", ");
                w.write(EXCEPTION_WRAPPERS.get(it.next()));
            }
        }
        w.write(" {\n");
        String higherVersionClass;
        String higherVersionClassName;
        int higherVersionClassIndex = className.indexOf("/*");
        if (higherVersionClassIndex > 0) {
            higherVersionClassIndex += 2;
            higherVersionClassName = className.substring(higherVersionClassIndex, className.indexOf("*/"));
            higherVersionClass = higherVersionClassName.substring(higherVersionClassName.lastIndexOf('.') + 1);
            higherVersionClass = Character.toLowerCase(higherVersionClass.charAt(0)) + higherVersionClass.substring(1) + "Class";
            w.write("        Class "+higherVersionClass+";\n"+
                    "        try {\n"+
                    "            "+higherVersionClass+" = org.openide.util.Lookup.getDefault().lookup(ClassLoader.class).loadClass(\""+higherVersionClassName+"\");\n"+
                    "        } catch (ClassNotFoundException ex) {\n"+
                    "            throw new IllegalStateException(ex);\n"+
                    "        }\n");

        } else {
            higherVersionClass = null;
            higherVersionClassName = null;
        }
        w.write("        if (org.netbeans.modules.debugger.jpda.JDIExceptionReporter.isLoggable()) {\n");
        w.write("            org.netbeans.modules.debugger.jpda.JDIExceptionReporter.logCallStart(\n"+
                "                    \""+className+"\",\n"+
                "                    \""+mName+"\",\n"+
                "                    \"JDI CALL: "+className+"({0})."+mName+"(");
        for (int i = 0; i < paramNames.length; i++) {
            if (i > 0) {
                w.write(", ");
            }
            w.write("{"+Integer.toString(i+1)+"}");
        }
        w.write(")\",\n"+
                "                    new Object[] {a");
        for (int i = 0; i < paramNames.length; i++) {
            w.write(", ");
            w.write(paramNames[i]);
        }
        w.write("});\n");
        w.write("        }\n"); // if

        boolean isVoidReturn = "void".equals(rType);
        if (!isVoidReturn) {
            w.write("        Object retValue = null;\n");
        }

        w.write("        try {\n");

        StringBuffer exec = new StringBuffer();
        if (!isVoidReturn) {
            if ("boolean".equals(rType)) rType = "Boolean";
            if ("int".equals(rType)) rType = "Integer";
            if ("long".equals(rType)) rType = "Long";
            exec.append("            ").append(rType).append(" ret;\n");
            exec.append("            ret = (").append(rType).append(") ");
        } else {
            exec.append("            ");
        }
        // Use reflection to invoke method from higher JDK version
        /* instead of:
        w.write("a."+mName+"(");
        for (int i = 0; i < paramNames.length; i++) {
            if (i > 0) {
                w.write(", ");
            }
            w.write(paramNames[i]);
        }
        w.write(");\n");
         use reflection: */
        if (higherVersionClass != null) {
            exec.append(higherVersionClass+".getMethod(\""+mName+"\"");
        } else {
            exec.append(className+".class.getMethod(\""+mName+"\"");
        }
        for (int i = 0; i < paramNames.length; i++) {
            exec.append(", ");
            String type = paramTypes.get(i);
            if (type.indexOf('<') > 0) type = type.substring(0, type.indexOf('<'));
            exec.append(type);
            exec.append(".class");
        }
        exec.append(").invoke(a");
        for (int i = 0; i < paramNames.length; i++) {
            exec.append(", ");
            exec.append(paramNames[i]);
        }
        exec.append(");\n");
        if (!isVoidReturn) {
            exec.append("            retValue = ret;\n");
            exec.append("            return ret;\n");
        }
        w.write(methodImpl(className, mName, exec.toString()));


        w.write("        } catch (NoSuchMethodException ex) {\n");
        if (!isVoidReturn) {
            w.write("            retValue = ex;\n");
        }
        w.write("            throw new IllegalStateException(ex);\n");
        w.write("        } catch (SecurityException ex) {\n");
        if (!isVoidReturn) {
            w.write("            retValue = ex;\n");
        }
        w.write("            throw new IllegalStateException(ex);\n");
        w.write("        } catch (IllegalAccessException ex) {\n");
        if (!isVoidReturn) {
            w.write("            retValue = ex;\n");
        }
        w.write("            throw new IllegalStateException(ex);\n");
        w.write("        } catch (IllegalArgumentException ex) {\n");
        if (!isVoidReturn) {
            w.write("            retValue = ex;\n");
        }
        w.write("            throw new IllegalStateException(ex);\n");
        w.write("        } catch (java.lang.reflect.InvocationTargetException ex) {\n");
        w.write("            Throwable t = ex.getTargetException();\n");
        if (!isVoidReturn) {
            w.write("            retValue = t;\n");
        }

        // First re-throw the checked exceptions:
        for (int i = 0; i < exceptionTypes.size(); i++) {
            w.write("            if (t instanceof "+exceptionTypes.get(i)+") {\n");
            //w.write(" ex) {\n");
            w.write("                throw ("+exceptionTypes.get(i)+") t;\n");
            w.write("            }\n");
            //w.write("        } catch (");
        }
        for (Iterator<Class> it = caughtExceptions.iterator(); it.hasNext(); ) {
            Class cex = it.next();
            w.write("            if (t instanceof "+cex.getName()+") {\n");
            if (com.sun.jdi.InternalException.class.equals(cex)) {
                String c = higherVersionClassName;
                if (c == null) c = className;
                w.write("    "+reportOfJDIException(c, mName, "("+com.sun.jdi.InternalException.class.getName()+") t"));
            }
            if (defaultReturn != null && SILENT_EXCEPTIONS.contains(cex)) {
                w.write("                return "+defaultReturn+";\n");
            } else {
                w.write("                throw new "+EXCEPTION_WRAPPERS.get(cex)+"(("+cex.getName()+") t);\n");
            }
            w.write("            }\n");
            //w.write("        } catch (");
        }
        w.write("            throw new IllegalStateException(t);\n");
        w.write("        }");
        w.write(" finally {\n");
        w.write("            if (org.netbeans.modules.debugger.jpda.JDIExceptionReporter.isLoggable()) {\n");
        w.write("                org.netbeans.modules.debugger.jpda.JDIExceptionReporter.logCallEnd(\n"+
                "                        \""+className+"\",\n"+
                "                        \""+mName+"\",\n"+
                (isVoidReturn ?
                "                        org.netbeans.modules.debugger.jpda.JDIExceptionReporter.RET_VOID);\n" :
                "                        retValue);\n"));
        w.write("            }\n");
        w.write("        }\n");
        w.write("    }\n\n");
    }

    private static Map<String, List<String>> getHigherVersionMethods(File rootResource, String jdkVersion) throws IOException {
        Map<String, List<String>> map = new LinkedHashMap<String, List<String>>();
        for (String resourceName : rootResource.list()) {
            if (resourceName.startsWith(METHODS_BY_JDK)) {
                String version = resourceName.substring(METHODS_BY_JDK.length());
                if (version.compareTo(jdkVersion) > 0) {
                    map.put(version, readMethodsLog(new File(rootResource, resourceName)));
                }
            }
        }
        return map;
    }

    private static List<String> readMethodsLog(File resource) throws IOException {
        List<String> list = new ArrayList<String>();
        BufferedReader log = new BufferedReader(new FileReader(resource));
        String line;
        while ((line = log.readLine()) != null) {
            list.add(line);
        }
        log.close();
        return list;
    }

    private static String substituteHigherClasses(String type, Set<String> higherVersionClasses) {
        int l = type.length();
        for (String higherVersionClass : higherVersionClasses) {
            int i0 = 0;
            int i1;
            StringBuilder sb = null;
            while ((i1 = type.indexOf(higherVersionClass, i0)) >= 0) {
                int i2 = i1 + higherVersionClass.length();
                if (i2 == l || type.charAt(i2) == ',' || type.charAt(i2) == '>') {
                    if (sb == null) {
                        sb = new StringBuilder(type.substring(i0, i1));
                    }
                    sb.append("Object/*");
                    sb.append(higherVersionClass);
                    sb.append("*/");
                }
                i0 = i2;
            }
            if (sb != null) {
                sb.append(type.substring(i0));
                type = sb.toString();
            }
        }
        return type;
    }

    private static String translateType(Type t) {
        if (t instanceof Class) {
            Class ct = ((Class) t).getComponentType();
            if (ct != null) {
                return translateType(ct)+"[]";
            }
            return ((Class) t).getName().replace('$', '.');
        }
        return t.toString().replace('$', '.');
    }

    private static String getDefaultReturn(Class returnType) {
        if (Integer.TYPE.equals(returnType)) {
            return "0";
        }
        if (Boolean.TYPE.equals(returnType)) {
            return "false";
        }
        if (java.util.List.class.equals(returnType)) {
            return "java.util.Collections.emptyList()";
        }
        if (java.util.Set.class.equals(returnType)) {
            return "java.util.Collections.emptySet()";
        }
        if (java.util.Map.class.equals(returnType)) {
            return "java.util.Collections.emptyMap()";
        }
        return null;
    }

    private static int findPair(String text, int index, char co, char cc) {
        int l = text.length();
        int ci = 1; // Expecting that opening character was already
        while (index < l) {
            char c = text.charAt(index);
            if (c == co) ci++;
            if (c == cc) ci--;
            if (ci != 0) index++;
            else break;
        }
        if (index < l) return index;
        else return -1;
    }
    
    private static String methodPreCode(String className, String methodName) {
        if (com.sun.jdi.ThreadReference.class.getName().equals(className)) {
            if (methodName.equals("currentContendedMonitor")) {
                String statusCheck        = "        int status = status0(a);\n"+
                                            "        if (status == com.sun.jdi.ThreadReference.THREAD_STATUS_NOT_STARTED ||\n" +
                                            "            status == com.sun.jdi.ThreadReference.THREAD_STATUS_ZOMBIE ||\n" +
                                            "            status == com.sun.jdi.ThreadReference.THREAD_STATUS_UNKNOWN) {\n" +
                                            "            \n" +
                                            "            // #253058, JDWP Error: 15\n" +
                                            "            return null;\n" +
                                            "        }\n";
                return statusCheck;
            }
        }
        return null;
    }

    // Custom code can be provided here to override the original invocation
    private static String methodImpl(String className, String methodName, String exec) {
        if (com.sun.jdi.ThreadReference.class.getName().equals(className)) {
            if (methodName.equals("popFrames")) {
                String catchJDWPException = "            try {\n"+
                                            "    "+exec+
                                            "            } catch ("+com.sun.jdi.InternalException.class.getName()+" iex) {\n"+
                                            "                if (iex.errorCode() == 32) { // OPAQUE_FRAME\n"+
                                            "                    // "+com.sun.jdi.NativeMethodException.class.getSimpleName()+" should be thrown here!\n"+
                                            "                    throw new "+com.sun.jdi.NativeMethodException.class.getName()+"(iex.getMessage());\n"+
                                            "                } else {\n"+
                                            "                    throw iex; // re-throw the original\n"+
                                            "                }\n"+
                                            "            }\n";
                return catchJDWPException;
            }
            if (methodName.equals("currentContendedMonitor")) {
                String catchJDWPException = "            try {\n"+
                                            "    "+exec+
                                            "            } catch ("+com.sun.jdi.InternalException.class.getName()+" iex) {\n"+
                                            "                if (iex.errorCode() == 13) { // THREAD_NOT_SUSPENDED\n"+
                                            "                    // "+com.sun.jdi.IncompatibleThreadStateException.class.getSimpleName()+" should be thrown here!\n"+
                                            "                    throw new "+com.sun.jdi.IncompatibleThreadStateException.class.getName()+"(iex.getMessage());\n"+
                                            "                } else {\n"+
                                            "                    throw iex; // re-throw the original\n"+
                                            "                }\n"+
                                            "            }\n";
                return catchJDWPException;
            }
            if (methodName.equals("frame")) {
                String catchNPE = "            try {\n"+
                                  "    "+exec+
                                  "            } catch ("+NullPointerException.class.getName()+" npex) {\n"+
                                  "                // See http://www.netbeans.org/issues/show_bug.cgi?id=159887\n"+
                                  "                throw new "+com.sun.jdi.IncompatibleThreadStateException.class.getName()+"(npex.getMessage());\n"+
                                  "            }\n";
                return catchNPE;
            }
            if (methodName.equals("resume")) {
                String catchJDWPException = "            try {\n"+
                                            "    "+exec+
                                            "            } catch ("+com.sun.jdi.InternalException.class.getName()+" iex) {\n"+
                                            "                if (iex.errorCode() == 13) { // THREAD_NOT_SUSPENDED\n"+
                                            "                    // Ignore, as we're resuming the thread.\n"+
                                            "                } else {\n"+
                                            "                    throw iex; // re-throw the original\n"+
                                            "                }\n"+
                                            "            }\n";
                return catchJDWPException;
            }
        }
        if (com.sun.jdi.ReferenceType.class.getName().equals(className) && methodName.equals("constantPool")) {
            String catchNPE = "            try {\n"+
                              "    "+exec+
                              "            } catch (NullPointerException ex) {\n"+
                              "                // JDI defect http://bugs.sun.com/view_bug.do?bug_id=6822627\n"+
                              "                return null;\n"+
                              "            }\n";
            return catchNPE;
        }
        if (com.sun.jdi.StackFrame.class.getName().equals(className) && methodName.equals("thisObject")) {
            String catchJDWPException = "            try {\n"+
                                        "    "+exec+
                                        "            } catch ("+com.sun.jdi.InternalException.class.getName()+" iex) {\n"+
                                        "                if (iex.errorCode() == 35) { // INVALID_SLOT, see http://www.netbeans.org/issues/show_bug.cgi?id=163652\n"+
                                        "                    return null;\n"+
                                        "                } else {\n"+
                                        "                    throw iex; // re-throw the original\n"+
                                        "                }\n"+
                                        "            }\n";
            return catchJDWPException;
        }
        if (com.sun.jdi.Location.class.getName().equals(className)) {
            if (methodName.equals("sourcePath") || methodName.equals("sourceName")) {
                String catchJDWPException = "            try {\n"+
                                            "    "+exec+
                                            "            } catch ("+com.sun.jdi.InternalException.class.getName()+" iex) {\n"+
                                            "                if (iex.errorCode() == 101) { // ABSENT_INFORMATION\n"+
                                            "                    throw new com.sun.jdi.AbsentInformationException(iex.getMessage());\n"+
                                            "                } else {\n"+
                                            "                    throw iex; // re-throw the original\n"+
                                            "                }\n"+
                                            "            }\n";
                return catchJDWPException;
            }
            if (methodName.equals("lineNumber")) {
                String catchJDWPException = "            try {\n"+
                                            "    "+exec+
                                            "            } catch ("+com.sun.jdi.InternalException.class.getName()+" iex) {\n"+
                                            "                if (iex.errorCode() == 101) { // ABSENT_INFORMATION\n"+
                                            "                    return -1;\n"+
                                            "                } else {\n"+
                                            "                    throw iex; // re-throw the original\n"+
                                            "                }\n"+
                                            "            }\n";
                return catchJDWPException;
            }
        }
        if (com.sun.jdi.ClassType.class.getName().equals(className) || com.sun.jdi.ObjectReference.class.getName().equals(className)) {
            if (methodName.equals("invokeMethod")) {
                String catchJDWPException = "            try {\n"+
                                            "    "+exec+
                                            "            } catch ("+com.sun.jdi.InternalException.class.getName()+" iex) {\n"+
                                            "                if (iex.errorCode() == 502) { // ALREADY_INVOKING\n"+
                                            "                    iex = ("+com.sun.jdi.InternalException.class.getName()+") org.openide.util.Exceptions.attachLocalizedMessage(iex, org.openide.util.NbBundle.getMessage(org.netbeans.modules.debugger.jpda.JPDADebuggerImpl.class, \"JDWPError502\"));\n"+
                                            "                    org.openide.util.Exceptions.printStackTrace(iex);\n"+
                                            "                    return null;\n"+
                                            "                } else {\n"+
                                            "                    throw iex; // re-throw the original\n"+
                                            "                }\n"+
                                            "            }\n";
                return catchJDWPException;
            }
        }
        if (com.sun.jdi.Method.class.getName().equals(className)) {
            if (methodName.equals("bytecodes")) {
                String catchJDWPException = "            try {\n"+
                                            "    "+exec+
                                            "            } catch ("+com.sun.jdi.InternalException.class.getName()+" iex) {\n"+
                                            "                if (iex.errorCode() == 113) { // INTERNAL, see https://netbeans.org/bugzilla/show_bug.cgi?id=255298\n"+
                                            "                    throw new InternalExceptionWrapper(iex);\n"+
                                            "                } else {\n"+
                                            "                    throw iex; // re-throw the original\n"+
                                            "                }\n"+
                                            "            }\n";
                return catchJDWPException;
            }
        }
        return exec;
    }
    
    private static final String JDI_EXC_REPORT1 =
            "            org.netbeans.modules.debugger.jpda.JDIExceptionReporter.report(";
    private static final String JDI_EXC_REPORT2 =
            ");\n";
    
    private static String reportOfJDIException(String className, String methodName, String ex) {
        if (com.sun.jdi.Method.class.getName().equals(className) && methodName.equals("bytecodes")) {
            return "            if (ex.errorCode() != 101) { // Ignore ABSENT_INFORMATION\n"+
                   "    "+JDI_EXC_REPORT1+ex+JDI_EXC_REPORT2+
                   "            }\n";
        } else {
            return JDI_EXC_REPORT1+ex+JDI_EXC_REPORT2;
        }
    }
    
    private static String disposeVMOnVMDisconnectedException(String className, String methodName) {
        if (com.sun.jdi.VirtualMachine.class.getName().equals(className) &&
            (methodName.equals("exit") || methodName.equals("dispose"))) {
            return ""; // Ignore
        }
        // We should not have to do this. See #243837
        return "            if (a instanceof com.sun.jdi.Mirror) {\n" +
               "                com.sun.jdi.VirtualMachine vm = ((com.sun.jdi.Mirror) a).virtualMachine();\n" +
               "                try {\n"+
               "                    vm.dispose();\n" +
               "                } catch (com.sun.jdi.VMDisconnectedException vmdex) {}\n"+
               "            }\n";
    }

    public static void main(String[] args) {
        /*if (args.length == 0) {
            System.err.println("Usage: Generate <output dir>");
        }*/
        File dir;
        if (args.length > 0) {
            dir = new File(args[0]);
        } else {
            dir = new File(System.getProperty("user.dir"));
        }
        dir = new File(dir, "src/"+PACKAGE.replace('.', '/'));
        dir.mkdirs();
        System.err.println("Generating into "+dir+" for JDK "+System.getProperty("java.version"));
        try {
            generate(dir);
        } catch (IOException ex) {
            Logger.getLogger(Generate.class.getName()).log(Level.SEVERE, null, ex);
        }
    }
}
