package net.anotheria.moskito.web.filters;

import net.anotheria.moskito.core.dynamic.EntryCountLimitedOnDemandStatsProducer;
import net.anotheria.moskito.core.dynamic.OnDemandStatsProducer;
import net.anotheria.moskito.core.dynamic.OnDemandStatsProducerException;
import net.anotheria.moskito.core.predefined.PageInBrowserStats;
import net.anotheria.moskito.core.predefined.PageInBrowserStatsFactory;
import net.anotheria.moskito.core.producers.IStatsProducer;
import net.anotheria.moskito.core.registry.ProducerRegistryFactory;
import net.anotheria.util.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;

/**
 * The filter collects HTML DOM related information - DOM load time, web page load time, etc.
 *
 * @author lrosenberg
 * @since 15.09.13 22:44
 */
public class JSTalkBackFilter implements Filter {
	/**
	 * Parameter name for the init parameter for the limit of dynamic case names (number of names) in the filter config. If the number of cases will exceed this limit,
	 * the new cases will be ignored (to prevent memory leakage).
	 */
	public static final String INIT_PARAM_LIMIT = "limit";
	/**
	 * Web page url path request parameter name.
	 */
	private static final String URL = "url";
	/**
	 * DOM load time request parameter name.
	 * Time value should be in milliseconds.
	 */
	private static final String DOM_LOAD_TIME = "domLoadTime";
	/**
	 * Web page load time request parameter name.
	 * Time value should be in milliseconds.
	 */
	private static final String WINDOW_LOAD_TIME = "windowLoadTime";
	/**
	 * Producer id request parameter name.
	 */
	private static final String PRODUCER_ID = "producerId";
	/**
	 * Producer category request parameter name.
	 */
	private static final String CATEGORY = "category";
	/**
	 * Producer subsystem request parameter name.
	 */
	private static final String SUBSYSTEM = "subsystem";
	/**
	 * {@link Logger} instance.
	 */
	private Logger log;
	/**
	 * Init parameter 'limit' value.
	 * See {@link #INIT_PARAM_LIMIT}.
	 */
	private int limit = -1;

	/**
	 * Constructor.
	 */
	public JSTalkBackFilter() {
		log = LoggerFactory.getLogger(getClass());
	}

	@Override
	public void init(FilterConfig filterConfig) throws ServletException {
		String pLimit = filterConfig.getInitParameter(INIT_PARAM_LIMIT);
		if (pLimit != null) {
			try {
				limit = Integer.parseInt(pLimit);
			} catch (NumberFormatException ignored) {
				log.warn("couldn't parse limit \"" + pLimit + "\", assume -1 aka no limit.");
			}
		}
	}

	@Override
	public void doFilter(final ServletRequest servletRequest, final ServletResponse servletResponse, final FilterChain filterChain) throws IOException, ServletException {
		if (!(servletRequest instanceof HttpServletRequest) || !(servletResponse instanceof HttpServletResponse))
			return;

		HttpServletRequest request = (HttpServletRequest) servletRequest;
		HttpServletResponse response = (HttpServletResponse) servletResponse;

		final String producerId = getValueOrDefault(request, PRODUCER_ID, getDefaultProducerId());
		final String category = getValueOrDefault(request, CATEGORY, getDefaultCategory());
		final String subsystem = getValueOrDefault(request, SUBSYSTEM, getDefaultSubsystem());

		final OnDemandStatsProducer producer = getProducer(producerId, category, subsystem);
		if (producer == null)
			return;

		final String urlPath = request.getParameter(URL);
		if (StringUtils.isEmpty(urlPath))
			return;

		final String domLoadTime = request.getParameter(DOM_LOAD_TIME);
		final String windowLoadTime = request.getParameter(WINDOW_LOAD_TIME);

		try {
			final PageInBrowserStats stats = (PageInBrowserStats) producer.getStats(urlPath);
			if (isLoadTimeValid(domLoadTime) && isLoadTimeValid(windowLoadTime)){
				stats.addLoadTime(Long.parseLong(domLoadTime), Long.parseLong(windowLoadTime));
				//Also add to cumulated
				PageInBrowserStats cumulated = (PageInBrowserStats)producer.getDefaultStats();
				cumulated.addLoadTime(Long.parseLong(domLoadTime), Long.parseLong(windowLoadTime));
				
			}

			writeNoContentResponse(response);
		} catch (OnDemandStatsProducerException e) {
			log.info("Couldn't get stats for : " + urlPath + ", probably limit reached");
		}
	}

	/**
	 * Returns parameter value from request if exists, otherwise - default value.
	 *
	 * @param req          {@link HttpServletRequest}
	 * @param paramName    name of the parameter
	 * @param defaultValue parameter default value
	 * @return request parameter value
	 */
	private String getValueOrDefault(HttpServletRequest req, String paramName, String defaultValue) {
		final String value = req.getParameter(paramName);
		return StringUtils.isEmpty(value) ? defaultValue : value;
	}

	/**
	 * Validate given load time request parameter.
	 *
	 * @param loadTimeParam string representation of load time
	 * @return {@code true} if given load time string is numeric value and greater than 0, otherwise - {@code false}
	 */
	private boolean isLoadTimeValid(final String loadTimeParam) {
		return !StringUtils.isEmpty(loadTimeParam) && isLong(loadTimeParam) && Long.valueOf(loadTimeParam) > 0;
	}

	/**
	 * Returns producer by given producer id.
	 * If it was not found then new producer will be created.
	 * If existing producer is not supported then producer with default producer id will be returned.
	 * If producer with default producer id is not supported then will be returned {@code null}.
	 *
	 * @param producerId id of the producer
	 * @param category   name of the category
	 * @param subsystem  name of the subsystem
	 * @return PageInBrowserStats producer
	 */
	@SuppressWarnings("unchecked")
	private OnDemandStatsProducer<PageInBrowserStats> getProducer(final String producerId, final String category, final String subsystem) {
		final IStatsProducer statsProducer = ProducerRegistryFactory.getProducerRegistryInstance().getProducer(producerId);
		// create new
		if (statsProducer == null)
			return createProducer(producerId, category, subsystem);
		// use existing
		if (statsProducer instanceof OnDemandStatsProducer && OnDemandStatsProducer.class.cast(statsProducer).getDefaultStats() instanceof PageInBrowserStats)
			return (OnDemandStatsProducer<PageInBrowserStats>) statsProducer;

		final IStatsProducer defaultStatsProducer = ProducerRegistryFactory.getProducerRegistryInstance().getProducer(getDefaultProducerId());
		// create default
		if (defaultStatsProducer == null)
			return createProducer(getDefaultProducerId(), category, subsystem);
		// use existing default
		if (statsProducer instanceof OnDemandStatsProducer && OnDemandStatsProducer.class.cast(statsProducer).getDefaultStats() instanceof PageInBrowserStats)
			return (OnDemandStatsProducer<PageInBrowserStats>) defaultStatsProducer;

		log.warn("Can't create OnDemandStatsProducer<BrowserStats> producer with passed id: [" + producerId + "] and default id: [" + getDefaultProducerId() + "].");

		return null;
	}

	/**
	 * Creates producer with given producer id, category and subsystem and register it in producer registry.
	 *
	 * @param producerId id of the producer
	 * @param category   name of the category
	 * @param subsystem  name of the subsystem
	 * @return PageInBrowserStats producer
	 */
	private OnDemandStatsProducer<PageInBrowserStats> createProducer(final String producerId, final String category, final String subsystem) {
		OnDemandStatsProducer<PageInBrowserStats> producer = limit == -1 ? new OnDemandStatsProducer<PageInBrowserStats>(producerId, category, subsystem, new PageInBrowserStatsFactory()) :
				new EntryCountLimitedOnDemandStatsProducer<PageInBrowserStats>(producerId, category, subsystem, new PageInBrowserStatsFactory(), limit);

		ProducerRegistryFactory.getProducerRegistryInstance().registerProducer(producer);
		return producer;
	}

	/**
	 * Writes {@link HttpServletResponse#SC_NO_CONTENT} to response and flushes the stream.
	 * This means that only header and empty body will be returned to the caller script.
	 *
	 * @param response {@link HttpServletResponse}
	 * @throws IOException on errors
	 */
	private void writeNoContentResponse(final HttpServletResponse response) throws IOException {
		response.setStatus(HttpServletResponse.SC_NO_CONTENT);
		response.setContentType("image/gif");

		final PrintWriter out = response.getWriter();
		out.flush();
	}

	@Override
	public void destroy() {
		// do nothing
	}

	/**
	 * Returns producer id.
	 *
	 * @return producer id
	 */
	public String getDefaultProducerId() {
		return getClass().getSimpleName();
	}

	/**
	 * Returns default producer category name.
	 *
	 * @return producer category name
	 */
	public String getDefaultCategory() {
		return "default";
	}

	/**
	 * Returns default subsystem name.
	 *
	 * @return subsystem name
	 */
	public String getDefaultSubsystem() {
		return "default";
	}

	/**
	 * Checks if given string is long.
	 *
	 * @param str {@link String}
	 * @return true if it is long or false otherwise
	 */
	private boolean isLong(final String str) {
		try {
			Long.valueOf(str);
			return true;
		} catch (NumberFormatException e) {
			return false;
		}
	}
}
