/* Copyright (c) Jython Developers */
package org.python.core.util;

import java.io.IOException;
import java.io.InputStream;
import java.util.EnumSet;
import org.python.core.BytecodeLoader;
import org.python.core.Py;
import org.python.core.PyCode;
import org.python.core.PyList;
import org.python.core.PyModule;
import org.python.core.PyObject;
import org.python.core.PyType;
import org.python.core.imp;

/**
 * A base class for PEP-302 path hooks. Handles looking through source, compiled, package and module
 * items in the right order, and creating and filling in modules.
 */
public abstract class importer<T> extends PyObject {

    static enum EntryType {
        IS_SOURCE, IS_BYTECODE, IS_PACKAGE
    };
    /** SearchOrder defines how we search for a module. */
    final SearchOrderEntry[] searchOrder;

    /** Module information */
    protected static enum ModuleInfo {
        ERROR, NOT_FOUND, MODULE, PACKAGE
    };

    public importer(PyType subType) {
        super(subType);
        searchOrder = makeSearchOrder();
    }

    public importer() {
        searchOrder = makeSearchOrder();
    }

    /**
     * Returns the separator between directories and files used by this type of importer.
     */
    protected abstract String getSeparator();

    /**
     * Returns the value to fill in __path__ on a module with the given full module name created by
     * this importer.
     */
    protected abstract String makePackagePath(String fullname);

    /**
     * Given a full module name, return the potential file path in the archive (without extension).
     */
    protected abstract String makeFilename(String fullname);

    /**
     * Given a full module name, return the potential file path including the archive (without
     * extension).
     */
    protected abstract String makeFilePath(String fullname);

    /**
     * Returns an entry for a filename from makeFilename with a potential suffix such that this
     * importer can make a bundle with it, or null if fullFilename doesn't exist in this importer.
     */
    protected abstract T makeEntry(String filenameAndSuffix);

    /**
     * Returns a Bundle for fullFilename and entry, the result from a makeEntry call for
     * fullFilename.
     */
    protected abstract Bundle makeBundle(String filenameAndSuffix, T entry);

    private SearchOrderEntry[] makeSearchOrder(){
        return new SearchOrderEntry[] {
            new SearchOrderEntry(getSeparator() + "__init__$py.class",
                                 EnumSet.of(EntryType.IS_PACKAGE, EntryType.IS_BYTECODE)),
            new SearchOrderEntry(getSeparator() + "__init__.py",
                                 EnumSet.of(EntryType.IS_PACKAGE, EntryType.IS_SOURCE)),
            new SearchOrderEntry("$py.class", EnumSet.of(EntryType.IS_BYTECODE)),
            new SearchOrderEntry(".py", EnumSet.of(EntryType.IS_SOURCE)),};
    }

    protected final PyObject importer_find_module(String fullname, String path) {
        ModuleInfo moduleInfo = getModuleInfo(fullname);
        if (moduleInfo == ModuleInfo.ERROR || moduleInfo == ModuleInfo.NOT_FOUND) {
            return Py.None;
        }
        return this;
    }

    protected final PyObject importer_load_module(String fullname) {
        ModuleCodeData moduleCodeData = getModuleCode(fullname);
        if (moduleCodeData == null) {
            return Py.None;
        }
        // the module *must* be in sys.modules before the loader executes the module code; the
        // module code may (directly or indirectly) import itself
        PyModule mod = imp.addModule(fullname);
        mod.__dict__.__setitem__("__loader__", this);
        if (moduleCodeData.isPackage) {
            // add __path__ to the module *before* the code gets executed
            PyList pkgpath = new PyList();
            pkgpath.add(makePackagePath(fullname));
            mod.__dict__.__setitem__("__path__", pkgpath);
        }
        imp.createFromCode(fullname, moduleCodeData.code, moduleCodeData.path);
        Py.writeDebug("import", "import " + fullname + " # loaded from " + moduleCodeData.path);
        return mod;
    }

    /**
     * Bundle is an InputStream, bundled together with a method that can close the input stream and
     * whatever resources are associated with it when the resource is imported.
     */
    protected abstract static class Bundle {
        public InputStream inputStream;

        public Bundle(InputStream inputStream) {
            this.inputStream = inputStream;
        }

        /**
         * Close the underlying resource if necessary. Raises an IOError if a problem occurs.
         */
        public abstract void close();
    }

    /**
     * Given a path to a compiled file in the archive, return the modification time of the
     * matching .py file.
     *
     * @param path to the compiled file
     * @return long mtime of the .py, or -1 if no source is available
     */
    protected abstract long getSourceMtime(String path);

    /**
     * Return module information for the module with the fully qualified name.
     *
     * @param fullname
     *            the fully qualified name of the module
     * @return the module's information
     */
    protected final ModuleInfo getModuleInfo(String fullname) {
        String path = makeFilename(fullname);

        for (SearchOrderEntry entry : searchOrder) {
            T importEntry = makeEntry(path + entry.suffix);
            if (importEntry == null) {
                continue;
            }

            if (entry.type.contains(EntryType.IS_PACKAGE)) {
                return ModuleInfo.PACKAGE;
            }
            return ModuleInfo.MODULE;
        }
        return ModuleInfo.NOT_FOUND;
    }

    /**
     * Return the code object and its associated data for the module with the fully qualified name.
     *
     * @param fullname
     *            the fully qualified name of the module
     * @return the module's ModuleCodeData object
     */
    protected final ModuleCodeData getModuleCode(String fullname) {
        String path = makeFilename(fullname);
        String fullPath = makeFilePath(fullname);

        if (path.length() < 0) {
            return null;
        }

        for (SearchOrderEntry entry : searchOrder) {
            String suffix = entry.suffix;
            String searchPath = path + suffix;
            String fullSearchPath = fullPath + suffix;

            Py.writeDebug("import", "# trying " + searchPath);
            T tocEntry = makeEntry(searchPath);
            if (tocEntry == null) {
                continue;
            }

            boolean isPackage = entry.type.contains(EntryType.IS_PACKAGE);
            boolean isBytecode = entry.type.contains(EntryType.IS_BYTECODE);
            long mtime = -1;
            if (isBytecode) {
                mtime = getSourceMtime(searchPath);
            }

            Bundle bundle = makeBundle(searchPath, tocEntry);
            byte[] codeBytes;
            try {
                if (isBytecode) {
                    try {
                        codeBytes = imp.readCode(fullname, bundle.inputStream, true, mtime);
                    } catch (IOException ioe) {
                        throw Py.ImportError(ioe.getMessage() + "[path=" + fullSearchPath + "]");
                    }
                    if (codeBytes == null) {
                        // bad magic number or non-matching mtime in byte code, try next
                        continue;
                    }
                } else {
                    codeBytes = imp.compileSource(fullname, bundle.inputStream, fullSearchPath);
                }
            } finally {
                bundle.close();
            }

            PyCode code = BytecodeLoader.makeCode(fullname + "$py", codeBytes, fullSearchPath);
            return new ModuleCodeData(code, isPackage, fullSearchPath);
        }
        return null;
    }

    /**
     * Container for PyModule code - whether or not it's a package - and its path.
     */
    protected class ModuleCodeData {
        public PyCode code;
        public boolean isPackage;
        public String path;

        public ModuleCodeData(PyCode code, boolean isPackage, String path) {
            this.code = code;
            this.isPackage = isPackage;
            this.path = path;
        }
    }

    /**
     * A step in the module search order: the file suffix and its entry type.
     */
    protected static class SearchOrderEntry {
        public String suffix;
        public EnumSet<EntryType> type;

        public SearchOrderEntry(String suffix, EnumSet<EntryType> type) {
            this.suffix = suffix;
            this.type = type;
        }
    }

}
