/*
 * 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.dom.DomConstants;
import woolpack.dom.DomContext;
import woolpack.dom.DomExpression;
import woolpack.dom.Serial;
import woolpack.dom.XPath;
import woolpack.utils.CheckUtils;
import woolpack.utils.NodeFindable;
import woolpack.utils.NodeFindableFactory;
import woolpack.utils.UtilsConstants;

/**
 * トランザクショントークンを検証/登録し DOM ノードへ設定する{@link DomExpression}のビルダ。
 * 画面遷移順序を保証するための機能である。 関数従属性を考慮した場合、画面遷移順序を保証する id を画面遷移定義に管理するデータモデルが妥当であるが、
 * 画面遷移順序保証と画面遷移定義は別のエンティティとするほうが保守性が維持できると判断した。 適用しているパターン：Hook Operation。
 * 
 * @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 DomContext#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 DomContext#getSession()}の位置。
	 * @param idCollection
	 *            トランザクションを検証する id の一覧。本クラスはこの引数の状態を変化させない。
	 * @param size
	 *            トークンのサイズ。
	 * @param factory
	 *            {@link NodeFindable}のファクトリ。
	 * @throws StringIndexOutOfBoundsException
	 *             key が空の場合。
	 * @throws NullPointerException
	 *             引数のいずれかが null の場合。
	 */
	public TransactionBuilder(final String key,
			final Collection<String> idCollection, final int size,
			final NodeFindableFactory factory) {
		CheckUtils.checkNotEmpty(key);
		CheckUtils.checkNotNull(idCollection);

		this.key = key;
		this.r = new Random();
		this.idCollection = idCollection;
		this.size = size;
		xpathForm = factory.newInstance("//FORM");
		xpathHref = factory.newInstance("//A[@href]");
	}

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

	/**
	 * トークンを格納する{@link DomContext#getSession()}の位置を返す。
	 * 
	 * @return トークンを格納する{@link DomContext#getSession()}の位置。
	 */
	public String getKey() {
		return key;
	}

	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 DomContext#getSession()}
	 * に格納する{@link DomExpression}を返す。
	 * 
	 * @param trueExpression
	 *            妥当なトランザクションの場合の委譲先。
	 * @param falseExpression
	 *            妥当でないトランザクションの場合の委譲先。
	 * @return トークンを検証する{@link DomExpression}。
	 * @throws NullPointerException
	 *             引数のいずれかが null の場合。
	 */
	public DomExpression getCheckExpression(final DomExpression trueExpression,
			final DomExpression falseExpression) {
		CheckUtils.checkNotNull(trueExpression);
		CheckUtils.checkNotNull(falseExpression);
		return new DomExpression() {
			public void interpret(final DomContext context) {
				if (idCollection.contains(context.getId())) {
					final Object oldToken = context.getSession().get(key);
					final List requestedTokens = UtilsConstants.toList(context
							.getInput().get(key));
					final Object newToken = generateToken();

					if (oldToken != null
							&& oldToken.equals(requestedTokens.get(0))
							&& context.getSession().replace(key, oldToken,
									newToken)) {
						trueExpression.interpret(context);
					} else {
						falseExpression.interpret(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.interpret(context);
				}
			}
		};
	}

	/**
	 * トークンを DOM ノードに設定する{@link DomExpression}を返す。 HTML フォームを検索し子ノードとして hidden
	 * エレメントを追加する。 そしてアンカーを検索し HTML にパラメータを追加する。
	 * {@link DomExpression#interpret(DomContext)}は引数または
	 * {@link DomContext#getNode()}または{@link DomContext#getSession()}が
	 * null の場合に{@link NullPointerException}を投げる。
	 * 
	 * @return トークンを DOM ノードに設定する{@link DomExpression}。
	 */
	public DomExpression getAppendExpression() {
		return new Serial(new XPath(xpathForm, new DomExpression() {
			public void interpret(final DomContext context) {
				final String token = (String) context.getSession().get(key);
				if (token != null) {
					final Element element = DomConstants.getDocumentNode(
							context.getNode()).createElement("INPUT");
					element.setAttribute("type", "hidden");
					element.setAttribute("name", key);
					element.setAttribute("value", token);
					context.getNode().appendChild(element);
				}
			}
		}), new XPath(xpathHref, new DomExpression() {
			public void interpret(final DomContext 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);
				}
			}
		}));
	}

	/**
	 * トランザクションを検証する id の一覧を返す。
	 * 
	 * @return トランザクションを検証する id の一覧。
	 */
	public Collection<String> getIdCollection() {
		return Collections.unmodifiableCollection(idCollection);
	}
}
