/**
 * Copyright (C) 2001-2005 France Telecom R&D
 * 
 * This library is free software; you can redistribute it and/or modify it under
 * the terms of the GNU Lesser General Public License as published by the Free
 * Software Foundation; either version 2 of the License, or (at your option) any
 * later version.
 * 
 * This library 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 Lesser General Public License for more
 * details.
 * 
 * You should have received a copy of the GNU Lesser General Public License
 * along with this library; if not, write to the Free Software Foundation, Inc.,
 * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
 */
package org.objectweb.util.ant;

import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.Project;
import org.apache.tools.ant.Task;
import org.apache.tools.ant.taskdefs.MatchingTask;
import org.apache.tools.ant.util.DOMElementWriter;
import org.objectweb.asm.AnnotationVisitor;
import org.objectweb.asm.Attribute;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.FieldVisitor;
import org.objectweb.asm.Label;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Type;
import org.objectweb.asm.signature.SignatureReader;
import org.objectweb.asm.signature.SignatureVisitor;
import org.w3c.dom.Document;
import org.w3c.dom.Element;

import java.io.BufferedWriter;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

/**
 * Is an Ant task analyzing a set of java classes for extracting dependencies
 * between groups of classes. Group is composed with a set of classes.
 * This task produces report on screen or in a file. The format can be xml
 * or indented text.
 * Example:
 * <dependency dir="${out.build}" showContent="true"
 *    output="${out.build}/dependency.xml">
 *    <group name="ejb">
 *       <include name="**EJB*.class"/>
 *    </group>
 *	  <group name="jdo">
 *	     <include name="**JDO*.class"/>
 *	  </group>
 *	  <group name="common">
 *		 <include name="**.class"/>
 *		 <exclude name="**EJB*.class"/>
 *		 <exclude name="**JDO*.class"/>
 *	  </group>
 * </dependency>
 *
 * @author S.Chassande-Barrioz
 */
public class DependencyAnalyzer extends Task {

	public final static String TAB = "\t";

	public interface ClassFilter {
	    boolean accept(File f);
	}
	
	public class Group extends MatchingTask {

		public String name;
		
		private String filterClassName;
		
		private ClassFilter filter;
		
		private List selectedFiles;
        
        private File groupDir;

		/**
		 * Map<Group, Map<String className, List<String className>>>
		 */
		public Map dependencies = new HashMap();

		public void setName(String n) {
			this.name = n;
		}
		
		public void setFilter(String filterClass) {
		    this.filterClassName = filterClass;
		}
        public void setDir(File d) {
            this.groupDir = d;
        }

		public Collection getSelectedFiles() {
		    if (selectedFiles == null) {
			    selectedFiles = Arrays.asList(getDirectoryScanner(groupDir)
						.getIncludedFiles());
				Collection toDeselct = Arrays.asList(getDirectoryScanner(groupDir)
						.getExcludedFiles());
				log(toDeselct.toString(), Project.MSG_DEBUG);
				selectedFiles.removeAll(toDeselct);
				if (filterClassName != null) {
				    try {
                        filter = (ClassFilter) Class.forName(filterClassName).newInstance();
                    } catch (Exception e) {
                        throw new BuildException(e.getMessage(), e);
                    }
				    for (Iterator it = selectedFiles.iterator(); it.hasNext();) {
                        String fn = (String) it.next();
                        if (!filter.accept(new File(dir, fn))) {
                            it.remove();
                        }
                    }
				}
                Collections.sort(selectedFiles);
		    }
			return selectedFiles;
		}

		public void toString(StringBuffer sb, String t0) {
			final String t1 = t0 + TAB;
			final String t2 = t1 + TAB;
			final String t3 = t2 + TAB;
			sb.append(t0).append("Group: ").append(name).append("\n");
			if (showGroupContent) {
				sb.append(t1).append("contains: \n");
				for (Iterator it = getSelectedFiles().iterator(); it.hasNext();) {
					String fn = (String) it.next();
					sb.append(t2).append(fn).append("\n");
				}
			}
			for (Iterator it = dependencies.entrySet().iterator(); it.hasNext();) {
				Map.Entry me = (Map.Entry) it.next();
				Group cg = (Group) me.getKey();
				Map realDep = (Map) me.getValue();
				sb.append(t1).append("depends on ").append(cg.name).append(
						": \n");
				for (Iterator it2 = realDep.entrySet().iterator(); it2
						.hasNext();) {
					Map.Entry me2 = (Map.Entry) it2.next();
					String cn = (String) me2.getKey();
					List cdeps = (List) me2.getValue();
					sb.append(t2).append("class: ").append(cn).append(
							" depends on:\n");
					for (Iterator it3 = cdeps.iterator(); it3.hasNext();) {
						sb.append(t3).append(it3.next()).append("\n");
					}
				}
			}
		}
        
        public void toXml(Document doc, Element parent) {
            Element group = doc.createElement("group");
            group.setAttribute("name", name);
            parent.appendChild(group);
            if (showGroupContent) {
                Element gc = doc.createElement("group-content");
                gc.setAttribute("size", "" + getSelectedFiles().size());
                group.appendChild(gc);
                for (Iterator it = getSelectedFiles().iterator(); it.hasNext();) {
                    String fn = (String) it.next();
                    Element c = doc.createElement("class");
                    c.setAttribute("name", fn);
                    gc.appendChild(c);
                }
            }
            List grouplist = new ArrayList(dependencies.keySet());
            Collections.sort(grouplist, new Comparator() {
                public int compare(Object o1, Object o2) {
                    return ((Group) o1).name.compareTo(((Group) o2).name);
                }
            });
            for (Iterator it = grouplist.iterator(); it.hasNext();) {
                Group cg = (Group) it.next();
                Map realDep = (Map) dependencies.get(cg);
                Element gd = doc.createElement("group-dependency");
                gd.setAttribute("group-name", cg.name);
                gd.setAttribute("size", "" + realDep.size());
                group.appendChild(gd);
                List classlist = new ArrayList(realDep.keySet());
                Collections.sort(classlist);
                for (Iterator it2 = classlist.iterator(); it2.hasNext();) {
                    String cn = (String) it2.next();
                    List cdeps = (List) realDep.get(cn);
                    Collections.sort(cdeps);
                    Element c = doc.createElement("class");
                    c.setAttribute("name", cn);
                    c.setAttribute("size", "" + cdeps.size());
                    gd.appendChild(c);
                    for (Iterator it3 = cdeps.iterator(); it3.hasNext();) {
                        Element cd = doc.createElement("class-dependency");
                        cd.setAttribute("name", (String) it3.next());
                        c.appendChild(cd);
                    }
                }
            }
        }
	}

	/**
	 * Group of classes
	 */
	List groups = new ArrayList();

	/**
	 * The root location of the file to analyze
	 */
	File dir = null;
	
	/**
	 * The report file
	 */
	File output = null;

	/**
	 * Indicates if the content of the groups must be included into the report
	 */
	boolean showGroupContent = false;

	/**
	 * map associating a analysed class name to its group
	 */
	Map className2group;
    
	/**
	 * The list of dependencies which do not belong any defined group
	 */
    Set otherClasses = new HashSet();
    
	public void setDir(File d) {
		this.dir = d;
	}
	public void setOutput(File o) {
		this.output = o;
	}

    public void setShowContent(boolean v) {
        this.showGroupContent = v;
    }

	public Group createGroup() {
		Group group = new Group();
		group.setProject(getProject());
		groups.add(group);
		return group;
	}
    
	/**
	 * Produces a report with a simple text format, using tab for indentation
	 * @param out is output stream to use. This OutputStream is not closed at 
	 * the end of the method, but only flushed.
	 */
    private void toTxt(OutputStream out) throws IOException {
        StringBuffer sb = new StringBuffer();
        for (Iterator it = groups.iterator(); it.hasNext();) {
            Group cg = (Group) it.next();
            cg.toString(sb, "");
        }
        DataOutputStream dos = new DataOutputStream(out);
        dos.writeUTF(sb.toString());
        dos.flush();
    }

	/**
	 * Produces an xml report.
	 * @param out is output stream to use. This OutputStream is not closed at 
	 * the end of the method, but only flushed.
	 */
    private void toXml(OutputStream out) throws IOException {
        DocumentBuilder builder;
        try {
            builder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
        } catch (ParserConfigurationException e1) {
            throw new IOException(e1.getMessage());
        }
        Document doc = builder.newDocument();
        Element rootElement = doc.createElement("dependency");
        doc.appendChild(rootElement);
        for (Iterator it = groups.iterator(); it.hasNext();) {
            Group cg = (Group) it.next();
            cg.toXml(doc, rootElement);
        }
        Element gd = doc.createElement("otherClasses");
        gd.setAttribute("size", "" + otherClasses.size());
        rootElement.appendChild(gd);
        List classes = new ArrayList(otherClasses);
        Collections.sort(classes);
        for (Iterator it = classes.iterator(); it.hasNext();) {
            Element c = doc.createElement("class");
            c.setAttribute("name", (String) it.next());
            gd.appendChild(c);
        }
        Writer wri = null;
        wri = new BufferedWriter(new OutputStreamWriter(out, "UTF8"));
        wri.write("<?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n");
        (new DOMElementWriter()).write(rootElement, wri, 0, "  ");
        wri.flush();
    }

	public void execute() throws BuildException {
		// build data structure
		className2group = new HashMap();
		for (Iterator itg = groups.iterator(); itg.hasNext();) {
			Group group = (Group) itg.next();
            if (group.groupDir == null) {
                group.groupDir = dir;
            }
			for (Iterator itc = group.getSelectedFiles().iterator(); itc
					.hasNext();) {
				String fn = (String) itc.next();
				className2group.put(fn, group);
			}
		}
		// analyze dependencies of all classes from all group using an ASM 
		// visitor (inner class below)
		DependencyVisitor dv = new DependencyVisitor();
		for (Iterator itg = groups.iterator(); itg.hasNext();) {
			Group group = (Group) itg.next();
			log("Visit the group: " + group.name, Project.MSG_DEBUG);
			for (Iterator itc = group.getSelectedFiles().iterator(); itc
					.hasNext();) {
				String fn = (String) itc.next();
				dv.className = fn;
				dv.group = group;
				log("Visit the class: " + fn, Project.MSG_DEBUG);
				try {
					File f = new File(group.groupDir, fn);
					new ClassReader(new FileInputStream(f)).accept(dv, false);
				} catch (FileNotFoundException e) {
					log(e.getMessage(), Project.MSG_ERR);
				} catch (IOException e) {
					log(e.getMessage(), Project.MSG_ERR);
				}
			}
		}
		//build the report
        OutputStream out = null;
        try {
            boolean isXmlOutput = false;
            if (output == null) {
            	//on screen
                out = System.out;
            } else {
            	//on file
                log("Dependency analyse report: " + output.getAbsolutePath());
                out = new FileOutputStream(output);
                //check the file extension if the report must be XML
                isXmlOutput = output.getName().endsWith(".xml");
            }
            if (isXmlOutput) {
                toXml(out);
            } else {
                toTxt(out);
            }
            out.flush();
        } catch (IOException exc) {
            throw new BuildException("Unable to write log file", exc);
        } finally {
            if (out != System.out && out != System.err) {
                if (out != null) {
                    try {
                        out.close();
                    } catch (IOException e) {
                        // ignore
                    }
                }
            }
        }

		    
	}

	private class DependencyVisitor 
		implements AnnotationVisitor, SignatureVisitor,
			ClassVisitor, FieldVisitor, MethodVisitor {

		/**
		 * The group of the current analyzed class
		 */
		public Group group;
		
		/**
		 * The file name of the analyzed class
		 */
		public String className;
		
		private boolean addDependency(String dep) {
		    if (dep == null) {
		        return false;
		    }
			dep = dep.replace('/', File.separatorChar) + ".class";
			log("has dependency: " + dep, Project.MSG_DEBUG);
			Group depGroup = (Group) className2group.get(dep);
			if (depGroup == null) {
				// depends on a class which belong any group (ex: java.lang.Integer)
				log("no group: " + dep, Project.MSG_DEBUG);
                otherClasses.add(dep);
				return false;
			}
			if (depGroup == group) {
				// both classes belong the same group
				log("same group", Project.MSG_DEBUG);
				return false;
			}
			// new dependency between classes from different groups
			Map classDep = (Map) group.dependencies.get(depGroup);
			if (classDep == null) {
				// the first group has no existing dependency to the second one
				log("first dependency on the group: " + depGroup.name, Project.MSG_DEBUG);
				classDep = new HashMap();
				group.dependencies.put(depGroup, classDep);
			}
			List cns = (List) classDep.get(className);
			if (cns == null) {
				// This class has no dependency to another class
				log("first dependency for the class: " + className, Project.MSG_DEBUG);
				cns = new ArrayList();
				classDep.put(className, cns);
				cns.add(dep);
				return true;
			} else if (cns.contains(dep)) {
				log("existing dependency", Project.MSG_DEBUG);
				return false;
			} else {
				cns.add(dep);
				log("new dependency", Project.MSG_DEBUG);
				return true;
			}
		}

		// ClassVisitor

		public void visit(int version, int access, String name,
				String signature, String superName, String[] interfaces) {
			if (signature == null) {
				addDependency(superName);
				addNames(interfaces);
			} else {
				addSignature(signature);
			}
		}

		public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
			addDesc(desc);
			return this;
		}

		public void visitAttribute(Attribute attr) {
		}

		public FieldVisitor visitField(int access, String name, String desc,
				String signature, Object value) {
			if (signature == null) {
				addDesc(desc);
			} else {
				addTypeSignature(signature);
			}
			if (value instanceof Type)
				addType((Type) value);
			return this;
		}

		public MethodVisitor visitMethod(int access, String name, String desc,
				String signature, String[] exceptions) {
			if (signature == null) {
				addMethodDesc(desc);
			} else {
				addSignature(signature);
			}
			addNames(exceptions);
			return this;
		}

		public void visitSource(String source, String debug) {
		}

		public void visitInnerClass(String name, String outerName,
				String innerName, int access) {
			// addName( outerName);
			// addName( innerName);
		}

		public void visitOuterClass(String owner, String name, String desc) {
			// addName(owner);
			// addMethodDesc(desc);
		}

		// MethodVisitor

		public AnnotationVisitor visitParameterAnnotation(int parameter,
				String desc, boolean visible) {
			addDesc(desc);
			return this;
		}

		public void visitTypeInsn(int opcode, String desc) {
			if (desc.charAt(0) == '[')
				addDesc(desc);
			else
				addDependency(desc);
		}

		public void visitFieldInsn(int opcode, String owner, String name,
				String desc) {
			addDependency(owner);
			addDesc(desc);
		}

		public void visitMethodInsn(int opcode, String owner, String name,
				String desc) {
			addDependency(owner);
			addMethodDesc(desc);
		}

		public void visitLdcInsn(Object cst) {
			if (cst instanceof Type)
				addType((Type) cst);
		}

		public void visitMultiANewArrayInsn(String desc, int dims) {
			addDesc(desc);
		}

		public void visitLocalVariable(String name, String desc,
				String signature, Label start, Label end, int index) {
			addTypeSignature(signature);
		}

		public AnnotationVisitor visitAnnotationDefault() {
			return this;
		}

		public void visitCode() {
		}

		public void visitInsn(int opcode) {
		}

		public void visitIntInsn(int opcode, int operand) {
		}

		public void visitVarInsn(int opcode, int var) {
		}

		public void visitJumpInsn(int opcode, Label label) {
		}

		public void visitLabel(Label label) {
		}

		public void visitIincInsn(int var, int increment) {
		}

		public void visitTableSwitchInsn(int min, int max, Label dflt,
				Label[] labels) {
		}

		public void visitLookupSwitchInsn(Label dflt, int[] keys, Label[] labels) {
		}

		public void visitTryCatchBlock(Label start, Label end, Label handler,
				String type) {
			addDependency(type);
		}

		public void visitLineNumber(int line, Label start) {
		}

		public void visitMaxs(int maxStack, int maxLocals) {
		}

		// AnnotationVisitor

		public void visit(String name, Object value) {
			if (value instanceof Type)
				addType((Type) value);
		}

		public void visitEnum(String name, String desc, String value) {
			addDesc(desc);
		}

		public AnnotationVisitor visitAnnotation(String name, String desc) {
			addDesc(desc);
			return this;
		}

		public AnnotationVisitor visitArray(String name) {
			return this;
		}

		// SignatureVisitor

		public void visitFormalTypeParameter(String name) {
		}

		public SignatureVisitor visitClassBound() {
			return this;
		}

		public SignatureVisitor visitInterfaceBound() {
			return this;
		}

		public SignatureVisitor visitSuperclass() {
			return this;
		}

		public SignatureVisitor visitInterface() {
			return this;
		}

		public SignatureVisitor visitParameterType() {
			return this;
		}

		public SignatureVisitor visitReturnType() {
			return this;
		}

		public SignatureVisitor visitExceptionType() {
			return this;
		}

		public void visitBaseType(char descriptor) {
		}

		public void visitTypeVariable(String name) {
			// TODO verify
		}

		public SignatureVisitor visitArrayType() {
			return this;
		}

		public void visitClassType(String name) {
			addDependency(name);
		}

		public void visitInnerClassType(String name) {
			addDependency(name);
		}

		public void visitTypeArgument() {
		}

		public SignatureVisitor visitTypeArgument(char wildcard) {
			return this;
		}

		// common

		public void visitEnd() {
		}

		private void addNames(String[] names) {
			for (int i = 0; names != null && i < names.length; i++)
				addDependency(names[i]);
		}

		private void addDesc(String desc) {
			addType(Type.getType(desc));
		}

		private void addMethodDesc(String desc) {
			addType(Type.getReturnType(desc));
			Type[] types = Type.getArgumentTypes(desc);
			for (int i = 0; i < types.length; i++)
				addType(types[i]);
		}

		private void addType(Type t) {
			switch (t.getSort()) {
			case Type.ARRAY:
				addType(t.getElementType());
				break;
			case Type.OBJECT:
				addDependency(t.getClassName().replace('.', '/'));
				break;
			}
		}

		private void addSignature(String signature) {
			if (signature != null)
				new SignatureReader(signature).accept(this);
		}

		private void addTypeSignature(String signature) {
			if (signature != null)
				new SignatureReader(signature).acceptType(this);
		}
	}

}
