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

import java.util.Collection;
import java.util.Iterator;

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

import woolpack.el.ArrayPathEL;
import woolpack.el.EL;
import woolpack.el.ELUtils;
import woolpack.el.GettingEL;
import woolpack.el.PropertyEL;
import woolpack.el.StateEL;
import woolpack.utils.Utils;
import woolpack.xml.AbstractNodeSeeker;
import woolpack.xml.NodeContext;
import woolpack.xml.TemplateCopier;

/**
 * DOM エレメントの属性値をプロパティ名としてコンポーネントから値を取得し、
 * DOM ノードに自動設定する関数です。
 * {@link #exec(NodeContext)}メソッドでは以下の処理を行います。
 * <ol>
 * <li>attrNamesのいずれかの属性名を持つノードを検索します。
 * <li>属性名に対応する属性値をプロパティ名とみなしてNodeContextインスタンスのcomponentELを基点とするプロパティ値を取得します。
 * <li>プロパティ値の型により以下の処理を行います。
 * <ol>BeanまたはMap型単純型のコレクションの場合
 * <li>単純型でないコレクションの場合は、コレクションの各値をcomponentELとし現在のDOMノードを基点とする本クラスのクローンを作成して委譲します。
 * <li>単純型のコレクションまたは単純型そのものの場合は、その値で{@link ValueUpdater}を生成して委譲します。
 * <li>単純型でないクラスの場合は、その値をcomponentELとし現在のDOMノードを基点とする本クラスのクローンを作成して委譲します。
 * </ol>
 * </ol>
 * コンポーネントのデータ構造に似た属性名マーカー付きDOMノードをテンプレートとして定義することにより、
 * データの流し込み処理を自動化することができます。
 * 業務系プログラムで作成したデータ構造を
 * HTMLのDOM表現に流し込む本機構により
 * プログラマとデザイナの結合を疎に保ちます。
 * @author nakamura
 * 
 * @param <E>
 */
public class AutoUpdater<E extends Exception> extends AbstractNodeSeeker<NodeContext, E> implements Cloneable {
	private Iterable<String> attrNames;
	private GettingEL componentEL;
	private GettingEL configEL;
	private Collection<Class<?>> atomCollection;
	private GettingEL errorEL;
	private boolean selectFlag;
	
	/**
	 * 
	 * @param attrNames 属性名の一覧。
	 * @param componentEL コンポーネントへの参照。
	 * @param configEL 設定値への参照。
	 * @param atomCollection 値の個数に関して原子的であるクラスの一覧。
	 * @param errorEL 値取得に失敗した場合の値の取得先。
	 * @param selectMode selected または checked 属性の有無を変更するなら true。value の属性値を変更するなら false。
	 */
	protected AutoUpdater(
			final Iterable<String> attrNames,
			final GettingEL componentEL,
			final GettingEL configEL,
			final Collection<Class<?>> atomCollection,
			final GettingEL errorEL,
			final boolean selectMode) {
		super();
		this.attrNames = attrNames;
		this.componentEL = componentEL;
		this.configEL = configEL;
		this.atomCollection = atomCollection;
		this.errorEL = errorEL;
		this.selectFlag = selectMode;
	}

	/**
	 * @param attrNames 属性名の一覧。
	 * @param componentEL コンポーネントへの参照。
	 * @param configEL 設定値への参照。
	 * @param atomCollection 値の個数に関して原子的であるクラスの一覧。
	 * @param errorEL 値取得に失敗した場合の値の取得先。
	 */
	public AutoUpdater(
			final Iterable<String> attrNames,
			final GettingEL componentEL,
			final GettingEL configEL,
			final Collection<Class<?>> atomCollection,
			final GettingEL errorEL) {
		this(attrNames, componentEL, configEL, atomCollection, errorEL, true);
	}

	/**
	 * 値取得に失敗した場合は何もしません。
	 * 
	 * @param attrNames 属性名の一覧。
	 * @param componentEL コンポーネントへの参照。
	 * @param configEL 設定値への参照。
	 */
	public AutoUpdater(
			final Iterable<String> attrNames,
			final GettingEL componentEL,
			final GettingEL configEL) {
		this(
				attrNames,
				componentEL,
				configEL,
				Utils.ATOM_SET,
				ELUtils.NULL
		);
	}

	@Override
	public Void exec(final NodeContext c) throws E {
		if (c.getNode().getNodeType() == Node.ELEMENT_NODE) {
			final Element e = (Element) c.getNode();
			final String attrName = getAttrName(e);
			if (attrName != null) {
				final String attrValue = e.getAttribute(attrName);
				final GettingEL valueEL = new ArrayPathEL(componentEL, new PropertyEL(attrValue));
				final Object value = getValue(c, valueEL);
				if (value != null) {
					final boolean collectionFlag;
					final boolean atomFlag;
					if (value instanceof Iterable) {
						collectionFlag = true;
						final Iterable<?> iterable = (Iterable) value;
						final Iterator<?> iterator = iterable.iterator();
						if (!iterator.hasNext()) {
							atomFlag = false;
						} else {
							atomFlag = atomCollection.contains(iterator.next().getClass());
						}
					} else if (value.getClass().isArray()) {
						collectionFlag = true;
						atomFlag = atomCollection.contains(value.getClass().getComponentType());
					} else {
						collectionFlag = false;
						atomFlag = atomCollection.contains(value.getClass());
					}
					if (atomFlag) {
						new ValueUpdater(
								valueEL,
								new ArrayPathEL(configEL, new PropertyEL(attrValue)),
								selectFlag).exec(c);
					} else {
						if (collectionFlag) {
							final EL tmpEL = new StateEL();
							final AutoUpdater<E> newUpdater = clone(tmpEL);
							new TemplateCopier<NodeContext, E>(valueEL, tmpEL, newUpdater).exec(c);
						} else {
							final EL tmpEL = new StateEL();
							tmpEL.setValue(c, value);
							clone(tmpEL).exec(c);
						}
					}
					return null;
				}
			}
		}
		return super.exec(c);
	}
	
	public Object clone() throws CloneNotSupportedException {
		return super.clone();
	}
	
	// クローンのキャストは必ず成功するので型警告を抑止することができる。
	@SuppressWarnings("unchecked")
	private AutoUpdater<E> clone(final GettingEL el) {
		final AutoUpdater<E> newUpdater;
		try {
			newUpdater = (AutoUpdater<E>) clone();
		} catch (final CloneNotSupportedException e1) {
			throw new IllegalStateException(e1);
		}
		newUpdater.selectFlag = false;
		newUpdater.componentEL = el;
		return newUpdater;
	}

	private Object getValue(final NodeContext c, final GettingEL valueEL) {
		try {
			return valueEL.getValue(c);
		} catch (final RuntimeException exception) {
			return errorEL.getValue(c);
		}
	}

	private String getAttrName(final Element e) {
		for (final String attrName : attrNames) {
			if (e.hasAttribute(attrName)) {
				return attrName;
			}
		}
		return null;
	}

	public Iterable<String> getAttrNames() {
		return attrNames;
	}
	public void setAttrNames(final Iterable<String> attrNames) {
		this.attrNames = attrNames;
	}
	public GettingEL getComponentEL() {
		return componentEL;
	}
	public void setComponentEL(final GettingEL componentEL) {
		this.componentEL = componentEL;
	}
	public GettingEL getErrorEL() {
		return errorEL;
	}
	public void setErrorEL(final GettingEL errorEL) {
		this.errorEL = errorEL;
	}
	public GettingEL getConfigEL() {
		return configEL;
	}
	public void setConfigEL(final GettingEL configEL) {
		this.configEL = configEL;
	}
	public Collection<Class<?>> getAtomCollection() {
		return atomCollection;
	}
	public void setAtomCollection(final Collection<Class<?>> atomCollection) {
		this.atomCollection = atomCollection;
	}
}
