/*
 * 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.test;

import java.io.IOException;
import java.io.PrintStream;
import java.io.Reader;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import woolpack.utils.Utils;

/**
 * ユーティリティです。
 * 
 * @author nakamura
 * 
 */
public final class TestUtils {
	private static final Pattern QUERY = Pattern.compile("[\\?&]?([^=\\?]+)=([^&]*)");

	private TestUtils() {
	}

	private static void addTo(final Map<String, List<String>> result,
			final String key, final String value) {
		List<String> list = result.get(key);
		if (list == null) {
			list = new ArrayList<String>();
			result.put(key, list);
		}
		list.add(value);
	}

	/**
	 * URL のクエリーからキーとその値を抽出します。
	 * 
	 * @param query URL。
	 * @return 抽出されたキー(属性値)と値のコレクションの{@link Map}。
	 */
	public static Map<String, List<String>> selectQuery(final String query) {
		// TODO URL クエリーの処理を厳密化する。
		final Map<String, List<String>> result = new LinkedHashMap<String, List<String>>();
		final Matcher m = QUERY.matcher(query);
		while (m.find()) {
			addTo(result, m.group(1), m.group(2));
		}
		return result;
	}

	private static String getText(final Node node) {
		final StringBuilder sb = new StringBuilder();
		final NodeList nodeList = node.getChildNodes();
		for (int i = 0; i < nodeList.getLength(); i++) {
			if (nodeList.item(i).getNodeType() == Node.TEXT_NODE) {
				sb.append(nodeList.item(i).getNodeValue());
			}
		}
		return sb.toString();
	}

	private static void selectForm(final Node node,
			final Map<String, List<String>> result, final String selectName) {
		final String name;
		if (node.getNodeType() == Node.ELEMENT_NODE) {
			final Element e = (Element) node;
			final String elementName = e.getNodeName();
			final String value;
			if ("OPTION".equals(elementName)) {
				name = selectName;
				value = e.hasAttribute("selected") ? e.getAttribute("value") : null;
			} else if (e.hasAttribute("name")) {
				name = e.getAttribute("name");
				if ("SELECT".equals(elementName)) {
					value = null;
				} else if ("INPUT".equals(elementName)) {
					final String inputType = e.getAttribute("type");
					if ("radio".equals(inputType) || "checkbox".equals(inputType)) {
						if (e.hasAttribute("checked")) {
							value = e.hasAttribute("value") ? e.getAttribute("value") : "";
						} else {
							value = null;
						}
					} else {
						value = e.hasAttribute("value") ? e.getAttribute("value") : "";
					}
				} else if ("BUTTON".equals(elementName)) {
					value = e.hasAttribute("value") ? e.getAttribute("value") : "";
				} else {
					value = getText(e);
				}
			} else {
				name = selectName;
				value = null;
			}
			if (value != null) {
				addTo(result, name, value);
			}
		} else {
			name = null;
		}
		final NodeList nodeList = node.getChildNodes();
		for (int i = 0; i < nodeList.getLength(); i++) {
			final Node n = nodeList.item(i);
			if (n.getNodeType() == Node.ELEMENT_NODE) {
				selectForm(n, result, name);
			}
		}
	}

	/**
	 * DOMノードをフォームとしてキーとその値を抽出します。
	 * 
	 * @param node 抽出対象。
	 * @return 抽出されたキー(属性値)と値のコレクションの{@link Map}。
	 */
	public static Map<String, List<String>> selectForm(final Node node) {
		final Map<String, List<String>> result = new LinkedHashMap<String, List<String>>();
		selectForm(node, result, null);
		return result;
	}

	/**
	 * DOM ノードからキーとその値を抽出します。
	 * 属性名の一覧のいずれかを属性名として持つ DOM エレメントを検索し、
	 * 属性値をキー、子テキストノードを全て結合した結果を値として抽出します。
	 * 
	 * @param attrNames 属性名の一覧。本クラスはこの引数の状態を変化させない。
	 * @param node 抽出対象。
	 * @return 抽出されたキー(属性値)と値のコレクションの{@link Map}。
	 */
	public static Map<String, List<String>> selectEmbedded(
			final Iterable<String> attrNames, final Node node) {
		final Map<String, List<String>> result = new LinkedHashMap<String, List<String>>();
		selectEmbedded(attrNames, node, result);
		return result;
	}

	private static void selectEmbedded(final Iterable<String> attrNames,
			final Node node, final Map<String, List<String>> result) {
		if (node.getNodeType() == Node.ELEMENT_NODE) {
			final Element e = (Element) node;
			for (final String attrName : attrNames) {
				if (!e.hasAttribute(attrName)) {
					continue;
				}
				addTo(result, e.getAttribute(attrName), getText(e));
				break;
			}
		}
		final NodeList nodeList = node.getChildNodes();
		for (int i = 0; i < nodeList.getLength(); i++) {
			selectEmbedded(attrNames, nodeList.item(i), result);
		}
	}

	/**
	 * {@link Reader}を読み込んで文字列に変換し、{@link Reader#close()}します。
	 * 
	 * @param reader 読み込み先。
	 * @return 引数の内容。
	 * @throws IOException
	 *             {@link Reader#read()}に失敗した場合。
	 */
	public static String toString(final Reader reader) throws IOException {
		try {
			final StringBuilder sb = new StringBuilder();
			int i = 0;
			while ((i = reader.read()) >= 0) {
				sb.append((char) i);
			}
			return sb.toString();
		} finally {
			reader.close();
		}
	}

	/**
	 * 構造化されたコレクションを再帰的に比較します。
	 * 同一の内容でない場合は引数を標準出力します。
	 * 引数が両方とも{@link List}の場合、各々同一インデックスの要素が同一の場合にふたつの{@link List}を同一とします。
	 * 配列は{@link List}とみなして同一性を判定します。
	 * 両方とも{@link Collection}であり片方が{@link List}でも配列でもない場合は順序に関係ない{@link Collection}の包含関係を比較します。
	 * 引数が両方とも{@link Map}の場合、{@link Map#keySet()}が同一で各々同一キーに対する値が同一の場合にふたつの{@link Map}を同一とします。
	 * 引数の両方とも{@link LinkedHashMap}の場合は要素の出現順序を同一性判定に含めます。
	 * 引数の両方とも{@link Map}で片方が{@link LinkedHashMap}でない場合は要素の出現順序を同一性判定に含めません。
	 * このクラスは LSP(The Liskov Substitution Principle) を満たしません。
	 * 
	 * @param a 比較元。
	 * @param b 比較先。
	 * @return 比較結果。
	 */
	public static boolean equals(final Object a, final Object b) {
		final boolean result = equalsPrivate(a, b);
		if (!result) {
			final PrintStream stream = System.out;
			stream.print("-- equals start -- expected --");
			stream.print(a);
			stream.print("-- but --");
			stream.print(b);
			stream.println("-- equals end --");
		}
		return result;
	}

	private static boolean equalsPrivate(final Object a, final Object b) {
		if (a == null) {
			return b == null;
		} else {
			if (b == null) {
				return false;
			}
		}
		if ((a instanceof LinkedHashMap) && (b instanceof LinkedHashMap)) {
			final LinkedHashMap aMap = (LinkedHashMap) a;
			final LinkedHashMap bMap = (LinkedHashMap) b;
			if (!equalsMap(aMap, bMap)) {
				return false;
			}
			final Iterator aIterator = aMap.keySet().iterator();
			final Iterator bIterator = bMap.keySet().iterator();
			while (aIterator.hasNext()) {
				if (!equalsPrivate(aIterator.next(), bIterator.next())) {
					return false;
				}
			}
			return true;
		}
		if (a instanceof Map) {
			if (b instanceof Map) {
				final Map aMap = (Map) a;
				final Map bMap = (Map) b;
				return equalsMap(aMap, bMap);
			} else {
				return false;
			}
		} else {
			if (b instanceof Map) {
				return false;
			}
		}
		if ((a instanceof List || a.getClass().isArray())
				&& (b instanceof List || a.getClass().isArray())) {
			final List aList = Utils.toList(a);
			final List bList = Utils.toList(b);
			if (aList.size() != bList.size()) {
				return false;
			}
			final int length = aList.size();
			for (int i = 0; i < length; i++) {
				if (!equalsPrivate(aList.get(i), bList.get(i))) {
					return false;
				}
			}
			return true;
		}
		if ((a instanceof Collection) && (b instanceof Collection)) {
			return equalsCollection((Collection) a, (Collection) b);
		}
		return a.equals(b);
	}

	private static boolean equalsMap(final Map a, final Map b) {
		if (!equalsCollection(a.keySet(), b.keySet())) {
			return false;
		}
		for (final Object entryObject : a.entrySet()) {
			final Entry entry = (Entry) entryObject;
			if (!equalsPrivate(entry.getValue(), b.get(entry.getKey()))) {
				return false;
			}
		}
		return true;
	}

	private static boolean equalsCollection(final Collection a,
			final Collection b) {
		return containsAll(a, b) && containsAll(b, a);
	}

	private static boolean containsAll(final Collection a, final Collection b) {
		return a.containsAll(b);
	}
}
