/* *******************************************************************
 * Copyright (c) 2004 IBM Corporation
 * All rights reserved. 
 * This program and the accompanying materials are made available 
 * under the terms of the Common Public License v1.0 
 * which accompanies this distribution and is available at 
 * http://www.eclipse.org/legal/cpl-v10.html 
 *  
 * Contributors: 
 *     Matthew Webster, Adrian Colyer, 
 *     Martin Lippert     initial implementation 
 * ******************************************************************/

package org.aspectj.weaver.tools;

import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.StringTokenizer;

import org.aspectj.bridge.AbortException;
import org.aspectj.bridge.IMessage;
import org.aspectj.bridge.IMessageHandler;
import org.aspectj.bridge.IMessage.Kind;
import org.aspectj.util.FileUtil;
import org.aspectj.weaver.IClassFileProvider;
import org.aspectj.weaver.IWeaveRequestor;
import org.aspectj.weaver.ResolvedTypeX;
import org.aspectj.weaver.bcel.BcelWeaver;
import org.aspectj.weaver.bcel.BcelWorld;
import org.aspectj.weaver.bcel.UnwovenClassFile;

/**
 * This adaptor allows the AspectJ compiler to be embedded in an existing
 * system to facilitate load-time weaving. It provides an interface for a
 * weaving class loader to provide a classpath to be woven by a set of
 * aspects. A callback is supplied to allow a class loader to define classes
 * generated by the compiler during the weaving process.
 * <p>
 * A weaving class loader should create a <code>WeavingAdaptor</code> before
 * any classes are defined, typically during construction. The set of aspects 
 * passed to the adaptor is fixed for the lifetime of the adaptor although the
 * classpath can be augmented. A system property can be set to allow verbose
 * weaving messages to be written to the console.
 *
 */
public class WeavingAdaptor {

	/**
	 * System property used to turn on verbose weaving messages 
	 */
	public static final String WEAVING_ADAPTOR_VERBOSE = "aj.weaving.verbose"; 

	private boolean enabled = true;
	private boolean verbose = getVerbose();
	private BcelWorld bcelWorld = null;
	private BcelWeaver weaver = null;
	private IMessageHandler messageHandler = null;
	private GeneratedClassHandler generatedClassHandler;
	private Map generatedClasses = new HashMap(); /* String -> UnwovenClassFile */ 

	/**
	 * Construct a WeavingAdaptor with a reference to a weaving class loader. The
	 * adaptor will automatically search the class loader hierarchy to resolve
	 * classes. The adaptor will also search the hierarchy for WeavingClassLoader
	 * instances to determine the set of aspects to be used ofr weaving. 
     * @param loader instance of <code>ClassLoader</code>
	 */
	public WeavingAdaptor (WeavingClassLoader loader) {
//		System.err.println("? WeavingAdaptor.<init>(" + loader +"," + aspectURLs.length + ")");
		generatedClassHandler = loader;
		init(getFullClassPath((ClassLoader)loader),getFullAspectPath((ClassLoader)loader/*,aspectURLs*/));
	}

	/**
	 * Construct a WeavingAdator with a reference to a
	 * <code>GeneratedClassHandler</code>, a full search path for resolving 
	 * classes and a complete set of aspects. The search path must include
	 * classes loaded by the class loader constructing the WeavingAdaptor and
	 * all its parents in the hierarchy.   
	 * @param handler <code>GeneratedClassHandler</code>
     * @param classURLs the URLs from which to resolve classes
     * @param aspectURLs the aspects used to weave classes defined by this class loader
	 */
	public WeavingAdaptor (GeneratedClassHandler handler, URL[] classURLs, URL[] aspectURLs) {
//		System.err.println("? WeavingAdaptor.<init>()");
		generatedClassHandler = handler;
		init(FileUtil.makeClasspath(classURLs),FileUtil.makeClasspath(aspectURLs));
	}
	
	private List getFullClassPath (ClassLoader loader) {
		List list = new LinkedList();
		for (; loader != null; loader = loader.getParent()) {
			if (loader instanceof URLClassLoader) {
				URL[] urls = ((URLClassLoader)loader).getURLs();
				list.addAll(0,FileUtil.makeClasspath(urls));
			}
			else {
				if (verbose) System.err.println("WeavingAdaptor: Warning - could not determine classpath for " + loader); 
			}
		}

		list.addAll(0,makeClasspath(System.getProperty("sun.boot.class.path")));

		return list;
	}
	
	private List getFullAspectPath (ClassLoader loader) {
		List list = new LinkedList();
		for (; loader != null; loader = loader.getParent()) {
			if (loader instanceof WeavingClassLoader) {
				URL[] urls = ((WeavingClassLoader)loader).getAspectURLs();
				list.addAll(0,FileUtil.makeClasspath(urls));
			}
		}

		return list;
	}
	
	private static boolean getVerbose () {
		return Boolean.getBoolean(WEAVING_ADAPTOR_VERBOSE);
	}
	
	private void init(List classPath, List aspectPath) {
		if (verbose) System.out.println("WeavingAdaptor: classPath='" + classPath + "'");
		
		// make sure the weaver can find all types...
		messageHandler = new MessageHandler();
		bcelWorld = new BcelWorld(classPath,messageHandler,null);
		bcelWorld.setXnoInline(false);
		bcelWorld.getLint().loadDefaultProperties();

		weaver = new BcelWeaver(bcelWorld);
		registerAspectLibraries(aspectPath);
	}
	
	/**
	 * Appends URL to path used by the WeavingAdptor to resolve classes
	 * @param url to be appended to search path
	 */
	public void addURL(URL url) {
		try {
			weaver.addLibraryJarFile(new File(url.getPath()));
		}
		catch (IOException ex) {
		}
	}

	/**
	 * Weave a class using aspects previously supplied to the adaptor.
	 * @param name the name of the class
	 * @param bytes the class bytes
	 * @return the woven bytes
     * @exception IOException weave failed
	 */
	public byte[] weaveClass (String name, byte[] bytes) throws IOException {
		if (shouldWeave(name)) {
			bytes = getWovenBytes(name, bytes);
		}
		return bytes;
	}

	private boolean shouldWeave (String name) {
		name = name.replace('/','.');
		boolean b = (enabled && !generatedClasses.containsKey(name) && shouldWeaveName(name) && shouldWeaveAspect(name));
		if (verbose) System.out.println("WeavingAdaptor: shouldWeave('" + name + "') " + b);
		return b;
	}
	
	private boolean shouldWeaveName (String name) {
		return !((name.startsWith("org.apache.bcel.") || name.startsWith("org.aspectj.") || name.startsWith("java.") || name.startsWith("javax.")));
	}
	
	private boolean shouldWeaveAspect (String name) {
		ResolvedTypeX type = bcelWorld.resolve(name);
		return (type == null || !type.isAspect());
	}

	/**
	 * Weave a set of bytes defining a class. 
	 * @param name the name of the class being woven
	 * @param bytes the bytes that define the class
	 * @return byte[] the woven bytes for the class
	 * @throws IOException
	 * @throws FileNotFoundException
	 */
	private byte[] getWovenBytes(String name, byte[] bytes) throws IOException {
		WeavingClassFileProvider wcp = new WeavingClassFileProvider(name,bytes);
		weaver.weave(wcp);
		return wcp.getBytes();		
		
//		UnwovenClassFile unwoven = new UnwovenClassFile(name,bytes);
//		
//		// weave
//		BcelObjectType bcelType = bcelWorld.addSourceObjectType(unwoven.getJavaClass());
//		LazyClassGen woven = weaver.weaveWithoutDump(unwoven,bcelType);
//		
//		byte[] wovenBytes = woven != null ? woven.getJavaClass(bcelWorld).getBytes() : bytes;
//		return wovenBytes;
	}
	
	private void registerAspectLibraries(List aspectPath) {
//		System.err.println("? WeavingAdaptor.registerAspectLibraries(" + aspectPath + ")");
		for (Iterator i = aspectPath.iterator(); i.hasNext();) {
			String lib = (String)i.next();
			File libFile = new File(lib);
			if (libFile.isFile() && lib.endsWith(".jar")) {
				try {
					if (verbose) System.out.println("WeavingAdaptor: adding aspect '" + lib + "' to weaver");
					addAspectLibrary(new File(lib));
				} catch (IOException ioEx) {
					if (verbose) System.err.println(
						"WeavingAdaptor: Warning - could not load aspect path entry " 
						+ lib + " : " + ioEx);
				}
			} else {
				if (verbose) System.err.println(
					"WeavingAdaptor: Warning - ignoring aspect path entry: " + lib);
			}
		}
		
		weaver.prepareForWeave();
	}

	/*
	 * Register an aspect library with this classloader for use during
	 * weaving. This class loader will also return (unmodified) any of the
	 * classes in the library in response to a <code>findClass()</code> request.
	 * The library is not required to be on the weavingClasspath given when this
	 * classloader was constructed. 
	 * @param aspectLibraryJarFile a jar file representing an aspect library
	 * @throws IOException
	 */
	private void addAspectLibrary(File aspectLibraryJarFile) throws IOException {
		weaver.addLibraryJarFile(aspectLibraryJarFile);
//		weaver.prepareForWeave();
	}
	
	private static List makeClasspath(String cp) {
		List ret = new ArrayList();
		if (cp != null) {
			StringTokenizer tok = new StringTokenizer(cp,File.pathSeparator);
			while (tok.hasMoreTokens()) {
				ret.add(tok.nextToken());
			}
		}
		return ret;
	}

	/**
	 * Processes messages arising from weaver operations. 
	 * Tell weaver to abort on any non-informational error.
	 */
	private class MessageHandler implements IMessageHandler {

		public boolean handleMessage(IMessage message) throws AbortException {
			if (!isIgnoring(message.getKind())) {
				System.err.println(message.getSourceLocation()+": "+message.getKind()+" "+message.getMessage());
				if (message.getKind() == IMessage.ERROR) throw new AbortException(message);
			}
			return true;
		}

		public boolean isIgnoring(Kind kind) {
			if (verbose) return false;
			else         return ((kind == IMessage.INFO) || (kind == IMessage.DEBUG));
		}
	}

	private class WeavingClassFileProvider implements IClassFileProvider {

		private List unwovenClasses = new ArrayList(); /* List<UnovenClassFile> */
		private UnwovenClassFile wovenClass;

		public WeavingClassFileProvider (String name, byte[] bytes) {
			UnwovenClassFile unwoven = new UnwovenClassFile(name,bytes);
			unwovenClasses.add(unwoven);
			bcelWorld.addSourceObjectType(unwoven.getJavaClass());
		}

		public byte[] getBytes () {
			return wovenClass.getBytes();
		}

		public Iterator getClassFileIterator() {
			return unwovenClasses.iterator();
		}

		public IWeaveRequestor getRequestor() {
			return new IWeaveRequestor() {

				public void acceptResult(UnwovenClassFile result) {
					if (wovenClass == null) {
						wovenClass = result;
					}
					
					/* Classes generated by weaver e.g. around closure advice */
					else {
						String className = result.getClassName();
						generatedClasses.put(className,result);
						generatedClassHandler.acceptClass(className,result.getBytes());
					}
				}

				public void processingReweavableState() {	}

				public void addingTypeMungers() {}

				public void weavingAspects() {}

				public void weavingClasses() {}

				public void weaveCompleted() {}
			};				
		}
	}
}
