/*
 * Copyright 2006 Takahiro Nakamura.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
 * either express or implied. See the License for the specific language
 * governing permissions and limitations under the License.
 */
package woolpack.ee;

import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Random;

import org.w3c.dom.Element;

import woolpack.fn.Fn;
import woolpack.fn.FnUtils;
import woolpack.utils.BuildableArrayList;
import woolpack.utils.Utils;
import woolpack.xml.NodeFindable;
import woolpack.xml.XmlUtils;

/**
 * トランザクショントークンを検証/登録し DOM ノードへ設定する{@link Fn}のビルダです。
 * 画面遷移順序を保証するための機能です。
 * 関数従属性を考慮した場合、画面遷移順序を保証する id を画面遷移定義に管理するデータモデルが妥当であるが、
 * 画面遷移順序保証と画面遷移定義は別のエンティティとするほうが保守性が維持できると判断しました。
 * <br/>適用しているデザインパターン：Builder。
 * 
 * @author nakamura
 * 
 */
public class TransactionBuilder {
	private static final int TOKEN_LENGTH = 32;
	private static final int DEGIT_COUNT = 10;
	private static final int TOKEN_CHAR_COUNT = 36;

	/**
	 * トランザクショントークンを格納する{@link EEContext#getSession()}上の位置のデフォルト値。
	 */
	public static final String KEY = "woolpack.transaction.TRANSACTION_TOKEN";

	private final String key;
	private final Random r;
	private final Collection<String> idCollection;
	private final int size;
	private final NodeFindable xpathForm;
	private final NodeFindable xpathHref;

	/**
	 * @param key トークンを格納する{@link EEContext#getSession()}の位置。
	 * @param idCollection トランザクションを検証する id の一覧。本クラスはこの引数の状態を変化させない。
	 * @param size トークンのサイズ。
	 * @param factory {@link NodeFindable}のファクトリ。
	 */
	public TransactionBuilder(
			final String key,
			final Collection<String> idCollection,
			final int size,
			final Fn<String, NodeFindable> factory) {
		this.key = key;
		this.r = new Random();
		this.idCollection = idCollection;
		this.size = size;
		xpathForm = factory.exec("//FORM");
		xpathHref = factory.exec("//A[@href]");
	}

	/**
	 * トークンのサイズを32とします。
	 * 
	 * @param idCollection トランザクションを検証する id の一覧。本クラスはこの引数の状態を変化させない。
	 * @param factory {@link NodeFindable}のファクトリ。
	 */
	public TransactionBuilder(
			final Collection<String> idCollection,
			final Fn<String, NodeFindable> factory) {
		this(KEY, idCollection, TOKEN_LENGTH, factory);
	}

	String generateToken() {
		final StringBuilder sb = new StringBuilder();
		for (int i = 0; i < size; i++) {
			// jは正の数
			final int j = r.nextInt(TOKEN_CHAR_COUNT);
			sb.append((j < DEGIT_COUNT)
					? ((char) ('0' + j))
							: ((char) ('A' + j - DEGIT_COUNT)));
		}
		return sb.toString();
	}

	/**
	 * トークンを検証しトークンを生成して
	 * {@link EEContext#getSession()}
	 * に格納する{@link Fn}を返します。
	 * 
	 * @param trueExpression 妥当なトランザクションの場合の委譲先。
	 * @param falseExpression 妥当でないトランザクションの場合の委譲先。
	 * @return トークンを検証する{@link Fn}。
	 */
	public Fn<EEContext, Void> getCheckExpression(
			final Fn<? super EEContext, Void> trueExpression,
			final Fn<? super EEContext, Void> falseExpression) {
		return new Fn<EEContext, Void>() {
			public Void exec(final EEContext context) {
				if (idCollection.contains(context.getId())) {
					final Object oldToken = context.getSession().get(key);
					final List requestedTokens = Utils.toList(context.getInput().get(key));
					final Object newToken = generateToken();

					if (oldToken != null
							&& oldToken.equals(requestedTokens.get(0))
							&& context.getSession().replace(key, oldToken, newToken)) {
						trueExpression.exec(context);
					} else {
						falseExpression.exec(context);
					}
				} else {
					while (true) {
						final Object oldToken = context.getSession().get(key);
						final Object newToken = generateToken();
						if (oldToken == null) {
							if (context.getSession().putIfAbsent(key, newToken) == null) {
								break;
							}
						} else {
							if (context.getSession().replace(key, oldToken, newToken)) {
								break;
							}
						}
					}
					trueExpression.exec(context);
				}
				return null;
			}
		};
	}

	/**
	 * トークンを DOM ノードに設定する{@link Fn}を返します。
	 * 返却値は、
	 * HTML フォームを検索し子ノードとして hidden エレメントを追加します。
	 * そしてアンカーを検索し HTML にパラメータを追加します。
	 * 
	 * @return トークンを DOM ノードに設定する{@link Fn}。
	 */
	public Fn<EEContext, Void> getAppendExpression() {
		return FnUtils.seq(new BuildableArrayList<Fn<? super EEContext, ? extends Void>>()
				.list(XmlUtils.findNode(xpathForm, new Fn<EEContext, Void>() {
					public Void exec(final EEContext context) {
						final String token = (String) context.getSession().get(key);
						if (token != null) {
							final Element element = XmlUtils.getDocumentNode(
									context.getNode()).createElement("INPUT");
							element.setAttribute("type", "hidden");
							element.setAttribute("name", key);
							element.setAttribute("value", token);
							context.getNode().appendChild(element);
						}
						return null;
					}
				}))
				.list(XmlUtils.findNode(xpathHref, new Fn<EEContext, Void>() {
					public Void exec(final EEContext context) {
						final String token = (String) context.getSession().get(key);
						if (token != null) {
							final Element element = (Element) context.getNode();
							final String href = element.getAttribute("href");
							final String newHref = href
									+ ((href.indexOf('?') >= 0) ? '&' : '?') + key
									+ '=' + token;
							element.setAttribute("href", newHref);
						}
						return null;
					}
				})));
	}

	public Collection<String> getIdCollection() {
		return Collections.unmodifiableCollection(idCollection);
	}
	public String getKey() {
		return key;
	}
}
