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

import java.io.Reader;

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

import woolpack.el.AbstractEL;
import woolpack.el.EL;
import woolpack.el.GettingEL;
import woolpack.fn.Fn;
import woolpack.fn.FnUtils;

/**
 * DOMを操作するユーティリティです。
 * 型推論で表記を簡略するためのスタティックメソッドと変数を含みます。
 * @author nakamura
 *
 */
public final class XmlUtils {
	
	/**
	 * DOM ノード名を返す関数です。
	 */
	public static final Fn<NodeContext, Node, RuntimeException> GET_NODE = new NodeGetter<RuntimeException>();
	
	/**
	 * DOM ノード名を返す関数です。
	 */
	public static final Fn<Node, String, RuntimeException> GET_NODE_NAME = new NodeNameGetter<RuntimeException>();
	
	/**
	 * 子ノードを全て含む DOM ノードを削除する関数です。
	 */
	public static final Fn<NodeContext, Void, RuntimeException> REMOVE_THIS = new ThisRemover<RuntimeException>();
	
	/**
	 * DOM ノードの全ての子ノードを削除する関数です。
	 */
	public static final Fn<NodeContext, Void, RuntimeException> REMOVE_CHILDREN = new ChildrenRemover<RuntimeException>();
	
	/**
	 * 現在の位置の DOM ノードのみを削除する関数です。
	 * 子ノードは指定されたノードの位置に挿入されます。
	 */
	public static final Fn<NodeContext, Void, RuntimeException> RETAIN_CHILDREN = new ChildrenRetain<RuntimeException>();
	
	/**
	 * {@link org.w3c.dom.Node#cloneNode(boolean)}で複製したノードを再設定する関数です。
	 */
	public static final Fn<NodeContext, Void, RuntimeException> CLONE_NODE = new NodeClone<RuntimeException>();
	
	/**
	 * ドキュメントノードに対して{@link org.w3c.dom.Node#normalize()}を呼び出す関数です。
	 */
	public static final Fn<NodeContext, Void, RuntimeException> NORMALIZE_NODE = new NodeNormalizer<RuntimeException>();

	/**
	 * {@link NodeContext}のアクセサにアクセスする式言語です。
	 */
	public static final EL NODE_EL = new AbstractEL() {
		@Override
		public Object getValue(final Object root, final Class clazz) {
			return ((NodeContext) root).getNode();
		}
		@Override
		public boolean setValue(final Object root, final Object value) {
			((NodeContext) root).setNode((Node) value);
			return true;
		}
	};
	
	private XmlUtils() {
	}
	
	/**
	 * DOM ノードを比較します。
	 * @param node0
	 * @param node1
	 * @return 同一内容を表すなら true。それ以外は false。
	 */
	public static boolean equalsNode(final Node node0, final Node node1) {
		if (node0 == null) {
			return node1 == null;
		}
		if (node1 == null) {
			return false;
		}
		if (node0.getNodeType() != node1.getNodeType()) {
			return false;
		}
		if (node0.getNodeType() == Node.TEXT_NODE
				|| node0.getNodeType() == Node.COMMENT_NODE) {
			return node0.getNodeValue().equals(node1.getNodeValue());
		}
		if (node0.getNodeType() == Node.ATTRIBUTE_NODE) {
			return node0.getNodeName().equals(node1.getNodeName())
					&& node0.getNodeValue().equals(node1.getNodeValue());
		}
		if (node0.getNodeType() == Node.DOCUMENT_NODE) {
			return equalsNode(((Document) node0).getDocumentElement(),
					((Document) node1).getDocumentElement());
		}
		if (!node0.getNodeName().equals(node1.getNodeName())) {
			return false;
		}

		final Element e0 = (Element) node0;
		final Element e1 = (Element) node1;
		final NamedNodeMap map0 = e0.getAttributes();
		final NamedNodeMap map1 = e1.getAttributes();
		if (map0.getLength() != map1.getLength()) {
			return false;
		}
		for (int i = 0; i < map0.getLength(); i++) {
			if (!equalsNode(map0.item(i), map1.item(i))) {
				return false;
			}
		}

		Node child0 = node0.getFirstChild();
		Node child1 = node1.getFirstChild();
		while (child0 != null || child1 != null) {
			if (!equalsNode(child0, child1)) {
				return false;
			}
			child0 = child0.getNextSibling();
			child1 = child1.getNextSibling();
		}
		return true;
	}

	/**
	 * 子ノードを含むDOMノードを削除します。
	 * @param node
	 */
	public static void removeThis(final Node node) {
		node.getParentNode().removeChild(node);
	}

	/**
	 * 指定された DOM ノードの全ての子ノードを削除します。
	 * @param node
	 */
	public static void removeChildren(final Node node) {
		Node child = null;
		while ((child = node.getFirstChild()) != null) {
			node.removeChild(child);
		}
	}

	/**
	 * 指定された DOM ノードのみを削除します。子ノードは指定された DOM ノードの位置に挿入されます。
	 * @param node
	 */
	public static void retainChildren(final Node node) {
		final Node parent = node.getParentNode();
		Node child = null;
		while ((child = node.getFirstChild()) != null) {
			parent.insertBefore(child, node);
		}
		parent.removeChild(node);
	}

	/**
	 * DOM ノードの子ノードにテキストノードを追加します。
	 * @param node
	 * @param text
	 */
	public static void appendText(final Node node, final String text) {
		node.appendChild(getDocumentNode(node).createTextNode(text));
	}

	/**
	 * DOM ドキュメントを返します。
	 * DOM ドキュメントに対して{@link Node#getOwnerDocument()}
	 * の呼び出しが失敗するためにこのメソッドを定義しました。
	 * @param node
	 * @return ドキュメントノード。
	 */
	public static Document getDocumentNode(final Node node) {
		return (node.getNodeType() == Node.DOCUMENT_NODE)
		? (Document) node : node.getOwnerDocument();
	}
	
	/**
	 * 属性値をキーとして委譲先を検索して委譲する関数を生成します。
	 * @param <C>
	 * @param <E>
	 * @param attrNames 属性値を取得するための属性名の候補一覧。
	 * @param fn 属性値と委譲先の対応表。
	 * @return 関数。
	 */
	public static <C extends NodeContext, E extends Exception> Fn<C, Void, E> branchByAttrValue(
			final Iterable<String> attrNames,
			final Fn<String, ? extends Fn<? super C, Void, ? extends E>, ? extends E> fn) {
		return new AttrValueBranch<C, E>(attrNames, fn);
	}
	
	/**
	 * ノードを検索して委譲する関数を生成します。
	 * 委譲先でノードを操作した場合、次の検索結果に影響するかは委譲先に依存します。
	 * @param <C>
	 * @param <E>
	 * @param findable ノードを検索する委譲先。
	 * @param firstFn 最初の委譲先(ポインタは検索結果)。
	 * @param pluralFn 2番目以降の委譲先(ポインタは検索結果)。
	 * @return 関数。
	 */
	public static <C extends NodeContext, E extends Exception> Fn<C, Void, E> findNode(
			final Fn<? super Node, ? extends NodeList, ? extends E> findable,
			final Fn<? super C, Void, ? extends E> firstFn,
			final Fn<? super C, Void, ? extends E> pluralFn) {
		return new NodeFinder<C, E>(findable, firstFn, pluralFn);
	}
	
	/**
	 * ノードを検索して委譲する関数を生成します。
	 * 委譲先でノードを操作した場合、次の検索結果に影響するかは委譲先に依存します。
	 * @param <C>
	 * @param <E>
	 * @param findable
	 * @param fn 委譲先(ポインタは検索結果)。
	 * @return 関数。
	 */
	public static <C extends NodeContext, E extends Exception> Fn<C, Void, E> findNode(
			final Fn<? super Node, ? extends NodeList, ? extends E> findable,
			final Fn<? super C, Void, ? extends E> fn) {
		return new NodeFinder<C, E>(findable, fn, fn);
	}
	
	/**
	 * DOM エレメントの属性値を返す関数を生成します。
	 * @param attrName 属性名。
	 * @return 関数。
	 */
	public static Fn<NodeContext, String, RuntimeException> getAttrValue(final String attrName) {
		return new AttrValueGetter<RuntimeException>(attrName);
	}
	
	/**
	 * 子ノードとして DOM エレメントを挿入する関数を生成します。
	 * @param <C>
	 * @param <E>
	 * @param elementName エレメント名。
	 * @param fn 委譲先(ポインタは挿入したエレメント)。
	 * @return 関数。
	 */
	public static <C extends NodeContext, E extends Exception> Fn<C, Void, E> insertElementToChild(
			final String elementName,
			final Fn<? super C, Void, ? extends E> fn) {
		return new ChildElementInserter<C, E>(elementName, fn);
	}
	
	/**
	 * 子ノードとして DOM エレメントを挿入する関数を生成します。
	 * @param <C>
	 * @param elementName エレメント名。
	 * @return 関数。
	 */
	public static <C extends NodeContext> Fn<C, Void, RuntimeException> insertElementToChild(
			final String elementName) {
		return new ChildElementInserter<C, RuntimeException>(
				elementName,
				FnUtils.<NodeContext, Void>fix(null));
	}
	
	/**
	 * 親ノードとして DOM エレメントを挿入する関数を生成します。
	 * @param <C>
	 * @param <E>
	 * @param elementName エレメント名。
	 * @param fn 委譲先(ポインタは挿入したエレメント)。
	 * @return 関数。
	 */
	public static <C extends NodeContext, E extends Exception> Fn<C, Void, E> insertElementToParent(
			final String elementName,
			final Fn<? super C, Void, ? extends E> fn) {
		return new ParentElementInserter<C, E>(elementName, fn);
	}
	
	/**
	 * 親ノードとして DOM エレメントを挿入する関数を生成します。
	 * @param <C>
	 * @param elementName エレメント名。
	 * @return 関数。
	 */
	public static <C extends NodeContext> ParentElementInserter<C, RuntimeException> insertElementToParent(
			final String elementName) {
		return new ParentElementInserter<C, RuntimeException>(
				elementName,
				FnUtils.<NodeContext, Void>fix(null));
	}
	
	/**
	 * コンテキスト役からコレクション取得し、
	 * コレクションの各値に対し DOM ノードをコピーして処理を委譲し、
	 * 最後にオリジナルのノードを削除する関数を生成します。
	 * HTML で項目を列挙する時に主に使用します。
	 * @param <C>
	 * @param <E>
	 * @param collectionEL コレクションへの参照。
	 * @param valueEL コレクションの各値の格納先。
	 * @param fn コレクションの各値に対する委譲先(ポインタはコピーしたノード)。
	 * @return 関数。
	 */
	public static <C extends NodeContext, E extends Exception> Fn<C, Void, E> copyTemplate(
			final GettingEL collectionEL,
			final EL valueEL,
			final Fn<? super C, Void, ? extends E> fn) {
		return new TemplateCopier<C, E>(collectionEL, valueEL, fn);
	}
	
	/**
	 * {@link Reader}と{@link XmlTransformer}を使用する{@link Node}のファクトリを生成します。
	 * @param <C>
	 * @param readerFactory リーダーのファクトリ。
	 * @param transformer XMLの変換器。
	 * @return ファクトリ。
	 */
	public static <C> Fn<C, Node, Exception> nodeFactory(
			final Fn<? super C, ? extends Reader, ? extends Exception> readerFactory,
			final Fn<XmlTransformerContext, Void, ? extends Exception> transformer) {
		return new NodeFactory<C>(readerFactory, transformer);
	}
	
	/**
	 * DOM エレメントの属性を削除する関数を生成します。
	 * @param attrName 削除対象の属性名。
	 * @return 関数。
	 */
	public static Fn<NodeContext, Void, RuntimeException> removeAttr(final String attrName) {
		return new AttrRemover<RuntimeException>(attrName);
	}
	
	/**
	 * 指定された DOM ノードをテキストノードに置き換える関数を生成します。
	 * 取得した値が null の場合は値を更新しません。
	 * @param <C>
	 * @param <E>
	 * @param fn 置き換え文字列の取得先。
	 * @return 関数。
	 */
	public static <C extends NodeContext, E extends Exception> Fn<C, Void, E> replaceText(
			final Fn<? super C, String, ? extends E> fn) {
		return new TextReplacer<C, E>(fn);
	}
	
	/**
	 * 指定された DOM ノードの子ノードをテキストノードに置き換える関数を生成します。
	 * 取得した値が null の場合は値を更新しません。
	 * @param <C>
	 * @param <E>
	 * @param fn 置き換え文字列の取得先。
	 * @return 関数。
	 */
	public static <C extends NodeContext, E extends Exception> Fn<C, Void, E> replaceTextToChild(
			final Fn<? super C, String, ? extends E> fn) {
		return new ChildTextReplacer<C, E>(fn);
	}
	
	/**
	 * DOMノードを再帰的にループしながら委譲する関数を生成します。
	 * 各層をスナップショットとしてループします。
	 * @param <C>
	 * @param <E>
	 * @param fn 委譲先(ポインタは各エレメント)。
	 * @return 関数。
	 */
	public static <C extends NodeContext, E extends Exception> Fn<C, Void, E> seekNode(
			final Fn<? super C, Void, ? extends E> fn) {
		return new NodeSeeker<C, E>(fn);
	}
	
	/**
	 * 委譲先で生成したノードを設定する関数を生成します。
	 * @param <C>
	 * @param <E>
	 * @param fn 置き換えノードの取得先。
	 * @return 関数。
	 */
	public static <C extends NodeContext, E extends Exception> Fn<C, Void, E> setNode(
			final Fn<? super C, ? extends Node, ? extends E> fn) {
		return new NodeSetter<C, E>(fn);
	}
	
	/**
	 * 委譲先の返却値で属性値を更新する関数を生成します。
	 * 取得した値が null の場合は値を更新しません。
	 * @param <C>
	 * @param <E>
	 * @param attrName 更新対象の属性名。
	 * @param fn 更新値を計算する委譲先。
	 * @return 関数。
	 */
	public static <C extends NodeContext, E extends Exception> Fn<C, Void, E> updateAttrValue(
			final String attrName,
			final Fn<? super C, String, ? extends E> fn) {
		return new AttrValueUpdater<C, E>(attrName, fn);
	}
}
