package online.filter;

import java.io.IOException;
import java.io.Serializable;
import java.util.HashSet;
import java.util.Objects;
import java.util.Set;
import java.util.function.Supplier;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import javax.servlet.http.HttpSessionBindingEvent;

import org.apache.logging.log4j.LogManager;

import core.util.NumberUtil;
import online.context.session.SessionAttributeUtil;
import online.filter.helper.ActionSessionList;
import online.filter.helper.ActionSessionMap;
import online.listener.SessionMutexListener;

/**
 * セション抽出・保存フィルタ
 *
 * @author Tadashi Nakayama
 */
public abstract class SessionAttributeFilter implements Filter {

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

	/** アクションセション最大数 */
	private int max = 3;
	/** 回数 */
	private int times = 30;
	/** 待ち時間 */
	private int millis = 1000;

	/** サーブレットコンテキスト */
	private ServletContext sc = null;

	/**
	 * @see javax.servlet.Filter#init(javax.servlet.FilterConfig)
	 */
	@Override
	public void init(final FilterConfig filterConfig) {
		this.sc = filterConfig.getServletContext();

		var val = filterConfig.getInitParameter("max");
		if (!Objects.toString(val, "").isEmpty()) {
			this.max = Math.max(NumberUtil.toInt(val, 3), 2);
		}
		val = filterConfig.getInitParameter("times");
		if (!Objects.toString(val, "").isEmpty()) {
			this.times = NumberUtil.toInt(val, 30);
		}
		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 response = HttpServletResponse.class.cast(svResponse);

			final var sid = getActionSessionKey(this.sc, request);
			final var prev = SessionAttributeUtil.getAttributeSid(request);
			if (sid == null || !sid.equals(prev) || request.getAttribute(prev) == null) {
				if (sid != null && isMultiple(this.sc, request)) {
					SessionAttributeUtil.setMultipleSession(request);
				}
				suspendSession(request);

				if (sid != null && resumeSession(sid, request, response)) {
					setNoCache(response);
					try {
						chain.doFilter(svRequest, svResponse);
					} finally {
						suspendSession(request);
					}
					return;
				}
			}
		}

		chain.doFilter(svRequest, svResponse);
	}

	/**
	 * アクションセションキー取得
	 *
	 * @param context サーブレットコンテキスト
	 * @param request サーブレットリクエスト
	 * @return アクションセションキー
	 */
	protected abstract String getActionSessionKey(
			ServletContext context, HttpServletRequest request);

	/**
	 * 多重セション確認
	 *
	 * @param context サーブレットコンテキスト
	 * @param request サーブレットリクエスト
	 * @return 多重セションの場合 true を返す。
	 */
	protected abstract boolean isMultiple(
			ServletContext context, HttpServletRequest request);

	/**
	 * セション開始済確認
	 *
	 * @param sid SID
	 * @param request リクエスト
	 * @return 開始済時 true を返す。
	 */
	protected static boolean hasSession(final String sid, final HttpServletRequest request) {
		final var session = request.getSession(false);
		if (session != null) {
			synchronized (SessionMutexListener.getMutex(session)) {
				return session.getAttribute(sid) != null
						|| sid.equals(SessionAttributeUtil.getAttributeSid(request));
			}
		}
		return false;
	}

	/**
	 * セション切り離し
	 *
	 * @param request サーブレットリクエスト
	 * @param sid SID
	 */
	public static void unbound(final HttpServletRequest request, final String sid) {
		final var session = request.getSession(false);
		if (session != null) {
			final Serializable removed;
			synchronized (SessionMutexListener.getMutex(session)) {
				final ActionSession as = getActionSession(session);
				as.getList().remove(sid);
				as.getInUse().remove(sid);
				session.setAttribute(CLAZZ, as);
				removed = removeSession(session, sid);
			}
			unboundSession(session, sid, removed);
		}
	}

	/**
	 * NoCache設定
	 *
	 * @param response レスポンス
	 */
	private void setNoCache(final HttpServletResponse response) {
		if (!response.containsHeader("Cache-Control")) {
			response.setHeader("Cache-Control", "no-cache,no-store,max-age=0");
			response.setHeader("Pragma", "no-cache");
			response.setDateHeader("Expires", 0);
		}
	}

	/**
	 * アクション属性情報設定
	 *
	 * @param sid アクション属性キー
	 * @param request リクエストオブジェクト
	 * @param response レスポンスオブジェクト
	 * @return 設定した場合 true を返す。
	 */
	private boolean resumeSession(final String sid,
			final HttpServletRequest request, final HttpServletResponse response) {
		for (var i = 0; i < this.times; i++) {
			final var session = request.getSession(false);
			if (session == null) {
				return false;
			}

			if (resumeActionSession(sid, request, session)) {
				return true;
			}

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

	/**
	 * セションオブジェクト設定
	 *
	 * @param sid アクション属性キー
	 * @param request リクエストオブジェクト
	 * @param session セションオブジェクト
	 * @return resumeした場合 true を返す。
	 */
	private boolean resumeActionSession(final String sid,
			final HttpServletRequest request, final HttpSession session) {
		synchronized (SessionMutexListener.getMutex(session)) {
			final var as = getActionSession(session);
			final var ret = !as.getInUse().contains(sid);
			if (ret) {
				request.setAttribute(sid, removeSession(session, sid));
				SessionAttributeUtil.setAttributeSid(request, sid);
				as.getList().remove(sid);
				as.getInUse().add(sid);
				session.setAttribute(CLAZZ, as);
			}
			return ret;
		}
	}

	/**
	 * セションリストア
	 *
	 * @param request リクエストオブジェクト
	 */
	private void suspendSession(final HttpServletRequest request) {
		final var sid = SessionAttributeUtil.getAttributeSid(request);
		if (sid != null) {
			final var session = request.getSession(false);
			if (session != null) {
				suspendActionSession(sid, request, session);
			}
			request.removeAttribute(sid);
		}
		SessionAttributeUtil.removeAttributeSid(request);
	}

	/**
	 * アクションセション追加
	 *
	 * @param sid アクション属性キー
	 * @param request リクエストオブジェクト
	 * @param session セションオブジェクト
	 */
	private void suspendActionSession(final String sid,
			final HttpServletRequest request, final HttpSession session) {
		// 記憶用モデル取得
		Serializable removed = null;
		String del = null;
		synchronized (SessionMutexListener.getMutex(session)) {
			// セションに追加
			// アクションセションリスト取得
			final var as = getActionSession(session);
			as.getList().remove(sid);
			as.getInUse().remove(sid);

			final var obj = Serializable.class.cast(request.getAttribute(sid));
			if (obj != null) {
				session.setAttribute(sid, obj);
				as.getList().add(sid);
			}

			// 最大記憶数確認
			if (this.max < as.getList().size()) {
				del = as.getList().remove(0);
				as.getInUse().remove(del);
				removed = removeSession(session, del);
			}
			session.setAttribute(CLAZZ, as);
		}
		unboundSession(session, del, removed);
	}

	/**
	 * アクションセション取得
	 *
	 * @param session セションオブジェクト
	 * @return アクションセション
	 */
	private static ActionSession getActionSession(final HttpSession session) {
		// アクションセションリスト取得
		final Supplier<ActionSession> supplier = () -> {
			session.setAttribute(ActionSessionMap.class.getName(), new ActionSessionMap());
			final var ret = new ActionSession(new ActionSessionList(), new HashSet<>());
			session.setAttribute(CLAZZ, ret);
			return ret;
		};
		return Objects.requireNonNullElseGet(
				ActionSession.class.cast(session.getAttribute(CLAZZ)), supplier);
	}

	/**
	 * セション削除処理
	 *
	 * @param session セションオブジェクト
	 * @param sid アクション属性キー
	 * @return 削除オブジェクト
	 */
	private static Serializable removeSession(final HttpSession session, final String sid) {
		final var ret = Serializable.class.cast(session.getAttribute(sid));
		session.removeAttribute(sid);
		return ret;
	}

	/**
	 * タイムアウト処理
	 *
	 * @param session セション
	 * @param sid 名前
	 * @param removed 削除オブジェクト
	 */
	private static void unboundSession(final HttpSession session,
			final String sid, final Serializable removed) {
		if (removed != null) {
			final var asm = new ActionSessionMap();
			asm.put(sid, removed);
			asm.valueUnbound(new HttpSessionBindingEvent(session, sid));
		}
	}

	/**
	 * アクションセション
	 *
	 * @author Tadashi Nakayama
	 */
	private static final class ActionSession implements Serializable {
		/** serialVersionUID */
		private static final long serialVersionUID = 1L;

		/** 優先順位リスト */
		private final ActionSessionList list;
		/** 使用中セット */
		private final Set<String> set;

		/**
		 * コンストラクタ
		 *
		 * @param l 優先順位リスト
		 * @param s 使用中セット
		 */
		ActionSession(final ActionSessionList l, final HashSet<String> s) {
			this.list = l;
			this.set = s;
		}

		/**
		 * 優先順位リスト取得
		 *
		 * @return 優先順位リスト
		 */
		public ActionSessionList getList() {
			return this.list;
		}

		/**
		 * 使用中セット取得
		 *
		 * @return 使用中セット
		 */
		public Set<String> getInUse() {
			return this.set;
		}
	}
}
