package online.struts.chain.command;

import java.util.Set;

import org.apache.commons.chain2.Processing;
import org.apache.struts.action.Action;
import org.apache.struts.chain.commands.ActionCommandBase;
import org.apache.struts.chain.contexts.ActionContext;
import org.apache.struts.chain.contexts.ServletActionContext;
import org.apache.struts.config.ForwardConfig;

import online.context.ActionParameter;
import online.context.RequestParameter;
import online.context.session.SessionAttributeUtil;
import online.context.token.TokenUtil;
import online.filter.FilterUtil;
import online.model.ModelUtil;
import online.struts.action.ActionProxy;
import online.struts.action.UniForm;
import online.struts.chain.command.ActionRequest.CommandRequest;
import online.struts.mapping.RequestMapping;

/**
 * アクション実行コマンド
 *
 * @author Tadashi Nakayama
 * @version 1.0.0
 */
public final class ExecuteActionProcessor extends ActionCommandBase implements CommandRequest {

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

	/**
	 * @see org.apache.struts.chain.commands.ActionCommandBase
	 * #execute(org.apache.struts.chain.contexts.ActionContext)
	 */
	@Override
	public Processing execute(final ActionContext<String, Object> context) {
		return ActionRequest.execute(context, this);
	}

	/**
	 * コマンド処理
	 *
	 * @param sac サーブレットアクションコンテキスト
	 * @param mapping アクションマッピング
	 * @param uf 汎用フォーム
	 * @return 処理結果
	 */
	@Override
	public Processing command(final ServletActionContext sac,
			final RequestMapping mapping, final UniForm uf) {

		boolean exit = false;
		Action action = sac.getAction();
		ForwardConfig af = sac.getForwardConfig();
		if (af == null && !sac.getResponse().isCommitted() && action != null) {
			try {
				processKeep(mapping, uf);

				if (mayInvoke(mapping, uf) || isCalled(sac)) {
					ActionProxy proxy = new ActionProxy(action);
					af = proxy.execute(mapping, uf, sac.getRequest(), sac.getResponse());
					setCalled(sac);
				} else {
					af = ActionProxy.getViewForward(mapping);
					uf.getActionParameter().last(true);
				}

			} finally {
				sac.setForwardConfig(af);

				if (!uf.getActionParameter().isRollbacked()) {
					exit = mapping.isSessionExit(uf.getActionParameter().getAid());

					reserve(mapping, uf);

					// セション保存オブジェクト再保存
					uf.getSessionAttribute().saveSessionParameters(sac.getRequest());
				}
			}
		} else {
			uf.getActionParameter().last(false);
		}

		// リトライ時も通過
		// アクションセション処理
		if (exit || !uf.getActionParameter().evacuate(sac.getRequest())) {
			SessionAttributeUtil.removeSessionAttribute(sac.getRequest());
			if (mapping.getKeepId() != null) {
				uf.getActionParameter().removeToken();
			} else {
				TokenUtil.setParameterTokenTo(sac.getRequest());
			}
		}

		return Processing.CONTINUE;
	}

	/**
	 * アクション呼び出し設定
	 * @param sac サーブレットアクションコンテキスト
	 */
	private void setCalled(final ServletActionContext sac) {
		sac.getRequest().setAttribute(CLAZZ, CLAZZ);
	}

	/**
	 * アクション呼び出し確認
	 * @param sac サーブレットアクションコンテキスト
	 * @return 1リクエスト内でアクションが呼び出された場合 true を返す。
	 * あまりないが、リクエストされたActionとjspからインクルードするActionが同じ場合など。
	 */
	private boolean isCalled(final ServletActionContext sac) {
		return sac.getRequest().getAttribute(CLAZZ) != null;
	}

	/**
	 * 継続処理
	 *
	 * @param mapping マッピング
	 * @param uf 汎用フォーム
	 */
	private void processKeep(final RequestMapping mapping, final UniForm uf) {
		ActionParameter ap = uf.getActionParameter();
		if (mapping.getKeepId() != null && ap.isFirst()) {
			if (!(ap.hasQueryString() ^ uf.hasQueryString())) {
				if (!mapping.getReservedSet(ap.getAid()).isEmpty()) {
					ap.copyReserved();
				}
			} else if (mapping.isSessionEntry() && ap.isGet()) {
				ap.expireReserved();
			}
		}
	}

	/**
	 * 呼び出し可能判断
	 *
	 * @param mapping マッピング
	 * @param uf 汎用フォーム
	 * @return 呼び出し可能の場合 true を返す。
	 */
	private boolean mayInvoke(final RequestMapping mapping, final UniForm uf) {
		return mapping.getKeepId() == null || !uf.getActionParameter().isFirst()
				|| !uf.getActionParameter().isGet() || mapping.isRestAction()
				|| !isSameParameter(uf) || !existsInSession(mapping, uf);
	}

	/**
	 * パラメタ値同一確認
	 *
	 * @param uf 汎用フォーム
	 * @return パラメタの値が変更されていた場合 false を返す。
	 */
	private boolean isSameParameter(final UniForm uf) {
		ActionParameter ap = uf.getActionParameter();
		if (ap.hasQueryString() ^ uf.hasQueryString()) {
			return false;
		}
		RequestParameter pm = ap.getParameter();
		Set<String> set = FilterUtil.toParameterKeySet(uf.getQueryString());
		for (final String key : set) {
			String[] prm = pm.getArrayValue(key);
			for (int i = 0; i < prm.length; i++) {
				if (!prm[i].equals(ModelUtil.getValueAsString(uf, key, i))) {
					return false;
				}
			}
		}
		return true;
	}

	/**
	 * セション存在確認
	 *
	 * @param mapping マッピング
	 * @param uf 汎用フォーム
	 * @return reserve値が全て存在している場合 true を返す。
	 */
	private boolean existsInSession(final RequestMapping mapping, final UniForm uf) {
		Set<String> set = mapping.getReservedSet(uf.getActionParameter().getAid());
		return !set.isEmpty() && hasAllKey(set, uf);
	}

	/**
	 * キー保持確認
	 * @param set キーセット
	 * @param uf 汎用モデル
	 * @return キーセットすべてがモデルに存在した場合 true を返す。
	 */
	private boolean hasAllKey(final Set<String> set, final UniForm uf) {
		// 複写済なので、現モデルをチェック
		return set.stream().allMatch(uf::containsKey);
	}

	/**
	 * リザーブ処理
	 *
	 * @param mapping マッピング
	 * @param uf 汎用フォーム
	 */
	private void reserve(final RequestMapping mapping, final UniForm uf) {
		if (mapping.getKeepId() != null) {
			// 全保存
			uf.keySet().stream().forEach(uf::reserve);
		}
	}
}
