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.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.lang.StringEscapeUtils;
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.functionality.lightui.browse.BrowseStatsEngine;
import eu.dnetlib.functionality.lightui.formatter.ResultFormatter;
import eu.dnetlib.functionality.lightui.formatter.ResultFormatterFactory;
import eu.dnetlib.functionality.lightui.utils.Base64Coder;
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 String logoutURL;

	private int pageSize;

	private boolean useBbqs;

	private String mainFormat;

	private String toolsPage;

	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);
		map.addAttribute("toolsPage", this.toolsPage);
	}

	/**
	 * 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;
		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, HttpServletRequest request) throws CQLParseException, IOException, NoSuchAlgorithmException, ISLookUpException {

		prepareCommonEnvironment(map);
		updateSession(request);
		fillMapWithSessionParams(map, request.getSession());

		log.info("showing results ");

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

		CQLNode query = queryBuilder.getQuery(parameters);

		String cqlQuery = query.toCQL();
		log.info(cqlQuery);
		searchEngine.prepareQuery(cqlQuery, mainFormat);

		map.addAttribute("query", cqlQuery.replaceAll("'", "\\'"));
		map.addAttribute("queryEscaped", StringEscapeUtils.escapeXml(cqlQuery));

		Map<String, String> params = new HashMap<String, String>();

		Enumeration<?> names = request.getParameterNames();
		while (names.hasMoreElements()) {
			String key = (String) names.nextElement();
			String[] values = request.getParameterValues(key);
			if (values.length == 1 && values[0] != null && !values[0].isEmpty()) {
				String value = values[0];
				if (values[0].contains("||")) {
					value = value.substring(value.indexOf("||") + 2);
				}
				params.put(key, StringEscapeUtils.escapeXml(value));
			}
		}

		map.addAttribute("params", params);
	}

	@RequestMapping("resultsCQL.do")
	public void resultsCQL(ModelMap map, HttpServletRequest request, @RequestParam(value = "query", required = true) String cqlQuery)
			throws ISLookUpException {

		prepareCommonEnvironment(map);
		fillMapWithSessionParams(map, request.getSession());

		log.info("showing results ");

		searchEngine.prepareQuery(cqlQuery, mainFormat);

		map.addAttribute("query", cqlQuery.replaceAll("'", "\\'"));
		map.addAttribute("queryEscaped", StringEscapeUtils.escapeXml(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,
			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,
			@RequestParam(value = "mobile", required = false) Object mobile) {

		String view = "query";
		String view_empty = "emptyQuery";

		if (mobile != null) {
			view = "query_mobile";
			view_empty = "emptyQuery_mobile";
		}

		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 view_empty;

			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> prevPages = new ArrayList<Integer>();
			Collection<Integer> nextPages = 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++) {
				int p = firstNavigablePage + i;
				if (p < page)
					prevPages.add(p);
				else if (p > page)
					nextPages.add(p);
			}

			map.addAttribute("pages", pages);
			map.addAttribute("currentPage", page);
			if (page > 1) {
				map.addAttribute("prevPage", page - 1);
				map.addAttribute("prevPages", prevPages);
			}
			if (page < pages) {
				map.addAttribute("nextPage", page + 1);
				map.addAttribute("nextPages", nextPages);
			}

			log.debug("Rendering results");
			return view;
		} catch (Exception e) {
			log.error("Error occurred", e);
			e.printStackTrace();
			return view_empty;
		}
	}

	/**
	 * 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";
		}
	}

	/**
	 * 
	 * 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 {

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

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

			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:" + this.logoutURL;
	}

	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("session element " + name + " has value: " + 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;
	}

	public String getLogoutURL() {
		return logoutURL;
	}

	public void setLogoutURL(String logoutURL) {
		this.logoutURL = logoutURL;
	}

	public String getToolsPage() {
		return toolsPage;
	}

	public void setToolsPage(String toolsPage) {
		this.toolsPage = toolsPage;
	}

}
