/**
 *  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.apache.xbean.finder;

import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import org.apache.xbean.osgi.bundle.util.BundleClassFinder;
import org.apache.xbean.osgi.bundle.util.BundleDescription;
import org.apache.xbean.osgi.bundle.util.ClassDiscoveryFilter;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.Opcodes;
import org.osgi.framework.Bundle;
import org.osgi.service.packageadmin.ExportedPackage;
import org.osgi.service.packageadmin.PackageAdmin;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * @version $Rev$ $Date$
 */
public class BundleAssignableClassFinder extends BundleClassFinder {

    private static final Logger logger = LoggerFactory.getLogger(BundleAssignableClassFinder.class);

    private Class<?>[] clses;

    private Set<String> targetClassNames = new HashSet<String>();

    private Set<String> targetInterfaceNames = new HashSet<String>();

    private Set<String> wiredImportedPackageNames = new HashSet<String>();

    /**
     * Create a new BundleClassFinder, it will search all the classes based the rule defined by the parameters via ASM tool
     * @param packageAdmin
     * @param bundle
     * @param clses
     * @param discoveryFilter
     */
    public BundleAssignableClassFinder(PackageAdmin packageAdmin, Bundle bundle, Class<?>[] clses, ClassDiscoveryFilter discoveryFilter) {
        super(packageAdmin, bundle, discoveryFilter);
        if (clses == null || clses.length == 0) {
            throw new IllegalArgumentException("At least one class or interface should be specified");
        }
        this.clses = clses;
        for (Class<?> cls : clses) {
            String asmStyleName = cls.getName().replace('.', '/');
            if (cls.isInterface()) {
                targetInterfaceNames.add(asmStyleName);
            } else {
                targetClassNames.add(asmStyleName);
            }
        }
        initialize();
    }

    public BundleAssignableClassFinder(PackageAdmin packageAdmin, Class<?>[] clses, Bundle bundle) {
        this(packageAdmin, bundle, clses, FULL_CLASS_DISCOVERY_FILTER);
    }

    @Override
    protected BundleClassFinder createSubBundleClassFinder(PackageAdmin packageAdmin, Bundle bundle, ClassDiscoveryFilter classDiscoveryFilter) {
        return new BundleAssignableClassFinder(packageAdmin, bundle, clses, classDiscoveryFilter);
    }

    @Override
    protected boolean isClassAcceptable(String name, InputStream in) throws IOException {
        ClassReader classReader = new ClassReader(in);
        String className = classReader.getClassName();
        if ((classReader.getAccess() & Opcodes.ACC_INTERFACE) == 0) {
            if (targetClassNames.contains(className)) {
                return true;
            }
        } else {
            if (targetInterfaceNames.contains(className)) {
                return true;
            }
        }
        String[] interfaceNames = classReader.getInterfaces();
        try {
            for (String interfaceName : interfaceNames) {
                if (wiredImportedPackageNames.contains(toASMStylePackageName(interfaceName))) {
                    return isClassAssignable(bundle.loadClass(toJavaStyleClassName(interfaceName)));
                } else {
                    if (isInterfaceAssignable(interfaceName)) {
                        return true;
                    }
                }
            }
            String superClassName = classReader.getSuperName();
            if (superClassName == null) {
                return false;
            }
            if (wiredImportedPackageNames.contains(toASMStylePackageName(superClassName))) {
                return isClassAssignable(bundle.loadClass(toJavaStyleClassName(superClassName)));
            }
            return isSuperClassAssignable(superClassName);
        } catch (ClassNotFoundException e) {
            return false;
        }
    }

    @Override
    protected boolean isClassAcceptable(URL url) {
        InputStream in = null;
        try {
            in = url.openStream();
            return isClassAcceptable("", in);
        } catch (IOException e) {
            logger.warn("Unable to check the class of url " + url, e);
            return false;
        } finally {
            if (in != null)
                try {
                    in.close();
                } catch (Exception e) {
                }
        }
    }

    private void initialize() {
        BundleDescription description = new BundleDescription(bundle.getHeaders());
        List<BundleDescription.ImportPackage> imports = description.getExternalImports();
        for (BundleDescription.ImportPackage packageImport : imports) {
            String packageName = packageImport.getName();
            ExportedPackage[] exports = packageAdmin.getExportedPackages(packageName);
            Bundle wiredBundle = isWired(bundle, exports);
            if (wiredBundle != null) {
                wiredImportedPackageNames.add(packageName.replace('.', '/'));
                break;
            }
        }
    }

    private boolean isClassAssignable(Class<?> cls) {
        for (Class<?> targetClass : clses) {
            if (targetClass.isAssignableFrom(cls)) {
                return true;
            }
        }
        return false;
    }

    /**
     *
     * @param interfaceName The interface name should be in the format of org/test/SimpleInterface
     * @return return true if the method parameter interfaceName is assignable to any interface in the expected interfaces
     */
    private boolean isInterfaceAssignable(String interfaceName) {
        //Check each interface in interfaceNames set
        if (targetInterfaceNames.contains(interfaceName)) {
            return true;
        }
        //Check ancestor intefaces
        URL url = bundle.getResource(interfaceName + ".class");
        if (url == null) {
            //TODO what should we do if we do not find the interface ?
            return false;
        }
        InputStream in = null;
        try {
            in = url.openStream();
            ClassReader classReader = new ClassReader(in);
            String[] superInterfaceNames = classReader.getInterfaces();
            for (String superInterfaceName : superInterfaceNames) {
                if (isInterfaceAssignable(superInterfaceName)) {
                    return true;
                }
            }
            return false;
        } catch (IOException e) {
            logger.warn("Unable to check the interface " + interfaceName, e);
            return false;
        } finally {
            if (in != null) {
                try {
                    in.close();
                } catch (Exception e) {
                }
            }
        }
    }

    /**
     *
     * @param superClassName The super class name should be in the format of org/test/SimpleClass
     * @return return true if the method parameter superClassName  is assignable to any interface in the expected interfaces or any class in the expected classes
     */
    private boolean isSuperClassAssignable(String superClassName) {
        if (targetClassNames.contains(superClassName)) {
            return true;
        } else if (superClassName.equals("java/lang/Object")) {
            return false;
        }
        
        //Check parent class
        URL url = bundle.getResource(superClassName + ".class");
        if (url == null) {
            //TODO what should we do if we do not find the super class ?
            return false;
        }
        InputStream in = null;
        try {
            in = url.openStream();
            ClassReader classReader = new ClassReader(in);
            
            //Check interfaces
            String[] superInterfaceNames = classReader.getInterfaces();            
            for (String superInterfaceName : superInterfaceNames) {
                if (isInterfaceAssignable(superInterfaceName)) {
                    return true;
                }                
            }
            
            //Check className
            return isSuperClassAssignable(classReader.getSuperName());            
        } catch (IOException e) {
            logger.warn("Unable to check the super class  " + superClassName, e);
            return false;
        } finally {
            if (in != null) {
                try {
                    in.close();
                } catch (Exception e) {
                }
            }
        }
    }

    /**
     * Get the ASM style package name from the parameter className.
     * If the className is ended with .class extension, e.g.  /org/apache/geronimo/TestCass.class or org.apache.geronimo.TestClass.class,
     *      then org/apache/geronimo is returned
     * If the className is not ended with .class extension, e.g.  /org/apache/geronimo/TestCass or org.apache.geronimo.TestClass,
     *      then org/apache/geronimo is returned
     * @param className
     * @return ASM style package name, should be in the format of  "org/apache/geronimo"
     */
    protected String toASMStylePackageName(String className) {
        if (className.endsWith(EXT)) {
            className = className.substring(0, className.length() - EXT.length());
        }
        className = className.replace('.', '/');
        int iLastDotIndex = className.lastIndexOf('/');
        if (iLastDotIndex != -1) {
            return className.substring(0, iLastDotIndex);
        } else {
            return "";
        }
    }
}
