package org.python.core;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;

/**
 * Helper class handling the VM specific java package detection.
 */
public class JavaImportHelper {

    private static final String DOT = ".";

    /**
     * Try to add the java package.
     * <p>
     * This is handy in cases where the package scan cannot run, or when the initial classpath does not contain all .jar
     * files (such as in J2EE containers).
     * <p>
     * There is some self-healing in the sense that a correct, explicit import of a java class will succeed even if
     * sys.modules already contains a Py.None entry for the corresponding java package.
     * 
     * @param packageName The dotted name of the java package
     * @param fromlist A tuple with the from names to import. Can be null or empty.
     * 
     * @return <code>true</code> if a java package was doubtlessly identified and added, <code>false</code>
     * otherwise.
     */
    protected static boolean tryAddPackage(final String packageName, PyObject fromlist) {
        // make sure we do not turn off the added flag, once it is set
        boolean packageAdded = false;

        if (packageName != null) {
            // check explicit imports first (performance optimization)

            // handle 'from java.net import URL' like explicit imports
            List<String> stringFromlist = getFromListAsStrings(fromlist);
            for (String fromName : stringFromlist) {
                if (isJavaClass(packageName, fromName)) {
                    packageAdded = addPackage(packageName, packageAdded);

                }
            }

            // handle 'import java.net.URL' style explicit imports
            int dotPos = packageName.lastIndexOf(DOT);
            if (dotPos > 0) {
                String lastDottedName = packageName.substring(dotPos + 1);
                String packageCand = packageName.substring(0, dotPos);
                if (isJavaClass(packageCand, lastDottedName)) {
                    packageAdded = addPackage(packageCand, packageAdded);
                }
            }

            // if all else fails, check already loaded packages
            if (!packageAdded) {
                // build the actual map with the packages known to the VM
                Map<String, String> packages = buildLoadedPackages();

                // add known packages
                String parentPackageName = packageName;
                if (isLoadedPackage(packageName, packages)) {
                    packageAdded = addPackage(packageName, packageAdded);
                }
                dotPos = 0;
                do {
                    dotPos = parentPackageName.lastIndexOf(DOT);
                    if (dotPos > 0) {
                        parentPackageName = parentPackageName.substring(0, dotPos);
                        if (isLoadedPackage(parentPackageName, packages)) {
                            packageAdded = addPackage(parentPackageName, packageAdded);
                        }
                    }
                } while (dotPos > 0);

                // handle package imports like 'from java import math'
                for (String fromName : stringFromlist) {
                    String fromPackageName = packageName + DOT + fromName;
                    if (isLoadedPackage(fromPackageName, packages)) {
                        packageAdded = addPackage(fromPackageName, packageAdded);
                    }
                }
            }
        }
        return packageAdded;
    }

    /**
     * Check if a java package is already known to the VM.
     * <p>
     * May return <code>false</code> even if the given package name is a valid java package !
     * 
     * @param packageName
     * 
     * @return <code>true</code> if the package with the given name is already loaded by the VM, <code>false</code>
     * otherwise.
     */
    protected static boolean isLoadedPackage(String packageName) {
        return isLoadedPackage(packageName, buildLoadedPackages());
    }

    /**
     * Convert the fromlist into a java.lang.String based list.
     * <p>
     * Do some sanity checks: filter out '*' and empty tuples, as well as non tuples.
     * 
     * @param fromlist
     * @return a list containing java.lang.String entries
     */
    private static final List<String> getFromListAsStrings(PyObject fromlist) {
        List<String> stringFromlist = new ArrayList<String>();

        if (fromlist != null && fromlist != Py.EmptyTuple && fromlist instanceof PyTuple) {
            Iterator iterator = ((PyTuple) fromlist).iterator();
            while (iterator.hasNext()) {
                Object obj = iterator.next();
                if (obj instanceof PyString) {
                    obj = ((PyString) obj).getString();
                }
                if (obj instanceof String) {
                    String fromName = (String) obj;
                    if (!"*".equals(fromName)) {
                        stringFromlist.add(fromName);
                    }
                }
            }
        }
        return stringFromlist;
    }

    /**
     * Faster way to check if a java package is already known to the VM.
     * <p>
     * May return <code>false</code> even if the given package name is a valid java package !
     * 
     * @param packageName
     * @param packages A Map containing all packages actually known to the VM. Such a Map can be obtained using
     * {@link JavaImportHelper.buildLoadedPackagesTree()}
     * 
     * @return <code>true</code> if the package with the given name is already loaded by the VM, <code>false</code>
     * otherwise.
     */
    private static boolean isLoadedPackage(String javaPackageName, Map<String, String> packages) {
        boolean isLoaded = false;
        if (javaPackageName != null) {
            isLoaded = packages.containsKey(javaPackageName);
        }
        return isLoaded;
    }

    /**
     * Build a <code>Map</code> of the currently known packages to the VM.
     * <p>
     * All parent packages appear as single entries like python modules, e.g. <code>java</code>,
     * <code>java.lang</code>, <code>java.lang.reflect</code>,
     */
    private static Map<String, String> buildLoadedPackages() {
        TreeMap<String, String> packageMap = new TreeMap<String, String>();
        Package[] packages = Package.getPackages();
        for (int i = 0; i < packages.length; i++) {
            String packageName = packages[i].getName();
            packageMap.put(packageName, "");
            int dotPos = 0;
            do {
                dotPos = packageName.lastIndexOf(DOT);
                if (dotPos > 0) {
                    packageName = packageName.substring(0, dotPos);
                    packageMap.put(packageName, "");
                }
            } while (dotPos > 0);
        }
        return packageMap;
    }

    /**
     * @return <code>true</code> if the java class can be found by the current
     *         Py classloader setup
     */
    private static boolean isJavaClass(String packageName, String className) {
        return className != null && className.length() > 0
                && Py.findClass(packageName + "." + className) != null;
    }

    /**
     * Add a java package to sys.modules, if not already done
     * 
     * @return <code>true</code> if something was really added, <code>false</code> otherwise
     */
    private static boolean addPackage(String packageName, boolean packageAdded) {
        PyObject modules = Py.getSystemState().modules;
        String internedPackageName = packageName.intern();
        PyObject module = modules.__finditem__(internedPackageName);
        // a previously failed import could have created a Py.None entry in sys.modules
        if (module == null || module == Py.None) {
            int dotPos = 0;
            do {
                PyJavaPackage p = PySystemState.add_package(packageName);
                if(dotPos == 0) {
                    modules.__setitem__(internedPackageName, p);
                } else {
                    module = modules.__finditem__(internedPackageName);
                    if (module == null || module == Py.None) {
                        modules.__setitem__(internedPackageName, p);
                    }
                }
                dotPos = packageName.lastIndexOf(DOT);
                if (dotPos > 0) {
                    packageName = packageName.substring(0, dotPos);
                    internedPackageName = packageName.intern();
                }
            } while(dotPos > 0);
            // make sure not to turn off the packageAdded flag
            packageAdded = true;
        }
        return packageAdded;
    }

}
