package eu.dnetlib.functionality.lightui.web;

import java.io.IOException;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import javax.xml.ws.wsaddressing.W3CEndpointReference;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.annotation.Required;
import org.springframework.security.context.SecurityContext;
import org.springframework.ui.ModelMap;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.z3950.zing.cql.CQLNode;
import org.z3950.zing.cql.CQLParseException;

import eu.dnetlib.data.index.IndexServiceException;
import eu.dnetlib.enabling.is.lookup.rmi.ISLookUpDocumentNotFoundException;
import eu.dnetlib.enabling.is.lookup.rmi.ISLookUpException;
import eu.dnetlib.enabling.resultset.rmi.ResultSetException;
import eu.dnetlib.functionality.lightui.browse.BrowseCategory;
import eu.dnetlib.functionality.lightui.browse.BrowseStatsEngine;
import eu.dnetlib.functionality.lightui.browse.BrowseValue;
import eu.dnetlib.functionality.lightui.formatter.ResultFormatter;
import eu.dnetlib.functionality.lightui.formatter.ResultFormatterFactory;
import eu.dnetlib.functionality.lightui.utils.AbstractQueryBuilder;
import eu.dnetlib.functionality.lightui.utils.Base64Coder;
import eu.dnetlib.functionality.lightui.utils.BrowseCollection;
import eu.dnetlib.functionality.lightui.utils.CachingPublisherClient;
import eu.dnetlib.functionality.lightui.utils.DriverCollection;
import eu.dnetlib.functionality.lightui.utils.DriverCollectionDao;
import eu.dnetlib.functionality.lightui.utils.EPRUtil;
import eu.dnetlib.functionality.lightui.utils.ODL_Utils;
import eu.dnetlib.functionality.lightui.utils.Querable;
import eu.dnetlib.functionality.lightui.utils.QueryBuilder;
import eu.dnetlib.functionality.lightui.utils.Repository;
import eu.dnetlib.functionality.lightui.utils.ResultSet;
import eu.dnetlib.miscutils.dom4j.XPathHelper;
import eu.dnetlib.soap.cxf.StandaloneCxfEndpointReferenceBuilder;

//@Controller
public class MainController {

	protected Log log = LogFactory.getLog(MainController.class);

	public static final String SESSION_PARAM_PREFIX = "session_param";
	
	@Resource
	protected ODL_Utils utils;

	/**
	 * EPR builder is necessary to create EPRs from base64 encoded resultet address+rsId pairs. TODO: we shouldn't
	 * encode the resultset anymore in the url
	 */
	@Resource
	private StandaloneCxfEndpointReferenceBuilder eprBuilder;

	//@Resource
	private DriverCollectionDao collectionDao;

	/**
	 * We ask the search engine to perform queries.
	 */
	@Resource
	private SearchEngine searchEngine;
	
	/**
	 * We ask the search engine to perform queries.
	 */
	@Resource
	private BrowseStatsEngine browseEngine;

	/**
	 * We fill the caching publisher with query results.
	 */
	@Resource
	protected CachingPublisherClient publisher;

	private int pageSize;

	private boolean useBbqs;

	private String mainFormat;

	public MainController() {
		this.pageSize = 10;
		this.useBbqs = false;
	}

	/**
	 * This object transforms raw resultset data in short pieces of HTML suitable for displaying in the search result
	 * page.
	 */
	private ResultFormatterFactory rowFormatterFactory;

	/**
	 * document formatter factory.
	 */
	private ResultFormatterFactory documentFormatterFactory;

	/**
	 * cql query builder.
	 */
	@Resource
	private QueryBuilder queryBuilder;

	private List<String> browseFields;

	protected void prepareCommonEnvironment(ModelMap map) throws ISLookUpException {
		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
	 * @throws ISLookUpException
	 */
	@RequestMapping("index.do")
	public void mainPage(ModelMap map, HttpServletRequest request) throws ISLookUpException {
		prepareCommonEnvironment(map);
		
		updateSession(request);
		fillMapWithSessionParams(map, request.getSession());
		
		Collection<DriverCollection> 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());
		
		try {
			if (collectionDao.getBrowseCollections() != null) {
				for (BrowseCollection bc : collectionDao.getBrowseCollections()) {
					map.addAttribute("browsecoll_" + bc.getBrowseField(), bc.getStringValues());
				}
			}
		} catch (Exception e) {
			log.error("Error showing browse collection", e);
		}
		
	}

	@RequestMapping("results.do")
	public String results(ModelMap map, HttpServletRequest request) throws CQLParseException, IOException, NoSuchAlgorithmException, ISLookUpException {

		prepareCommonEnvironment(map);
		updateSession(request);
		fillMapWithSessionParams(map, request.getSession());
		
		boolean lucky = false;

		log.info("showing results ");

		@SuppressWarnings("unchecked")
		Map<String, String[]> parameters = new HashMap<String, String[]>(request.getParameterMap());

		if (parameters.get("lucky") != null) {
			parameters.remove("lucky");
			lucky = true;
		}

		CQLNode query = queryBuilder.getQuery(parameters);

		String cqlQuery = query.toCQL();
		log.info(cqlQuery);
		searchEngine.prepareQuery(cqlQuery, mainFormat);
				
		// TODO: escape as a javascript string
		map.addAttribute("query", cqlQuery);
		map.addAttribute("params", request.getParameterMap());

		log.info("PATH INFO " + request.getServletPath());
		
		final String view = request.getServletPath().replace(".do",""); 
		
		if (lucky)
			return feelingLucky(view, cqlQuery);
		return view;
	}
	
	@RequestMapping("resultsCQL.do")
	public String resultsCQL(ModelMap map, HttpServletRequest request, @RequestParam(value = "query", required = true) String query) throws ISLookUpException {

		prepareCommonEnvironment(map);
		fillMapWithSessionParams(map, request.getSession());
				
		log.info("showing results ");

		searchEngine.prepareQuery(query, mainFormat);
		map.addAttribute("query", query);
		
		return request.getServletPath().replace("CQL.do","");
	}
	
	private String feelingLucky(String failbackView, String cqlQuery) {
		try {
			ResultSet res = searchEngine.resultSetForQueryOrEpr(cqlQuery, "oai_efg", null);
			if(res.getNumberOfElements() > 0) {
				String first = res.getResult(1, 1).get(0);
				
				String objIdentifier = XPathHelper.selectElement(first, "//*[local-name()='objIdentifier']").getText();
				return "redirect:document.do?id=" + objIdentifier;
			}
		} catch (IndexServiceException e) {
			log.warn("index failed, I'm not lucky", e);
		} catch (ResultSetException e) {
			log.warn("index resultset failed, I'm not lucky", e);
		}
		return failbackView;
	}

	/**
	 * 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, HttpSession session,
			@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) {

		try {
			log.info("query.do");
			
			fillMapWithSessionParams(map, session);
			
			if (page == null)
				page = 1;

			int fromPosition = 1 + (page - 1) * pageSize;
			int toPosition = fromPosition + pageSize - 1;

			W3CEndpointReference epr = null;
			if (rsId != null) {
				epr = eprBuilder.getEndpointReference(rsAddress, null, null, rsAddress + "?wsdl", rsId, null);
			}

			ResultSet resultset = searchEngine.resultSetForQueryOrEpr(query, mainFormat, 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);

			ResultFormatter ff = rowFormatterFactory.newInstance(session);
			for (String element : rawResults) {
				String row = ff.viewDocument(element);
				String objIdentifier = XPathHelper.selectElement(element, "//*[local-name()='objIdentifier']").getText();
				publisher.updateRecord(objIdentifier, getMainFormat(), element);
				rowMap.put(objIdentifier, row);
			}

			map.addAttribute("query", query);
			map.addAttribute("maxElements", maxElements);
			map.addAttribute("rows", rowMap);
			map.addAttribute("rsId", EPRUtil.getResourceIdentifier(epr));
			map.addAttribute("rsAddress", EPRUtil.getAddress(epr));
			map.addAttribute("format", mainFormat);

			// 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";
		} catch (Exception e) {
			log.error("Error occurred", e);
			e.printStackTrace();
			return "emptyQuery";
		}
	}
	
	
	/**
	 * This action is called via Ajax.
	 * @param map
	 *            data for the view
	 * @param session
	 *            HTTP session
	 * @param query
	 *            CQL query
	 */
	@RequestMapping("browseStats.do")
	public String browseStats(
			ModelMap map, HttpSession session,
			@RequestParam(value = "query", required = false) String query) {
		try {
			log.info("browseStats.do");
				
			fillMapWithSessionParams(map, session);
					
			map.addAttribute("browseStats", browseEngine.getBrowseStats(query, mainFormat, browseFields));
		
			return "browseStats";
		} catch (Exception e) {
			log.error("Error obtaining browse stats", e);
			return "emptyBrowseStats";
		}
	}
	
	@RequestMapping("browseClouds.do")
	public String browseClouds(ModelMap map, HttpSession session) {
		try {
			log.info("browseClouds.do");
				
			fillMapWithSessionParams(map, session);
			
			String defaultQuery = "textual";
			if (queryBuilder instanceof AbstractQueryBuilder) {
				defaultQuery = ((AbstractQueryBuilder) queryBuilder).getDefaultQuery();
			}
			
			List<BrowseCategory> list = browseEngine.getBrowseStats(defaultQuery, mainFormat, browseFields);
			
			for (BrowseCategory cat : list) {
				Collections.shuffle(cat.getValues());
				for (BrowseValue val : cat.getValues()) {
					val.setSize((int) (Math.round(Math.log10(val.getSize())) * 10));
				}
			}
			
			
			map.addAttribute("browseData", list);
		
			return "browseClouds";
		} catch (Exception e) {
			log.error("Error obtaining browse stats", e);
			return "emptyBrowseStats";
		}
	}
	

	/**
	 * 
	 * This method is used to obtain a document by query.
	 * 
	 * @param map
	 *            data for the view
	 * @param query
	 *            CQL query
	 */
	@RequestMapping("documentCQL.do")
	public String getDocumentCQL(
			ModelMap map,
			@RequestParam(value = "key", required = false) String key,
			@RequestParam(value = "value", required = false) String value,
			@RequestParam(value = "query", required = false) String query) {

		try {
			log.info("Searching for document: " + query);

			if ((query == null) || (query.length() == 0)) {
				if ((key != null) && (key.length() > 0)) {
					query = key + "=\"" + value + "\"";
				} else {
					query = "\"" + value + "\"";
				}
			}

			searchEngine.prepareQuery(query, mainFormat);
			W3CEndpointReference epr = null;
			ResultSet resultset = searchEngine.resultSetForQueryOrEpr(query, mainFormat, epr);
			epr = resultset.getEpr();

			int maxElements = resultset.getNumberOfElements();
			if (maxElements == 0)
				return "emptyQuery";

			List<String> rawResults = resultset.getResult(1, 1);
			String element = rawResults.get(0);

			String id = XPathHelper.selectElement(element, "//*[local-name()='objIdentifier']").getText();

			return "redirect:document.do?id=" + id;

			//			map.addAttribute("id", id);
			//			map.addAttribute("format", mainFormat);
			//
			//			map.addAttribute("body", documentFormatterFactory.newInstance().viewDocument(element));
			//
			//			log.info("Returned document: " + id);
			//			return "document";
		} catch (Exception e) {
			log.error("Error occurred", e);
			e.printStackTrace();
			return "emptyQuery";
		}
	}

	/**
	 * Used to view a single document.
	 * 
	 * @param map
	 *            data for the view
	 * @param document
	 *            id
	 * @param metadata
	 *            format
	 */
	@RequestMapping("document.do")
	public void document(ModelMap map, HttpServletRequest request, @RequestParam("id") String id) {
		HttpSession session = request.getSession();
		
		String body = publisher.getResourceById(id, mainFormat);
		
		updateSession(request);
		fillMapWithSessionParams(map, session);
		
		map.addAttribute("id", id);
		map.addAttribute("format", mainFormat);

		map.addAttribute("body", documentFormatterFactory.newInstance(session).viewDocument(body));
	}
	
	@RequestMapping("vocabularies.do")
	public String vocabularies(ModelMap map, HttpServletRequest request, 
				@RequestParam(required=true,value="v") String vocabularies) throws Exception {
		Map<String, List<String>> vocs = new HashMap<String, List<String>>();
		for (String s : vocabularies.split(",")) {
			String voc = s.trim();
			vocs.put(voc, utils.listItemsForVocabulary(voc));
		}
		map.addAttribute("vocabularies", vocs);
		return "vocabularies";
	}

	@RequestMapping("logout.do")
	public String logout(HttpSession session) {
		SecurityContext context = (SecurityContext) session.getAttribute("SPRING_SECURITY_CONTEXT");
		
		context.setAuthentication(null);
		
		return "redirect:http://auth.research-infrastructures.eu/logout";
	}

	
	protected ResultSet resultSetForQueryOrEpr(String query, W3CEndpointReference epr) throws IndexServiceException {
		return searchEngine.resultSetForQueryOrEpr(query, mainFormat, epr);
	}
	
	
	private void clearSession(HttpServletRequest request) {
		HttpSession session = request.getSession();
		
		Enumeration<?> oldnames = session.getAttributeNames();
		while (oldnames.hasMoreElements()) {
			session.removeAttribute(oldnames.nextElement().toString());
		}
		log.info("Session params cleaned");
	}

	private void updateSession(HttpServletRequest request) {
		if (request.getParameterMap().containsKey("_newsession"))
			clearSession(request);

		HttpSession session = request.getSession();
		Enumeration<?> names = request.getParameterNames();
		while (names.hasMoreElements()) {
			String name = names.nextElement().toString();
			String[] values = request.getParameterValues(name);
			if (name.startsWith("_") && !name.equals("_newsession") && values.length > 0) {
				session.setAttribute(SESSION_PARAM_PREFIX + name, values[0]);
			}
		}
	}
	
	private void fillMapWithSessionParams(ModelMap map, HttpSession session) {
		if (session == null) return;
		map.addAttribute("session", session);
		Enumeration<?> names = session.getAttributeNames();
		while (names.hasMoreElements()) {
			String name = names.nextElement().toString();
			map.addAttribute(name, session.getAttribute(name));
			
			log.info("Param " + name);
		}
	}
		
	/**
	 * 
	 * @param querables
	 *            list of querable objects
	 * @return map with base64-encoded queries as keys, and querable names as values
	 * @throws ISLookUpException
	 * @throws ISLookUpDocumentNotFoundException
	 */
	protected Map<String, String> base64Querables(Collection<? extends Querable> querables) throws ISLookUpDocumentNotFoundException, ISLookUpException {
		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;
	}

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

	public ResultFormatterFactory getRowFormatterFactory() {
		return rowFormatterFactory;
	}

	public void setRowFormatterFactory(ResultFormatterFactory rowFormatterFactory) {
		this.rowFormatterFactory = rowFormatterFactory;
	}

	public ResultFormatterFactory getDocumentFormatterFactory() {
		return documentFormatterFactory;
	}

	public void setDocumentFormatterFactory(ResultFormatterFactory documentFormatterFactory) {
		this.documentFormatterFactory = documentFormatterFactory;
	}

	public QueryBuilder getQueryBuilder() {
		return queryBuilder;
	}

	public void setQueryBuilder(QueryBuilder queryBuilder) {
		this.queryBuilder = queryBuilder;
	}

	public List<String> getBrowseFields() {
		return browseFields;
	}

	@Required
	public void setBrowseFields(List<String> browseFields) {
		this.browseFields = browseFields;
	}

}
