package online.struts.mapping;

import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.Arrays;
import java.util.Collections;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;

import org.apache.logging.log4j.LogManager;
import org.apache.struts.Globals;
import org.apache.struts.action.Action;
import org.apache.struts.action.ActionForward;
import org.apache.struts.action.ActionMapping;
import org.apache.struts.config.ActionConfig;
import org.apache.struts.config.BaseConfig;
import org.apache.struts.config.ForwardConfig;
import org.apache.struts.config.ModuleConfig;

import core.config.Factory;
import online.annotation.Aid;
import online.annotation.Gid;
import online.annotation.NoRevert;
import online.annotation.NoTransaction;
import online.annotation.SessionEntry;
import online.annotation.SessionExit;
import online.annotation.SessionReserved;
import online.filter.FilterUtil;
import online.struts.action.UniForm;

/**
 * リクエストマッピング
 *
 * @author Tadashi Nakayama
 * @version 1.0.0
 */
public final class RequestMapping extends ActionMapping {
	/** serialVersionUID */
	private static final long serialVersionUID = 1L;

	/** 拡張子 */
	private static final String SUFFIX_DO = ".do";
	/** 拡張子 */
	private static final String SUFFIX_JSP = ".jsp";
	/** 拡張子 */
	private static final String SUFFIX_JSPX = ".jspx";

	/** 設定チェックマップ */
	private static final ConcurrentMap<String, String> MAP = new ConcurrentHashMap<>();

	/** 画面ID */
	private volatile String gid = null;
	/** URL種別 */
	private String[] method = null;
	/** 単一 */
	private String single = null;
	/** 多重 */
	private String multiple = null;

	/**
	 * コンストラクタ
	 *
	 */
	public RequestMapping() {
		setScope("request");
		setValidate(false);
	}

	/**
	 * jspチェック
	 *
	 * @param af アクションフォワード
	 * @return jspの時 true を返す。
	 */
	public static boolean isJsp(final ForwardConfig af) {
		return af != null && isJsp(af.getPath());
	}

	/**
	 * リダイレクトチェック
	 *
	 * @param af アクションフォワード
	 * @return リダイレクトの場合 true を返す。
	 */
	public static boolean isRedirect(final ForwardConfig af) {
		return af != null && af.getRedirect();
	}

	/**
	 * 改行削除
	 *
	 * @param str 文字列
	 * @return 削除後文字列
	 */
	public static String dropLineTerminators(final String str) {
		return Objects.toString(str, "").replaceAll("[\r\n\u0085\u2028\u2029]", "");
	}

	/**
	 * リクエストマッピング検索
	 *
	 * @param context サーブレットコンテキスト
	 * @param request サーブレットリクエスト
	 * @return リクエストマッピング
	 */
	public static RequestMapping findRequestMapping(
			final ServletContext context, final HttpServletRequest request) {
		final var mc = ModuleConfig.class.cast(context.getAttribute(Globals.MODULE_KEY));
		if (mc != null) {
			final var ac = mc.findActionConfig(toPath(request));
			if (RequestMapping.class.isInstance(ac)) {
				return RequestMapping.class.cast(ac);
			}
		}
		return null;
	}

	/**
	 * パス取得
	 *
	 * @param request リクエストオブジェクト
	 * @return パス
	 */
	private static String toPath(final HttpServletRequest request) {
		final var path = FilterUtil.getRequestURI(request);
		final var loc = path.indexOf(SUFFIX_DO);
		return (0 <= loc) ? path.substring(path.lastIndexOf('/', loc), loc) : "";
	}

	/**
	 * jspチェック
	 *
	 * @param val 文字列
	 * @return jspの時 true を返す。
	 */
	private static boolean isJsp(final String val) {
		return val != null && (val.endsWith(SUFFIX_JSP) || val.endsWith(SUFFIX_JSPX));
	}

	/**
	 * RestAction判断（依存性排除のため、文字列で判断）
	 * @return RestActionの場合 true を返す。
	 */
	public boolean isRestAction() {
		var cls = Factory.loadClass(getType());
		while (cls != null) {
			if ("online.struts.action.RestAction".equals(cls.getName())) {
				return true;
			}
			cls = cls.getSuperclass();
		}
		return false;
	}

	/**
	 * トランザクション判断
	 *
	 * @return トランザクション要の場合 true を返す。
	 */
	public boolean hasTransaction() {
		for (Class<?> c = getActionClass();
				c != null && !Object.class.equals(c); c = c.getSuperclass()) {
			final var trn = c.getAnnotation(NoTransaction.class);
			if (trn != null) {
				return false;
			}
		}
		return true;
	}

	/**
	 * SessionEntry判断
	 *
	 * @return SessionEntry開始の場合、true を返す。
	 */
	public boolean isSessionEntry() {
		final Class<? extends Action> cls = getActionClass();
		if (cls != null) {
			final var se = cls.getAnnotation(SessionEntry.class);
			return se != null && se.value();
		}
		return false;
	}

	/**
	 * SessionEntry判断
	 *
	 * @return SessionEntry中の場合、true を返す。
	 */
	public boolean isInSessionEntry() {
		final Class<? extends Action> cls = getActionClass();
		if (cls != null) {
			final var se = cls.getAnnotation(SessionEntry.class);
			return se != null && !se.value();
		}
		return false;
	}

	/**
	 * SessionExit判断
	 *
	 * @param aid アクションID
	 * @return SessionExitの場合 true を返す。
	 */
	public boolean isSessionExit(final String aid) {
		final var mt = getActionMethod(getActionClass(), aid, UniForm.class);
		return mt != null && mt.getAnnotation(SessionExit.class) != null;
	}

	/**
	 * Reservedアノテーション付項目名集合取得
	 * @param aid アクションID
	 * @return Reservedアノテーション付項目名集合
	 */
	public Set<String> getReservedSet(final String aid) {
		final Class<? extends Action> cls = getActionClass();
		final var ret = getFieldSet(cls, SessionReserved.class);
		final var se = getAnnotation(cls, aid, SessionReserved.class);
		if (se != null) {
			ret.addAll(Arrays.asList(se.value()));
		}
		return ret;
	}

	/**
	 * 戻し対象外ノテーション付項目名集合取得
	 * @param aid アクションID
	 * @return 戻し対象外アノテーション付項目名集合
	 */
	public Set<String> getExclusionSet(final String aid) {
		final Class<? extends Action> cls = getActionClass();
		final var ret = getFieldSet(cls, NoRevert.class);
		final var rb = getAnnotation(cls, aid, NoRevert.class);
		if (rb != null) {
			ret.addAll(Arrays.asList(rb.value()));
		}
		return ret;
	}

	/**
	 * クラスからアノテーションの付加されたフィールド値の集合を取得
	 * @param cls クラス
	 * @param anno アノテーションクラス
	 * @return フィールド値集合
	 */
	private Set<String> getFieldSet(final Class<? extends Action> cls,
			final Class<? extends Annotation> anno) {
		return Optional.ofNullable(cls).map(
			c -> Stream.of(c.getDeclaredFields()).
				filter(this::isStaticFinalString).
				filter(f -> f.getAnnotation(anno) != null).
				map(this::getFieldValue).collect(Collectors.toSet())
		).orElse(Collections.emptySet());
	}

	/**
	 * アノテーション取得
	 * @param <T> タイプ
	 * @param cls クラス
	 * @param aid アクションID
	 * @param anno アノテーションクラス
	 * @return アノテーション
	 */
	private <T extends Annotation> T getAnnotation(final Class<? extends Action> cls,
			final String aid, final Class<T> anno) {
		T se = cls.getAnnotation(anno);
		final var mt = getActionMethod(cls, aid, UniForm.class);
		if (mt != null) {
			se = mt.getAnnotation(anno);
		}
		return se;
	}

	/**
	 * StaticFinalString判断
	 *
	 * @param f フィールド
	 * @return static final Stringの場合 true を返す。
	 */
	private boolean isStaticFinalString(final Field f) {
		if (f != null && !f.isSynthetic()) {
			f.setAccessible(true);
			final var mod = f.getModifiers();
			return Modifier.isStatic(mod) && Modifier.isFinal(mod)
						&& String.class.equals(f.getType());
		}
		return false;
	}

	/**
	 * フィールド値取得
	 *
	 * @param f フィールド
	 * @return フィールド値
	 */
	private String getFieldValue(final Field f) {
		final PrivilegedAction<Void> pa = () -> {
			f.setAccessible(true);
			return null;
		};

		AccessController.doPrivileged(pa);

		try {
			return Objects.toString(f.get(null), null);
		} catch (final IllegalAccessException ex) {
			LogManager.getLogger().info(ex.getMessage());
			return null;
		}
	}

	/**
	 * アクションクラス取得
	 * @param <T> Type
	 * @return アクションクラス
	 */
	private <T extends Action> Class<T> getActionClass() {
		return (getType() != null) ? Factory.loadClass(getType()) : null;
	}

	/**
	 * メソッド取得
	 *
	 * @param cls アクションクラス
	 * @param aid アクションID
	 * @param params パラメタクラス
	 * @return メソッドオブジェクト
	 */
	public static Method getActionMethod(final Class<? extends Action> cls,
			final String aid, final Class<?>... params) {
		if (cls != null) {
			for (final var mt : cls.getMethods()) {
				if (!Modifier.isPublic(mt.getModifiers())
						|| Modifier.isStatic(mt.getModifiers())
						|| !String.class.equals(mt.getReturnType())
						|| mt.isBridge() || mt.isSynthetic()) {
					continue;
				}

				final var exp = mt.getExceptionTypes();
				if (0 < exp.length) {
					continue;
				}

				final var act = mt.getAnnotation(Aid.class);
				if ((act != null && act.value().equalsIgnoreCase(aid))
						|| mt.getName().equals(aid)) {
					if (checkParameter(params, mt.getParameterTypes())) {
						return mt;
					}
				}
			}
		}
		return null;
	}

	/**
	 * パラメータチェック
	 * @param caller 指定パラメータクラス配列
	 * @param callee 呼出すメソッドのパラメータクラス配列
	 * @return パラメータが適当な場合 true を返す。指定が無い場合は、常に true を返す。
	 */
	private static boolean checkParameter(final Class<?>[] caller, final Class<?>[] callee) {
		if (caller != null && 0 < caller.length) {
			if (callee == null || caller.length != callee.length) {
				return false;
			}
			for (var i = 0; i < caller.length; i++) {
				if (!Factory.toReference(callee[i]).isAssignableFrom(
						Factory.toReference(caller[i]))) {
					return false;
				}
			}
		}
		return true;
	}

	/**
	 * 画面ID設定
	 *
	 * @param val 画面ID
	 */
	public void setGid(final String val) {
		this.gid = val;
	}

	/**
	 * 画面ID取得
	 *
	 * @return 画面ID
	 */
	public String getGid() {
		if (Objects.toString(this.gid, "").isEmpty()) {
			final Class<? extends Action> cls = getActionClass();
			if (cls != null) {
				final var id = cls.getAnnotation(Gid.class);
				if (id != null) {
					this.gid = id.value();
					return this.gid;
				}
			}
			this.gid = getPath().substring("/".length());
		}
		return this.gid;
	}

	/**
	 * メソッド種別設定
	 *
	 * @param val 種別
	 */
	public void setMethod(final String val) {
		this.method = val.split(",");
	}

	/**
	 * メソッド種別取得
	 *
	 * @return メソッド種別
	 */
	public String getMethod() {
		return String.join(",", this.method);
	}

	/**
	 * メソッド受入判定
	 *
	 * @param val メソッド名
	 * @return 受入 true を返す。
	 */
	public boolean isAcceptable(final String val) {
		return this.method == null || this.method.length == 0
				|| Stream.of(this.method).anyMatch(val::equalsIgnoreCase);
	}

	/**
	 * 単一設定
	 *
	 * @param val 単一
	 */
	public void setSingle(final String val) {
		if (val != null && this.multiple != null) {
			throw new IllegalStateException(getPath() + ":" + val);
		}
		if (!isValid(val, "single")) {
			throw new IllegalStateException("This value was set as multiple.(" + val + ")");
		}
		this.single = val;
	}

	/**
	 * 単一取得
	 *
	 * @return 単一
	 */
	public String getSingle() {
		return this.single;
	}

	/**
	 * 多重設定
	 *
	 * @param val 多重
	 */
	public void setMultiple(final String val) {
		if (val != null && this.single != null) {
			throw new IllegalStateException(getPath() + ":" + val);
		}
		if (!isValid(val, "multiple")) {
			throw new IllegalStateException("This value was set as single.(" + val + ")");
		}
		this.multiple = val;
	}

	/**
	 * 多重取得
	 *
	 * @return 多重
	 */
	public String getMultiple() {
		return this.multiple;
	}

	/**
	 * 継続ID取得
	 *
	 * @return 継続ID
	 */
	public String getKeepId() {
		return Objects.toString(this.single, this.multiple);
	}

	/**
	 * 多重確認
	 *
	 * @return 多重の場合 true を返す。
	 */
	public boolean isMultipleKeep() {
		return this.multiple != null;
	}

	/**
	 * 拡張パス付加
	 *
	 * @param request リクエスト
	 * @param pass フォワード先
	 * @return 拡張パス付加フォワード先
	 */
	public String addPathInfo(final HttpServletRequest request, final String pass) {
		final var pre = getInfix(FilterUtil.getServletPath(request));
		final var info = getPathInfo(FilterUtil.getServletPath(request));
		if (!Objects.toString(info, "").isEmpty() && !"/".equals(info)) {
			// パス取得
			var path1 = pass;
			var path2 = "";
			final var loc = path1.indexOf('?');
			if (0 <= loc) {
				path2 = path1.substring(loc);
				path1 = path1.substring(0, loc);
			}
			return pre + path1 + info + path2;
		}
		return pre + pass;
	}

	/**
	 * suffixパス取得
	 *
	 * @param srv サーブレットパス
	 * @return suffixパス
	 */
	private String getInfix(final String srv) {
		final var loc = srv.lastIndexOf(getPath());
		return (0 < loc) ? srv.substring(0, loc) : "";
	}

	/**
	 * PathInfo取得
	 *
	 * @param srv サーブレットパス
	 * @return PathInfo
	 */
	private String getPathInfo(final String srv) {
		var loc = srv.lastIndexOf(getPath());
		if (0 <= loc) {
			loc = srv.indexOf('/', loc + getPath().length());
			if (0 <= loc) {
				return srv.substring(loc);
			}
		}
		return "";
	}

	/**
	 * @see org.apache.struts.config.ActionConfig
	 * #addForwardConfig(org.apache.struts.config.ForwardConfig)
	 */
	@Override
	public void addForwardConfig(final ForwardConfig config) {
		if (config.getCommand() != null && config.getCatalog() == null) {
			config.setCatalog("struts");
		}
		super.addForwardConfig(config);
	}

	/**
	 * @see org.apache.struts.config.ActionConfig#setCommand(java.lang.String)
	 */
	@Override
	public void setCommand(final String val) {
		if (getCatalog() == null) {
			setCatalog("struts");
		}
		super.setCommand(val);
	}

	/**
	 * @return Properties
	 */
	public Map<String, String> getPropertiesMap() {
		return Factory.cast(getProperties());
	}

	/**
	 * @see org.apache.struts.config.ActionConfig
	 * #setModuleConfig(org.apache.struts.config.ModuleConfig)
	 */
	@Override
	public void setModuleConfig(final ModuleConfig val) {
		super.setModuleConfig(val);
		final var fbc = val.findFormBeanConfigs();
		if (fbc != null && 0 < fbc.length) {
			setName(fbc[0].getName());
		}
	}

	/**
	 * フォワード先検索
	 *
	 * @param arg0 フォワード先名
	 * @return フォワード先
	 */
	@Override
	public ActionForward findForward(final String arg0) {
		final var fc = Objects.requireNonNullElseGet(
				findForwardConfig(arg0),
				() -> getModuleConfig().findForwardConfig(arg0));
		return ActionForward.class.isInstance(fc) ? ActionForward.class.cast(fc) : null;
	}

	/**
	 * @see org.apache.struts.config.ActionConfig
	 * #inheritForwards(org.apache.struts.config.ActionConfig)
	 */
	@Override
	protected void inheritForwards(final ActionConfig baseConfig) {

		super.inheritForwards(baseConfig);

		for (final var fc : baseConfig.findForwardConfigs()) {
			final var copy = findForwardConfig(fc.getName());
			if (copy != null && !"VIEW".equalsIgnoreCase(copy.getName())) {
				// 拡張部分(親パスAAA→子パスAAA2の2)も追加
				final var loc = getSameLoc();
				if (getExtends().length() == getPath().length()) {
					copy.setPath(replace(copy.getPath(), getExtends().substring(loc),
									getPath().substring(loc)));
				} else if (getExtends().length() < getPath().length()) {
					copy.setPath(replace(copy.getPath(), "", getPath().substring(loc)));
				}
			}
		}
	}

	/**
	 * 置換処理
	 * @param org 対象文字列
	 * @param from 元文字列
	 * @param to 後文字列
	 * @return 置換後文字列
	 */
	private static String replace(final String org, final String from, final String to) {
		final var dot = org.contains(SUFFIX_DO);
		var begin = org.length() - from.length();
		if (dot) {
			begin = begin - SUFFIX_DO.length();
		}
		var ret = org.substring(0, begin) + to;
		if (dot) {
			ret = ret + SUFFIX_DO;
		}
		return ret;
	}

	/**
	 * @see org.apache.struts.config.BaseConfig
	 * #inheritProperties(org.apache.struts.config.BaseConfig)
	 */
	@Override
	protected void inheritProperties(final BaseConfig baseConfig) {
		super.inheritProperties(baseConfig);

		if (RequestMapping.class.isInstance(baseConfig)) {
			final var rm = RequestMapping.class.cast(baseConfig);
			if (this.method == null) {
				setMethod(rm.getMethod());
			}
			if (this.single == null && this.multiple == null) {
				setSingle(rm.getSingle());
				setMultiple(rm.getMultiple());
			}
		}
	}

	/**
	 * 適正判断
	 *
	 * @param key キー
	 * @param val 値
	 * @return 保存されていないか、キーと値が等しい場合 true を返す。
	 */
	private static boolean isValid(final String key, final String val) {
		if (key != null) {
			final var str = MAP.putIfAbsent(key, val);
			return str == null || val.equals(str);
		}
		return true;
	}

	/**
	 * 同一文字列位置取得
	 *
	 * @return 同一文字列位置
	 */
	private int getSameLoc() {
		var loc = 0;
		while (loc < getExtends().length() && loc < getPath().length()) {
			if (getExtends().charAt(loc) != getPath().charAt(loc)) {
				break;
			}
			loc++;
		}
		return loc;
	}
}
