package cn.bran.play;

import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
// available only with newer commons-lang
// import org.apache.commons.lang.text.StrSubstitutor;

import play.Play;
import play.classloading.enhancers.LocalvariablesNamesEnhancer.LocalVariablesNamesTracer;
import play.exceptions.ActionNotFoundException;
import play.exceptions.NoRouteFoundException;
import play.exceptions.PlayException;
import play.exceptions.UnexpectedException;
import play.mvc.ActionInvoker;
import play.mvc.Router;
import play.mvc.Http.Request;
import play.mvc.Router.ActionDefinition;
import play.mvc.Router.Route;
import play.templates.GroovyTemplate;
import cn.bran.japid.util.StringUtils;
import cn.bran.japid.util.UrlMapper;

/**
 * the logic is modeled after the the Play!'s {@code Template.ActionBridge.}
 * 
 * 
 * @author bran
 * 
 */
public class RouteAdapter implements UrlMapper {
	// String controllerName;

	/**
	 * 
	 * @param controllerName
	 *            the current controller's name. If null is specified, the
	 *            Request.current().controller will be used
	 */
	public RouteAdapter() {
		super();
		// this.controllerName = controllerName;
	}

	/**
	 * Some sample form of action: CRUD.index(), blank(), attachment(_id, _name)
	 * this one has method call inside of controller param list
	 * 
	 * @{Application.show(post.previous().id)
	 * 
	 *                                        this is a super long one: *
	 * 
	 * @{list( ).remove('page').add('search', params.search).add('order',
	 *         (_caller.order == 'DESC' ? 'ASC' : 'DESC'))} in the table.html in
	 * 
	 *         the CRUD module
	 * 
	 *         this one has property access in the param list:
	 * @{show(_caller.object.id)
	 * 
	 * @param actionString
	 *            the leasing part of the whole expression, e.g.
	 *            Application.show in @{Application.show(post.previous().id)}.
	 *            it can also bear the form of a simple function call, such as:
	 *            show
	 * @param params
	 *            the runtime value of the parameters used to call the action,
	 *            e.g. 123 from evaluating "post.previous().id"
	 */
	@Override
	public String lookup(String actionString, Object[] params) {
		ActionBridge ab = new ActionBridge(false);

		ActionDefinition ad = ab.invokeMethod(actionString, params);
		return ad.toString();
	}

	@Override
	public String lookupAbs(String action, Object[] args) {
		return Request.current().getBase() + this.lookup(action, args);
	}

	public String lookupStatic(String resource, boolean isAbs) {
		return reverseStaticLookup(resource, isAbs);
		// return Router.reverseWithCheck(resource,
		// Play.getVirtualFile(resource));
	}

	@Override
	public String lookupStaticAbs(String resource) {
//		return Request.current().getBase() + this.lookupStatic(resource);
		return this.lookupStatic(resource, true);
	}

	/**
	 * 
	 * add cache check before passing to the heavy reverse check
	 * 
	 * @author Bing Ran<bing_ran@hotmail.com>
	 * @param action
	 * @param args
	 * @return
	 * @deprecated use the modified {@link ActionBridge}
	 */
	public static String reverseWithCache(String actionString, Object[] params) {
		// compose the key:

		try {
			String controllerName = Request.current().controller;
			// forms: Controller.action, action, package.Controller.action
			String action = actionString;
//			String methodName = actionString;
			if (actionString.indexOf(".") > 0) {
				int lastIndexOf = actionString.lastIndexOf('.');
//				methodName = actionString.substring(lastIndexOf + 1);
				controllerName = actionString.substring(0, lastIndexOf);
				// fell spec with controller name
			} else {
				action = controllerName + "." + actionString;
			}
			if (action.endsWith(".call")) {
				action = action.substring(0, action.length() - 5);
			}
			try {
				// find out the param types
				String[] targetParamNames = getActionParamCache().get(action);
				if (targetParamNames == null) {
					Method actionMethod = (Method) ActionInvoker.getActionMethod(action)[1];
					Class<?> actionClass = actionMethod.getDeclaringClass();
					Integer methodHash = LocalVariablesNamesTracer.computeMethodHash(actionMethod.getParameterTypes());
					String synthFieldForMethod = "$" + actionMethod.getName() + methodHash;
					targetParamNames = (String[]) actionClass.getDeclaredField(synthFieldForMethod).get(null);
					getActionParamCache().put(action, targetParamNames);
				}
				// too many parameters versus action, possibly a developer
				// error. we must warn him.
				if (targetParamNames.length < params.length) {
					throw new NoRouteFoundException(action, null);
				}

				String k = action;
				for (String p : targetParamNames) {
					k += "_" + p;
				}

				// assemble the named parameter hash
				Map<String, Object> paramMap = new HashMap<String, Object>();
				for (int i = 0; i < params.length; i++) {
					String key = i < targetParamNames.length ? targetParamNames[i] : "";
					String value = params[i] == null ? null : params[i].toString();
					paramMap.put(key, value);
				}

				// check cache first before go the expensive result
				String urlPattern = getActionCache().get(k);
				// format /action/{id}/{tag} etc
				// no cache for now to keep compatibility with Play code

				if (false && urlPattern != null) {
					/*
					 * if ("/{controller}/{action}".equals(urlPattern)) { //
					 * this is special // code copied from Router to compose the
					 * query string String qs =
					 * StringUtils.buildQuery(paramMap); return "/" +
					 * controllerName + "/" + methodName + "?" + qs; } else {
					 * StrSubstitutor sub = new StrSubstitutor(paramMap);
					 * sub.setVariablePrefix("{"); sub.setVariableSuffix("}");
					 * String url = sub.replace(urlPattern); return url; }
					 */
					return null;
				} else {
					ActionDefinition actionDef = Router.reverse(action, paramMap);
					// was doing some hacking for caching. don't do it now for
					// Compatibility with remote
					// Route route = actionDef.route;
					// // find is the route is cacheable. it's hard to cache
					// path
					// // with constrained parameter
					// //
					// String path = route.path;
					// if (!path.contains("<")) {
					// // cacheable
					// if (path.endsWith("?"))
					// path = path.substring(0, path.length() - 1);
					// getActionCache().put(k, path);
					// }
					return actionDef.toString();
				}
			} catch (ActionNotFoundException e) {
				throw new RuntimeException(e + ". No matching route in reverse lookup: " + actionString);
//				throw new NoRouteFoundException(action, null);
			}
			catch(NoRouteFoundException e) {
				throw new RuntimeException(e + ". No matching route in reverse lookup: " + actionString);
			}
		} catch (Exception e) {
			if (e instanceof PlayException) {
				throw (PlayException) e;
			}
			throw new UnexpectedException(e);
		}
	}

	/**
	 * @return
	 */
	private static HashMap<String, String> getActionCache() {
		HashMap<String, String> hash = (HashMap<String, String>) Request.current().args.get("actionReverseCache");
		if (hash == null) {
			hash = new HashMap<String, String>();
			Request.current().args.put("actionReverseCache", hash);
		}

		// put in the threadlocal is a problem because threads are reused in the
		// pool
		// HashMap<String, String> hash = actionReverseCache.get();
		// if (hash == null) {
		// hash = new HashMap<String, String>();
		// actionReverseCache.set(hash);
		// }
		return hash;
	}

	/**
	 * @return
	 */
	private static HashMap<String, String[]> getActionParamCache() {
		// the cache is bound to a request
		HashMap<String, String[]> hash = (HashMap<String, String[]>) Request.current().args.get("actionParamNamesCache");
		if (hash == null) {
			hash = new HashMap<String, String[]>();
			Request.current().args.put("actionParamNamesCache", hash);
		}

		// HashMap<String, String[]> hash = actionParamNamesCache.get();
		// if (hash == null) {
		// hash = new HashMap<String, String[]>();
		// actionParamNamesCache.set(hash);
		// }
		return hash;
	}

	/**
	 * reverse lookup a static resource
	 * 
	 * provide cache
	 * 
	 * @author Bing Ran<bing_ran@hotmail.com>
	 * @param resource
	 * @return
	 */
	public static String reverseStaticLookup(String resource, boolean isAbs) {
		try {
			HashMap<String, String> hash = getStaticCache();
			String url = hash.get(resource);
			if (url == null) {
				url = Router.reverseWithCheck(resource, Play.getVirtualFile(resource), isAbs);
				hash.put(resource, url);
			}
			return url;
		} catch (RuntimeException e) {
			throw new RuntimeException(e + ". No matching route in reverse lookup: " + resource);
		}
	}

	/**
	 * @return
	 */
	private static HashMap<String, String> getStaticCache() {
		HashMap<String, String> hash = staticCache.get();
		if (hash == null) {
			hash = new HashMap<String, String>();
			staticCache.set(hash);
		}
		return hash;
	}

	// cache lookups on the current thread.
	// ideally they can be done in the router for longer persistence and be
	// synched with route table reloading.

	// store quick reverse lookup
	// TODO should consider concurrent hashmap, mm but it's only thread local!
	// should this be shared among all thread(in the case concurrentThreadLocal
	// is required
	//
	static ThreadLocal<HashMap<String, String>> staticCache = new ThreadLocal<HashMap<String, String>>();
	// <action & param hash, url pattern>
	static ThreadLocal<HashMap<String, String>> actionReverseCache = new ThreadLocal<HashMap<String, String>>();
	// <action, paramNames>
	static ThreadLocal<HashMap<String, String[]>> actionParamNamesCache = new ThreadLocal<HashMap<String, String[]>>();

	@Override
	public String lookupStatic(String resource) {
		return lookupStatic(resource, false);
	}

}
