/* PackageIdentifier Copyright (C) 1999-2002 Jochen Hoenicke.
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2, or (at your option)
 * any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; see the file COPYING.  If not, write to
 * the Free Software Foundation, 675 Mass Ave, Cambridge, MA 02139, USA.
 *
 * $Id: PackageIdentifier.java.in,v 1.6.2.3 2002/05/28 17:34:14 hoenicke Exp $
 */

package jode.obfuscator;
import jode.GlobalOptions;
import jode.bytecode.ClassInfo;
import jode.bytecode.FieldInfo;
import jode.bytecode.MethodInfo;
import java.lang.reflect.Modifier;
import java.io.*;
import java.util.Vector;
import java.util.Enumeration;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

import @COLLECTIONS@.Map;
import @COLLECTIONS@.HashMap;
import @COLLECTIONS@.Iterator;
import @COLLECTIONS@.List;
import @COLLECTIONS@.ArrayList;
import @COLLECTIONS@.Arrays;
import @COLLECTIONS@.Collections;

public class PackageIdentifier extends Identifier {
    ClassBundle bundle;
    PackageIdentifier parent;
    String name;
    String fullName;

    boolean loadOnDemand;
    Map loadedClasses;
    List swappedClasses;

    public PackageIdentifier(ClassBundle bundle, 
			     PackageIdentifier parent,
			     String fullName, String name) {
	super(name);
	this.bundle = bundle;
	this.parent = parent;
	this.fullName = fullName;
	this.name = name;
	this.loadedClasses = new HashMap();
    }

    /**
     * Marks the parent package as preserved, too.
     */
    protected void setSinglePreserved() {
	if (parent != null)
	    parent.setPreserved();
    }

    public void setLoadOnDemand() {
	if (loadOnDemand)
	    return;
	loadOnDemand = true;
	if ((Main.stripping & Main.STRIP_UNREACH) == 0) {
	    String fullNamePrefix =
		(fullName.length() > 0) ? fullName + "." : "";

	    // Load all classes and packages now, so they don't get stripped
	    Enumeration enum = 
		ClassInfo.getClassesAndPackages(getFullName());
	    while (enum.hasMoreElements()) {
		String subclazz = ((String)enum.nextElement()).intern();
		if (loadedClasses.containsKey(subclazz))
		    continue;
		String subFull = (fullNamePrefix + subclazz).intern();
		
		if (ClassInfo.isPackage(subFull)) {
		    PackageIdentifier ident = new PackageIdentifier
			(bundle, this, subFull, subclazz);
		    loadedClasses.put(subclazz, ident);
		    swappedClasses = null;
		    ident.setLoadOnDemand();
		} else {
		    ClassIdentifier ident = new ClassIdentifier
			(this, subFull, subclazz, ClassInfo.forName(subFull));
		    
		    if (GlobalOptions.verboseLevel > 1)
			GlobalOptions.err.println("preloading Class "
						  + subFull);
		    loadedClasses.put(subclazz, ident);
		    swappedClasses = null;
		    bundle.addClassIdentifier(ident);
		    ((ClassIdentifier) ident).initClass();
		}
	    }
	    // Everything is loaded, we don't need to load on demand anymore.
	    loadOnDemand = false;
	}
    }

    public Identifier getIdentifier(String name) {
	if (loadOnDemand) {
	    Identifier ident = loadClass(name);
	    return ident;
	}
	int index = name.indexOf('.');
	if (index == -1)
	    return (Identifier) loadedClasses.get(name);
	else {
	    PackageIdentifier pack = (PackageIdentifier) 
		loadedClasses.get(name.substring(0, index));
	    if (pack != null)
		return pack.getIdentifier(name.substring(index+1));
	    else
		return null;
	}
    }

    public Identifier loadClass(String name) {
	int index = name.indexOf('.');
	if (index == -1) {
	    Identifier ident = (Identifier) loadedClasses.get(name);
	    if (ident == null) {
		String subFull = 
		   (fullName.length() > 0) ? fullName + "."+ name : name;
		subFull = subFull.intern();
		if (ClassInfo.isPackage(subFull)) {
		    PackageIdentifier pack
			= new PackageIdentifier(bundle, this, subFull, name);
		    loadedClasses.put(name, pack);
		    swappedClasses = null;
		    pack.setLoadOnDemand();
		    ident = pack;
		} else if (!ClassInfo.exists(subFull)) {
		    GlobalOptions.err.println("Warning: Can't find class "
					      + subFull);
		    Thread.dumpStack();
		} else {
		    ident = new ClassIdentifier(this, subFull, name, 
						ClassInfo.forName(subFull));
		    loadedClasses.put(name, ident);
		    swappedClasses = null;
		    bundle.addClassIdentifier(ident);
		    ((ClassIdentifier) ident).initClass();
		}
	    }
	    return ident;
	} else {
	    String subpack = name.substring(0, index);
	    PackageIdentifier pack = 
		(PackageIdentifier) loadedClasses.get(subpack);
	    if (pack == null) {
		String subFull = (fullName.length() > 0)
		    ? fullName + "."+ subpack : subpack;
		subFull = subFull.intern();
		if (ClassInfo.isPackage(subFull)) {
		    pack = new PackageIdentifier(bundle, this, 
						 subFull, subpack);
		    loadedClasses.put(subpack, pack);
		    swappedClasses = null;
		    if (loadOnDemand)
			pack.setLoadOnDemand();
		}
	    }
	    
	    if (pack != null)
		return pack.loadClass(name.substring(index+1));
	    else
		return null;
	}
    }

    public void loadMatchingClasses(IdentifierMatcher matcher) {
	String component = matcher.getNextComponent(this);
	if (component != null) {
	    Identifier ident = (Identifier) loadedClasses.get(component);
	    if (ident == null) {
		component = component.intern();
		String subFull = (fullName.length() > 0)
		    ? fullName + "."+ component : component;
		subFull = subFull.intern();
		if (ClassInfo.isPackage(subFull)) {
		    ident = new PackageIdentifier(bundle, this, 
						  subFull, component);
		    loadedClasses.put(component, ident);
		    swappedClasses = null;
		    if (loadOnDemand)
			((PackageIdentifier) ident).setLoadOnDemand();
		} else if (ClassInfo.exists(subFull)) {
		    if (GlobalOptions.verboseLevel > 1)
			GlobalOptions.err.println("loading Class " +subFull);
		    ident = new ClassIdentifier(this, subFull, component, 
						ClassInfo.forName(subFull));
		    if (loadOnDemand || matcher.matches(ident)) {
			loadedClasses.put(component, ident);
			swappedClasses = null;
			bundle.addClassIdentifier(ident);
			((ClassIdentifier) ident).initClass();
		    }
		} else {
		    GlobalOptions.err.println
			("Warning: Can't find class/package " + subFull);
		}
	    }
	    if (ident instanceof PackageIdentifier) {
		if (matcher.matches(ident)) {
		    if (GlobalOptions.verboseLevel > 0)
			GlobalOptions.err.println("loading Package "
						  +ident.getFullName());
		    ((PackageIdentifier) ident).setLoadOnDemand();
		}

		if (matcher.matchesSub(ident, null))
		    ((PackageIdentifier) ident).loadMatchingClasses(matcher);
	    }
	} else {
	    String fullNamePrefix = 
		(fullName.length() > 0) ? fullName + "." : "";
	    /* Load all matching classes and packages */
	    Enumeration enum = 
		ClassInfo.getClassesAndPackages(getFullName());
	    while (enum.hasMoreElements()) {
		String subclazz = ((String)enum.nextElement()).intern();
		if (loadedClasses.containsKey(subclazz))
		    continue;
		String subFull = (fullNamePrefix + subclazz).intern();
		
		if (matcher.matchesSub(this, subclazz)) {
		    if (ClassInfo.isPackage(subFull)) {
			if (GlobalOptions.verboseLevel > 0)
			    GlobalOptions.err.println("loading Package "
						      + subFull);
			PackageIdentifier ident = new PackageIdentifier
			    (bundle, this, subFull, subclazz);
			loadedClasses.put(subclazz, ident);
			swappedClasses = null;
			if (loadOnDemand || matcher.matches(ident))
			    ident.setLoadOnDemand();
		    } else {
			ClassIdentifier ident = new ClassIdentifier
			    (this, subFull, subclazz, 
			     ClassInfo.forName(subFull));

			if (loadOnDemand || matcher.matches(ident)) {
			    if (GlobalOptions.verboseLevel > 1)
				GlobalOptions.err.println("loading Class "
							  + subFull);
			    loadedClasses.put(subclazz, ident);
			    swappedClasses = null;
			    bundle.addClassIdentifier(ident);
			    ((ClassIdentifier) ident).initClass();
			}
		    }
		}
	    }
	    List list = new ArrayList();
	    list.addAll(loadedClasses.values());
	    for (Iterator i = list.iterator(); i.hasNext(); ) {
		Identifier ident = (Identifier) i.next();
		if (ident instanceof PackageIdentifier) {
		    if (matcher.matches(ident))
			((PackageIdentifier) ident).setLoadOnDemand();
		    
		    if (matcher.matchesSub(ident, null))
			((PackageIdentifier) ident)
			    .loadMatchingClasses(matcher);
		}
	    }
	}
    }

    public void applyPreserveRule(IdentifierMatcher preserveRule) {
	if (loadOnDemand)
	    loadMatchingClasses(preserveRule);
	super.applyPreserveRule(preserveRule);
    }

    /**
     * @return the full qualified name.
     */
    public String getFullName() {
	return fullName;
    }

    /**
     * @return the full qualified alias.
     */
    public String getFullAlias() {
	if (parent != null) {
	    String parentAlias = parent.getFullAlias();
	    String alias = getAlias();
	    if (alias.length() == 0)
		return parentAlias;
	    else if (parentAlias.length() == 0)
		return alias;
	    else
		return parentAlias + "." + alias;
	}
	return "";
    }

    public void buildTable(Renamer renameRule) {
	loadOnDemand = false;
	super.buildTable(renameRule);
    }

    public void doTransformations() {
	for (Iterator i = getChilds(); i.hasNext(); ) {
	    Identifier ident = (Identifier) i.next();
	    if (ident instanceof ClassIdentifier) {
		((ClassIdentifier) ident).doTransformations();
	    } else
		((PackageIdentifier) ident).doTransformations();
	}
    }

    public void readTable(Map table) {
	if (parent != null)
	    setAlias((String) table.get(getFullName()));
	for (Iterator i = loadedClasses.values().iterator(); i.hasNext(); ) {
	    Identifier ident = (Identifier) i.next();
	    if ((Main.stripping & Main.STRIP_UNREACH) == 0
		|| ident.isReachable())
		ident.readTable(table);
	}
    }

    public Identifier getParent() {
	return parent;
    }

    public String getName() {
	return name;
    }

    public String getType() {
	return "package";
    }

    public Iterator getChilds() {
	/* Since loadedClasses is somewhat sorted by the hashcode
	 * of the _original_ names, swap it here to prevent to guess
	 * even parts of the names.
	 */
	if (swappedClasses == null) {
	    swappedClasses = Arrays.asList(loadedClasses.values().toArray());
	    Collections.shuffle(swappedClasses, Main.rand);
	}
	return swappedClasses.iterator();
    }

    public void storeClasses(ZipOutputStream zip) {
	for (Iterator i = getChilds(); i.hasNext(); ) {
	    Identifier ident = (Identifier) i.next();
	    if ((Main.stripping & Main.STRIP_UNREACH) != 0
		&& !ident.isReachable()) {
		if (GlobalOptions.verboseLevel > 4)
		    GlobalOptions.err.println("Class/Package "
					   + ident.getFullName()
					   + " is not reachable");
		continue;
	    }
	    if (ident instanceof PackageIdentifier)
		((PackageIdentifier) ident).storeClasses(zip);
	    else {
		try {
		    String filename = ident.getFullAlias().replace('.','/')
			+ ".class";
		    zip.putNextEntry(new ZipEntry(filename));
		    DataOutputStream out = new DataOutputStream
			(new BufferedOutputStream(zip));
		    ((ClassIdentifier) ident).storeClass(out);
		    out.flush();
		    zip.closeEntry();
		} catch (java.io.IOException ex) {
		    GlobalOptions.err.println("Can't write Class "
					      + ident.getName());
		    ex.printStackTrace(GlobalOptions.err);
		}
	    }
	}
    }

    public void storeClasses(File destination) {
	File newDest = (parent == null) ? destination 
	    : new File(destination, getAlias());
	if (!newDest.exists() && !newDest.mkdir()) {
	    GlobalOptions.err.println("Could not create directory "
				   +newDest.getPath()+", check permissions.");
	}
	for (Iterator i = getChilds(); i.hasNext(); ) {
	    Identifier ident = (Identifier) i.next();
	    if ((Main.stripping & Main.STRIP_UNREACH) != 0
		&& !ident.isReachable()) {
		if (GlobalOptions.verboseLevel > 4)
		    GlobalOptions.err.println("Class/Package "
					      + ident.getFullName()
					      + " is not reachable");
		continue;
	    }
	    if (ident instanceof PackageIdentifier)
		((PackageIdentifier) ident)
		    .storeClasses(newDest);
	    else {
		try {
		    File file = new File(newDest, ident.getAlias()+".class");
// 		    if (file.exists()) {
// 			GlobalOptions.err.println
// 			    ("Refuse to overwrite existing class file "
// 			     +file.getPath()+".  Remove it first.");
// 			return;
// 		    }
		    DataOutputStream out = new DataOutputStream
			(new BufferedOutputStream
			 (new FileOutputStream(file)));
		    ((ClassIdentifier) ident).storeClass(out);
		    out.close();
		} catch (java.io.IOException ex) {
		    GlobalOptions.err.println("Can't write Class "
					   + ident.getName());
		    ex.printStackTrace(GlobalOptions.err);
		}
	    }
	}
    }

    public String toString() {
	return (parent == null) ? "base package" : getFullName();
    }

    public boolean contains(String newAlias, Identifier except) {
	for (Iterator i = loadedClasses.values().iterator(); i.hasNext(); ) {
	    Identifier ident = (Identifier)i.next();
	    if (ident != except) {
		if (((Main.stripping & Main.STRIP_UNREACH) == 0
		     || ident.isReachable())
			&& ident.getAlias().equalsIgnoreCase(newAlias))
		    return true;
		if (ident instanceof PackageIdentifier
		    && ident.getAlias().length() == 0
		    && (((PackageIdentifier) ident)
			.contains(newAlias, this)))
		    return true;
	    }
	}
	if (getAlias().length() == 0
	    && parent != null
	    && parent != except
	    && parent.contains(newAlias, this))
	    return true;
	return false;
    }

    public boolean conflicting(String newAlias) {
	return parent.contains(newAlias, this);
    }
}
