
package railo.runtime.converter;

import java.io.IOException;
import java.io.Writer;
import java.util.Calendar;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

import railo.commons.date.JREDateTimeUtil;
import railo.commons.lang.StringUtil;
import railo.runtime.PageContext;
import railo.runtime.engine.ThreadLocalPageContext;
import railo.runtime.op.Caster;
import railo.runtime.op.Decision;
import railo.runtime.type.Array;
import railo.runtime.type.Collection;
import railo.runtime.type.Collection.Key;
import railo.runtime.type.Query;
import railo.runtime.type.Struct;
import railo.runtime.type.dt.DateTime;
import railo.runtime.type.util.CollectionUtil;


/**
 * class to serialize to Convert CFML Objects (query,array,struct usw) to a JavaScript representation
 */
public final class JSConverter extends ConverterSupport {

	private static final String NULL = "null";
	private boolean useShortcuts=false;
	private boolean useWDDX=true;

	/**
	 * serialize a CFML object to a JavaScript Object
	 * @param object object to serialize
	 * @param clientVariableName name of the variable to create 
	 * @return vonverte Javascript Code as String
	 * @throws ConverterException
	 */
	public String serialize(Object object, String clientVariableName) throws ConverterException {
		StringBuffer sb=new StringBuffer();
		_serialize(clientVariableName,object,sb,new HashSet<Object>());
		String str = sb.toString().trim();
		return clientVariableName+"="+str+(StringUtil.endsWith(str, ';')?"":";");
		//return sb.toString();
	}

	

	   
	@Override
	public void writeOut(PageContext pc, Object source, Writer writer) throws ConverterException, IOException {
		writer.write(_serialize(source));
		writer.flush();
	}
	private String _serialize(Object object) throws ConverterException {
		StringBuffer sb=new StringBuffer();
		_serialize("tmp",object,sb,new HashSet<Object>());
		String str = sb.toString().trim();
		return str+(StringUtil.endsWith(str, ';')?"":";");
		//return sb.toString();
	}
	
	
	
	private void _serialize(String name,Object object,StringBuffer sb,Set<Object> done) throws ConverterException {
		// NULL
		if(object==null) {
			sb.append(goIn());
			sb.append(NULL+";");
			return;
		}
		// String
		if(object instanceof String || object instanceof StringBuffer) {
			sb.append(goIn());
			sb.append("\"");
			sb.append(StringUtil.escapeJS(object.toString()));
			sb.append("\";");
			return;
		}
		// Number
		if(object instanceof Number) {
			sb.append(goIn());
			sb.append("\"");
			sb.append(Caster.toString(((Number)object)));
			sb.append("\";");
			return;
		}
		// Date
		if(Decision.isDateSimple(object,false)) {
			_serializeDateTime(Caster.toDate(object,false,null,null),sb);
			return;
		}
		// Boolean
		if(object instanceof Boolean) {
			sb.append(goIn());
			sb.append("\"");
			sb.append((((Boolean)object).booleanValue()?"true":"false"));
			sb.append("\";");
			return;
		}
		
		Object raw = LazyConverter.toRaw(object);
		if(done.contains(raw)){
			sb.append(NULL+";");
			return;
		}
		done.add(raw);
		try {
			// Struct
			if(object instanceof Struct) {
				_serializeStruct(name,(Struct)object,sb,done);
				return;
			}
			// Map
			if(object instanceof Map) {
				_serializeMap(name,(Map)object,sb,done);
				return;
			}
			// List
			if(object instanceof List) {
				_serializeList(name,(List)object,sb,done);
				return;
			}
			// Array
			if(Decision.isArray(object)) {
				_serializeArray(name,Caster.toArray(object,null),sb,done);
				return;
			}
			// Query
			if(object instanceof Query) {
				_serializeQuery(name,(Query)object,sb,done);
				return;
			}
		}
		finally {
			done.remove(raw);
		}
		
		throw new ConverterException("can't serialize Object of type ["+Caster.toClassName(object)+"] to a js representation");
		//deep--;
		//return rtn;
	}
	

	
	/**
	 * serialize a Array
	 * @param name 
	 * @param array Array to serialize
	 * @param sb 
	 * @param done 
	 * @return serialized array
	 * @throws ConverterException
	 */
	private void _serializeArray(String name, Array array, StringBuffer sb, Set<Object> done) throws ConverterException {
		_serializeList(name,array.toList(),sb,done);
	}
	
	/**
	 * serialize a List (as Array)
	 * @param name 
	 * @param list List to serialize
	 * @param sb 
	 * @param done 
	 * @return serialized list
	 * @throws ConverterException
	 */
	private void _serializeList(String name, List list, StringBuffer sb, Set<Object> done) throws ConverterException {
		
		
		if(useShortcuts)sb.append("[];");
		else sb.append("new Array();");
		
		ListIterator it=list.listIterator();
		int index=-1;
		while(it.hasNext()) {
			//if(index!=-1)sb.append(",");
			index = it.nextIndex();
			sb.append(name+"["+index+"]=");
			_serialize(name+"["+index+"]",it.next(),sb,done);
			//sb.append(";");
		}
	}

	/**
	 * serialize a Struct
	 * @param name 
	 * @param struct Struct to serialize
	 * @param done 
	 * @param sb2 
	 * @return serialized struct
	 * @throws ConverterException
	 */
	private String _serializeStruct(String name, Struct struct, StringBuffer sb, Set<Object> done) throws ConverterException {
		if(useShortcuts)sb.append("{};");
		else sb.append("new Object();");
		
		Iterator<Entry<Key, Object>> it = struct.entryIterator();
		Entry<Key, Object> e;
		while(it.hasNext()) {
			e = it.next();
			// lower case ist ok!
			String key=StringUtil.escapeJS(Caster.toString(e.getKey().getLowerString(),""));
            sb.append(name+"[\""+key+"\"]=");
			//try {
				_serialize(name+"[\""+key+"\"]",e.getValue(),sb,done);
			/*} 
			catch (PageException pe) {
				_serialize(name+"[\""+key+"\"]",pe.getMessage(),sb,done);
			}*/
		}
        return sb.toString();
	}

	/**
	 * serialize a Map (as Struct)
	 * @param name 
	 * @param map Map to serialize
	 * @param done 
	 * @param sb2 
	 * @return serialized map
	 * @throws ConverterException
	 */
	private String _serializeMap(String name, Map map, StringBuffer sb, Set<Object> done) throws ConverterException {

		if(useShortcuts)sb.append("{}");
		else sb.append("new Object();");
		Iterator it=map.keySet().iterator();
		while(it.hasNext()) {
			Object key=it.next();
			String skey=StringUtil.toLowerCase(StringUtil.escapeJS(key.toString()));
            sb.append(name+"[\""+skey+"\"]=");
			_serialize(name+"[\""+skey+"\"]",map.get(key),sb,done);
			//sb.append(";");
		}
		return sb.toString();
	}
	
	/**
	 * serialize a Query
	 * @param query Query to serialize
	 * @param done 
	 * @return serialized query
	 * @throws ConverterException
	 */
	private void _serializeQuery(String name,Query query,StringBuffer sb, Set<Object> done) throws ConverterException {
		if(useWDDX)_serializeWDDXQuery(name,query,sb,done);
		else _serializeASQuery(name,query,sb,done);
	}

	private void _serializeWDDXQuery(String name,Query query,StringBuffer sb, Set<Object> done) throws ConverterException {
		Iterator<Key> it = query.keyIterator();
		Key k;
		sb.append("new WddxRecordset();");
		
		int recordcount=query.getRecordcount();
		int i=-1;
		while(it.hasNext()) {
			i++;
			k = it.next();
			if(useShortcuts)sb.append("col"+i+"=[];");
			else sb.append("col"+i+"=new Array();");
			// lower case ist ok!
			String skey = StringUtil.escapeJS(k.getLowerString());
			for(int y=0;y<recordcount;y++) {
				
				sb.append("col"+i+"["+y+"]=");
				
				_serialize("col"+i+"["+y+"]",query.getAt(k,y+1,null),sb,done);
				
			}
			sb.append(name+"[\""+skey+"\"]=col"+i+";col"+i+"=null;");
		}
	}

	private void _serializeASQuery(String name,Query query,StringBuffer sb, Set<Object> done) throws ConverterException {

		Collection.Key[] keys = CollectionUtil.keys(query);
		String[] strKeys = new String[keys.length];
		for(int i=0;i<strKeys.length;i++) {
			strKeys[i] = StringUtil.escapeJS(keys[i].getString());
		}
		if(useShortcuts)sb.append("[];");
		else sb.append("new Array();");
		
		int recordcount=query.getRecordcount();
		for(int i=0;i<recordcount;i++) {
			if(useShortcuts)sb.append(name+"["+i+"]={};");
			else sb.append(name+"["+i+"]=new Object();");
			
			for(int y=0;y<strKeys.length;y++) {
				sb.append(name+"["+i+"]['"+strKeys[y]+"']=");
				_serialize(name+"["+i+"]['"+strKeys[y]+"']",query.getAt(keys[y],i+1,null),sb,done);
			}
		}
	}
	
	

	/**
	 * serialize a DateTime
	 * @param dateTime DateTime to serialize
	 * @param sb 
	 * @param sb
	 * @throws ConverterException
	 */
	private synchronized void _serializeDateTime(DateTime dateTime, StringBuffer sb) {
	   
		Calendar c = JREDateTimeUtil.getThreadCalendar(ThreadLocalPageContext.getTimeZone());
		c.setTime(dateTime);
	    sb.append(goIn());
	    sb.append("new Date(");
	    sb.append(c.get(Calendar.YEAR));
	    sb.append(",");
	    sb.append(c.get(Calendar.MONTH));
	    sb.append(",");
	    sb.append(c.get(Calendar.DAY_OF_MONTH));
	    sb.append(",");
	    sb.append(c.get(Calendar.HOUR_OF_DAY));
	    sb.append(",");
	    sb.append(c.get(Calendar.MINUTE));
	    sb.append(",");
	    sb.append(c.get(Calendar.SECOND));
	    sb.append(");");
	}

	private String goIn() {
		//StringBuffer rtn=new StringBuffer(deep);
		//for(int i=0;i<deep;i++) rtn.append('\t');
		return "";//rtn.toString();
	}

	public void useShortcuts(boolean useShortcuts) { 
		this.useShortcuts=useShortcuts;
		
	}

	public void useWDDX(boolean useWDDX) {
		this.useWDDX=useWDDX;
	}
	
	/*
	 * @param args
	 * @throws Exception
	 
	public static void main(String[] args) throws Exception {
		JSConverter js=new JSConverter();
		Query query=QueryNew.call(null,"aaa,bbb,ccc");
		QueryAddRow.call(null,query);
		QuerySetCell.call(null,query,"aaa","1.1");
		QuerySetCell.call(null,query,"bbb","1.2");
		QuerySetCell.call(null,query,"ccc","1.3");
		QueryAddRow.call(null,query);
		QuerySetCell.call(null,query,"aaa","2.1");
		QuerySetCell.call(null,query,"bbb","2.2");
		QuerySetCell.call(null,query,"ccc","2.3");
		QueryAddRow.call(null,query);
		QuerySetCell.call(null,query,"aaa","3.1");
		QuerySetCell.call(null,query,"bbb","3.2");
		QuerySetCell.call(null,query,"ccc","3.3<hello>");
		Array arr2=List ToArray.call(null,"111,222");
		Array arr=List ToArray.call(null,"aaaa,bbb,ccc,dddd,eee");
		
		arr.set(10,arr2);

		Struct sct= new Struct();
		sct.set("aaa","val1");
		sct.set("bbb","val2");
		sct.set("ccc","val3");
		sct.set("ddd",arr2);
		
		/*
	}*/
}