/*
 * This file is part of Nuts Framework.
 * Copyright(C) 2009-2012 Nuts Develop Team.
 *
 * Nuts Framework 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 3 of the License any later version.
 * 
 * Nuts Framework 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 Nuts Framework. If not, see <http://www.gnu.org/licenses/>.
 */
package nuts.core.lang;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.LineNumberReader;
import java.io.OutputStream;
import java.io.StringReader;
import java.net.URI;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;

import javax.tools.Diagnostic;
import javax.tools.DiagnosticCollector;
import javax.tools.DiagnosticListener;
import javax.tools.FileObject;
import javax.tools.ForwardingJavaFileManager;
import javax.tools.JavaCompiler;
import javax.tools.JavaFileManager;
import javax.tools.JavaFileObject;
import javax.tools.JavaFileObject.Kind;
import javax.tools.SimpleJavaFileObject;
import javax.tools.ToolProvider;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
 * DynamicClassLoader
 */
public class DynamicClassLoader extends ClassLoader {
	private static Log log = LogFactory.getLog(DynamicClassLoader.class);

    private static class StringJavaFileObject extends SimpleJavaFileObject {
		public final static String SCHEME = "string://";
		
		private String source;
		private ByteArrayOutputStream binary;

		public StringJavaFileObject(String name, Kind kind) {
			super(URI.create(SCHEME + '/' + name.replace('.', '/') + kind.extension), kind);
		}

		public StringJavaFileObject(String className, String source) {
			super(URI.create(SCHEME + '/' + className.replace('.', '/') + Kind.SOURCE.extension),
					Kind.SOURCE);
			this.source = source;
		}

		@Override
		public CharSequence getCharContent(boolean ignoreEncodingErrors) {
			return source;
		}

		@Override
		public OutputStream openOutputStream() throws IOException {
			binary = new ByteArrayOutputStream();
			return binary;
		}

		public byte[] getBinary() {
			return binary.toByteArray();
		}

	    @Override
	    public String toString() {
	        return toUri().toString();
	    }
	}

	private static class StringJavaFileManager extends ForwardingJavaFileManager<JavaFileManager> {
		private DynamicClassLoader classLoader;
		
		public StringJavaFileManager(JavaCompiler compiler,
				DiagnosticListener<? super JavaFileObject> listener,
				DynamicClassLoader classLoader) {
			super(compiler.getStandardFileManager(listener, null, null));
			this.classLoader = classLoader;
		}

		@Override
		public JavaFileObject getJavaFileForOutput(Location location, String className, Kind kind,
				FileObject sibling) throws IOException {
			synchronized (classLoader.javaObjects) {
				StringJavaFileObject javaObj = new StringJavaFileObject(className, kind);
				classLoader.javaObjects.put(className, javaObj);
				return javaObj;
			}
		}

		@Override
		public ClassLoader getClassLoader(Location location) {
			return classLoader.delegate;
		}
	}
	
	private static void logSource(String className, String source) throws Exception {
		if (!log.isTraceEnabled()) {
			return;
		}

		StringBuilder sb = new StringBuilder();
		sb.append("Compile: ").append(className).append('\n');

		LineNumberReader lnr = new LineNumberReader(new StringReader(source));
		String line;
		while ((line = lnr.readLine()) != null) {
			sb.append(StringUtils.rightPad(String.valueOf(lnr.getLineNumber()), 5));
			sb.append(":  ").append(line).append('\n');
		}
		log.trace(sb.toString());
	}
	
	private class JavaObjectClassLoader extends ClassLoader {
		private final Map<String, StringJavaFileObject> javaObjects;

		public JavaObjectClassLoader(final ClassLoader pParent, final Map<String, StringJavaFileObject> javaObjects) {
	        super(pParent);
	        this.javaObjects = javaObjects;
	    }

		@Override
		protected Class<?> findClass(String name) throws ClassNotFoundException {
			synchronized (javaObjects) {
				StringJavaFileObject obj = javaObjects.get(name);
				if (obj == null) {
					throw new ClassNotFoundException(name);
				}

				byte[] b = obj.getBinary();
				Class<?> clazz = super.defineClass(name, b, 0, b.length);

				return clazz;
			}
		}
	}

	private List<String> options;
	private final ClassLoader parent;
    private ClassLoader delegate;
	private Map<String, StringJavaFileObject> javaObjects = new HashMap<String, StringJavaFileObject>();

    public DynamicClassLoader() {
        this(ClassUtils.getClassLoader());
        
        //runtime test
        new StringJavaFileObject("test", Kind.SOURCE);
    }

    public DynamicClassLoader(final ClassLoader parent) {
        super(parent);
        this.parent = parent;        
        this.delegate = new JavaObjectClassLoader(parent, javaObjects);

        setCompileOptions();
    }

	protected void setCompileOptions() {
        options = new ArrayList<String>();
		if (log.isTraceEnabled()) {
	        options.add("-verbose");
		}

		try {
			URL[] urls = ((URLClassLoader)parent).getURLs();
			StringBuilder buf = new StringBuilder(1000);
			buf.append(".");
			String separator = System.getProperty("path.separator");
			for (URL url : urls) {
				buf.append(separator).append(url.getFile());
			}
			options.add("-classpath");
			options.add(buf.toString());
		}
		catch (Exception e) {
			log.warn("Failed to set classpath", e);
		}
	}

	/**
	 * Compile source & load class
	 * @param className class name
	 * @param source source
	 * @return class
	 * @throws Exception if an error occurs
	 */
	public Class<?> loadClass(String className, String source) throws Exception {
		defineClass(className, source);
		return loadClass(className);
	}

	/**
	 * Compile java source
	 * @param className class name
	 * @param source source
	 * @throws Exception if an error occurs
	 */
	public void defineClass(String className, String source) throws Exception {
		try {
			delegate.loadClass(className);
			log.debug("Reload class - " + className);
	        delegate = new JavaObjectClassLoader(parent, javaObjects);
		}
		catch (ClassNotFoundException e) {
		}
		
		logSource(className, source);
		
		StringJavaFileObject srcObj = new StringJavaFileObject(className, source);
		
		JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
		DiagnosticCollector<JavaFileObject> collector = new DiagnosticCollector<JavaFileObject>();
		JavaFileManager fileManager = new StringJavaFileManager(compiler, collector, this);

		try {
			List<JavaFileObject> sources = new ArrayList<JavaFileObject>();
			sources.add(srcObj);

			JavaCompiler.CompilationTask task = compiler.getTask(null, fileManager, collector,
					options, null, sources);

			if (!task.call()) {
				StringBuilder error = new StringBuilder();
				error.append("---- Compile ERROR ----\n");
				for (Diagnostic<? extends JavaFileObject> diagnostic : collector.getDiagnostics()) {
					error.append(diagnostic.getMessage(Locale.getDefault())).append('\n');
				}
				error.append("---- Source ----\n").append(source);
				throw new RuntimeException(error.toString());
			}
		}
		finally {
			try {
				fileManager.close();
			}
			catch (Exception e) {
				// ignore
			}
		}
	}
	
    public void clearAssertionStatus() {
        delegate.clearAssertionStatus();
    }
    public URL getResource(String name) {
        return delegate.getResource(name);
    }
    public InputStream getResourceAsStream(String name) {
        return delegate.getResourceAsStream(name);
    }
    public Class<?> loadClass(String name) throws ClassNotFoundException {
        return delegate.loadClass(name);
    }
    public void setClassAssertionStatus(String className, boolean enabled) {
        delegate.setClassAssertionStatus(className, enabled);
    }
    public void setDefaultAssertionStatus(boolean enabled) {
        delegate.setDefaultAssertionStatus(enabled);
    }
    public void setPackageAssertionStatus(String packageName, boolean enabled) {
        delegate.setPackageAssertionStatus(packageName, enabled);
    }
}

