package online.filter;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.function.Predicate;
import java.util.function.Supplier;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.logging.log4j.LogManager;

import core.config.Factory;

/**
 * フィルタ共通
 *
 * @author Tadashi Nakayama
 */
public final class FilterUtil {

	/** キー */
	private static final String CLAZZ = FilterUtil.class.getName();

	/** コンテキストPATH */
	private static final String CONTEXT = "CONTEXT";
	/** リクエストPATH */
	private static final String REQUEST_PATH = "REQUEST_PATH";
	/** リクエストURI */
	private static final String REQUEST_URI = "REQUEST_URI";
	/** クエリ文字列 */
	private static final String REQUEST_QUERY = "REQUEST_QUERY";
	/** レスポンスPATH */
	private static final String RESPONSE_PATH = "RESPONSE_PATH";
	/** レスポンスURI */
	private static final String RESPONSE_URI = "RESPONSE_URI";
	/** クエリ文字列 */
	private static final String RESPONSE_QUERY = "RESPONSE_QUERY";
	/** リダイレクト */
	private static final String REDIRECT_URL = "REDIRECT_URL";

	/** リダイレクトなし */
	private static final String NO_REDIRECT = CLAZZ + ".NO_REDIRECT";

	/** too many requests */
	private static final int SC_TOO_MANY_REQUESTS = 429;

	/**
	 * コンストラクタ
	 */
	private FilterUtil() {
		throw new AssertionError();
	}

	/**
	 * 属性設定
	 *
	 * @param request リクエスト
	 * @param key キー
	 * @param val 値
	 */
	private static void setAttribute(final HttpServletRequest request,
			final String key, final Object val) {
		final var map = getAttributeMap(request);
		map.put(key, val);
	}

	/**
	 * 属性取得
	 *
	 * @param <T> ジェネリクス
	 * @param request リクエスト
	 * @param key キー
	 * @return 属性値
	 */
	private static <T> T getAttribute(final HttpServletRequest request, final String key) {
		final var map = getAttributeMap(request);
		return Factory.cast(map.get(key));
	}

	/**
	 * 属性マップ取得
	 *
	 * @param request リクエスト
	 * @return 属性マップ
	 */
	private static Map<String, Object> getAttributeMap(final HttpServletRequest request) {
		final Supplier<Map<String, Object>> supplier = () -> {
			final var m = new HashMap<String, Object>();
			request.setAttribute(CLAZZ, m);
			return m;
		};
		return Objects.requireNonNullElseGet(Factory.cast(request.getAttribute(CLAZZ)), supplier);
	}

	/**
	 * 属性展開
	 *
	 * @param request リクエスト
	 */
	public static void extractTo(final HttpServletRequest request) {
		getAttributeMap(request).forEach(request::setAttribute);
	}

	/**
	 * キー値をクエリ文字列から除外
	 *
	 * @param query クエリ文字列
	 * @param keys キー値
	 * @return 除外後文字列
	 */
	public static String stripParameter(final String query, final String... keys) {
		return Optional.ofNullable(keys).map(
			k -> Stream.of(k).map(key -> key + "=").toArray(String[]::new)
		).flatMap(
			k -> Optional.ofNullable(query).map(
				qry -> Stream.of(qry.split("&")).filter(
					q -> Stream.of(k).noneMatch(q::startsWith)
				).collect(Collectors.joining("&"))
			)
		).orElse(query);
	}

	/**
	 * Viewフォーワド確認
	 *
	 * @param request リクエスト
	 * @return Viewフォワードの場合 true を返す。
	 */
	public static boolean isView(final HttpServletRequest request) {
		final var path = toPlainURI(request.getRequestURI());
		return path.endsWith(".jsp") || path.endsWith(".jspx");
	}

	/**
	 * クエリ文字列キー値集合化
	 *
	 * @param query クエリ文字列
	 * @return クエリ文字列キー値集合
	 */
	public static Set<String> toParameterKeySet(final String query) {
		return Stream.of(Objects.toString(query, "").split("&")).
				map(v -> v.split("=", 2)[0]).
				filter(Predicate.not(String::isEmpty)).
				collect(Collectors.toSet());
	}

	/**
	 * クエリ文字列マップ化
	 *
	 * @param query クエリ文字列
	 * @param encoding エンコーディング
	 * @return マップ
	 */
	public static Map<String, String[]> toParameterMap(final String query, final String encoding) {
		return Stream.of(Objects.toString(query, "").split("&")).
				map(v -> v.split("=", 2)).
				filter(v -> v.length == 2).
				peek(v -> v[1] = decode(v[1], encoding)).filter(v -> v[1] != null).
				collect(Collectors.toMap(v -> v[0], v -> new String[]{v[1]}, FilterUtil::extend));
	}

	/**
	 * 配列拡張
	 *
	 * @param <T> ジェネリックス
	 * @param base 配列
	 * @param vals 拡張する値
	 * @return 拡張配列
	 */
	@SafeVarargs
	private static <T> T[] extend(final T[] base, final T... vals) {
		T[] ret = Optional.ofNullable(base).orElse(vals);
		if (base != null && vals != null) {
			ret = Arrays.copyOf(base, base.length + vals.length);
			System.arraycopy(vals, 0, ret, base.length, vals.length);
		}
		return ret;
	}

	/**
	 * デコード
	 *
	 * @param val デコード対象
	 * @param encoding エンコーディング
	 * @return デコード文字列
	 */
	private static String decode(final String val, final String encoding) {
		try {
			return URLDecoder.decode(val, encoding);
		} catch (final UnsupportedEncodingException | IllegalArgumentException ex) {
			LogManager.getLogger().warn(ex.getMessage());
			return null;
		}
	}

	/**
	 * クエリ文字列取得
	 *
	 * @param request リクエストオブジェクト
	 * @return クエリ文字列
	 */
	public static String getQueryString(final HttpServletRequest request) {
		if (isInclude(request)) {
			return String.class.cast(request.getAttribute("javax.servlet.include.query_string"));
		}
		return request.getQueryString();
	}

	/**
	 * リクエストURI取得
	 *
	 * @param request リクエストオブジェクト
	 * @return リクエストURI
	 */
	public static String getRequestURI(final HttpServletRequest request) {
		if (isInclude(request)) {
			return String.class.cast(request.getAttribute("javax.servlet.include.request_uri"));
		}
		return request.getRequestURI();
	}

	/**
	 * サーブレットパス取得
	 *
	 * @param request リクエストオブジェクト
	 * @return サーブレットパス
	 */
	public static String getServletPath(final HttpServletRequest request) {
		if (isInclude(request)) {
			return String.class.cast(request.getAttribute("javax.servlet.include.servlet_path"));
		}
		return request.getServletPath();
	}

	/**
	 * インクルード判断
	 *
	 * @param request リクエストオブジェクト
	 * @return インクルードの場合 true を返す。
	 */
	public static boolean isInclude(final HttpServletRequest request) {
		return request.getAttribute("javax.servlet.include.request_uri") != null;
	}

	/**
	 * jsessionidなしURI
	 *
	 * @param val URI
	 * @return jsessionidなしURI
	 */
	public static String toPlainURI(final String val) {
		final var loc = val.indexOf(';');
		return (0 <= loc) ? val.substring(0, loc) : val;
	}

	/**
	 * GETメソッド判断
	 *
	 * @param method メソッド
	 * @return GETの場合 true を返す。
	 */
	public static boolean isGetMethod(final String method) {
		return "GET".equalsIgnoreCase(method) || "HEAD".equalsIgnoreCase(method);
	}

	/**
	 * POSTメソッド判断
	 *
	 * @param method メソッド
	 * @return POSTの場合 true を返す。
	 */
	public static boolean isPostMethod(final String method) {
		return "POST".equalsIgnoreCase(method);
	}

	/**
	 * コンテキストPATH取得
	 *
	 * @param request リクエスト
	 * @return コンテキストPATH
	 */
	public static String getContextPath(final HttpServletRequest request) {
		return getAttribute(request, CONTEXT);
	}

	/**
	 * リクエストPATH取得
	 *
	 * @param request リクエスト
	 * @return リクエストPATH
	 */
	public static String getRequestPath(final HttpServletRequest request) {
		return getAttribute(request, REQUEST_PATH);
	}

	/**
	 * レスポンスPATH取得
	 *
	 * @param request レスポンス
	 * @return レスポンスPATH
	 */
	public static String getResponsePath(final HttpServletRequest request) {
		return getAttribute(request, RESPONSE_PATH);
	}

	/**
	 * リクエストURI取得
	 *
	 * @param request リクエスト
	 * @return リクエストURI
	 */
	public static String getRequestUri(final HttpServletRequest request) {
		return getAttribute(request, REQUEST_URI);
	}

	/**
	 * レスポンスURI取得
	 *
	 * @param request レスポンス
	 * @return レスポンスURI
	 */
	public static String getResponseUri(final HttpServletRequest request) {
		return getAttribute(request, RESPONSE_URI);
	}

	/**
	 * リクエストクエリ文字列取得
	 *
	 * @param request リクエスト
	 * @return リクエストクエリ文字列
	 */
	public static String getRequestQuery(final HttpServletRequest request) {
		return getAttribute(request, REQUEST_QUERY);
	}

	/**
	 * レスポンスクエリ文字列取得
	 *
	 * @param request リクエスト
	 * @return レスポンスクエリ文字列
	 */
	public static String getResponseQuery(final HttpServletRequest request) {
		return getAttribute(request, RESPONSE_QUERY);
	}

	/**
	 * リダイレクトなし設定
	 *
	 * @param request リクエスト
	 */
	public static void setNoRedirect(final HttpServletRequest request) {
		request.setAttribute(NO_REDIRECT, NO_REDIRECT);
	}

	/**
	 * リダイレクト確認
	 *
	 * @param request リクエスト
	 * @return リダイレクトの場合 true を返す。
	 */
	static boolean isNoRedirect(final HttpServletRequest request) {
		return request.getAttribute(NO_REDIRECT) != null;
	}

	/**
	 * リダイレクトURL取得
	 *
	 * @param request リクエスト
	 * @return リダイレクトURL
	 */
	static String getRedirect(final HttpServletRequest request) {
		return getAttribute(request, REDIRECT_URL);
	}

	/**
	 * 記録
	 *
	 * @param request リクエスト
	 * @param location URL
	 */
	static void setRedirect(final HttpServletRequest request, final String location) {
		setAttribute(request, REDIRECT_URL, location);
	}

	/**
	 * 記録
	 *
	 * @param request リクエスト
	 */
	static void setContext(final HttpServletRequest request) {
		setAttribute(request, CONTEXT, request.getContextPath());
	}

	/**
	 * 記録
	 *
	 * @param request リクエスト
	 */
	static void setRequest(final HttpServletRequest request) {
		setAttribute(request, REQUEST_PATH, request.getRequestURI().substring(
						request.getContextPath().length() + "/".length()));
		setAttribute(request, REQUEST_URI, request.getRequestURI());
		setAttribute(request, REQUEST_QUERY, request.getQueryString());
	}

	/**
	 * 記録
	 *
	 * @param request リクエスト
	 */
	static void setResponse(final HttpServletRequest request) {
		setAttribute(request, RESPONSE_PATH, request.getRequestURI().substring(
						request.getContextPath().length() + "/".length()));
		setAttribute(request, RESPONSE_URI, request.getRequestURI());
		setAttribute(request, RESPONSE_QUERY, request.getQueryString());
	}

	/**
	 * エラー送信
	 *
	 * @param response レスポンス
	 */
	static void sendTooMeny(final HttpServletResponse response) {
		try {
			if (!response.isCommitted()) {
				response.sendError(SC_TOO_MANY_REQUESTS);
				response.flushBuffer();
			}
		} catch (final IOException ex) {
			LogManager.getLogger().info(ex.getMessage());
		}
	}

	/**
	 * リダイレクト
	 *
	 * @param response レスポンス
	 * @param location URL
	 */
	static void redirect(final HttpServletResponse response, final String location) {
		try {
			response.reset();
			response.resetBuffer();
			response.setStatus(HttpServletResponse.SC_SEE_OTHER);
			response.setHeader("Location", response.encodeRedirectURL(location));
			response.setContentLength(0);
			response.flushBuffer();
		} catch (final IOException ex) {
			LogManager.getLogger().info(ex.getMessage());
		}
	}

	/**
	 * Location取得
	 *
	 * @param request リクエスト
	 * @return Location
	 */
	static String getRequestLocation(final HttpServletRequest request) {
		final var ret = getRequestUri(request);
		final var qry = getRequestQuery(request);
		return Objects.toString(qry, "").isEmpty() ? ret : ret + "?" + qry;
	}

	/**
	 * Location取得
	 *
	 * @param request リクエスト
	 * @return Location
	 */
	static String getResponseLocation(final HttpServletRequest request) {
		final var uri = getResponseUri(request);
		final var qry = getResponseQuery(request);
		return Objects.toString(qry, "").isEmpty() ? uri : uri + "?" + qry;
	}
}
