package cn.bran.japid.template;

import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

import cn.bran.japid.compiler.OpMode;
import cn.bran.japid.compiler.TranslateTemplateTask;
import cn.bran.japid.rendererloader.RendererClass;
import cn.bran.japid.rendererloader.RendererCompiler;
import cn.bran.japid.rendererloader.TemplateClassLoader;
import cn.bran.japid.util.DirUtil;
import cn.bran.japid.util.JapidFlags;
import cn.bran.japid.util.StackTraceUtils;

public class JapidRenderer {

	public static JapidTemplateBaseWithoutPlay getRenderer(String name) {
		Class<? extends JapidTemplateBaseWithoutPlay> c = getClass(name);
		try {
			return c.newInstance();
		} catch (Exception e) {
			throw new RuntimeException(e);
		}
	}

	/**
	 * Get a newly loaded class for the template renderer
	 * 
	 * @param name
	 * @return
	 */
	public static Class<? extends JapidTemplateBaseWithoutPlay> getClass(String name) {

		refreshClasses(name);

		RendererClass rc = classes.get(name);
		if (rc == null)
			throw new RuntimeException("renderer class not found: " + name);
		else {
			try {
				Class<? extends JapidTemplateBaseWithoutPlay> cls = rc.getClz();
				if (cls != null) {
					return cls;
				}
			} catch (Exception e) {
				throw new RuntimeException(e);
			}
		}

		// always clear the mark to reload all
		for (String c : classes.keySet()) {
			RendererClass rendererClass = classes.get(c);
			rendererClass.setLastUpdated(0);
		}
		
		ClassLoader cl = _parentClassLoader == null ? JapidRenderer.class.getClassLoader() : _parentClassLoader;
		// do I need to new instance of TemplateClassLoader for each invocation? likely...
		TemplateClassLoader classReloader = new TemplateClassLoader(cl);
		try {
			Class<JapidTemplateBaseWithoutPlay> loadClass = (Class<JapidTemplateBaseWithoutPlay>) classReloader.loadClass(name);
			rc.setClz(loadClass);
			return loadClass;
		} catch (ClassNotFoundException e) {
			throw new RuntimeException(e);
		}

	}

	static boolean timeToRefresh() {
		long now = System.currentTimeMillis();
		if (now - lastRefreshed > refreshInterval) {
			lastRefreshed = now;
			return true;
		} else
			return false;

	}

	static synchronized void refreshClasses(String targetClass) {
		if (!timeToRefresh())
			return;

		try {
			// find out all removed classes
			String[] allHtml = DirUtil.getAllTemplateHtmlFiles(new File(templateRoot));
			Set<String> currentClassesOnDir = createNameSet(allHtml);
			Set<String> tmp = new HashSet<String>(currentClassesOnDir);

			Set<String> keySet = classes.keySet();
			tmp.removeAll(keySet); // added html

			removeRemoved(currentClassesOnDir, keySet);

			for (String c : tmp) {
				RendererClass rc = newRendererClass(c);
				classes.put(c, rc);
			}
			// now all the class set size is up to date

			// now update any Java source code
			List<File> gen = gen(templateRoot);

			// this would include both new and updated java
			Set<String> updatedClasses = new HashSet<String>();
			if (gen.size() > 0) {
				// int i = 0;
				for (File f : gen) {
					String className = getClassName(f);
					updatedClasses.add(className);
					RendererClass rendererClass = classes.get(className);
					if (rendererClass == null) {
						// this should not happen, since
						throw new RuntimeException("any new key should have been in the classes container: " + className);
						// rendererClass = newRendererClass(className);
						// classes.put(className, rendererClass);
					}
					rendererClass.setSourceCode(readSource(f));
					removeInnerClasses(className);
					cleanClassHolder(rendererClass);
				}
			}

			// find all render class without bytecode
			for (Iterator<String> i = classes.keySet().iterator(); i.hasNext();) {
				String k = i.next();
				RendererClass rc = classes.get(k);
				if (rc.getSourceCode() == null) {
					if (!rc.getClassName().contains("$")) {
						rc.setSourceCode(getSourceCode(k));
						cleanClassHolder(rc);
						updatedClasses.add(k);
					} else {
						rc.setLastUpdated(0);
					}
				} else {
					if (rc.getBytecode() == null) {
						cleanClassHolder(rc);
						updatedClasses.add(k);
					}
				}
			}

			// compile all
			if (updatedClasses.size() > 0) {
				String[] names = new String[updatedClasses.size()];
				int i = 0;
				for (String s : updatedClasses) {
					names[i++] = s;
				}
				long t = System.currentTimeMillis();
				compiler.compile(names);
				howlong("compile time for " + names.length + " classes", t);

				// clear the global class cache
				for (String k : classes.keySet()) {
					RendererClass rc = classes.get(k);
					rc.setClz(null);
				}
			}
		} catch (Exception e) {
			throw new RuntimeException(e);
		}
	}

	public static void removeInnerClasses(String className) {
		for (Iterator<String> i = classes.keySet().iterator(); i.hasNext();) {
			String k = i.next();
			if (k.startsWith(className + "$")) {
				i.remove();
			}
		}
	}

	/**
	 * @param currentClassesOnDir
	 * @param keySet
	 */
	public static void removeRemoved(Set<String> currentClassesOnDir, Set<String> keySet) {
		// need to consider inner classes
		// keySet.retainAll(currentClassesOnDir);

		for (Iterator<String> i = keySet.iterator(); i.hasNext();) {
			String k = i.next();
			int q = k.indexOf('$');
			if (q > 0) {
				k = k.substring(0, q);
			}
			if (!currentClassesOnDir.contains(k)) {
				i.remove();
			}
		}
	}

	// <classname RendererClass>
	public final static Map<String, RendererClass> classes = new ConcurrentHashMap<String, RendererClass>();
	public static TemplateClassLoader crlr;

	public static TemplateClassLoader getCrlr() {
		return crlr;
	}

	public static RendererCompiler compiler;
	public static String templateRoot = "plainjapid";
	public static final String JAPIDVIEWS = "japidviews";
	public static String sep = File.separator;
	public static String japidviews = templateRoot + sep + JAPIDVIEWS + sep;
	// such as java.utils.*
	public static List<String> importlines = new ArrayList<String>();
	public static int refreshInterval;
	public static long lastRefreshed;
	private static boolean inited;

	public static boolean isInited() {
		return inited;
	}

	private static OpMode opMode;

	public static OpMode getOpMode() {
		return opMode;
	}

	static void howlong(String string, long t) {
		if (JapidFlags.verbose) System.out.println(string + ":" + (System.currentTimeMillis() - t) + "ms");
	}

	/**
	 * @param rendererClass
	 */
	static void cleanClassHolder(RendererClass rendererClass) {
		rendererClass.setBytecode(null);
		rendererClass.setClz(null);
		rendererClass.setLastUpdated(0);
	}

	static Set<String> createNameSet(String[] allHtml) {
		// the names start with template root
		Set<String> names = new HashSet<String>();
		for (String f : allHtml) {
			names.add(getClassName(new File(f)));
		}
		return names;
	}

	static String getSourceCode(String k) {
		String pathname = templateRoot + sep + k;
		pathname = pathname.replace(".", sep);
		File f = new File(pathname + ".java");
		return readSource(f);
	}

	/**
	 * @param c
	 * @return
	 */
	static RendererClass newRendererClass(String c) {
		RendererClass rc = new RendererClass();
		rc.setClassName(c);
		// the source code of the Java file might not be available yet
		// rc.setSourceCode(getSouceCode(c));
		rc.setLastUpdated(0);
		return rc;
	}

	static String readSource(File f) {
		try {
			FileInputStream fis = new FileInputStream(f);
			BufferedInputStream bis = new BufferedInputStream(fis);
			BufferedReader br = new BufferedReader(new InputStreamReader(bis, "UTF-8"));
			StringBuilder b = new StringBuilder();
			String line = null;
			while ((line = br.readLine()) != null) {
				b.append(line + "\n");
			}
			br.close();
			return b.toString();
		} catch (Exception e) {
			throw new RuntimeException(e);
		}
	}

	static String getClassName(File f) {
		String path = f.getPath();
		String substring = path.substring(path.indexOf(JAPIDVIEWS));
		substring = substring.replace('/', '.').replace('\\', '.');
		if (substring.endsWith(".java")) {
			substring = substring.substring(0, substring.length() - 5);
		} else if (substring.endsWith(".html")) {
			substring = substring.substring(0, substring.length() - 5);
		}
		return substring;
	}

	/**
	 * set the interval to check template changes.
	 * 
	 * @param i
	 *            the interval in seconds. Set it to {@link Integer.MAX_VALUE}
	 *            to effectively disable refreshing
	 */
	static void setRefreshInterval(int i) {
		refreshInterval = i * 1000;
	}

	static void setTemplateRoot(String root) {
		templateRoot = root;
		japidviews = templateRoot + sep + JAPIDVIEWS + sep;
	}

	/**
	 * The entry point for the command line tool japid.bat and japid.sh
	 * 
	 * The "gen" and "regen" are probably the most useful ones.
	 * 
	 * @param args
	 * @throws IOException
	 */
	public static void main(String[] args) throws IOException {
		if (args.length > 0) {
			String arg0 = args[0];

			setTemplateRoot(".");
			if ("gen".equals(arg0)) {
				gen(templateRoot);
			} else if ("regen".equals(arg0)) {
				regen(templateRoot);
			} else if ("clean".equals(arg0)) {
				delAllGeneratedJava(getJapidviewsDir(templateRoot));
			} else if ("mkdir".equals(arg0)) {
				mkdir(templateRoot);
			} else if ("changed".equals(arg0)) {
				changed(japidviews);
			} else {
				System.err.println("help:  optionas are: gen, regen, mkdir and clean");
			}
		} else {
			System.err.println("help:  optionas are: gen, regen, mkdir and clean");
		}
	}

	private static void changed(String root) {
		List<File> changedFiles = DirUtil.findChangedSrcFiles(new File(root));
		for (File f : changedFiles) {
			System.out.println("changed: " + f.getPath());
		}

	}

	/**
	 * create the basic layout: app/japidviews/_javatags app/japidviews/_layouts
	 * app/japidviews/_tags
	 * 
	 * then create a dir for each controller. //TODO
	 * 
	 * @throws IOException
	 * 
	 */
	static List<File> mkdir(String root) throws IOException {
		String japidviewsDir = getJapidviewsDir(root);
		File layouts = new File(japidviewsDir + "_layouts");
		if (!layouts.exists()) {
			boolean mkdirs = layouts.mkdirs();
			assert mkdirs == true;
			log("created: " + layouts.getPath());
		}

		File tags = new File(japidviewsDir + "_tags");
		if (!tags.exists()) {
			boolean mkdirs = tags.mkdirs();
			assert mkdirs == true;
			log("created: " + tags.getPath());
		}

		File[] dirs = new File[] { layouts, tags };
		List<File> res = new ArrayList<File>();
		res.addAll(Arrays.asList(dirs));

		return res;

	}

	/**
	 * @param root
	 * @return
	 */
	private static String getJapidviewsDir(String root) {
		return root + sep + JAPIDVIEWS + sep;
	}

	static void regen() throws IOException {
		regen(templateRoot);
	}

	public static void regen(String root) throws IOException {
		delAllGeneratedJava(getJapidviewsDir(root));
		gen(root);
	}

	static void delAllGeneratedJava(String pathname) {
		String[] javas = DirUtil.getAllFileNames(new File(pathname), new String[] { "java" });

		for (String j : javas) {
			log("removed: " + pathname + j);
			boolean delete = new File(pathname + File.separatorChar + j).delete();
			if (!delete)
				throw new RuntimeException("file was not deleted: " + j);
		}
		// log("removed: all none java tag java files in " +
		// JapidPlugin.JAPIDVIEWS_ROOT);
	}

	/**
	 * update the java files from the html files, for the changed only
	 * 
	 * @throws IOException
	 */
	static List<File> gen(String packageRoot) throws IOException {
		// mkdir(packageRoot);
		// moved to reloadChanged
		List<File> changedFiles = reloadChanged(packageRoot);
		if (changedFiles.size() > 0) {
//			for (File f : changedFiles) {
//				// log("updated: " + f.getName().replace("html", "java"));
//			}
		} else {
			log("All java files are up to date.");
		}

		rmOrphanJava(packageRoot);
		return changedFiles;
	}

	/**
	 * @param root
	 *            the package root "/"
	 * @return the updated Java files.
	 */
	static List<File> reloadChanged(String root) {
		try {
			mkdir(root);
		} catch (Exception e) {
			throw new RuntimeException(e);
		}

		TranslateTemplateTask t = new TranslateTemplateTask();
		t.setUsePlay(false);
		File rootDir = new File(root);
		t.setPackageRoot(rootDir);
		t.setInclude(new File(japidviews));
		if (DirUtil.hasLayouts(root))
			t.addImport("japidviews ._layouts.*");
		if (DirUtil.hasJavaTags(root))
			t.addImport(root + "/" + "._javatags.*");
		if (DirUtil.hasTags(root))
			t.addImport("japidviews ._tags.*");

		for (String imp : importlines) {
			t.addImport(imp);
		}

		t.execute();
		// List<File> changedFiles = t.getChangedFiles();
		return t.getChangedTargetFiles();
		// return changedFiles;
	}

	/**
	 * get all the java files in a dir with the "java" removed
	 * 
	 * @return
	 */
	static File[] getAllJavaFilesInDir(String root) {
		// from source files only
		String[] allFiles = DirUtil.getAllFileNames(new File(root), new String[] { ".java" });
		File[] fs = new File[allFiles.length];
		int i = 0;
		for (String f : allFiles) {
			String path = f.replace(".java", "");
			fs[i++] = new File(path);
		}
		return fs;
	}

	/**
	 * delete orphaned java artifacts from the japidviews directory
	 * 
	 * @param packageRoot
	 * 
	 * @return
	 */
	static boolean rmOrphanJava(String packageRoot) {

		boolean hasRealOrphan = false;
		try {
			String pathname = getJapidviewsDir(packageRoot);
			File src = new File(pathname);
			if (!src.exists()) {
				log("Could not find required Japid root directory: " + pathname);
				return hasRealOrphan;
			}

			Set<File> oj = DirUtil.findOrphanJava(src, null);
			for (File j : oj) {
				String path = j.getPath();
				// log("found: " + path);
				hasRealOrphan = true;
				String realfile = pathname + File.separator + path;
				File file = new File(realfile);
				boolean r = file.delete();
				if (r)
					log("deleted orphan " + realfile);
				else
					log("failed to delete: " + realfile);
			}

		} catch (Exception e) {
			e.printStackTrace();
		}
		return hasRealOrphan;
	}

	static List<File> reloadChanged() {
		return reloadChanged(templateRoot);
	}

	static void log(String m) {
		if (JapidFlags.verbose) System.out.println("[JapidRender]: " + m);
	}

	static void gen() {
		if (templateRoot == null) {
			throw new RuntimeException("the template root directory must be set");
		} else {
			try {
				gen(templateRoot);
			} catch (IOException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
		}
	}

	// /**
	// * set to development mode
	// */
	// public static void setDevMode() {
	// devMode = true;
	// }

	// /**
	// * set to production mode
	// */
	// public static void setProdMode() {
	// devMode = false;
	// }
	//

	public static boolean isDevMode() {
		return opMode == OpMode.dev;
	}

	static String removeSemi(String imp) {
		imp = imp.trim();
		if (imp.endsWith(";")) {
			imp = imp.substring(0, imp.length() - 1);
		}
		return imp;
	}

	//
	// public <T extends JapidTemplateBaseWithoutPlay> String render(Class<T> c,
	// Object... args) {
	// int modifiers = c.getModifiers();
	// if (Modifier.isAbstract(modifiers)) {
	// throw new
	// RuntimeException("Cannot init the template class since it's an abstract class: "
	// + c.getName());
	// }
	// try {
	// Constructor<T> ctor = c.getConstructor(StringBuilder.class);
	// StringBuilder sb = new StringBuilder(8000);
	// T t = ctor.newInstance(sb);
	// RenderResult rr = RenderInvokerUtils.render(t, args);
	// return rr.getContent().toString();
	// } catch (NoSuchMethodException e) {
	// throw new
	// RuntimeException("Could not match the arguments with the template args.");
	// } catch (InstantiationException e) {
	// throw new
	// RuntimeException("Could not instantiate the template object. Abstract?");
	// } catch (Exception e) {
	// if (e instanceof RuntimeException)
	// throw (RuntimeException) e;
	// else
	// throw new RuntimeException("Could not invoke the template object: " + e);
	// }
	// }

	public static void addStaticImportLine(String imp) {
		importlines.add(" static " + removeSemi(imp));
	}

	/**
	 * 
	 * @param imp
	 *            the part after import, no ";"
	 */
	public static void addImportLine(String imp) {
		importlines.add(removeSemi(imp));
	}

	/**
	 * one of the ways to invoke a renderer
	 * 
	 * @param cls
	 * @param args
	 * @return
	 */
	public String render(Class<? extends JapidTemplateBaseWithoutPlay> cls, Object... args) {
		return JapidPlainController.renderWith(cls, args);
	}

	/**
	 * The <em>required</em> initialization step in using the JapidRender.
	 * 
	 * @param opMode
	 *            the operational mode of Japid. When set to OpMode.prod, it's
	 *            assumed that all Java derivatives are already been generated
	 *            and used directly. When set to OpMode.dev, and using
	 *            none-static linking to using the renderer, file system changes
	 *            are detected for every rendering request given the refresh
	 *            interval is respected. New Java files are generated and
	 *            compiled and new classes are loaded to serve the request.
	 * @param templateRoot
	 *            the root directory to contain the "japidviews" directory tree.
	 * @param refreshInterval
	 *            the minimal time, in second, that must elapse before trying to
	 *            detect any changes in the file system.
	 */
	public static void init(OpMode opMode, String templateRoot, int refreshInterval, ClassLoader parentClassLoader) {
		inited = true;
		JapidRenderer.opMode = opMode;
		setTemplateRoot(templateRoot);
		setRefreshInterval(refreshInterval);
		_parentClassLoader = parentClassLoader;
		if (_parentClassLoader == null) {
			_parentClassLoader = JapidRenderer.class.getClassLoader();
		}
		crlr = new TemplateClassLoader(parentClassLoader);
		compiler = new RendererCompiler(classes, crlr);
	}

	private static ClassLoader _parentClassLoader = null;
	/**
	 * a facet method to wrap implicit template binding. The default template is
	 * named as the class and method that immediately invoke this method. e.g.
	 * for an invocation scenario like this
	 * 
	 * <pre>
	 * package pack;
	 * 
	 * public class Foo {
	 * 	public String bar() {
	 * 		return JapidRender.render(p);
	 * 	}
	 * }
	 * </pre>
	 * 
	 * The template to use is "{templateRoot}/japidviews/pack/Foo/bar.html".
	 * 
	 * @param p
	 * @return
	 */
	public static String render(Object... args) {
		String templateName = findTemplate();
		return JapidPlainController.renderJapidWith(templateName, args);
	}

	/**
	 * render with the specified template.
	 * 
	 * @param templateName
	 *            The template must be rooted in the {templateRoot/}/japidviews
	 *            tree. The template name starts with or without "japidviews".
	 *            The naming pattern is the same as in ClassLoader.getResource(). 
	 * @param args
	 * @return the result string
	 */
	public static String renderWith(String templateName, Object... args) {
		return JapidPlainController.renderJapidWith(templateName, args);
	}

	private static String findTemplate() {
		String japidRenderInvoker = StackTraceUtils.getJapidRenderInvoker();
		return japidRenderInvoker;
	}

	public static String renderWith(Class<? extends JapidTemplateBaseWithoutPlay> cla, Object... args) {
		return JapidPlainController.renderWith(cla, args);
	}

}
