// Copyright (c) Corporation for National Research Initiatives
// Copyright 2000 Samuele Pedroni

package org.python.core;

import java.util.Hashtable;
import java.util.Vector;
import java.util.Enumeration;
import java.util.zip.ZipInputStream;
import java.util.zip.ZipEntry;
import java.io.*;
import java.net.URL;
import java.net.URLConnection;
//import java.net.URLDecoder;
import java.lang.reflect.Modifier;

/** Abstract package manager that gathers info about statically known classes
 * from a set of jars. This info can be eventually cached.
 * Off-the-shelf this class offers a local file-system based cache impl.
 */
public abstract class CachedJarsPackageManager extends PackageManager {

    /** Message log method - hook. This default impl does nothing.
     * @param msg message text
     */
    protected void message(String msg) {
    }
    /** Warning log method - hook. This default impl does nothing.
     * @param warn warning text
     */
    protected void warning(String warn) {
    }
    /** Comment log method - hook. This default impl does nothing.
     * @param msg message text
     */
    protected void comment(String msg) {
    }
    /** Debug log method - hook. This default impl does nothing.
     * @param msg message text
     */
    protected void debug(String msg) {
    }

    /** Filter class/pkg by name helper method - hook.
     * The default impl. is used by {@link #addJarToPackages} in order
     * to filter out classes whose name contains '$' (e.g. inner classes,...).
     * Should be used or overriden by derived classes too.
     * Also to be used in {@link #doDir}.
     * @param name class/pkg name
     * @param pkg if true, name refers to a pkg
     * @return true if name must be filtered out
     */
    protected boolean filterByName(String name,boolean pkg) {
        return name.indexOf('$') != -1;
    }

    /** Filter class by access perms helper method - hook.
     * The default impl. is used by {@link #addJarToPackages} in order
     * to filter out non-public classes.
     * Should be used or overriden by derived classes too.
     * Also to be used in {@link #doDir}.
     * Access perms can be read with {@link #checkAccess}.
     * @param name class name
     * @param acc class access permissions as int
     * @return true if name must be filtered out
     */
    protected boolean filterByAccess(String name,int acc) {
        return (acc & Modifier.PUBLIC) != Modifier.PUBLIC;
    }

    private boolean indexModified;
    private Hashtable jarfiles;

    private static String vectorToString(Vector vec) {
        int n = vec.size();
        StringBuffer ret = new StringBuffer();
        for(int i=0; i<n; i++) {
            ret.append((String)vec.elementAt(i));
            if (i<n-1) ret.append(",");
        }
        return ret.toString();
    }

    // Add a single class from zipFile to zipPackages
    // Only add valid, public classes
    private void addZipEntry(Hashtable zipPackages,
                             ZipEntry entry,
                             ZipInputStream zip) throws IOException
    {
        String name = entry.getName();
        //System.err.println("entry: "+name);
        if (!name.endsWith(".class")) return;

        char sep = '/';
        int breakPoint = name.lastIndexOf(sep);
        if (breakPoint == -1) {
            breakPoint = name.lastIndexOf('\\');
            sep = '\\';
        }

        String packageName;
        if (breakPoint == -1) {
            packageName = "";
        } else {
            packageName = name.substring(0,breakPoint).replace(sep, '.');
        }

        String className = name.substring(breakPoint+1, name.length()-6);

        if (filterByName(className,false)) return;

        Vector[] vec = (Vector[])zipPackages.get(packageName);
        if (vec == null) {
            vec = new Vector[] { new Vector(), new Vector() };
            zipPackages.put(packageName, vec);
        }
        int access = checkAccess(zip);
        if ((access != -1) && !filterByAccess(name,access)) {
            vec[0].addElement(className);
        } else {
            vec[1].addElement(className);
        }
    }

    // Extract all of the packages in a single jarfile
    private Hashtable getZipPackages(InputStream jarin) throws IOException {
        Hashtable zipPackages = new Hashtable();

        ZipInputStream zip=new ZipInputStream(jarin);

        ZipEntry entry;
        while ((entry = zip.getNextEntry()) != null) {
            addZipEntry(zipPackages, entry, zip);
            zip.closeEntry();
        }

        // Turn each vector into a comma-separated String
        for (Enumeration e = zipPackages.keys() ; e.hasMoreElements() ;) {
            Object key = e.nextElement();
            Vector[] vec = (Vector[])zipPackages.get(key);
            String classes = vectorToString(vec[0]);
            if (vec[1].size() > 0) {
                classes += '@' + vectorToString(vec[1]);
            }
            zipPackages.put(key, classes);
        }

        return zipPackages;
    }

    /** Gathers classes info from jar specified by jarurl URL.
     * Eventually just using previously cached info.
     * Eventually updated info is not cached.
     * Persistent cache storage access goes through
     * inOpenCacheFile() and outCreateCacheFile().
     */
    public void addJarToPackages(java.net.URL jarurl) {
        addJarToPackages(jarurl,null,false);
    }

    /** Gathers classes info from jar specified by jarurl URL.
     * Eventually just using previously cached info.
     * Eventually updated info is (re-)cached if param cache is true.
     * Persistent cache storage access goes through
     * inOpenCacheFile() and outCreateCacheFile().
     */
    public void addJarToPackages(URL jarurl,boolean cache) {
        addJarToPackages(jarurl,null,cache);
    }

    /** Gathers classes info from jar specified by File jarfile.
     * Eventually just using previously cached info.
     * Eventually updated info is not cached.
     * Persistent cache storage access goes through
     * inOpenCacheFile() and outCreateCacheFile().
     */
    public void addJarToPackages(File jarfile) {
        addJarToPackages(null,jarfile,false);
    }

    /** Gathers classes info from jar specified by File jarfile.
     * Eventually just using previously cached info.
     * Eventually updated info is (re-)cached if param cache is true.
     * Persistent cache storage access goes through
     * inOpenCacheFile() and outCreateCacheFile().
     */
    public void addJarToPackages(File jarfile,boolean cache) {
        addJarToPackages(null,jarfile,cache);
    }

    private void addJarToPackages(URL jarurl,File jarfile,boolean cache) {
        try {
            boolean caching = jarfiles!=null;

            URLConnection jarconn = null;
            boolean localfile = true;

            if (jarfile == null) {
                jarconn = jarurl.openConnection();
                // This is necessary because 'file:' url-connections
                // return always 0 through getLastModified (bug?).
                // And in order to handle localfiles (from urls too)
                // uniformly.
                if(jarconn.getURL().getProtocol().equals("file")) {
                    // ??pending: need to use java2 URLDecoder.decode?
                    // but under 1.1 this is absent and should be simulated.
                    String jarfilename = jarurl.getFile();
                    jarfilename = jarfilename.replace('/',File.separatorChar);
                    jarfile = new File(jarfilename);
                }
                else localfile = false;
            }

            if (localfile && !jarfile.exists()) return;

            Hashtable zipPackages = null;

            long mtime = 0;
            String jarcanon = null;
            JarXEntry entry = null;
            boolean brandNew = false;

            if(caching) {

                if(localfile) {
                    mtime = jarfile.lastModified();
                    jarcanon = jarfile.getCanonicalPath();
                } else {
                    mtime = jarconn.getLastModified();
                    jarcanon = jarurl.toString();
                }

                entry = (JarXEntry)jarfiles.get(jarcanon);

                if (entry == null && cache) {
                    message("processing new jar, '"+
                    jarcanon+"'");

                    String jarname;
                    if(localfile) {
                        jarname = jarfile.getName();
                    } else {
                        jarname = jarurl.getFile();
                        int slash = jarname.lastIndexOf('/');
                        if (slash != -1)
                        jarname=jarname.substring(slash+1);
                    }
                    jarname=jarname.substring(0,jarname.length()-4);

                    entry = new JarXEntry(jarname);
                    jarfiles.put(jarcanon, entry);

                    brandNew = true;
                }


                if (mtime != 0 && entry != null && entry.mtime == mtime) {
                    zipPackages = readCacheFile(entry, jarcanon);
                }

            }

            if (zipPackages == null) {
                caching = caching && cache;

                if(caching) {
                    indexModified = true;
                    if (entry.mtime != 0) {
                        message("processing modified jar, '"+
                        jarcanon+"'");
                    }
                    entry.mtime = mtime;
                }

                InputStream jarin;
                if (jarconn == null)
                    jarin = new BufferedInputStream(
                            new FileInputStream(jarfile));
                else
                    jarin = jarconn.getInputStream();

                zipPackages = getZipPackages(jarin);

                if (caching)
                    writeCacheFile(entry, jarcanon, zipPackages, brandNew);
            }

            addPackages(zipPackages, jarcanon);
        } catch (IOException ioe) {
            // silently skip any bad directories
            warning("skipping bad jar, '" +
                    (jarfile != null ?
                          jarfile.toString() :
                          jarurl.toString()) +
                    "'");
        }

    }

    private void addPackages(Hashtable zipPackages, String jarfile) {
        for (Enumeration e = zipPackages.keys() ; e.hasMoreElements() ;) {
            String pkg = (String)e.nextElement();
            String classes = (String)zipPackages.get(pkg);

            int idx = classes.indexOf('@');
            if (idx >= 0 && Options.respectJavaAccessibility) {
                classes = classes.substring(0, idx);
            }

            makeJavaPackage(pkg, classes, jarfile);
        }
    }

    // Read in cache file storing package info for a single .jar
    // Return null and delete this cachefile if it is invalid
    private Hashtable readCacheFile(JarXEntry entry,String jarcanon)
    {
        String cachefile = entry.cachefile;
        long mtime = entry.mtime;

        debug("reading cache, '"+jarcanon+"'");

        try {
            DataInputStream istream = inOpenCacheFile(cachefile);
            String old_jarcanon = istream.readUTF();
            long old_mtime = istream.readLong();
            if ((!old_jarcanon.equals(jarcanon)) ||
            (old_mtime != mtime))
            {
                comment("invalid cache file: "+
                cachefile+", "+jarcanon+":"+
                old_jarcanon+", "+mtime+":"+old_mtime);
                deleteCacheFile(cachefile);
                return null;
            }
            Hashtable packs = new Hashtable();
            try {
                while (true) {
                    String packageName = istream.readUTF();
                    String classes = istream.readUTF();
                    packs.put(packageName, classes);
                }
            } catch (EOFException eof) {
                ;
            }
            istream.close();

            return packs;
        } catch (IOException ioe) {
            // if (cachefile.exists()) cachefile.delete();
            return null;
        }
    }

    // Write a cache file storing package info for a single .jar
    private void writeCacheFile(JarXEntry entry,String jarcanon,
    Hashtable zipPackages,boolean brandNew)
    {
        try {
            DataOutputStream ostream = outCreateCacheFile(entry, brandNew);
            ostream.writeUTF(jarcanon);
            ostream.writeLong(entry.mtime);
            comment("rewriting cachefile for '"+jarcanon+"'");

            for (Enumeration e = zipPackages.keys() ; e.hasMoreElements() ;) {
                String packageName = (String)e.nextElement();
                String classes = (String)zipPackages.get(packageName);
                ostream.writeUTF(packageName);
                ostream.writeUTF(classes);
            }
            ostream.close();
        } catch (IOException ioe) {
            warning("can't write cache file for '"+jarcanon+"'");
        }
    }

    /** Initializes cache. Eventually reads back cache index.
     * Index persistent storage is accessed through inOpenIndex().
     */
    protected void initCache() {
        indexModified = false;
        jarfiles = new Hashtable();

        try {
            DataInputStream istream = inOpenIndex();
            if (istream == null) return;

            try {
                while (true) {
                    String jarcanon = istream.readUTF();
                    String cachefile = istream.readUTF();
                    long mtime = istream.readLong();
                    jarfiles.put(jarcanon, new JarXEntry(cachefile,mtime));
                }
            } catch (EOFException eof) {
                ;
            }
            istream.close();
        } catch (IOException ioe) {
            warning("invalid index file");
        }

    }

    /** Write back cache index.
     * Index persistent storage is accessed through outOpenIndex().
     */
    public void saveCache() {
        if(jarfiles == null || !indexModified ) return;

        indexModified = false;

        comment("writing modified index file");

        try {
            DataOutputStream ostream = outOpenIndex();
            for (Enumeration e = jarfiles.keys(); e.hasMoreElements();) {
                String jarcanon = (String)e.nextElement();
                JarXEntry entry = (JarXEntry)jarfiles.get(jarcanon);
                ostream.writeUTF(jarcanon);
                ostream.writeUTF(entry.cachefile);
                ostream.writeLong(entry.mtime);
            }
            ostream.close();
        } catch (IOException ioe) {
            warning("can't write index file");
        }
    }

    // hooks for changing cache storage

    /** To pass a cachefile id by ref. And for internal use.
     * See outCreateCacheFile
     */
    public static class JarXEntry extends Object {
        /** cachefile id */
        public String cachefile;

        public long mtime;

        public JarXEntry(String cachefile) {
            this.cachefile = cachefile;
        }

        public JarXEntry(String cachefile,long mtime) {
            this.cachefile = cachefile;
            this.mtime = mtime;
        }


    }

    /** Open cache index for reading from persistent storage - hook.
     * Must Return null if this is absent.
     * This default impl is part of the off-the-shelf local
     * file-system cache impl.
     * Can be overriden.
     */
    protected DataInputStream inOpenIndex() throws IOException {
        File indexFile = new File(cachedir, "packages.idx");

        if (!indexFile.exists()) return null;

        DataInputStream istream = new DataInputStream(
        new BufferedInputStream(new FileInputStream(indexFile)));

        return istream;
    }

    /** Open cache index for writing back to persistent storage - hook.
     * This default impl is part of the off-the-shelf local
     * file-system cache impl.
     * Can be overriden.
     */
    protected DataOutputStream outOpenIndex() throws IOException {
        File indexFile = new File(cachedir, "packages.idx");

        return new DataOutputStream(
        new BufferedOutputStream(new FileOutputStream(indexFile)));
    }

    /** Open cache file for reading from persistent storage - hook.
     * This default impl is part of the off-the-shelf local
     * file-system cache impl.
     * Can be overriden.
     */
    protected DataInputStream inOpenCacheFile(String cachefile)
        throws IOException
    {
        return new DataInputStream(
               new BufferedInputStream(
               new FileInputStream(cachefile)));
    }

    /** Delete (invalidated) cache file from persistent storage - hook.
     * This default impl is part of the off-the-shelf local
     * file-system cache impl.
     * Can be overriden.
     */
    protected void deleteCacheFile(String cachefile) {
        new File(cachefile).delete();
    }

    /**
     * Create/open cache file for rewriting back to persistent storage - hook.
     * If create is false, cache file is supposed to exist and must be opened
     * for rewriting, entry.cachefile is a valid cachefile id.
     * If create is true, cache file must be created. entry.cachefile is a
     * flat jarname to be used to produce a valid cachefile id (to be put
     * back in entry.cachefile on exit).
     * This default impl is part of the off-the-shelf local file-system
     * cache impl.
     * Can be overriden.
     */
    protected DataOutputStream outCreateCacheFile(JarXEntry entry,
                                                  boolean create)
        throws IOException
    {
        File cachefile = null;

        if(create) {
            int index = 1;
            String suffix = "";
            String jarname = entry.cachefile;
            while (true) {
                cachefile = new File(cachedir, jarname+suffix+".pkc");
                //System.err.println("try cachefile: "+cachefile);
                if (!cachefile.exists()) break;
                suffix = "$"+index;
                index += 1;
            }
            entry.cachefile = cachefile.getCanonicalPath();
        } else cachefile = new File(entry.cachefile);

        return new DataOutputStream(
               new BufferedOutputStream(
               new FileOutputStream(cachefile)));
    }

    // for default cache (local fs based) impl

    private File cachedir;

    /** Initialize off-the-shelf (default) local file-system cache impl.
     * Must be called before {@link #initCache}.
     * cachedir is the cache repository directory, this is eventually created.
     * Returns true if dir works.
     */
    protected boolean useCacheDir(File cachedir) {
        if(cachedir == null) return false;
        if (!cachedir.isDirectory() && cachedir.mkdirs() == false) {
            warning("can't create package cache dir, '"+cachedir+"'");
            return false;
        }

        this.cachedir = cachedir;

        return true;
    }

}
