package railo.runtime.tag;

import java.io.IOException;
import java.util.Iterator;
import java.util.Map.Entry;

import javax.servlet.jsp.JspWriter;
import javax.servlet.jsp.tagext.Tag;

import railo.commons.lang.StringUtil;
import railo.runtime.Component;
import railo.runtime.Mapping;
import railo.runtime.PageContext;
import railo.runtime.PageContextImpl;
import railo.runtime.PageSource;
import railo.runtime.component.ComponentLoader;
import railo.runtime.component.Member;
import railo.runtime.config.ConfigWebImpl;
import railo.runtime.customtag.CustomTagUtil;
import railo.runtime.customtag.InitFile;
import railo.runtime.engine.ThreadLocalPageContext;
import railo.runtime.exp.ApplicationException;
import railo.runtime.exp.CasterException;
import railo.runtime.exp.ExpressionException;
import railo.runtime.exp.PageException;
import railo.runtime.exp.PageRuntimeException;
import railo.runtime.exp.PageServletException;
import railo.runtime.ext.tag.AppendixTag;
import railo.runtime.ext.tag.BodyTagTryCatchFinallyImpl;
import railo.runtime.ext.tag.DynamicAttributes;
import railo.runtime.op.Caster;
import railo.runtime.op.Decision;
import railo.runtime.type.Collection;
import railo.runtime.type.Collection.Key;
import railo.runtime.type.KeyImpl;
import railo.runtime.type.Struct;
import railo.runtime.type.StructImpl;
import railo.runtime.type.scope.Caller;
import railo.runtime.type.scope.CallerImpl;
import railo.runtime.type.scope.Undefined;
import railo.runtime.type.scope.Variables;
import railo.runtime.type.scope.VariablesImpl;
import railo.runtime.type.util.ArrayUtil;
import railo.runtime.type.util.ComponentUtil;
import railo.runtime.type.util.KeyConstants;
import railo.runtime.type.util.ListUtil;
import railo.runtime.type.util.Type;
import railo.runtime.util.QueryStack;
import railo.runtime.util.QueryStackImpl;
import railo.transformer.library.tag.TagLibTag;
import railo.transformer.library.tag.TagLibTagAttr;


/**
* Creates a CFML Custom Tag
**/
public class CFTag extends BodyTagTryCatchFinallyImpl implements DynamicAttributes,AppendixTag {

	private static Collection.Key GENERATED_CONTENT=KeyImpl.intern("GENERATEDCONTENT");
	private static Collection.Key EXECUTION_MODE=KeyImpl.intern("EXECUTIONMODE");      
	private static Collection.Key EXECUTE_BODY=KeyImpl.intern("EXECUTEBODY");
	private static Collection.Key PARENT=KeyImpl.intern("PARENT");
	private static Collection.Key CFCATCH=KeyConstants._CFCATCH;
	private static Collection.Key SOURCE=KeyImpl.intern("SOURCE");
	
	private static final Collection.Key ON_ERROR = KeyImpl.intern("onError");
	private static final Collection.Key ON_FINALLY = KeyImpl.intern("onFinally");
	private static final Collection.Key ON_START_TAG = KeyImpl.intern("onStartTag");
	private static final Collection.Key ON_END_TAG = KeyImpl.intern("onEndTag");

	private static final Collection.Key ATTRIBUTE_TYPE = KeyImpl.intern("attributetype");
	private static final Collection.Key RT_EXPR_VALUE = KeyImpl.intern("rtexprvalue");
	private static final String MARKER = "2w12801";
	
    /**
     * Field <code>attributesScope</code>
     */
    // new scopes
    protected StructImpl attributesScope;
    private Caller callerScope;
    private StructImpl thistagScope;

    private Variables ctVariablesScope;

    private boolean hasBody;

    /**
     * Field <code>filename</code>
     */
    //protected String filename;

    /**
     * Field <code>source</code>
     */
    protected InitFile source;
    private String appendix;
	//private boolean doCustomTagDeepSearch;
	
	private Component cfc;
	private boolean isEndTag;
	
	
	
    /**
    * constructor for the tag class
    **/
    public CFTag() {
    	attributesScope = new StructImpl();
        callerScope = new CallerImpl();
        //thistagScope = new StructImpl();
    }

    @Override
    public void setDynamicAttribute(String uri, String name, Object value) {
    	TagUtil.setDynamicAttribute(attributesScope,KeyImpl.init(name),value,TagUtil.ORIGINAL_CASE);
    }

    @Override
    public void setDynamicAttribute(String uri, Collection.Key name, Object value) {
    	TagUtil.setDynamicAttribute(attributesScope,name,value,TagUtil.ORIGINAL_CASE);
    }

    @Override
    public void release()   {
        super.release();

        hasBody=false;
        //filename=null;      

        attributesScope=new StructImpl();//.clear();
        callerScope = new CallerImpl();
        if(thistagScope!=null)thistagScope=null;
        if(ctVariablesScope!=null)ctVariablesScope=null;  
        

        isEndTag=false;     
        
        //cfc=null;
        source=null;
    }

    /**
     * sets the appendix of the class
     * @param appendix
     */
    public void setAppendix(String appendix) {
        this.appendix=appendix;
        //filename = appendix+'.'+pageContext.getConfig().getCFMLExtension();
    }

    @Override
    public int doStartTag() throws PageException    {
    	PageContextImpl pci=(PageContextImpl) pageContext;
		boolean old=pci.useSpecialMappings(true);
		try{
			initFile();
	    	callerScope.initialize(pageContext);
	        if(source.isCFC())return cfcStartTag();
	        return cfmlStartTag();	
		}
		finally{
			pci.useSpecialMappings(old);
		}
    }

    @Override
    public int doEndTag()   {
    	PageContextImpl pci=(PageContextImpl) pageContext;
		boolean old=pci.useSpecialMappings(true);
		try{
			if(source.isCFC())_doCFCFinally();
	        return EVAL_PAGE;
		}
		finally{
			pci.useSpecialMappings(old);
		}
    }

    @Override
    public void doInitBody()    {
        
    }

    @Override
    public int doAfterBody() throws PageException   {
    	if(source.isCFC())return cfcEndTag();
        return cfmlEndTag();
    }
    

	@Override
    public void doCatch(Throwable t) throws Throwable {
    	if(source.isCFC()){
	    	String source=isEndTag?"end":"body";
	    	isEndTag=false;
	    	_doCFCCatch(t,source);
    	}
    	else super.doCatch(t);
	}
    
    void initFile() throws PageException {
        source=initFile(pageContext);
    }

    public InitFile initFile(PageContext pageContext) throws PageException {
    	return CustomTagUtil.loadInitFile(pageContext, appendix);
    }
    
	private int cfmlStartTag() throws PageException {
		callerScope.initialize(pageContext);
        
		// thistag
		if(thistagScope==null)thistagScope=new StructImpl(StructImpl.TYPE_LINKED);
        thistagScope.set(GENERATED_CONTENT,"");
        thistagScope.set(EXECUTION_MODE,"start");      
        thistagScope.set(EXECUTE_BODY,Boolean.TRUE);
        thistagScope.set(KeyConstants._HASENDTAG,Caster.toBoolean(hasBody));
        
		
		ctVariablesScope=new VariablesImpl();
        ctVariablesScope.setEL(KeyConstants._ATTRIBUTES,attributesScope);
        ctVariablesScope.setEL(KeyConstants._CALLER,callerScope);
        ctVariablesScope.setEL(KeyConstants._THISTAG,thistagScope);
        
        
        // include
        doInclude();
        
        return Caster.toBooleanValue(thistagScope.get(EXECUTE_BODY))?EVAL_BODY_BUFFERED:SKIP_BODY;
    } 
	
    private int cfmlEndTag() throws PageException {
        // thistag     
    	String genConBefore = bodyContent.getString();
    	thistagScope.set(GENERATED_CONTENT,genConBefore);
        thistagScope.set(EXECUTION_MODE,"end");
        thistagScope.set(EXECUTE_BODY,Boolean.FALSE);
        writeEL(bodyContent, MARKER);
        
        // include
        try{
        	doInclude();
        }
        catch(Throwable t){
        	writeOut(genConBefore);
        	throw Caster.toPageException(t);
        }
        
        writeOut(genConBefore);

        return Caster.toBooleanValue(thistagScope.get(EXECUTE_BODY))?EVAL_BODY_BUFFERED:SKIP_BODY;
    }

    

	private void writeOut(String genConBefore) throws PageException {
		String output = bodyContent.getString(); 
		bodyContent.clearBody();
    	String genConAfter = Caster.toString(thistagScope.get(GENERATED_CONTENT));
    	
    	if(genConBefore!=genConAfter){
        	if(output.startsWith(genConBefore+MARKER)){
    			output=output.substring((genConBefore+MARKER).length());
    		}
    		output=genConAfter+output;
    	}
    	else {
    		if(output.startsWith(genConBefore+MARKER)){
    			output=output.substring((genConBefore+MARKER).length());
    			output=genConBefore+output;
    		}
    	}
    	
    	
    	writeEL(bodyContent.getEnclosingWriter(),output);
	}

	private void writeEL(JspWriter writer, String str) throws PageException {
		try {
			writer.write(str);
		} catch (IOException e) {
			throw Caster.toPageException(e);
		}
	}

	void doInclude() throws PageException {
        Variables var=pageContext.variablesScope();
        pageContext.setVariablesScope(ctVariablesScope);
        
        
        QueryStack cs=null;
        Undefined undefined=pageContext.undefinedScope();
        int oldMode=undefined.setMode(Undefined.MODE_NO_LOCAL_AND_ARGUMENTS);
        if(oldMode!=Undefined.MODE_NO_LOCAL_AND_ARGUMENTS)
        	callerScope.setScope(var,pageContext.localScope(),pageContext.argumentsScope(),true);
        else 
        	callerScope.setScope(var,null,null,false);
        
        if(pageContext.getConfig().allowImplicidQueryCall()) {
            cs=undefined.getQueryStack();
            undefined.setQueryStack(new QueryStackImpl());
        }
            
        try {
            pageContext.doInclude(new PageSource[]{source.getPageSource()},false);
        }
        catch (Throwable t) {
            throw Caster.toPageException(t);
        }
        finally {
            undefined.setMode(oldMode);
            //varScopeData=variablesScope.getMap();
            pageContext.setVariablesScope(var);
            if(pageContext.getConfig().allowImplicidQueryCall()) {
                undefined.setQueryStack(cs);
            }
        }
    
    }
    
    
    // CFC
    
    private int cfcStartTag() throws PageException {
    	
    	callerScope.initialize(pageContext);
        try {
			cfc = ComponentLoader.loadComponent(pageContext,null,source.getPageSource(), source.getFilename().substring(0,source.getFilename().length()-(pageContext.getConfig().getCFCExtension().length()+1)), false,true);
		}
		catch (PageException e) {
			Mapping m = source.getPageSource().getMapping();
			ConfigWebImpl c=(ConfigWebImpl) pageContext.getConfig();
			if(m==c.getTagMapping()) m=c.getServerTagMapping();
			else m=null;
			// is te page source from a tag mapping, so perhaps it was moved from server to web context
			if(m!=null){
				PageSource ps = m.getPageSource(source.getFilename());
				try {
					cfc = ComponentLoader.loadComponent(pageContext,null,ps, source.getFilename().substring(0,source.getFilename().length()-(pageContext.getConfig().getCFCExtension().length()+1)), false,true);
				}
				catch (PageException e1) {
					throw e;
				}
				
			}
		}
        
        validateAttributes(cfc,attributesScope,StringUtil.ucFirst(ListUtil.last(source.getPageSource().getComponentName(),'.')));
        
        boolean exeBody = false;
        try	{
			Object rtn=Boolean.TRUE;
			if(cfc.contains(pageContext, KeyConstants._init)){
	        	Tag parent=getParent();
	        	while(parent!=null && !(parent instanceof CFTag && ((CFTag)parent).isCFCBasedCustomTag())) {
	    			parent=parent.getParent();
	    		}
				Struct args=new StructImpl(StructImpl.TYPE_LINKED);
				args.set(KeyConstants._HASENDTAG, Caster.toBoolean(hasBody));
	    		if(parent instanceof CFTag) {
	    			args.set(PARENT, ((CFTag)parent).getComponent());
	    		}
	        	rtn=cfc.callWithNamedValues(pageContext, KeyConstants._init, args);
	        }
			
	        if(cfc.contains(pageContext, ON_START_TAG)){
	        	Struct args=new StructImpl();
	        	args.set(KeyConstants._ATTRIBUTES, attributesScope);
	        	setCaller(pageContext,args);
	        	
	        	rtn=cfc.callWithNamedValues(pageContext, ON_START_TAG, args);	
		    }
	        exeBody=Caster.toBooleanValue(rtn,true);
        }
        catch(Throwable t){
        	_doCFCCatch(t,"start");
        }
        return exeBody?EVAL_BODY_BUFFERED:SKIP_BODY;
    }
    
    private void setCaller(PageContext pageContext, Struct args) throws PageException {
    	callerScope.initialize(pageContext);
    	boolean checkAgs=pageContext.undefinedScope().getCheckArguments();
    	if(checkAgs)
        	callerScope.setScope(pageContext.variablesScope(),pageContext.localScope(),pageContext.argumentsScope(),true);
        else 
        	callerScope.setScope(pageContext.variablesScope(),null,null,false);
        
    	
    	args.set(KeyConstants._CALLER, callerScope);
    	
    	
    	
    	//args.set(KeyConstants._CALLER, Duplicator.duplicate(pageContext.undefinedScope(),false));
	}

	private static void validateAttributes(Component cfc,StructImpl attributesScope,String tagName) throws ApplicationException, ExpressionException {
		
		TagLibTag tag=getAttributeRequirments(cfc,false);
		if(tag==null) return;
		
		if(tag.getAttributeType()==TagLibTag.ATTRIBUTE_TYPE_FIXED || tag.getAttributeType()==TagLibTag.ATTRIBUTE_TYPE_MIXED){
			Iterator<Entry<String, TagLibTagAttr>> it = tag.getAttributes().entrySet().iterator();
			int count=0;
			Collection.Key key;
			TagLibTagAttr attr;
			Object value;
			Entry<String, TagLibTagAttr> entry;
			// check existing attributes
			while(it.hasNext()){
				entry = it.next();
				count++;
				key=KeyImpl.toKey(entry.getKey(),null);
				attr=entry.getValue();
				value=attributesScope.get(key,null);
				if(value==null){
					if(attr.getDefaultValue()!=null){
						value=attr.getDefaultValue();
						attributesScope.setEL(key, value);
					}
					else if(attr.isRequired())
						throw new ApplicationException("attribute ["+key.getString()+"] is required for tag ["+tagName+"]");
				}
				if(value!=null) {
					if(!Decision.isCastableTo(attr.getType(),value,true,true,-1)) 
						throw new CasterException(createMessage(attr.getType(), value));
				
				}
			}
			
			// check if there are attributes not supported
			if(tag.getAttributeType()==TagLibTag.ATTRIBUTE_TYPE_FIXED && count<attributesScope.size()){
				Collection.Key[] keys = attributesScope.keys();
				for(int i=0;i<keys.length;i++){
					if(tag.getAttribute(keys[i].getLowerString())==null)
						throw new ApplicationException("attribute ["+keys[i].getString()+"] is not supported for tag ["+tagName+"]");
				}
				
			 	//Attribute susi is not allowed for tag cfmail
			}
		}
	}
    
    private static String createMessage(String type, Object value) {
    	if(value instanceof String) return "can't cast String ["+value+"] to a value of type ["+type+"]";
    	else if(value!=null) return "can't cast Object type ["+Type.getName(value)+"] to a value of type ["+type+"]";
		else return "can't cast Null value to value of type ["+type+"]";

    } 
    

	private static TagLibTag getAttributeRequirments(Component cfc, boolean runtime) throws ExpressionException {
		
		Struct meta=null;
    	//try {
    		//meta = Caster.toStruct(cfc.get(Component.ACCESS_PRIVATE, METADATA),null,false);
    		Member mem = ComponentUtil.toComponentAccess(cfc).getMember(Component.ACCESS_PRIVATE, KeyConstants._metadata,true,false);
    		if(mem!=null)meta = Caster.toStruct(mem.getValue(),null,false);
		//}catch (PageException e) {e.printStackTrace();}
    	if(meta==null) return null;
    	
    	TagLibTag tag=new TagLibTag(null);
    // TAG

    	// type    	
    	String type=Caster.toString(meta.get(ATTRIBUTE_TYPE,"dynamic"),"dynamic");
    	
    	if("fixed".equalsIgnoreCase(type))tag.setAttributeType(TagLibTag.ATTRIBUTE_TYPE_FIXED);
    	//else if("mixed".equalsIgnoreCase(type))tag.setAttributeType(TagLibTag.ATTRIBUTE_TYPE_MIXED);
    	//else if("noname".equalsIgnoreCase(type))tag.setAttributeType(TagLibTag.ATTRIBUTE_TYPE_NONAME);
    	else tag.setAttributeType(TagLibTag.ATTRIBUTE_TYPE_DYNAMIC);
    	
    	if(!runtime){
    		// hint
    		String hint=Caster.toString(meta.get(KeyConstants._hint,null),null);
    		if(!StringUtil.isEmpty(hint))tag.setDescription(hint);
    	}
    	
    // ATTRIBUTES
    	Struct attributes=Caster.toStruct(meta.get(KeyConstants._ATTRIBUTES,null),null,false);
    	if(attributes!=null) {
    		Iterator<Entry<Key, Object>> it = attributes.entryIterator();
    		//Iterator it = attributes.entrySet().iterator();
    		Entry<Key, Object> entry;
    		TagLibTagAttr attr;
    		Struct sct;
    		String name;
    		Object defaultValue;
    		while(it.hasNext()){
    			entry=it.next();
    			name=Caster.toString(entry.getKey(),null);
    			if(StringUtil.isEmpty(name)) continue;
    			attr=new TagLibTagAttr(tag);
    			attr.setName(name);
    			
    			sct=Caster.toStruct(entry.getValue(),null,false);
    			if(sct!=null){
    				attr.setRequired(Caster.toBooleanValue(sct.get(KeyConstants._required,Boolean.FALSE),false));
    				attr.setType(Caster.toString(sct.get(KeyConstants._type,"any"),"any"));
    				
    				defaultValue= sct.get(KeyConstants._default,null);
    				if(defaultValue!=null)attr.setDefaultValue(defaultValue);
    				
    				
    				if(!runtime){
    					attr.setDescription(Caster.toString(sct.get(KeyConstants._hint,null),null));
    					attr.setRtexpr(Caster.toBooleanValue(sct.get(RT_EXPR_VALUE,Boolean.TRUE),true));
    				}
    			}
    			tag.setAttribute(attr);
    			
    		}
    	}
    	return tag;
	}

	private int cfcEndTag() throws PageException {
        
    	boolean exeAgain = false;
        try{
	    	String output=null;
	    	Object rtn=Boolean.FALSE;
	        
	    	
	    	if(cfc.contains(pageContext, ON_END_TAG)){
	    		try {
		        	output=bodyContent.getString();
		            bodyContent.clearBody();
		            //rtn=cfc.call(pageContext, ON_END_TAG, new Object[]{attributesScope,pageContext.variablesScope(),output});
		            
		            Struct args=new StructImpl(StructImpl.TYPE_LINKED);
		        	args.set(KeyConstants._ATTRIBUTES, attributesScope);
		        	setCaller(pageContext, args);
		        	args.set(GENERATED_CONTENT, output);
		        	rtn=cfc.callWithNamedValues(pageContext, ON_END_TAG, args);	
			    
		            
		            
	        	}
	        	finally	{
	        		writeEnclosingWriter();
	        	}
	        }
	    	else writeEnclosingWriter();
	    	
	        exeAgain= Caster.toBooleanValue(rtn,false);
	    }
        catch(Throwable t){
        	isEndTag=true;
        	throw Caster.toPageException(t);
        }
        return exeAgain?EVAL_BODY_BUFFERED:SKIP_BODY;
    	
    }
    
    public void _doCFCCatch(Throwable t, String source) throws PageException {
    	writeEnclosingWriter();
    	
    	// remove PageServletException wrap
    	if(t instanceof PageServletException) {
		    PageServletException pse=(PageServletException)t;
		    t=pse.getPageException();
		}
    	
    	// abort 
    	try {
			if(railo.runtime.exp.Abort.isAbort(t)){
				if(bodyContent!=null){
					bodyContent.writeOut(bodyContent.getEnclosingWriter());
					bodyContent.clearBuffer();
				}
				throw Caster.toPageException(t);
			}
		}
		catch(IOException ioe){
			throw Caster.toPageException(ioe);
		}
    	
    	
    	
    	try {
			if(cfc.contains(pageContext, ON_ERROR)){
	        	PageException pe = Caster.toPageException(t);
	        	//Object rtn=cfc.call(pageContext, ON_ERROR, new Object[]{pe.getCatchBlock(pageContext),source});
	    		
	        	Struct args=new StructImpl(StructImpl.TYPE_LINKED);
	        	args.set(CFCATCH, pe.getCatchBlock(ThreadLocalPageContext.getConfig(pageContext)));
	        	args.set(SOURCE, source);
	        	Object rtn=cfc.callWithNamedValues(pageContext, ON_ERROR, args);	
		    
	        	if(Caster.toBooleanValue(rtn,false))
					throw t;
	        }
			else throw t;
    	}
    	catch(Throwable th) {
    		writeEnclosingWriter();
    		_doCFCFinally();
    		throw Caster.toPageException(th);
    	}
    	writeEnclosingWriter();
	}
    
    private void _doCFCFinally() {
		if(cfc.contains(pageContext, ON_FINALLY)){
			try {
				cfc.call(pageContext, ON_FINALLY, ArrayUtil.OBJECT_EMPTY);
			} 
			catch (PageException pe) {
				throw new PageRuntimeException(pe);
			}
			finally{
				writeEnclosingWriter();
			}
        }
	}
    
    
    private void writeEnclosingWriter()  {
    	if(bodyContent!=null){
			try {
				String output = bodyContent.getString(); 
				bodyContent.clearBody();
	            bodyContent.getEnclosingWriter().write(output);    
	        } 
			catch (IOException e) {
	        	//throw Caster.toPageException(e);
	        }
    	}
	}
    
    

    /**
     * sets if tag has a body or not
     * @param hasBody
     */
    public void hasBody(boolean hasBody) {
    	this.hasBody=hasBody;
    }

    /**
     * @return Returns the appendix.
     */
    public String getAppendix() {
        return appendix;
    }

    /**
     * @return return thistag
     */
    public Struct getThis() {
    	if(isCFCBasedCustomTag()){
    		return cfc;
    	}
        return thistagScope;
    }
    
    /**
     * @return return thistag
     */
    public Struct getCallerScope() {
        return callerScope;
    }
    
    /**
     * @return return thistag
     */
    public Struct getAttributesScope() {
        return attributesScope;
    }

	/**
	 * @return the ctVariablesScope
	 */
	public Struct getVariablesScope() {
		if(isCFCBasedCustomTag())	{
			return cfc.getComponentScope();
		}
		return ctVariablesScope;
	}
	
    /**
	 * @return the cfc
	 */
	public Component getComponent() {
		return cfc;
	}
	
	public boolean isCFCBasedCustomTag() {
		return getSource().isCFC();
	}
	
	private InitFile getSource() {
		if(source==null){
			try {
				source=initFile(pageContext);
			} catch (PageException e) {
				e.printStackTrace();
			}
		}
		return source;
	}

	/*class InitFile {
		PageSource ps;
		String filename;
		boolean isCFC;

		public InitFile(PageSource ps,String filename,boolean isCFC){
			this.ps=ps;
			this.filename=filename;
			this.isCFC=isCFC;
		}
	}*/
	
	
}