/*
 * 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.openide.execution;

import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.net.URLClassLoader;
import java.security.AllPermission;
import java.security.CodeSource;
import java.security.Permission;
import java.security.PermissionCollection;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.jar.Manifest;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.openide.filesystems.FileObject;
import org.openide.filesystems.FileSystem;
import org.openide.filesystems.FileUtil;
import org.openide.filesystems.URLMapper;
import org.openide.util.Exceptions;
import org.openide.util.Lookup;
import org.openide.windows.InputOutput;

/** A class loader which is capable of loading classes from the Repository.
 * XXX the only useful thing this class does is effectively make
 * ExecutionEngine.createPermissions public! Consider deprecating this class...
 * @see <a href="@org-netbeans-api-java-classpath@/org/netbeans/api/java/classpath/ClassPath.html#getClassLoader(boolean)"><code>ClassPath.getClassLoader(...)</code></a>
* @author Ales Novak, Petr Hamernik, Jaroslav Tulach, Ian Formanek
*/
public class NbClassLoader extends URLClassLoader {
    /** I/O for classes defined by this classloader. May be <code>null</code>. */
    protected InputOutput inout;
    /** Cached PermissionCollections returned from ExecutionEngine. */
    private HashMap permissionCollections;
    /** Default permissions */
    private PermissionCollection defaultPermissions;
    
    /** Works onthe top of file (jar:file) - directly delegates to URLClassLoader*/
    private final boolean fast;
    
    private static ClassLoader systemClassLoader() {
        return (ClassLoader)Lookup.getDefault().lookup(ClassLoader.class);
    }
    
    /** Create a new class loader retrieving classes from the core IDE as well as the Repository.
     * @see FileSystemCapability#EXECUTE
     * @see FileSystemCapability#fileSystems
     * @deprecated Misuses classpath.
    */
    public NbClassLoader () {
        super(new URL[0], systemClassLoader());
        fast = false;
    }

    /** Create a new class loader retrieving classes from the core IDE as well as the Repository,
    * and redirecting system I/O.
     * @param io an I/O tab in the Output Window
     * @see org.openide.filesystems.Repository#getFileSystems
     * @deprecated Misuses classpath.
     */
    public NbClassLoader(InputOutput io) {
        super(new URL[0], systemClassLoader());
        fast = false;
        inout = io;
    }
    
    /**
     * Create a new class loader retrieving classes from a set of package roots.
     * @param roots a set of package roots
     * @param parent a parent loader
     * @param io an I/O tab in the Output Window, or null
     * @since XXX
     */
    static ThreadLocal<Boolean> f = new ThreadLocal<Boolean>();
    public NbClassLoader(FileObject[] roots, ClassLoader parent, InputOutput io) {
        super(createRootURLs(roots), parent);
        fast = canOptimize(getURLs());
        inout = io;
    }
        
    /** Create a new class loader retrieving classes from the core IDE as well as specified file systems.
     * @param fileSystems file systems to load classes from
     * @deprecated Misuses classpath.
    */
    public NbClassLoader (FileSystem[] fileSystems) {
        super(new URL[0], systemClassLoader(), null);
        fast = false;
        Thread.dumpStack();
    }

    /** Create a new class loader.
     * @param fileSystems file systems to load classes from
     * @param parent fallback class loader
     * @deprecated Misuses classpath.
    */
    public NbClassLoader (FileSystem[] fileSystems, ClassLoader parent) {
        super(new URL[0], parent);
        fast = false;
        Thread.dumpStack();
    }
    
    /** Create a URL to a resource specified by name.
    * Same behavior as in the super method, but handles names beginning with a slash.
    * @param name resource name
    * @return URL to that resource or <code>null</code>
    */
    public URL getResource (String name) {
        return super.getResource (name.startsWith ("/") ? name.substring (1) : name); // NOI18N
    }

    /* Needs to be overridden so that packages are correctly defined
       based on manifest for e.g. JarFileSystem's in the repository.
       Otherwise URLClassLoader, not understanding nbfs:/..../foo.jar,
       would simply define packages loaded from such a URL with no
       particular info. We want it to have specification version and
       all that good stuff. */
    protected Class findClass (final String name) throws ClassNotFoundException {
        if (!fast && name.indexOf ('.') != -1) {
            Logger.getLogger(NbClassLoader.class.getName()).log(Level.FINE, "NBFS used!");
            String pkg = name.substring (0, name.lastIndexOf ('.'));
            if (getPackage (pkg) == null) {
                String resource = name.replace ('.', '/') + ".class"; // NOI18N
                URL[] urls = getURLs ();
                for (int i = 0; i < urls.length; i++) {
                    // System.err.println (urls[i].toString ());
                    FileObject root = URLMapper.findFileObject(urls[i]);

                    if (root == null) {
                        continue;
                    }
                    try {
                        FileObject fo = root.getFileObject(resource);

                        if (fo != null) {
                            // Got it. If there is an associated manifest, load it.
                            FileObject manifo = root.getFileObject("META-INF/MANIFEST.MF");

                            if (manifo == null)
                                manifo = root.getFileObject("meta-inf/manifest.mf");
                            if (manifo != null) {
                                // System.err.println (manifo.toString () + " " + manifo.getClass ().getName () + " " + manifo.isValid ());
                                Manifest mani = new Manifest();
                                InputStream is = manifo.getInputStream();

                                try {
                                    mani.read(is);
                                }
                                finally {
                                    is.close();
                                }
                                definePackage(pkg, mani, urls[i]);
                            }
                            break;
                        }
                    }
                    catch (IOException ioe) {
                        Exceptions.attachLocalizedMessage(ioe,
                                                          urls[i].toString());
                        Exceptions.printStackTrace(ioe);
                        continue;
                    }
                }
            }
        }
        return super.findClass (name);
    }
    
    /** Sets a PermissionsCollectio which will be used
     * for ProtectionDomain of newly created classes.
     *
     * @param defaultPerms
     */
    public void setDefaultPermissions(PermissionCollection defaultPerms) {
        if (defaultPerms != null && !defaultPerms.isReadOnly()) {
            defaultPerms.setReadOnly();
        }
        this.defaultPermissions = defaultPerms;
    }

    /* @return a PermissionCollection for given CodeSource. */
    protected final synchronized PermissionCollection getPermissions(CodeSource cs) {

        if (permissionCollections != null) {
            PermissionCollection pc = (PermissionCollection) permissionCollections.get(cs);
            if (pc != null) {
                return pc;
            }
        }

        return createPermissions(cs, inout);
    }

    /**
    * @param cs CodeSource
    * @param inout InputOutput passed to @seeExecutionEngine#createPermissions(java.security.CodeSource, org.openide.windows.InpuOutput).
    * @return a PermissionCollection for given CodeSource.
    */
    private PermissionCollection createPermissions(CodeSource cs, InputOutput inout) {
        PermissionCollection pc;
        if (inout == null) {
            if (defaultPermissions != null) {
                pc = defaultPermissions;
            } else {
                pc = super.getPermissions(cs);
            }
        } else {
            ExecutionEngine engine = ExecutionEngine.getDefault();
            pc = engine.createPermissions(cs, inout);
            if (defaultPermissions != null) {
                addAllPermissions(pc, defaultPermissions);
            } else {
                pc.add(new AllPermission());
            }
        }
        if (permissionCollections == null) {
            permissionCollections = new HashMap(7);
        }
        permissionCollections.put(cs, pc);
        return pc;
    }
    
    /**
     * Copies all permissions from <tt>src</tt> into <tt>target</tt>
     *
     * @param target To where put permissions
     * @param src From where take paermissions
     */
    private static void addAllPermissions(PermissionCollection target, PermissionCollection src) {
        Enumeration e = src.elements();
        
        while (e.hasMoreElements()) {
            target.add((Permission) e.nextElement());
        }
    }


    /**
     * Creates URLs for file objects.
     * @param roots file roots
     * @return array of URLs
     */
    private static URL[] createRootURLs(FileObject[] roots) {
        URL[] urls = new URL[roots.length];
        for (int i = 0; i < roots.length; i++) {
            urls[i] = roots[i].toURL();
            }
        return urls;
    }
    
    private static boolean canOptimize (URL[] urls) {
        assert urls != null;        
        for (int i=0; i<urls.length; i++) {
            URL url = urls[i];
            URL au = FileUtil.getArchiveFile(url);
            if (au != null) {
                if (!url.toExternalForm().endsWith("!/")) { //NOI18N
                    //Nested path - not supported fast mode 
                    return false;
                }
                url = au;
            }
            if (!"file".equals(url.getProtocol())) {        //NOI18N
                //Not file - not supported fast mode
                return false;
            }            
        }        
        return true;
    }
}
