/*
 * Javassist, a Java-bytecode translator toolkit.
 * Copyright (C) 1999- Shigeru Chiba. All Rights Reserved.
 *
 * The contents of this file are subject to the Mozilla Public License Version
 * 1.1 (the "License"); you may not use this file except in compliance with
 * the License.  Alternatively, the contents of this file may be used under
 * the terms of the GNU Lesser General Public License Version 2.1 or later,
 * or the Apache License Version 2.0.
 *
 * Software distributed under the License is distributed on an "AS IS" basis,
 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
 * for the specific language governing rights and limitations under the
 * License.
 */

package javassist.util.proxy;

import java.lang.reflect.Method;
import java.io.BufferedOutputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.security.ProtectionDomain;

import javassist.CannotCompileException;
import javassist.bytecode.ClassFile;

/**
 * A helper class for implementing <code>ProxyFactory</code>.
 * The users of <code>ProxyFactory</code> do not have to see this class.
 *
 * @see ProxyFactory
 */
public class FactoryHelper {
    private static java.lang.reflect.Method defineClass1, defineClass2;

    static {
        try {
            Class cl = Class.forName("java.lang.ClassLoader");
            defineClass1 = SecurityActions.getDeclaredMethod(
                        cl,
                        "defineClass",
                        new Class[] { String.class, byte[].class,
                                      int.class, int.class });

            defineClass2 = SecurityActions.getDeclaredMethod(
                        cl,
                        "defineClass",
                        new Class[] { String.class, byte[].class,
                              int.class, int.class, ProtectionDomain.class });
        }
        catch (Exception e) {
            throw new RuntimeException("cannot initialize");
        }
    }

    /**
     * Returns an index for accessing arrays in this class.
     *
     * @throws RuntimeException     if a given type is not a primitive type.
     */
    public static final int typeIndex(Class type) {
        Class[] list = primitiveTypes;
        int n = list.length;
        for (int i = 0; i < n; i++)
            if (list[i] == type)
                return i;

        throw new RuntimeException("bad type:" + type.getName());
    }

    /**
     * <code>Class</code> objects representing primitive types.
     */
    public static final Class[] primitiveTypes = {
        Boolean.TYPE, Byte.TYPE, Character.TYPE, Short.TYPE, Integer.TYPE,
        Long.TYPE, Float.TYPE, Double.TYPE, Void.TYPE
    };

    /**
     * The fully-qualified names of wrapper classes for primitive types.
     */
    public static final String[] wrapperTypes = {
        "java.lang.Boolean", "java.lang.Byte", "java.lang.Character",
        "java.lang.Short", "java.lang.Integer", "java.lang.Long",
        "java.lang.Float", "java.lang.Double", "java.lang.Void"
    };

    /**
     * The descriptors of the constructors of wrapper classes.
     */
    public static final String[] wrapperDesc = {
        "(Z)V", "(B)V", "(C)V", "(S)V", "(I)V", "(J)V",
        "(F)V", "(D)V"
    };

    /**
     * The names of methods for obtaining a primitive value
     * from a wrapper object.  For example, <code>intValue()</code>
     * is such a method for obtaining an integer value from a
     * <code>java.lang.Integer</code> object.
     */
    public static final String[] unwarpMethods = {
        "booleanValue", "byteValue", "charValue", "shortValue",
        "intValue", "longValue", "floatValue", "doubleValue"
    };

    /**
     * The descriptors of the unwrapping methods contained
     * in <code>unwrapMethods</code>.
     */
    public static final String[] unwrapDesc = {
        "()Z", "()B", "()C", "()S", "()I", "()J", "()F", "()D" 
    };

    /**
     * The data size of primitive types.  <code>long</code>
     * and <code>double</code> are 2; the others are 1.
     */
    public static final int[] dataSize = {
        1, 1, 1, 1, 1, 2, 1, 2
    };

    /**
     * Loads a class file by a given class loader.
     * This method uses a default protection domain for the class
     * but it may not work with a security manager or a sigend jar file.
     *
     * @see #toClass(ClassFile,ClassLoader,ProtectionDomain)
     */
    public static Class toClass(ClassFile cf, ClassLoader loader)
        throws CannotCompileException
    {
        return toClass(cf, loader, null);
    }

    /**
     * Loads a class file by a given class loader.
     *
     * @param domain        if it is null, a default domain is used.
     * @since 3.3
     */
    public static Class toClass(ClassFile cf, ClassLoader loader, ProtectionDomain domain)
            throws CannotCompileException
    {
        try {
            byte[] b = toBytecode(cf);
            Method method;
            Object[] args;
            if (domain == null) {
                method = defineClass1;
                args = new Object[] { cf.getName(), b, new Integer(0),
                        new Integer(b.length) };
            }
            else {
                method = defineClass2;
                args = new Object[] { cf.getName(), b, new Integer(0),
                        new Integer(b.length), domain };
            }

            return toClass2(method, loader, args);
        }
        catch (RuntimeException e) {
            throw e;
        }
        catch (java.lang.reflect.InvocationTargetException e) {
            throw new CannotCompileException(e.getTargetException());
        }
        catch (Exception e) {
            throw new CannotCompileException(e);
        }
    }

    private static synchronized Class toClass2(Method method,
                                        ClassLoader loader, Object[] args)
        throws Exception
    {
        SecurityActions.setAccessible(method, true);
        Class clazz = (Class)method.invoke(loader, args);
        SecurityActions.setAccessible(method, false);
        return clazz;
    }

    private static byte[] toBytecode(ClassFile cf) throws IOException {
        ByteArrayOutputStream barray = new ByteArrayOutputStream();
        DataOutputStream out = new DataOutputStream(barray);
        try {
            cf.write(out);
        }
        finally {
            out.close();
        }

        return barray.toByteArray();
    }

    /**
     * Writes a class file.
     */
    public static void writeFile(ClassFile cf, String directoryName)
            throws CannotCompileException {
        try {
            writeFile0(cf, directoryName);
        }
        catch (IOException e) {
            throw new CannotCompileException(e);
        }
    }

    private static void writeFile0(ClassFile cf, String directoryName)
            throws CannotCompileException, IOException {
        String classname = cf.getName();
        String filename = directoryName + File.separatorChar
                + classname.replace('.', File.separatorChar) + ".class";
        int pos = filename.lastIndexOf(File.separatorChar);
        if (pos > 0) {
            String dir = filename.substring(0, pos);
            if (!dir.equals("."))
                new File(dir).mkdirs();
        }

        DataOutputStream out = new DataOutputStream(new BufferedOutputStream(
                new FileOutputStream(filename)));
        try {
            cf.write(out);
        }
        catch (IOException e) {
            throw e;
        }
        finally {
            out.close();
        }
    }
}
