/*
 * 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 java.io.StringReader;
import java.io.StringWriter;
import java.io.Writer;

import javax.xml.transform.dom.DOMResult;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;

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

import woolpack.el.AbstractEL;
import woolpack.el.EL;
import woolpack.el.GettingEL;
import woolpack.factory.FactoryUtils;
import woolpack.fn.Fn;
import woolpack.misc.MiscUtils;

/**
 * ユーティリティです。
 * 型推論で表記を簡略するためのスタティックメソッドと変数を含みます。
 * @author nakamura
 *
 */
public final class XmlUtils {
	
	/**
	 * 恒等変換のデフォルト値です。複数のスレッドで同時に使用することができます。
	 */
	public static final Fn<XmlTransformerContext, Void> TRANSFORMER
	= MiscUtils.loadBalancer(FactoryUtils.newInstance(XmlTransformer.class), 5);

	public static final NodeGetter GET_NODE = new NodeGetter();
	public static final NodeNameGetter GET_NODE_NAME = new NodeNameGetter();
	public static final ThisRemover REMOVE_THIS = new ThisRemover();
	public static final ChildrenRemover REMOVE_CHILDREN = new ChildrenRemover();
	public static final ChildrenRetain RETAIN_CHILDREN = new ChildrenRetain();
	public static final NodeClone CLONE_NODE = new NodeClone();
	public static final NodeNormalizer NORMALIZE_NODE = new NodeNormalizer();
	public static final NodeFindableFactory NODE_FINDABLE_FACTORY = new NodeFindableFactory();
	

	/**
	 * {@link NodeContext}のアクセサにアクセスする{@link EL}です。
	 */
	public static final EL NODE_EL = new AbstractEL() {
		@Override
		public Object getValue(final Object root, final Class clazz) {
			return ((NodeContext) root).getNode();
		}
		@Override
		public void setValue(final Object root, final Object value) {
			((NodeContext) root).setNode((Node) value);
		}
	};
	
	private XmlUtils() {
	}
	
	/**
	 * XMLの文字列表現をDOM表現に変換します。
	 * @param s
	 * @return 変換結果。
	 */
	public static Node convert(final String s) {
		final DOMResult result = new DOMResult();
		final XmlTransformerContext context = new XmlTransformerContext();
		context.setSource(new StreamSource(new StringReader(s)));
		context.setResult(result);
		TRANSFORMER.exec(context);
		return result.getNode();
	}
	
	/**
	 * XMLのDOM表現を文字列表現に変換します。
	 * @param node
	 * @return 変換結果。
	 */
	public static String convert(final Node node) {
		final Writer w = new StringWriter();
		final XmlTransformerContext context = new XmlTransformerContext();
		context.setSource(new DOMSource(node));
		context.setResult(new StreamResult(w));
		TRANSFORMER.exec(context);
		return w.toString();
	}
	
	/**
	 * 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();
	}
	
	public static <C extends NodeContext> AttrValueBranch<C> branchByAttrValue(
			final Iterable<String> attrNames,
			final Fn<String, ? extends Fn<? super C, Void>> fn) {
		return new AttrValueBranch<C>(attrNames, fn);
	}
	
	public static <C extends NodeContext> NodeFinder<C> findNode(
			final NodeFindable findable,
			final Fn<? super C, Void> firstFn,
			final Fn<? super C, Void> pluralFn) {
		return new NodeFinder<C>(findable, firstFn, pluralFn);
	}
	
	public static <C extends NodeContext> NodeFinder<C> findNode(
			final NodeFindable findable,
			final Fn<? super C, Void> fn) {
		return new NodeFinder<C>(findable, fn);
	}
	
	public static AttrValueGetter getAttrValue(final String attrName) {
		return new AttrValueGetter(attrName);
	}
	
	public static <C extends NodeContext> ChildElementInserter<C> insertElementToChild(
			final String elementName,
			final Fn<? super C, Void> fn) {
		return new ChildElementInserter<C>(elementName, fn);
	}
	
	public static <C extends NodeContext> ChildElementInserter<C> insertElementToChild(
			final String elementName) {
		return new ChildElementInserter<C>(elementName);
	}
	
	public static <C extends NodeContext> ParentElementInserter<C> insertElementToParent(
			final String elementName,
			final Fn<? super C, Void> fn) {
		return new ParentElementInserter<C>(elementName, fn);
	}
	
	public static <C extends NodeContext> ParentElementInserter<C> insertElementToParent(
			final String elementName) {
		return new ParentElementInserter<C>(elementName);
	}
	
	public static <C extends NodeContext> TemplateCopier<C> copyTemplate(
			final GettingEL collectionEL,
			final EL valueEL,
			final Fn<? super C, Void> fn) {
		return new TemplateCopier<C>(collectionEL, valueEL, fn);
	}
	
	public static NodeFactory nodeFactory(
			final Fn<? super String, ? extends Reader> readerFactory,
			final Fn<XmlTransformerContext, Void> transformer) {
		return new NodeFactory(readerFactory, transformer);
	}
	
	public static AttrRemover removeAttr(final String attrName) {
		return new AttrRemover(attrName);
	}
	
	public static <C extends NodeContext> TextReplacer<C> replaceText(
			final Fn<? super C, String> fn) {
		return new TextReplacer<C>(fn);
	}
	
	public static <C extends NodeContext> ChildTextReplacer<C> replaceTextToChild(
			final Fn<? super C, String> fn) {
		return new ChildTextReplacer<C>(fn);
	}
	
	public static <C extends NodeContext> NodeSeeker<C> seekNode(
			final Fn<? super C, Void> fn) {
		return new NodeSeeker<C>(fn);
	}
	
	public static <C extends NodeContext> NodeSetter<C> setNode(
			final Fn<? super C, ? extends Node> fn) {
		return new NodeSetter<C>(fn);
	}
	
	public static <C extends NodeContext> AttrValueUpdater<C> updateAttrValue(
			final String attrName,
			final Fn<? super C, String> fn) {
		return new AttrValueUpdater<C>(attrName, fn);
	}
}
