package railo.runtime.tag;

import java.util.Iterator;
import java.util.Map;

import railo.commons.lang.StringUtil;
import railo.runtime.exp.ApplicationException;
import railo.runtime.exp.PageException;
import railo.runtime.ext.tag.TagImpl;
import railo.runtime.op.Caster;
import railo.runtime.op.Decision;
import railo.runtime.search.AddionalAttrs;
import railo.runtime.search.SearchCollection;
import railo.runtime.search.SearchData;
import railo.runtime.search.SearchDataImpl;
import railo.runtime.search.SearchEngine;
import railo.runtime.search.SearchException;
import railo.runtime.search.SuggestionItem;
import railo.runtime.tag.util.DeprecatedUtil;
import railo.runtime.type.KeyImpl;
import railo.runtime.type.QueryImpl;
import railo.runtime.type.Struct;
import railo.runtime.type.StructImpl;
import railo.runtime.type.util.KeyConstants;
import railo.runtime.type.util.ListUtil;

public final class Search extends TagImpl {

	private static final String[] EMPTY = new String[0];

	private static final int SUGGESTIONS_ALWAYS = Integer.MAX_VALUE;
	private static final int SUGGESTIONS_NEVER = -1;

	private static final railo.runtime.type.Collection.Key FOUND = KeyImpl.intern("found");
	private static final railo.runtime.type.Collection.Key SEARCHED = KeyImpl.intern("searched");
	private static final railo.runtime.type.Collection.Key KEYWORDS = KeyImpl.intern("keywords");
	private static final railo.runtime.type.Collection.Key KEYWORD_SCORE = KeyImpl.intern("keywordScore");
	
	/** Specifies the criteria type for the search. */
	private short type=SearchCollection.SEARCH_TYPE_SIMPLE;

	/** Specifies the maximum number of entries for index queries. If omitted, all rows are returned. */
	private int maxrows=-1;

	/** Specifies the criteria for the search following the syntactic rules specified by type. */
	private String criteria="";

	/** Specifies the first row number to be retrieved. Default is 1. */
	private int startrow=1;

	/** The logical collection name that is the target of the search operation or an external collection 
	** 		with fully qualified path. */
	private SearchCollection[] collections;

	/** A name for the search query. */
	private String name;

	private String[] category=EMPTY;
	private String categoryTree="";
	private String status;
	private int suggestions=SUGGESTIONS_NEVER;
	private int contextPassages=0;
	private int contextBytes=300;
	private String contextHighlightBegin="<b>";
	private String contextHighlightEnd="</b>";
	private String previousCriteria;

	//private int spellCheckMaxLevel=10;
	//private String result=null;
	
	@Override
	public void release()	{
		super.release();
		type=SearchCollection.SEARCH_TYPE_SIMPLE;
		maxrows=-1;
		criteria="";
		startrow=1;
		collections=null;
		
		category=EMPTY;
		categoryTree="";
		status=null;
		suggestions=SUGGESTIONS_NEVER;
		contextPassages=0;
		contextBytes=300;
		contextHighlightBegin="<b>";
		contextHighlightEnd="</b>";
		previousCriteria=null;
		

		//spellCheckMaxLevel=10;
		//result=null;
		
	}

	/** set the value type
	*  Specifies the criteria type for the search.
	* @param type value to set
	 * @throws ApplicationException
	**/
	public void setType(String type) throws ApplicationException	{
		if(type==null) return;
	    type=type.toLowerCase().trim();
	    if(type.equals("simple"))this.type=SearchCollection.SEARCH_TYPE_SIMPLE;
	    else if(type.equals("explicit"))this.type=SearchCollection.SEARCH_TYPE_EXPLICIT;
	    else 
	        throw new ApplicationException("attribute type of tag search has an invalid value, valid values are [simple,explicit] now is ["+type+"]");

	}

	/** set the value maxrows
	*  Specifies the maximum number of entries for index queries. If omitted, all rows are returned.
	* @param maxrows value to set
	**/
	public void setMaxrows(double maxrows)	{
		this.maxrows=(int) maxrows;
	}

	/** set the value criteria
	*  Specifies the criteria for the search following the syntactic rules specified by type.
	* @param criteria value to set
	**/
	public void setCriteria(String criteria)	{
		this.criteria=criteria;
	}

	/** set the value startrow
	*  Specifies the first row number to be retrieved. Default is 1.
	* @param startrow value to set
	**/
	public void setStartrow(double startrow)	{
		this.startrow=(int) startrow;
	}

	/** set the value collection
	*  The logical collection name that is the target of the search operation or an external collection 
	* 		with fully qualified path.
	* @param collection value to set
	 * @throws PageException
	**/
	public void setCollection(String collection) throws PageException	{
		String[] collNames=ListUtil.toStringArrayTrim(ListUtil.listToArrayRemoveEmpty(collection,','));
	    collections=new SearchCollection[collNames.length];
	    SearchEngine se = pageContext.getConfig().getSearchEngine();
	    try { 
		    for(int i=0;i<collections.length;i++) {
		        collections[i]=se.getCollectionByName(collNames[i]);
		    }
	    } catch (SearchException e) {
	        collections=null;
	        throw Caster.toPageException(e);
	    }
	}

	/** set the value language
	* @param language value to set
	**/
	public void setLanguage(String language)	{
		DeprecatedUtil.tagAttribute(pageContext,"Search", "language");
	}

	/** set the value external
	* @param external value to set
	 * @throws ApplicationException
	**/
	public void setExternal(boolean external) throws ApplicationException	{
		DeprecatedUtil.tagAttribute(pageContext,"Search", "external");
	}

	/** set the value name
	*  A name for the search query.
	* @param name value to set
	**/
	public void setName(String name)	{
		this.name=name;
	}  
	
	

	/**
	 * @param category the category to set
	 * @throws ApplicationException 
	 */
	public void setCategory(String listCategories)  {
		if(StringUtil.isEmpty(listCategories)) return;
		this.category = ListUtil.trimItems(ListUtil.listToStringArray(listCategories, ','));
	}


	/**
	 * @param categoryTree the categoryTree to set
	 * @throws ApplicationException 
	 */
	public void setCategorytree(String categoryTree) {
		if(StringUtil.isEmpty(categoryTree)) return;
		categoryTree=categoryTree.replace('\\', '/').trim();
		if(StringUtil.startsWith(categoryTree, '/'))categoryTree=categoryTree.substring(1);
		if(!StringUtil.endsWith(categoryTree, '/') && categoryTree.length()>0)categoryTree+="/";
		this.categoryTree = categoryTree;
	}

	/**
	 * @param contextBytes the contextBytes to set
	 * @throws ApplicationException 
	 */
	public void setContextbytes(double contextBytes) throws ApplicationException {
		this.contextBytes = (int)contextBytes;
	}

	/**
	 * @param contextHighlightBegin the contextHighlightBegin to set
	 * @throws ApplicationException 
	 */
	public void setContexthighlightbegin(String contextHighlightBegin) {
		this.contextHighlightBegin = contextHighlightBegin;
	}

	/**
	 * @param contextHighlightEnd the contextHighlightEnd to set
	 * @throws ApplicationException 
	 */
	public void setContexthighlightend(String contextHighlightEnd) {
		this.contextHighlightEnd = contextHighlightEnd;
	}

	/**
	 * @param contextPassages the contextPassages to set
	 * @throws ApplicationException 
	 */
	public void setContextpassages(double contextPassages) throws ApplicationException {
		this.contextPassages = (int)contextPassages;
	}
	
	
	

	/**
	 * @param previousCriteria the previousCriteria to set
	 * @throws ApplicationException 
	 */
	public void setPreviouscriteria(String previousCriteria) throws ApplicationException {
		this.previousCriteria = previousCriteria;
		throw new ApplicationException("attribute previousCriteria for tag search is not supported yet");
		// TODO impl tag attribute
	}

	/**
	 * @param status the status to set
	 * @throws ApplicationException 
	 */
	public void setStatus(String status) {
		if(!StringUtil.isEmpty(status))this.status = status;
	}

	/**
	 * @param suggestions the suggestions to set
	 * @throws ApplicationException 
	 */
	public void setSuggestions(String suggestions) throws PageException {
		if(StringUtil.isEmpty(suggestions))return;
		suggestions = suggestions.trim().toLowerCase();
		if("always".equals(suggestions)) this.suggestions=SUGGESTIONS_ALWAYS;
		else if("never".equals(suggestions)) this.suggestions=SUGGESTIONS_NEVER;
		else if(Decision.isNumeric(suggestions)) {
			this.suggestions=Caster.toIntValue(suggestions);
		}
		else	
			throw new ApplicationException("attribute suggestions has an invalid value ["+suggestions+"], valid values are [always,never,<positive numeric value>]");
		
		
	}

	@Override
	public int doStartTag() throws PageException	{
	    //SerialNumber sn = pageContext.getConfig().getSerialNumber();
	    //if(sn.getVersion()==SerialNumber.VERSION_COMMUNITY)
	    //    throw new SecurityException("no access to this functionality with the "+sn.getStringVersion()+" version of railo");
        final String v="VARCHAR",d="DOUBLE";
        String[] cols = new String[]{"title","url","summary","score","recordssearched","key","custom1","custom2","custom3","custom4",
        							"categoryTree","category","context","size","rank","author","type","collection"};
        
        // TODO support context
        String[] types = new String[]{v,v,v,d,d,v,v,v,v,v,v,v,v,d,d,v,v,v};
        SearchData data=new SearchDataImpl(suggestions);
        SuggestionItem item=null;// this is already here to make sure the classloader load this sinstance 
        
        
        railo.runtime.type.Query qry=new QueryImpl(cols,types,0,"query");
	    	
	    SearchCollection collection;
	    long time=System.currentTimeMillis();
	    AddionalAttrs.setAddionalAttrs(contextBytes,contextPassages,contextHighlightBegin,contextHighlightEnd);
	    try {
		    for(int i=0;i<collections.length;i++) {
		    	collection=collections[i];
		    	startrow=collection.search(data,qry,criteria,collection.getLanguage(),type,startrow,maxrows,categoryTree,category); 
		        
		        if(maxrows>=0 && qry.getRecordcount()>=maxrows) break;
		    }
		    pageContext.setVariable(name,qry);
	    }
	    catch(SearchException se) {
	        throw Caster.toPageException(se);
	    }
	    finally{
	    	AddionalAttrs.removeAddionalAttrs();
	    }
	    
	    time=System.currentTimeMillis()-time;
	    Double recSearched=new Double(data.getRecordsSearched());
	    int len=qry.getRecordcount();
	    for(int i=1;i<=len;i++) {
	        qry.setAt("recordssearched",i,recSearched);
	    }
	    
	    // status
	    if(status!=null) {
	    	Struct sct=new StructImpl();
	    	pageContext.setVariable(status, sct);
	    	sct.set(FOUND, new Double(qry.getRecordcount()));
	    	sct.set(SEARCHED, recSearched);
	    	sct.set(KeyConstants._time, new Double(time));
	    	
	    	// TODO impl this values
	    	
	    	Map s = data.getSuggestion();
	    	if(s.size()>0) {
	    		String key;
	    		
	    		Iterator it = s.keySet().iterator();
	    		Struct keywords=new StructImpl();
	    		Struct keywordScore=new StructImpl();
	    		sct.set(KEYWORDS, keywords);
		    	sct.set(KEYWORD_SCORE, keywordScore);
		    	Object obj;
		    	
	    		while(it.hasNext()) {
	    			key=(String) it.next();
	    			// FUTURE add SuggestionItem as interface to public interface
	    			
	    			// the problem is a conflict between the SuggestionItem version from core and extension
	    			obj=s.get(key);
	    			if(obj instanceof SuggestionItem){
	    				item=(SuggestionItem)obj;
	    				keywords.set(key, item.getKeywords());
		    			keywordScore.set(key, item.getKeywordScore());
	    			}
	    			else {
	    				Class clazz = obj.getClass();
	    				try {
							keywords.set(key, clazz.getMethod("getKeywords", new Class[0]).invoke(obj, new Object[0]));
							keywordScore.set(key, clazz.getMethod("getKeywordScore", new Class[0]).invoke(obj, new Object[0]));
						} 
	    				catch (Exception e) {}
	    			}
	    			
	    			
	    			
	    		}
		    	
	    		
	    		String query = data.getSuggestionQuery();
	    		if(query!=null) {
	    			String html = StringUtil.replace(query, "<suggestion>", "<b>", false);
	    			html = StringUtil.replace(html, "</suggestion>", "</b>", false);
		    		sct.set("suggestedQueryHTML", html);
		    		
	    			String plain = StringUtil.replace(query, "<suggestion>", "", false);
	    			plain = StringUtil.replace(plain, "</suggestion>", "", false);
		    		sct.set("suggestedQuery", plain);
	    		}
	    		
	    		
	    	}
	    	
	    	//if(suggestions!=SUGGESTIONS_NEVER)sct.set("suggestedQuery", "");
	    	//sct.set("keywords", "");
	    	//sct.set("keywordScore", "");
	    	
	    	
	    }
	    
	    
		return SKIP_BODY;
	}

	@Override
	public int doEndTag()	{
		return EVAL_PAGE;
	}

	
}