/*
 * 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.invoke.MethodHandle;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.URL;

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

/**
 * Helper class for invoking {@link ClassLoader#defineClass(String,byte[],int,int)}.
 *
 * @since 3.22
 */
public class DefinePackageHelper
{
            
    private static enum SecuredPrivileged
    {
        JAVA_9 {
            // definePackage has been discontinued for JAVA 9
            @Override
            protected Package definePackage(ClassLoader loader, String name, String specTitle,
                    String specVersion, String specVendor, String implTitle, String implVersion,
                    String implVendor, URL sealBase) throws IllegalArgumentException
            {
                throw new RuntimeException("define package has been disabled for jigsaw");
            }
        },
        JAVA_7 {
            private final SecurityActions stack = SecurityActions.stack;
            private final MethodHandle definePackage = getDefinePackageMethodHandle();
            private MethodHandle getDefinePackageMethodHandle()
            {
                if (stack.getCallerClass() != this.getClass())
                    throw new IllegalAccessError("Access denied for caller.");
                try {
                    return SecurityActions.getMethodHandle(ClassLoader.class, 
                            "definePackage", new Class[] {
                                String.class, String.class, String.class, String.class,
                                String.class, String.class, String.class, URL.class 
                            });
                } catch (NoSuchMethodException e) {
                    throw new RuntimeException("cannot initialize", e);
                }
            }
            
            @Override
            protected Package definePackage(ClassLoader loader, String name, String specTitle,
                    String specVersion, String specVendor, String implTitle, String implVersion,
                    String implVendor, URL sealBase) throws IllegalArgumentException {
                if (stack.getCallerClass() != DefinePackageHelper.class)
                    throw new IllegalAccessError("Access denied for caller.");
                try {
                    return (Package) definePackage.invokeWithArguments(loader, name, specTitle,
                        specVersion, specVendor, implTitle, implVersion, implVendor, sealBase);
                } catch (Throwable e) {
                    if (e instanceof IllegalArgumentException) throw (IllegalArgumentException) e;
                    if (e instanceof RuntimeException) throw (RuntimeException) e;
                }
                return null;
            }
        },
        JAVA_OTHER {
            private final SecurityActions stack = SecurityActions.stack;
            private final Method definePackage = getDefinePackageMethod();
            private Method getDefinePackageMethod()
            {
                if (stack.getCallerClass() != this.getClass())
                    throw new IllegalAccessError("Access denied for caller.");
                try {
                    return SecurityActions.getDeclaredMethod(ClassLoader.class, 
                            "definePackage", new Class[] {
                                String.class, String.class, String.class, String.class,
                                String.class, String.class, String.class, URL.class 
                            });
                } catch (NoSuchMethodException e) {
                    throw new RuntimeException("cannot initialize", e);
                }
            }
            
            @Override
            protected Package definePackage(ClassLoader loader, String name, String specTitle,
                    String specVersion, String specVendor, String implTitle, String implVersion,
                    String implVendor, URL sealBase) throws IllegalArgumentException
            {
                if (stack.getCallerClass() != DefinePackageHelper.class)
                    throw new IllegalAccessError("Access denied for caller.");
                try {
                    definePackage.setAccessible(true);
                    return (Package) definePackage.invoke(loader, new Object[] {
                        name, specTitle, specVersion, specVendor, implTitle,
                        implVersion, implVendor, sealBase                                
                    });
                } catch (Throwable e) {
                    if (e instanceof InvocationTargetException) {
                        Throwable t = ((InvocationTargetException) e).getTargetException();
                        if (t instanceof IllegalArgumentException)
                            throw (IllegalArgumentException) t;
                    }
                    if (e instanceof RuntimeException) throw (RuntimeException) e;
                }
                finally {
                    definePackage.setAccessible(false);
                }
                return null;
            }
        };

        protected abstract Package definePackage(ClassLoader loader, String name, String specTitle,
                String specVersion, String specVendor, String implTitle, String implVersion,
                String implVendor, URL sealBase) throws IllegalArgumentException;
    }

    private static final SecuredPrivileged privileged = ClassFile.MAJOR_VERSION >= ClassFile.JAVA_9
            ? SecuredPrivileged.JAVA_9
            : ClassFile.MAJOR_VERSION >= ClassFile.JAVA_7
                ? SecuredPrivileged.JAVA_7
                : SecuredPrivileged.JAVA_OTHER;


    /**
     * Defines a new package.  If the package is already defined, this method
     * performs nothing.
     *
     * <p>You do not necessarily need to
     * call this method.  If this method is called, then  
     * <code>getPackage()</code> on the <code>Class</code> object returned 
     * by <code>toClass()</code> will return a non-null object.</p>
     *
     * <p>The jigsaw module introduced by Java 9 has broken this method.
     * In Java 9 or later, the VM argument
     * <code>--add-opens java.base/java.lang=ALL-UNNAMED</code>
     * has to be given to the JVM so that this method can run.
     * </p>
     *
     * @param loader        the class loader passed to <code>toClass()</code> or
     *                      the default one obtained by <code>getClassLoader()</code>.
     * @param name          the package name.
     * @see #getClassLoader()
     * @see #toClass(CtClass)
     * @see CtClass#toClass() */
    public static void definePackage(String className, ClassLoader loader)
        throws CannotCompileException
    {
        try {
            privileged.definePackage(loader, className, 
                    null, null, null, null, null, null, null);
        }
        catch (IllegalArgumentException e) {
            // if the package is already defined, an IllegalArgumentException
            // is thrown.
            return;
        }
        catch (Exception e) {
            throw new CannotCompileException(e);
        }
    }
    
    private DefinePackageHelper() {}
}
