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.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.stereotype.Controller;
import org.springframework.ui.ModelMap;
import org.springframework.web.bind.annotation.PathVariable;
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.provision.index.rmi.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.utils.Base64Coder;
import eu.dnetlib.functionality.lightui.utils.CachingPublisherClient;
import eu.dnetlib.functionality.lightui.utils.DriverCollection;
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.Repository;
import eu.dnetlib.functionality.lightui.utils.ResultSet;
import eu.dnetlib.miscutils.dom4j.XPathHelper;
import eu.dnetlib.soap.cxf.StandaloneCxfEndpointReferenceBuilder;

@Controller
public class MainController implements ApplicationContextAware {

	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;

	/**
	 * 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 toolsPage;

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

	/**
	 * cql query builder.
	 */

	private ApplicationContext applicationContext;

	protected void prepareCommonEnvironment(final ModelMap map, final ContextConfiguration conf) throws ISLookUpException {
		Collection<DriverCollection> collections;
		collections = conf.getCollectionDao().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("/lightUI/{context}/index.do")
	public void mainPage(final ModelMap map, final HttpServletRequest request, @PathVariable final String context) throws ISLookUpException {
		ContextConfiguration conf = getConfiguration(context);

		prepareCommonEnvironment(map, conf);

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

		Collection<DriverCollection> collections;
		collections = conf.getCollectionDao().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", conf.getCollectionDao().getLanguages());
	}

	@RequestMapping("/lightUI/{context}/results.do")
	public void results(final ModelMap map, final HttpServletRequest request, @PathVariable final String context) throws CQLParseException, IOException,
			NoSuchAlgorithmException, ISLookUpException {

		ContextConfiguration conf = getConfiguration(context);
		prepareCommonEnvironment(map, conf);

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

		log.info("showing results ");

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

		CQLNode query = conf.getQueryBuilder().getQuery(parameters);

		String cqlQuery = query.toCQL();
		log.info(cqlQuery);
		searchEngine.prepareQuery(cqlQuery, conf.getMainFormat(), conf.getLayout(), conf.getInterpretation());

		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("/lightUI/{context}/resultsCQL.do")
	public void resultsCQL(final ModelMap map,
			final HttpServletRequest request,
			@PathVariable final String context,
			@RequestParam(value = "query", required = true) final String cqlQuery) throws ISLookUpException {

		ContextConfiguration conf = getConfiguration(context);
		prepareCommonEnvironment(map, conf);
		fillMapWithSessionParams(map, request.getSession());

		log.info("showing results ");

		searchEngine.prepareQuery(cqlQuery, conf.getMainFormat(), conf.getLayout(), conf.getInterpretation());

		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("/lightUI/{context}/query.do")
	public String results(final ModelMap map,
			final HttpSession session,
			@PathVariable final String context,
			@RequestParam(value = "query", required = false) final String query,
			@RequestParam(value = "PageNumber", required = false) Integer page,
			@RequestParam(value = "rsId", required = false) final String rsId,
			@RequestParam(value = "resultset_address", required = false) final String rsAddress,
			@RequestParam(value = "mobile", required = false) final Object mobile) {

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

		ContextConfiguration conf = getConfiguration(context);

		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, conf.getMainFormat(), conf.getLayout(), conf.getInterpretation(), 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 = conf.getRowFormatterFactory().newInstance(session);
			for (String element : rawResults) {
				String row = ff.viewDocument(element);
				String objIdentifier = XPathHelper.selectElement(element, "//*[local-name()='objIdentifier']").getText();
				publisher.updateRecord(objIdentifier, conf.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", conf.getMainFormat());

			// 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("/lightUI/{context}/browseStats.do")
	public String browseStats(final ModelMap map,
			final HttpSession session,
			@PathVariable final String context,
			@RequestParam(value = "query", required = false) final String query) {
		try {
			log.info("browseStats.do");
			ContextConfiguration conf = getConfiguration(context);
			fillMapWithSessionParams(map, session);
			map.addAttribute("browseStats",
					browseEngine.getBrowseStats(query, conf.getBrowseFields(), conf.getMainFormat(), conf.getLayout(), conf.getInterpretation()));
			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("/lightUI/{context}/documentCQL.do")
	public String getDocumentCQL(final ModelMap map,
			@PathVariable final String context,
			@RequestParam(value = "key", required = false) final String key,
			@RequestParam(value = "value", required = false) final 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);
			ContextConfiguration conf = getConfiguration(context);
			searchEngine.prepareQuery(query, conf.getMainFormat(), conf.getLayout(), conf.getInterpretation());
			W3CEndpointReference epr = null;
			ResultSet resultset = searchEngine.resultSetForQueryOrEpr(query, conf.getMainFormat(), conf.getLayout(), conf.getInterpretation(), 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("/lightUI/{context}/document.do")
	public void document(final ModelMap map, final HttpServletRequest request, @PathVariable final String context, @RequestParam("id") final String id) {
		HttpSession session = request.getSession();
		ContextConfiguration conf = getConfiguration(context);
		String body = publisher.getResourceById(id, conf.getMainFormat(), conf.getLayout(), conf.getInterpretation());

		updateSession(request);
		fillMapWithSessionParams(map, session);

		map.addAttribute("id", id);
		map.addAttribute("format", conf.getMainFormat());

		map.addAttribute("body", conf.getDocumentFormatterFactory().newInstance(session).viewDocument(body));
	}

	@RequestMapping("/lightUI/{context}/vocabularies.do")
	public String vocabularies(final ModelMap map,
			final HttpServletRequest request,
			@PathVariable final String context,
			@RequestParam(required = true, value = "v") final 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("/lightUI/{context}/logout.do")
	public String logout(final HttpSession session, @PathVariable final String context) {
		ContextConfiguration conf = getConfiguration(context);
		SecurityContext securityContext = (SecurityContext) session.getAttribute("SPRING_SECURITY_CONTEXT");

		securityContext.setAuthentication(null);

		return "redirect:" + conf.getLogoutURL();
	}

	protected ResultSet resultSetForQueryOrEpr(final ContextConfiguration conf, final String query, final W3CEndpointReference epr)
			throws IndexServiceException {
		return searchEngine.resultSetForQueryOrEpr(query, conf.getMainFormat(), conf.getLayout(), conf.getInterpretation(), epr);
	}

	private void clearSession(final 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(final 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(final ModelMap map, final 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(final 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(final int pageSize) {
		this.pageSize = pageSize;
	}

	public boolean isUseBbqs() {
		return useBbqs;
	}

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

	public String getToolsPage() {
		return toolsPage;
	}

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

	public ContextConfiguration getConfiguration(final String contextName) {
		return applicationContext.getBean("lightUI_" + contextName, ContextConfiguration.class);
	}

	@Override
	public void setApplicationContext(final ApplicationContext appContext) throws BeansException {
		this.applicationContext = appContext;
	}

}
