/*
 * NativeCapable is Dynamic Install and Loading Framework for Java Application and Applet.
 * Copyright (C) 2008  Shinobu Izumi
 * 
 * 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.1 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., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 */
package jp.ac.kyutech.ai.ylab.shiva.utils.nativecapable;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;

import javassist.CannotCompileException;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.NotFoundException;
import javassist.expr.ExprEditor;
import javassist.expr.MethodCall;

public class NaiveCapableClassLoader extends URLClassLoader {

	private ClassPool pool;

	private Collection<String> targets;

	private List<File> cachedJarFiles = new ArrayList<File>();

	private boolean delegateTargets = false;

	NaiveCapableClassLoader(URL[] urls, ClassLoader parent, File installDir) {
		super(urls, parent);
		pool = new ClassPool();
		pool.appendSystemPath();

		System.out.println("urls = " + Arrays.asList(urls));

		// for (URL url : urls) {
		// try {
		// URLConnection con = url.openConnection();
		// InputStream is = con.getInputStream();
		//
		// String name = url.toString();
		// name = name.replaceAll("\\\\", "/");
		// name = name.substring(name.lastIndexOf("/") + 1);
		//
		// File f = new File(installDir, name);
		// System.out.println("Coping jar : " + f.getAbsolutePath());
		// byte[] ba = Misc.readAll(is);
		// BufferedOutputStream os = new BufferedOutputStream(
		// new FileOutputStream(f));
		// os.write(ba);
		// os.flush();
		// os.close();
		// cachedJarFiles.add(f);
		// } catch (IOException e) {
		// }
		// }
	}

	@Override
	protected synchronized Class<?> loadClass(String name, boolean resolve)
			throws ClassNotFoundException {

		// Never load with this loader.
		if (name
				.equals("jp.ac.kyutech.ai.ylab.shiva.utils.nativecapable.NativeCapableModule")
				|| name
						.equals("jp.ac.kyutech.ai.ylab.shiva.utils.nativecapable.LibInstaller")) {
			return getParent().loadClass(name);
		}

		LABEL: if (targets != null) {
			if (!delegateTargets) {
				for (String str : targets) {
					if (name.startsWith(str)) {
						break LABEL;
					}
				}
				return getParent().loadClass(name);
			} else {
				for (String str : targets) {
					if (name.startsWith(str)) {
						return getParent().loadClass(name);
					}
				}
			}
		}

		Class<?> c = null;
		try {
			c = findClass(name);
		} catch (ClassNotFoundException cnfEx) {
			// nothing to do with
			System.out.println("try loading from url = " + name);
		}
		if (c == null) {
			// String path = name.replace('.', '/').concat(".class");
			// Resource res = ucp.getResource(path, false);

			c = super.findClass(name);
		}

		if (resolve) {
			resolveClass(c);
		}
		return c;
	}

	/**
	 * Finds a specified class. Classes will be modified as need.
	 */
	protected Class<?> findClass(String name) throws ClassNotFoundException {
		// System.out.println("find class : " + name);
		try {
			String path = name.replace('.', '/').concat(".class");
			InputStream is = null;
			try {
				is = getResourceAsStream(path);
			} catch (Exception e) {
				e.printStackTrace();
			}
			if (is == null) {
				for (File file : cachedJarFiles) {
					try {
						JarFile jf = new JarFile(file);
						JarEntry je = jf.getJarEntry(path);
						if (je == null) {
							continue;
						} else {
							System.out.println("loading : " + je);
							is = jf.getInputStream(je);
						}
					} catch (IOException e) {
						continue;
					}
					break;
				}
			}
			if (is != null) {
				CtClass cc = pool.makeClass(is);
				is.close();
				try {
					cc.instrument(new ExprEditor() {
						@Override
						public void edit(MethodCall m)
								throws CannotCompileException {
							if (m.getMethodName().equals("loadLibrary")) {
								try {
									if ("java.lang.System".equals(m.getMethod()
											.getDeclaringClass().getName())) {
										// m.replace("");
										m
												.replace("{"
														+ " try{"
														+ "$_ = $proceed($$); "
														+ "}catch(java.lang.Error e){"
														+ "System.out.println(e);"
														+ "System.out.println(\"might be loaded in advance\");"
														+ "}" + "}");
									}
								} catch (NotFoundException e) {
								}
							}
						}
					});
					// modify the CtClass object here
					byte[] b = cc.toBytecode();
					return defineClass(name, b, 0, b.length);
				} catch (CannotCompileException e) {
					System.err.println(cc);
					e.printStackTrace();
					throw new ClassNotFoundException();
				}
			} else {
				throw new ClassNotFoundException();
			}
		} catch (IOException e) {
			throw new ClassNotFoundException();
		}
	}

	/**
	 * Set collection of target packages and classes
	 * 
	 * @param targets
	 *            Collection of targets
	 */
	public void setTargets(Collection<String> targets) {
		this.targets = targets;
	}

	/**
	 * Set whether delegate targets to parent class loader or load with the
	 * loader.
	 * 
	 * @param delegateTargets
	 *            <br>
	 *            true - Delegate targets to parent loader. Others are loaded by
	 *            this loader.<br>
	 *            false - Load targets with this loader. Others are loaded by
	 *            parent loader.
	 * 
	 */
	public void setDelegateTargets(boolean delegateTargets) {
		this.delegateTargets = delegateTargets;
	}

	// public static void main(String[] args) throws Throwable {
	//
	// DLLInstaller di = new DLLInstaller();
	// try {
	// di.installDLLs("dllfiles.dat");
	// } catch (IOException e) {
	// e.printStackTrace();
	// }
	//
	// ClassLoader oldClsLoader = Thread.currentThread()
	// .getContextClassLoader();
	//
	// DllEnabledAppletClassLoader s = new DllEnabledAppletClassLoader(
	// new URL[0], oldClsLoader);
	// Thread.currentThread().setContextClassLoader(s);
	//
	// Class<?> c = s.loadClass("javassist.LibLoaderLoader");
	//
	// Object lll = c.newInstance();
	// Method m = lll.getClass().getMethod("load", new Class[0]);
	// m.invoke(lll, new Object[0]);
	//
	// }
}
