package it.cnr.isti.driver.web;

import it.cnr.isti.driver.utils.Base64Coder;
import it.cnr.isti.driver.utils.DriverCollection;
import it.cnr.isti.driver.utils.DriverCollectionDao;
import it.cnr.isti.driver.utils.ODL_EPR;
import it.cnr.isti.driver.utils.ODL_Utils;
import it.cnr.isti.driver.utils.PublisherServiceLocator;
import it.cnr.isti.driver.utils.Querable;
import it.cnr.isti.driver.utils.Repository;
import it.cnr.isti.driver.utils.ResultFormatter;
import it.cnr.isti.driver.utils.ResultSet;

import java.io.IOException;
import java.math.BigInteger;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Set;

import javax.annotation.Resource;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.ui.ModelMap;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.z3950.zing.cql.CQLAndNode;
import org.z3950.zing.cql.CQLNode;
import org.z3950.zing.cql.CQLParseException;
import org.z3950.zing.cql.CQLParser;
import org.z3950.zing.cql.CQLRelation;
import org.z3950.zing.cql.CQLTermNode;
import org.z3950.zing.cql.ModifierSet;

import eu.rinfrastructures.util.dom4j.XPathHelper;

//@Controller
public class MainController {

	protected Log log = LogFactory.getLog(MainController.class);
	
	@Autowired
	protected ODL_Utils utils;

	//@Autowired
	private DriverCollectionDao collectionDao;
	
	@Autowired
	private SearchEngine searchEngine;
	
	@Autowired
	protected PublisherServiceLocator publisherLocator;

	private int pageSize;
	
	private boolean useBbqs;
	
	private String mainFormat;
	
	public MainController() {
		this.pageSize = 10;
		this.useBbqs = false;
		this.mainFormat = "DMF";
	}
	
	/**
	 * This object transforms raw resultset data in short
	 * pieces of HTML suitable for displaying in the search result page
	 */
	@Resource(name="resultRowFormatter")
	private ResultFormatter rowFormatter;

	protected void prepareCommonEnvironment(ModelMap map) {
		Collection<DriverCollection> collections;
		collections = collectionDao.getCollections();

		Set<Repository> allRepositories = new LinkedHashSet<Repository>();
		for (DriverCollection collection : collections)
			allRepositories.addAll(collection.getRepositories());

		map.addAttribute("repositoryObjects", allRepositories);
	}
	
	/**
	 * This is the main page "index.do". The view will show the search form.
	 * We need to provide the view all the needed data: the list of collections
	 * and repositories, and encode the CQL their queries.
	 * 
	 * The view expects two maps:
	 * 
	 * 
	 * @param map data for the view, automatically passed by spring-mvc
	 */
	@RequestMapping("index.do")
	public void mainPage(ModelMap map) {
		prepareCommonEnvironment(map);
		
		Collection<DriverCollection> collections;
		collections = collectionDao.getCollections();

		Set<Repository> allRepositories = new LinkedHashSet<Repository>();
		for (DriverCollection collection : collections)
			allRepositories.addAll(collection.getRepositories());

		map.addAttribute("collections", base64Querables(collections));
		map.addAttribute("repositories", base64Querables(allRepositories));
		map.addAttribute("languages", collectionDao.getLanguages());
	}

	@RequestMapping("results.do")
	public void results(ModelMap map,
			@RequestParam("AllField") String allField, 
			@RequestParam("repositoryName")	String encodedCondition, 
			@RequestParam("dc:title") String title, 
			@RequestParam("dc:creator")	String creator, 
			@RequestParam("dc:description")	String description, 
			@RequestParam("dc:language") String language) throws CQLParseException, IOException, NoSuchAlgorithmException {

		prepareCommonEnvironment(map);
		
		String conditionSource = Base64Coder.decodeString(encodedCondition);
		MessageDigest m;
		String bbqName = null;
		
		if(isUseBbqs() && conditionSource.length() > 100) {
			m = MessageDigest.getInstance("MD5");
			m.update(conditionSource.getBytes(), 0, conditionSource.length());
			bbqName = new BigInteger(1,m.digest()).toString(16);
		}
		
		log.info("showing results ");
		
		Collection<CQLNode> nodes = new ArrayList<CQLNode>();
		CQLParser parser = new CQLParser();
		
		// TODO: better tokenization
		if(!allField.equals(""))
			for (String token : allField.split(" "))
				nodes.add(parser.parse("\"" + token + "\""));
		
		if (!title.equals("")) {
			nodes.add(new CQLTermNode("title", new CQLRelation("any"), title));
		}
		if (!creator.equals("")) {
			nodes.add(new CQLTermNode("creator", new CQLRelation("any"), creator));
		}
		if (!description.equals("")) {
			nodes.add(new CQLTermNode("description", new CQLRelation("any"), description));
		}
		if (!language.equals("")) {
			nodes.add(new CQLTermNode("language", new CQLRelation("="), language));
		}

		nodes.add(parser.parse(conditionSource));
		
		ModifierSet plain = new ModifierSet("and");
		ModifierSet qualified = new ModifierSet("and");
		if(bbqName != null)
			qualified.addModifier("driver.bbq", "=", bbqName);
		int i = nodes.size() - 1;
		
		CQLNode query = null;
		for(CQLNode node : nodes)
			if(query == null)
				query = node;
			else
				query = new CQLAndNode(node, query, (i-- > 1 ? plain : qualified));
			
		// forces the usage of BBQ
		if(nodes.size() == 1 && bbqName != null)
			query = new CQLAndNode(query, parser.parse("\"Textual\""), qualified);
		
		String cqlQuery = query.toCQL();
		log.info(cqlQuery);
		searchEngine.prepareQuery(cqlQuery);
		// TODO: escape as a javascript string 
		map.addAttribute("query", cqlQuery);
	}

	/**
	 * This action is called via Ajax. 
	 * The first query will have the CQL "query" parameter,
	 * while subsequent calls will have "rsId" and "rsAddress", along
	 * with the page number.
	 * 
	 * The page number can be also provided along with the CQL query, if
	 * used without Ajax (stateless). In this case the view should be changed. 
	 * 
	 * @param map data for the view
	 * @param query CQL query 
	 * @param page optional page number (counted from one)
	 * @param rsId optional resultset id
	 * @param rsAddress optional resultset URI (soap)
	 * @return view name: 'query' or 'emptyQuery'
	 */
	@RequestMapping("query.do")
	public String results(ModelMap map,
			@RequestParam(value="query", required=false) String query,
			@RequestParam(value="PageNumber", required=false) Integer page,
			@RequestParam(value="rsId", required=false) String rsId,
			@RequestParam(value="resultset_address", required=false) String rsAddress
			) {
		log.info("query.do");
		
		if(page == null)
			page = 1;
		
		int fromPosition = 1 + (page-1) * pageSize;
		int toPosition = fromPosition + pageSize - 1;
		
		ODL_EPR epr = null;
		if(rsId != null) {
			epr = new ODL_EPR();
			epr.setResourceIdentifier(rsId);
			epr.setAddress(rsAddress);
		}		
		
		ResultSet resultset = searchEngine.resultSetForQueryOrEpr(query, epr);
		epr = resultset.getEpr();
		
		int maxElements = resultset.getNumberOfElements();
		if(maxElements == 0)
			return "emptyQuery";
				
		int pages = (int) Math.ceil((float)maxElements / pageSize);
		
		Map<String, String> rowMap = new LinkedHashMap<String, String>();
		
		Collection<String> rawResults = resultset.getResult(fromPosition, toPosition);
		
		for(String element : rawResults) {
			String row = rowFormatter.viewDocument(element); 
			String objIdentifier = XPathHelper.selectElement(element, "//*[local-name()='objIdentifier']").getText();
			publisherLocator.getService().updateRecord(objIdentifier, getMainFormat(), element);
			rowMap.put(objIdentifier, row);
		}
				
		map.addAttribute("query", query);
		map.addAttribute("maxElements", maxElements);
		map.addAttribute("rows", rowMap);
		map.addAttribute("rsId", epr.getResourceIdentifier());
		map.addAttribute("rsAddress", epr.getAddress());
	
		// pagination
		
		Collection<Integer> pageNumbers = new ArrayList<Integer>();
		int pageWindow = 10;
		int firstNavigablePage = page - pageWindow/2;
		if(firstNavigablePage <= 0)
			firstNavigablePage = 1;
		if(firstNavigablePage + pageWindow >= pages)
			pageWindow = pages - firstNavigablePage + 1;
		
		for(int i=0; i<pageWindow; i++)
			pageNumbers.add(firstNavigablePage + i);
		
		map.addAttribute("pages", pages);
		map.addAttribute("currentPage", page);
		map.addAttribute("isFirstPage", page <= 1);
		map.addAttribute("isLastPage", page >= pages);
		map.addAttribute("pageNumbers", pageNumbers);
		
		log.debug("Rendering results");
		return "query";
	}

	protected ResultSet resultSetForQueryOrEpr(String query, ODL_EPR epr) {
		return searchEngine.resultSetForQueryOrEpr(query, epr);
	}
	
	/**
	 * 
	 * @param querables
	 *            list of querable objects
	 * @return map with base64-encoded queries as keys, and querable names as
	 *         values
	 */
	protected Map<String, String> base64Querables(
			Collection<? extends Querable> querables) {
		Map<String, String> encodedQuerables = new LinkedHashMap<String, String>();
		for (Querable querable : querables) {
			encodedQuerables.put(Base64Coder.encodeString(querable.getQuery()),
					querable.getAlias());
		}
		return encodedQuerables;
	}

	public int getPageSize() {
		return pageSize;
	}

	public void setPageSize(int pageSize) {
		this.pageSize = pageSize;
	}

	public DriverCollectionDao getCollectionDao() {
		return collectionDao;
	}

	public void setCollectionDao(DriverCollectionDao collectionDao) {
		this.collectionDao = collectionDao;
	}

	public boolean isUseBbqs() {
		return useBbqs;
	}

	public void setUseBbqs(boolean useBbqs) {
		this.useBbqs = useBbqs;
	}

	public String getMainFormat() {
		return mainFormat;
	}

	public void setMainFormat(String mainFormat) {
		this.mainFormat = mainFormat;
	}
	
}
