package online.filter;

import java.io.IOException;
import java.util.Objects;

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 org.apache.logging.log4j.LogManager;

import core.util.NumberUtil;
import online.filter.helper.ActionSessionMap;
import online.listener.SessionMutexListener;

/**
 * 一意リクエストフィルタ(POST時のみ)
 *
 * @author Tadashi Nakayama
 */
public class UniqueRequestFilter implements Filter {

	/** クラス名 */
	private static final String CLAZZ = UniqueRequestFilter.class.getName();

	/** 回数 */
	private int times = 60;
	/** 待ち時間 */
	private int millis = 1000;

	/**
	 * @see javax.servlet.Filter#init(javax.servlet.FilterConfig)
	 */
	@Override
	public void init(final FilterConfig filterConfig) {
		var val = filterConfig.getInitParameter("times");
		if (!Objects.toString(val, "").isEmpty()) {
			this.times = NumberUtil.toInt(val, 60);
		}
		val = filterConfig.getInitParameter("millis");
		if (!Objects.toString(val, "").isEmpty()) {
			this.millis = NumberUtil.toInt(val, 1000);
		}
	}

	/**
	 * @see javax.servlet.Filter#destroy()
	 */
	@Override
	public void destroy() {
		return;
	}

	/**
	 * @see javax.servlet.Filter#doFilter(javax.servlet.ServletRequest,
	 * javax.servlet.ServletResponse, javax.servlet.FilterChain)
	 */
	@Override
	public void doFilter(final ServletRequest svRequest, final ServletResponse svResponse,
			final FilterChain chain) throws IOException, ServletException {
		if (HttpServletRequest.class.isInstance(svRequest)
				&& HttpServletResponse.class.isInstance(svResponse)) {
			final var request = HttpServletRequest.class.cast(svRequest);

			// サーブレット取得
			final var path = getServletPath(request);
			if (path != null && request.getAttribute(CLAZZ) == null
							&& !FilterUtil.isGetMethod(request.getMethod())) {
				if (setRoute(path, request)) {
					try {
						// サーブレット実行
						chain.doFilter(svRequest, svResponse);
					} finally {
						delRoute(request);
					}
				} else {
					FilterUtil.sendTooMeny(HttpServletResponse.class.cast(svResponse));
				}
				return;
			}
		}

		chain.doFilter(svRequest, svResponse);
	}

	/**
	 * サーブレットパス取得
	 *
	 * @param request リクエスト
	 * @return サーブレットパス
	 */
	private String getServletPath(final HttpServletRequest request) {
		var srv = FilterUtil.getServletPath(request);
		if (!Objects.toString(srv, "").isEmpty()) {
			final var loc = srv.lastIndexOf('/');
			if (0 < loc) {
				srv = srv.substring(loc);
			}
			return srv;
		}
		return null;
	}

	/**
	 * 行先設定
	 *
	 * @param path 行先
	 * @param request リクエスト
	 * @return 設定された場合 true を返す。
	 */
	private boolean setRoute(final String path, final HttpServletRequest request) {
		for (var i = 0; i < this.times; i++) {
			if (addRouteInfo(path, request)) {
				return true;
			}

			try {
				Thread.sleep(this.millis);
			} catch (final InterruptedException ex) {
				LogManager.getLogger().info(ex.getMessage());
				break;
			}
		}
		return false;
	}

	/**
	 * 行先追加
	 *
	 * @param path 行先
	 * @param request リクエスト
	 * @return 追加された場合 true を返す。
	 */
	private boolean addRouteInfo(final String path, final HttpServletRequest request) {
		final var session = request.getSession(false);
		if (session != null) {
			synchronized (SessionMutexListener.getMutex(session)) {
				final var asm = Objects.requireNonNullElseGet(
					ActionSessionMap.class.cast(session.getAttribute(CLAZZ)),
					ActionSessionMap::new);
				if (asm.get(path) != null) {
					return false;
				}

				asm.put(path, CLAZZ);
				session.setAttribute(CLAZZ, asm);
			}
			request.setAttribute(CLAZZ, path);
		}
		return true;
	}

	/**
	 * 行先削除
	 *
	 * @param request リクエスト
	 */
	private void delRoute(final HttpServletRequest request) {
		final String path = String.class.cast(request.getAttribute(CLAZZ));
		if (path != null) {
			final var session = request.getSession(false);
			if (session != null) {
				synchronized (SessionMutexListener.getMutex(session)) {
					final var asm = ActionSessionMap.class.cast(session.getAttribute(CLAZZ));
					if (asm != null) {
						asm.remove(path);
					}
					session.setAttribute(CLAZZ, asm);
				}
			}
			request.removeAttribute(CLAZZ);
		}
	}
}
