/* JSFComponentBase.java

	Purpose:
		
	Description:
		
	History:
		2013/6/26 Dennis

Copyright (C) 2010 Potix Corporation. All Rights Reserved.

*/
package org.zkoss.zss.jsf.ui.impl;

import java.io.IOException;
import java.io.Serializable;
import java.io.StringWriter;
import java.lang.ref.WeakReference;
import java.lang.reflect.Method;
import java.util.AbstractMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

import javax.el.ELContext;
import javax.el.ValueExpression;
import javax.faces.component.UIComponentBase;
import javax.faces.context.ExternalContext;
import javax.faces.context.FacesContext;
import javax.faces.context.ResponseWriter;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.zkoss.io.NullWriter;
import org.zkoss.lang.Classes;
import org.zkoss.lang.Exceptions;
import org.zkoss.lang.Strings;
import org.zkoss.util.logging.Log;
import org.zkoss.zk.ui.Component;
import org.zkoss.zk.ui.Desktop;
import org.zkoss.zk.ui.GenericRichlet;
import org.zkoss.zk.ui.Page;
import org.zkoss.zk.ui.UiException;
import org.zkoss.zk.ui.ext.AfterCompose;
import org.zkoss.zk.ui.ext.DynamicPropertied;
import org.zkoss.zk.ui.metainfo.Property;
import org.zkoss.zk.ui.metainfo.PropertyNotFoundException;
import org.zkoss.zk.ui.sys.DesktopCtrl;
import org.zkoss.zk.ui.util.Composer;
import org.zkoss.zk.ui.util.DesktopCleanup;
import org.zkoss.zkplus.embed.Renders;
import org.zkoss.zssex.ui.UpdateBridge;
import org.zkoss.zssex.ui.impl.BridgeFix;

/**
 * The base of zk jsf component
 * @author Dennis
 *
 */
public abstract class JSFComponentBase extends UIComponentBase implements Serializable {
	private static final long serialVersionUID = -1655187379163830077L;
	
	private static final Log log = Log.lookup(JSFComponentBase.class);
	private transient WeakReference<Component> component;
	
	private String desktopId;
	private String compUuid;
	
	//current implement just guess and try to set props to component, this will has issue because in JSF it could set any name to attr and value expression
	private LinkedHashMap<String,PROP_TYPE> stateProps = new LinkedHashMap<String,PROP_TYPE>();
//	private List<String> attrTypes = new LinkedList<String>();
	
	AttrMap attrsMap = new AttrMap();
	
	private enum PROP_TYPE{
		ATTR,VALUE_EXPRESSION
	}
	
	@Override
	public void setValueExpression(String name, ValueExpression binding) {
		super.setValueExpression(name, binding);
		if(name.indexOf(".")==-1){
			stateProps.put(name, PROP_TYPE.VALUE_EXPRESSION);
		}
	}
	
	public Map<String, Object> getAttributes() {
		return attrsMap;
	}
	
	//currently, if I don't write the tag, then I can just use attribute to get the property
	class AttrMap extends AbstractMap<String, Object>{

		@Override
		public Set<java.util.Map.Entry<String, Object>> entrySet() {
			return JSFComponentBase.super.getAttributes().entrySet();
		}
		
		@Override
		public Object put(String key,Object value){
			
			if(key.indexOf(".")==-1){
				stateProps.put(key, PROP_TYPE.ATTR);
			}
			return JSFComponentBase.super.getAttributes().put(key, value);
		}
		
		@Override
		public void clear(){
			for(String nm:keySet()){
				stateProps.remove(nm);
			}
			JSFComponentBase.super.getAttributes().clear();
		}
		
		@Override
		public Object remove(Object key){
			stateProps.remove(key.toString());
			return JSFComponentBase.super.getAttributes().remove(key);
		}
	}
	
	protected abstract Component newZKComponent();
	
	protected String getZKComponentUuid(){
		return compUuid;
	}
	
	public Component getZKComponent(){
		Component comp = component!=null?component.get():null;
		if(comp!=null){
			return comp;
		}
		
		if(desktopId==null || compUuid==null){
			return null;
		}

		Desktop dt = getZKDesktop();
		if(dt==null){
			return null;
		}
		comp = dt.getComponentByUuidIfAny(compUuid);

		if(comp==null){
			return null;
		}
		
		component = new WeakReference<Component>(comp);
		return component.get();
	}
	
	protected String getZKDesktopId(){
		return desktopId;
	}
	
	public Desktop getZKDesktop(){
		if(desktopId==null){
			return null;
		}
		FacesContext ctx = FacesContext.getCurrentInstance();
		if(ctx==null)
			return null;
		ExternalContext extCtx = ctx.getExternalContext();
		Object servletCtx = extCtx.getContext();
		if(!(servletCtx instanceof ServletContext)){
			return null;
		}
		Object request = extCtx.getRequest();
		
		Desktop dt =  UpdateBridge.findDesktopIfAny((ServletContext)servletCtx,(HttpServletRequest)request, desktopId);
		//TODO timeout issue
		return dt;
	}
	
//	 @Override
//	 public void processDecodes(FacesContext context) {
//		 super.processDecodes(context);
//	 }

	@Override
	public void encodeBegin(final FacesContext context) throws IOException {
		final ServletContext svlctx = (ServletContext)context.getExternalContext().getContext();
		final HttpServletRequest request = (HttpServletRequest) context.getExternalContext().getRequest();
		final HttpServletResponse response = (HttpServletResponse) context.getExternalContext().getResponse();
		final String id =  getClientId(context);
		final ResponseWriter responseWriter = context.getResponseWriter();
		final NullWriter nullwriter = new NullWriter();
		final boolean isAjax = FacesUtil.isAjaxRequest();
		final Component oldcomp = getZKComponent();//when pure post back, it is possible we have a component, so clean it. 
		String cleanUpResult = null;
		boolean desktopCleanup = false;
		if(oldcomp!=null){
			//should clean this component to release resource it related
			Desktop desktop = getZKDesktop();
			BridgeFix zbridge = new BridgeFix();
			zbridge.start(svlctx, request, response, desktop);
			try{
				releaseResource(oldcomp);//to clean resource, e.x book
				oldcomp.detach();// to clean component
				
				if(isAjax){
					cleanUpResult = zbridge.getResult();//clean up and detach
				}
			}catch(Exception e){
				log.warning(e.getMessage(),e);//eat
			}finally{
				zbridge.close();
			}
		}
		
		if(cleanUpResult!=null){
			responseWriter.append(FacesUtil.toScriptString(cleanUpResult));
		}
		
		//this block is for eating unnecessary header when zk render in ajax update
		if(isAjax){
			try{
				Renders.render(svlctx, request, response, 
					new GenericRichlet() {
						public void service(Page page) throws Exception {
							Desktop dt = page.getDesktop();
							((DesktopCtrl)dt).removePage(page);
						}
					}, null, nullwriter);
			} catch (ServletException e) {
				throw new IOException(e.getMessage());
			}
			desktopCleanup = true;
		}
		
		String renderResult;
		try {
			desktopId = null;//reset
			component = null;//reset
			StringWriter writer = new StringWriter();
			Renders.render(svlctx, request, response, 
				new GenericRichlet() {
					public void service(Page page) throws Exception {
						desktopId = page.getDesktop().getId();
						
						//ZSS-641, has to add before attach component to desktop to prevent desktop-cleanup called too late.
						//see Spreadsheet BookCleaner, it register a cleanup too.
						DeadComponentHolderListener holder = null;
						if(ObjectHolderContext.hasHolder()){//only add holder for dead component when context has holder (e.g has ObjectHolderFilter)
							holder = newDeadComponentHolderListener();
							page.getDesktop().addListener(holder);
						}
						
						Component comp = oldcomp==null?newZKComponent():oldcomp;
						comp.setPage(page);
						
						component = new WeakReference<Component>(comp);
						compUuid = comp.getUuid();//get id after attached to tree
						
						if(holder!=null){
							holder.setComponentUuid(compUuid);//ZSS-641, attach compUuid after it was created.
						}
						
						String id = getId();
						if(id!=null){
							comp.setId(id);
						}
						applyProperties(context,comp);
						doAfterCompose(comp);
					}
				}, null, writer);
			
			
			if(desktopCleanup){
				//use zk client script to sync/clean old desktop
				writer.write(FacesUtil.toScriptString("zk.Desktop.sync(500);"));
			}
			
			renderResult = writer.toString();
			//fix double CDATA issue in jsf ajax update
			//could be remove and test in zk 6.5.4
//			String cds = "//<![CDATA[";
//			String cde = "//]]>";
//			
//			int index = renderResult.indexOf(cds);
//			if(index!=-1){
//				renderResult = renderResult.substring(0,index)+renderResult.substring(index+cds.length(),renderResult.length());
//				index = renderResult.indexOf(cde,index);
//				renderResult = renderResult.substring(0,index)+renderResult.substring(index+cde.length(),renderResult.length());
//			}
			

		} catch (ServletException e) {
			throw new IOException(e.getMessage());
		}
		if(renderResult!=null){
			responseWriter.write(renderResult);
		}
		//component is renew , it is not necessary to execute any update, clean it, or will get js error
		FacesUtil.clearUpdateScript(id);
		
	}
	
	protected DeadComponentHolderListener newDeadComponentHolderListener() {
		return new DeadComponentHolderListener();
	}

	/**
	 * call back to cleanup a component resource, it is in zk execution scope
	 */
	protected void releaseResource(Component comp) {
		
	}

	protected Object getStateProperty(String name){
		PROP_TYPE pt = stateProps.get(name);
		if(pt==PROP_TYPE.ATTR){
			return getAttributes().get(name);
		}else if(pt==PROP_TYPE.VALUE_EXPRESSION){
			return getStateHelper().eval(name);
		}
		return null;
	}

	protected void applyProperties(FacesContext context,Component component) {	
		for(Entry<String, PROP_TYPE> e : stateProps.entrySet()){
			String nm = e.getKey();
			
			if ("apply".equals(nm)) {
				// ignore composer
			} else {
				Object value = getStateProperty(nm);
				try {
					applyProperty(component, nm, value);
				} catch (PropertyNotFoundException pnfe) {
					log.debug(pnfe.getMessage());
				}
			}
		}
	}
	
	protected void applyProperty(Component comp,String nm,Object value){
		assignProperty(comp, nm, value);
	}
	

	//Property#assign doesn't allow object
	protected static void assignProperty(Component comp, String name, Object value) {
		final Method mtd = Property.resolveMethod(comp.getClass(), name);
		if (mtd != null) {
			try {
				Object val = Classes.coerce(mtd.getParameterTypes()[0], value);
				mtd.invoke(comp, val);
			} catch (Exception ex) {
				log.error("Failed to assign "+value+" to "+comp+"\n"+Exceptions.getMessage(ex));
				throw UiException.Aide.wrap(ex);
			}
		} else {
			((DynamicPropertied)comp).setDynamicProperty(name, value);
		}
	}

	protected void doAfterCompose(Component component) throws Exception {
		if (component instanceof AfterCompose)
			((AfterCompose)component).afterCompose();
		if(!stateProps.containsKey("apply"))
			return;
		Object o = getStateProperty("apply");
		if(o instanceof String) {
			o = Classes.newInstanceByThread(o.toString());
		}
		if(o instanceof Composer) {
			((Composer)o).doAfterCompose(component);
		}else{
			throw new IllegalStateException("apply is not a composer is "+o);
		}
		
		
	}

	
	// following are the official way to handle component property (code from HtmlInputSecret
//  public java.lang.String getAccesskey() {
//      return (java.lang.String) getStateHelper().eval(PropertyKeys.accesskey);
//
//  }
//  public void setAccesskey(java.lang.String accesskey) {
//      getStateHelper().put(PropertyKeys.accesskey, accesskey);
//      handleAttribute("accesskey", accesskey);
//  }	
	
	public String getFamily() {
		return "zk";
	}
	
	@Override
	public void processUpdates(FacesContext context) {
		if (context == null) {
			throw new NullPointerException();
		}

		// Skip processing if our rendered flag is false
		if (!isRendered()) {
			return;
		}

		super.processUpdates(context);
		try {
			
			ValueExpression ve = getValueExpression("actionBridge");
			if (ve != null) {
				ELContext ctx = context.getELContext();
				if(!ve.isReadOnly(ctx)){
					ve.setValue(context.getELContext(), new ActionBridgeImpl(getClientId(context),getZKComponentUuid()));
				}
			}
		} catch (RuntimeException e) {
			context.renderResponse();
			throw e;
		}
	}
	
	@Override
	public void restoreState(FacesContext context, Object state) {
		Object values[] = (Object[]) state;
		super.restoreState(context, values[0]);
		desktopId = (String) values[1];
		compUuid = (String) values[2];
		stateProps = (LinkedHashMap<String,PROP_TYPE>)values[3];
//		System.out.println("load state "+desktopId+","+compUuid+" on "+ this);
	}

	@Override
	public Object saveState(FacesContext context) {
		Object state[] = new Object[4];
		state[0] = super.saveState(context);
		state[1] = desktopId;
		state[2] = compUuid;
		state[3] = stateProps;
//		System.out.println("save state "+desktopId+","+compUuid+" on "+ this);
		return state;
	}
	
	
	protected static String appendStyle(String style,String attr,String value){
		StringBuilder sb = new StringBuilder();
		if(!Strings.isEmpty(style)){
			sb.append(style).append(";");
		}
		sb.append(attr).append(":").append(value);
		return sb.toString();
	}
	
	static protected class DeadComponentHolderListener implements DesktopCleanup,Serializable{
		private static final long serialVersionUID = 1L;
		private String compUuid;
		protected DeadComponentHolderListener(){
		}
		
		public void setComponentUuid(String compUuid){
			this.compUuid = compUuid;
		}
		
		public String getComponentUuid(){
			return this.compUuid;
		}
		
		public void cleanup(Desktop desktop) throws Exception {
			Component comp = desktop.getComponentByUuidIfAny(compUuid);
			holdData(desktop,comp);
		}
		
		protected void holdData(Desktop desktop, Component comp) {
			//default nothing
		}
	}
}
