/*
 * Copyright (C) 2007 The Android Open Source Project
 *
 * Licensed under the Eclipse Public License, Version 1.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.eclipse.org/org/documents/epl-v10.php
 *
 * 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 com.android.ide.eclipse.adt.internal.sdk;

import com.android.ide.eclipse.adt.AndroidConstants;

import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.SubMonitor;

import java.io.FileInputStream;
import java.io.IOException;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;

import javax.management.InvalidAttributeValueException;

/**
 * Custom class loader able to load a class from the SDK jar file.
 */
public class AndroidJarLoader extends ClassLoader implements IAndroidClassLoader {
    
    /**
     * Wrapper around a {@link Class} to provide the methods of
     * {@link IAndroidClassLoader.IClassDescriptor}.
     */
    public final static class ClassWrapper implements IClassDescriptor {
        private Class<?> mClass;

        public ClassWrapper(Class<?> clazz) {
            mClass = clazz;
        }

        public String getFullClassName() {
            return mClass.getCanonicalName();
        }

        public IClassDescriptor[] getDeclaredClasses() {
            Class<?>[] classes = mClass.getDeclaredClasses();
            IClassDescriptor[] iclasses = new IClassDescriptor[classes.length];
            for (int i = 0 ; i < classes.length ; i++) {
                iclasses[i] = new ClassWrapper(classes[i]);
            }

            return iclasses;
        }

        public IClassDescriptor getEnclosingClass() {
            return new ClassWrapper(mClass.getEnclosingClass());
        }

        public String getSimpleName() {
            return mClass.getSimpleName();
        }

        public IClassDescriptor getSuperclass() {
            return new ClassWrapper(mClass.getSuperclass());
        }
        
        @Override
        public boolean equals(Object clazz) {
            if (clazz instanceof ClassWrapper) {
                return mClass.equals(((ClassWrapper)clazz).mClass);
            }
            return super.equals(clazz);
        }
        
        @Override
        public int hashCode() {
            return mClass.hashCode();
        }


        public boolean isInstantiable() {
            int modifiers = mClass.getModifiers();
            return Modifier.isAbstract(modifiers) == false && Modifier.isPublic(modifiers) == true;
        }

        public Class<?> wrappedClass() {
            return mClass;
        }

    }
    
    private String mOsFrameworkLocation;
    
    /** A cache for binary data extracted from the zip */
    private final HashMap<String, byte[]> mEntryCache = new HashMap<String, byte[]>();
    /** A cache for already defined Classes */
    private final HashMap<String, Class<?> > mClassCache = new HashMap<String, Class<?> >();
    
    /**
     * Creates the class loader by providing the os path to the framework jar archive
     * 
     * @param osFrameworkLocation OS Path of the framework JAR file
     */
    public AndroidJarLoader(String osFrameworkLocation) {
        super();
        mOsFrameworkLocation = osFrameworkLocation;
    }
    
    public String getSource() {
        return mOsFrameworkLocation;
    }
    
    /**
     * Pre-loads all class binary data that belong to the given package by reading the archive
     * once and caching them internally.
     * <p/>
     * This does not actually preload "classes", it just reads the unzipped bytes for a given
     * class. To obtain a class, one must call {@link #findClass(String)} later.
     * <p/>
     * All classes which package name starts with "packageFilter" will be included and can be
     * found later.
     * <p/>
     * May throw some exceptions if the framework JAR cannot be read.
     * 
     * @param packageFilter The package that contains all the class data to preload, using a fully
     *                    qualified binary name (.e.g "com.my.package."). The matching algorithm
     *                    is simple "startsWith". Use an empty string to include everything.
     * @param taskLabel An optional task name for the sub monitor. Can be null.
     * @param monitor A progress monitor. Can be null. Caller is responsible for calling done.
     * @throws IOException
     * @throws InvalidAttributeValueException
     * @throws ClassFormatError
     */
    public void preLoadClasses(String packageFilter, String taskLabel, IProgressMonitor monitor)
        throws IOException, InvalidAttributeValueException, ClassFormatError {
        // Transform the package name into a zip entry path
        String pathFilter = packageFilter.replaceAll("\\.", "/"); //$NON-NLS-1$ //$NON-NLS-2$
        
        SubMonitor progress = SubMonitor.convert(monitor, taskLabel == null ? "" : taskLabel, 100);
        
        // create streams to read the intermediary archive
        FileInputStream fis = new FileInputStream(mOsFrameworkLocation);
        ZipInputStream zis = new ZipInputStream(fis);
        ZipEntry entry;       
        while ((entry = zis.getNextEntry()) != null) {
            // get the name of the entry.
            String entryPath = entry.getName();
            
            if (!entryPath.endsWith(AndroidConstants.DOT_CLASS)) {
                // only accept class files
                continue;
            }

            // check if it is part of the package to preload
            if (pathFilter.length() > 0 && !entryPath.startsWith(pathFilter)) {
                continue;
            }
            String className = entryPathToClassName(entryPath);

            if (!mEntryCache.containsKey(className)) {
                long entrySize = entry.getSize();
                if (entrySize > Integer.MAX_VALUE) {
                    throw new InvalidAttributeValueException();
                }
                byte[] data = readZipData(zis, (int)entrySize);
                mEntryCache.put(className, data);
            }

            // advance 5% of whatever is allocated on the progress bar
            progress.setWorkRemaining(100);
            progress.worked(5);
            progress.subTask(String.format("Preload %1$s", className));
        }
    }

    /**
     * Finds and loads all classes that derive from a given set of super classes.
     * <p/>
     * As a side-effect this will load and cache most, if not all, classes in the input JAR file.
     * 
     * @param packageFilter Base name of package of classes to find.
     *                      Use an empty string to find everyting.
     * @param superClasses The super classes of all the classes to find. 
     * @return An hash map which keys are the super classes looked for and which values are
     *         ArrayList of the classes found. The array lists are always created for all the
     *         valid keys, they are simply empty if no deriving class is found for a given
     *         super class. 
     * @throws IOException
     * @throws InvalidAttributeValueException
     * @throws ClassFormatError
     */
    public HashMap<String, ArrayList<IClassDescriptor>> findClassesDerivingFrom(
            String packageFilter,
            String[] superClasses)
            throws IOException, InvalidAttributeValueException, ClassFormatError {

        packageFilter = packageFilter.replaceAll("\\.", "/"); //$NON-NLS-1$ //$NON-NLS-2$

        HashMap<String, ArrayList<IClassDescriptor>> mClassesFound =
                new HashMap<String, ArrayList<IClassDescriptor>>();

        for (String className : superClasses) {
            mClassesFound.put(className, new ArrayList<IClassDescriptor>());
        }

        // create streams to read the intermediary archive
        FileInputStream fis = new FileInputStream(mOsFrameworkLocation);
        ZipInputStream zis = new ZipInputStream(fis);
        ZipEntry entry;
        while ((entry = zis.getNextEntry()) != null) {
            // get the name of the entry and convert to a class binary name
            String entryPath = entry.getName();
            if (!entryPath.endsWith(AndroidConstants.DOT_CLASS)) {
                // only accept class files
                continue;
            }
            if (packageFilter.length() > 0 && !entryPath.startsWith(packageFilter)) {
                // only accept stuff from the requested root package.
                continue;
            }
            String className = entryPathToClassName(entryPath);
      
            Class<?> loaded_class = mClassCache.get(className);
            if (loaded_class == null) {
                byte[] data = mEntryCache.get(className);
                if (data == null) {    
                    // Get the class and cache it
                    long entrySize = entry.getSize();
                    if (entrySize > Integer.MAX_VALUE) {
                        throw new InvalidAttributeValueException();
                    }
                    data = readZipData(zis, (int)entrySize);
                }
                loaded_class = defineAndCacheClass(className, data);
            }

            for (Class<?> superClass = loaded_class.getSuperclass();
                    superClass != null;
                    superClass = superClass.getSuperclass()) {
                String superName = superClass.getCanonicalName();
                if (mClassesFound.containsKey(superName)) {
                    mClassesFound.get(superName).add(new ClassWrapper(loaded_class));
                    break;
                }
            }
        }

        return mClassesFound;
    }

    /** Helper method that converts a Zip entry path into a corresponding
     *  Java full qualified binary class name.
     *  <p/>
     *  F.ex, this converts "com/my/package/Foo.class" into "com.my.package.Foo".
     */
    private String entryPathToClassName(String entryPath) {
        return entryPath.replaceFirst("\\.class$", "").replaceAll("[/\\\\]", "."); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
    }

    /**
     * Finds the class with the specified binary name.
     * 
     * {@inheritDoc}
     */
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        try {
            // try to find the class in the cache
            Class<?> cached_class = mClassCache.get(name);
            if (cached_class == ClassNotFoundException.class) {
                // we already know we can't find this class, don't try again
                throw new ClassNotFoundException(name);
            } else if (cached_class != null) {
                return cached_class;
            }
            
            // if not found, look it up and cache it
            byte[] data = loadClassData(name);
            if (data != null) {
                return defineAndCacheClass(name, data);
            } else {
                // if the class can't be found, record a CNFE class in the map so
                // that we don't try to reload it next time
                mClassCache.put(name, ClassNotFoundException.class);
                throw new ClassNotFoundException(name);
            }
        } catch (ClassNotFoundException e) {
            throw e;
        } catch (Exception e) {
            throw new ClassNotFoundException(e.getMessage()); 
        }
    }

    /**
     * Defines a class based on its binary data and caches the resulting class object.
     * 
     * @param name The binary name of the class (i.e. package.class1$class2)
     * @param data The binary data from the loader.
     * @return The class defined
     * @throws ClassFormatError if defineClass failed.
     */
    private Class<?> defineAndCacheClass(String name, byte[] data) throws ClassFormatError {
        Class<?> cached_class;
        cached_class = defineClass(null, data, 0, data.length);

        if (cached_class != null) {
            // Add new class to the cache class and remove it from the zip entry data cache
            mClassCache.put(name, cached_class);
            mEntryCache.remove(name);
        }
        return cached_class;
    }
    
    /**
     * Loads a class data from its binary name.
     * <p/>
     * This uses the class binary data that has been preloaded earlier by the preLoadClasses()
     * method if possible.
     * 
     * @param className the binary name
     * @return an array of bytes representing the class data or null if not found
     * @throws InvalidAttributeValueException 
     * @throws IOException 
     */
    private synchronized byte[] loadClassData(String className)
            throws InvalidAttributeValueException, IOException {

        byte[] data = mEntryCache.get(className);
        if (data != null) {
            return data;
        }
        
        // The name is a binary name. Something like "android.R", or "android.R$id".
        // Make a path out of it.
        String entryName = className.replaceAll("\\.", "/") + AndroidConstants.DOT_CLASS; //$NON-NLS-1$ //$NON-NLS-2$

       // create streams to read the intermediary archive
        FileInputStream fis = new FileInputStream(mOsFrameworkLocation);
        ZipInputStream zis = new ZipInputStream(fis);
        
        // loop on the entries of the intermediary package and put them in the final package.
        ZipEntry entry;

        while ((entry = zis.getNextEntry()) != null) {
            // get the name of the entry.
            String currEntryName = entry.getName();
            
            if (currEntryName.equals(entryName)) {
                long entrySize = entry.getSize();
                if (entrySize > Integer.MAX_VALUE) {
                    throw new InvalidAttributeValueException();
                }

                data = readZipData(zis, (int)entrySize);
                return data;
            }
        }

        return null;
    }

    /**
     * Reads data for the <em>current</em> entry from the zip input stream.
     * 
     * @param zis The Zip input stream
     * @param entrySize The entry size. -1 if unknown.
     * @return The new data for the <em>current</em> entry.
     * @throws IOException If ZipInputStream.read() fails.
     */
    private byte[] readZipData(ZipInputStream zis, int entrySize) throws IOException {
        int block_size = 1024;
        int data_size = entrySize < 1 ? block_size : entrySize; 
        int offset = 0;
        byte[] data = new byte[data_size];
        
        while(zis.available() != 0) {
            int count = zis.read(data, offset, data_size - offset);
            if (count < 0) {  // read data is done
                break;
            }
            offset += count;
            
            if (entrySize >= 1 && offset >= entrySize) {  // we know the size and we're done
                break;
            }

            // if we don't know the entry size and we're not done reading,
            // expand the data buffer some more.
            if (offset >= data_size) {
                byte[] temp = new byte[data_size + block_size];
                System.arraycopy(data, 0, temp, 0, data_size);
                data_size += block_size;
                data = temp;
                block_size *= 2;
            }
        }
        
        if (offset < data_size) {
            // buffer was allocated too large, trim it
            byte[] temp = new byte[offset];
            if (offset > 0) {
                System.arraycopy(data, 0, temp, 0, offset);
            }
            data = temp;
        }
        
        return data;
    }

    /**
     * Returns a {@link IAndroidClassLoader.IClassDescriptor} by its fully-qualified name.
     * @param className the fully-qualified name of the class to return.
     * @throws ClassNotFoundException
     */
    public IClassDescriptor getClass(String className) throws ClassNotFoundException {
        try {
            return new ClassWrapper(loadClass(className));
        } catch (ClassNotFoundException e) {
            throw e;  // useful for debugging
        }
    }
}
