package cn.bran.play;

import java.io.Externalizable;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectOutput;
import java.io.Serializable;

import play.mvc.Controller;
import play.mvc.Http.Request;

import cn.bran.japid.template.ActionRunner;
import cn.bran.japid.template.RenderResult;

/**
 * The class defines a cached adapter to invoke a piece of code.
 * 
 * This class can be used in actions to run a piece of rendering code
 * conditionally
 * 
 * @author Bing Ran<bing_ran@hotmail.com>
 * 
 */
public abstract class CacheableRunner extends ActionRunner /*implements Externalizable*/{
//	private Object[] key = null;
	protected String ttlAbs = null;
	private String keyString;
	protected boolean noCache = true;
	private boolean readThru;

	/**
	 * 
	 * @param ttl
	 *            time to live for the cached item, format in "1s", "2mn", "3h",
	 *            "4d" for example, "29d" max
	 * @param args
	 *            objects to make a key from their string values
	 */
	public CacheableRunner(String ttl, Object... args) {
		init(ttl, args);
	}

	public CacheableRunner() {
	}

	/**
	 * @param ttl
	 * @param args
	 */
	protected void init(String ttl, Object... args) {
		if (args == null || args.length == 0)
			this.noCache = true;

		if (ttl == null || ttl.trim().length() == 0) {
			this.noCache = true;
		} else {
			this.noCache = false;
			if (ttl.startsWith("-")) {
				this.ttlAbs = ttl.substring(1);
				this.readThru = true;
			} else {
				this.ttlAbs = ttl;
			}
		}

		this.setKeyString(buildKey(args));
	}
	
	/**
	 * a constructor that use a key generated from the current query string to store the render result
	 * @param ttl
	 */
	public CacheableRunner(String ttl) {
		this(ttl, genCacheKey());
	}
	
	/**
	 * the main entry point this action runner
	 */
	@Override
	public RenderResult run() {
		if (!shouldCache()) {
			return render();
		} else {
			// cache in work
			RenderResult rr = null;
			if (!readThru) {
				try {
					rr = RenderResultCache.get(getKeyString());
					if (rr != null)
						return rr;
				} catch (ShouldRefreshException e) {
					// let's refresh the cache, flow through
				}
			}

			try {
				rr = render();
			} catch (JapidResult e) {
				rr = e.getRenderResult();
			}
			cacheResult(rr);
			return (rr);
		}
		//
	}

	/**
	 * @return
	 */
	protected boolean shouldCache() {
//		System.out.println("CacheableRunner: should cache: " + !noCache);
		return !noCache;
	}

	/**
	 * @param rr1
	 */
	protected void cacheResult(RenderResult rr1) {
//		System.out.println("CacheableRunner: put in cache");
		RenderResultCache.set(getKeyString(), rr1, ttlAbs);
	}

	/**
	 * @return
	 */
	public static String buildKey(Object[] keys) {
		String keyString = "";
		for (Object k : keys) {
			keyString += ":" + String.valueOf(k);
		}
		if (keyString.startsWith(":"))
			if ( keyString.length() > 1)
				keyString = keyString.substring(1);
			else
				keyString = "";
		return keyString;
	}

	/**
	 * the render method can either return a RenderResult or throw a JapidResult
	 * 
	 * @return
	 */
	protected abstract RenderResult render();
	
	public static void deleteCache(Object...objects) {
		RenderResultCache.delete(buildKey(objects));
	}

	/**
	 * remove a cached Japid RenderResult from cache indexed by a Japid invoke directive. The key is synthesized from 
	 * the controller name, action name and the arguments. 
	 * 
	 * Keep in mind that the result cache from a regular action annotated with CacheFor by Play is keyed with URL and query string
	 * thus different from what we have here. 
	 * 
	 * @author Bing Ran (bing.ran@hotmail.com)
	 * @param controllerClass
	 * @param actionName
	 * @param args the arguments to the action
	 */
	public static <C extends Controller> void deleteCache(Class<C> controllerClass, String actionName, Object... args) {
		Object[] fullArgs = buildCacheKeyParts(controllerClass, actionName, args);
		deleteCache(fullArgs);
	}
	
	public static Object[] buildCacheKeyParts(Class<? extends Controller> controllerClass, String actionName,
			Object... args) {
		// shall we support the id attribute of id of CacheFor?
				Object[] fullArgs = new Object[args.length + 2];
				System.arraycopy(args, 0, fullArgs, 0, args.length);
				fullArgs[args.length] = controllerClass.getName();
				fullArgs[args.length + 1] = actionName;
				return fullArgs;
			}

	public void writeExternal(ObjectOutput out) throws IOException {
		out.writeUTF(ttlAbs);
		out.writeUTF(getKeyString());
		out.writeBoolean(noCache);
		out.writeBoolean(readThru);
	}


	public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException {
		ttlAbs = in.readUTF();
		setKeyString(in.readUTF());
		noCache = in.readBoolean();
		readThru = in.readBoolean();
		
	}

	/**
	 * @return the keyString
	 */
	public String getKeyString() {
		return keyString;
	}

	/**
	 * @param keyString the keyString to set
	 */
	public void setKeyString(String keyString) {
		this.keyString = keyString;
	}

	/**
	 * can be used to generate a key based on the query
	 * 
	 * @return
	 */
	public static String genCacheKey() {
		return "japidcache:" + Request.current().action + ":"
				+ Request.current().querystring;
	}
	
	
}
