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

import java.beans.PropertyDescriptor;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import woolpack.utils.Switchable;

/**
 * 定数と静的メソッドの集まり。
 * 
 * @author nakamura
 * 
 */
public final class JsVisitorConstants {
	/**
	 * パッケージ名を取り除いたクラス名を検索する正規表現。
	 */
	private static final Pattern LOCAL_CLASS_NAME = Pattern
			.compile("([^\\.]+\\.)*([^\\.]*)");

	/**
	 * {@link Visitor#getContext()}を{@link StringBuilder}にキャストして、 v
	 * のコンストラクタ引数を JS 表現に変換する{@link Acceptable}。
	 * 
	 * @see VisitorConstants#getConstructorGetterList(Class)
	 */
	public static final Acceptable<Object> PARAMS = new Acceptable<Object>() {
		public void accept(final Visitor visitor, final Object v) {
			final StringBuilder sb = (StringBuilder) visitor.getContext();
			boolean firstFlag = true;
			final List<PropertyDescriptor> methodList = VisitorConstants
					.getConstructorGetterList(v.getClass());
			for (final PropertyDescriptor property : methodList) {
				final Object value = VisitorConstants.get(v, property
						.getReadMethod());
				if (firstFlag) {
					firstFlag = false;
				} else {
					sb.append(',');
				}
				visitor.visit(value);
			}
		}
	};

	/**
	 * {@link Visitor#getContext()}を{@link StringBuilder}
	 * にキャストして、 v を{@link Iterable}として
	 * JS 表現に変換する{@link Acceptable}。
	 */
	public static final Acceptable<Iterable> ITERABLE
	= new Acceptable<Iterable>() {
		public void accept(final Visitor visitor, final Iterable v) {
			final StringBuilder sb = (StringBuilder) visitor.getContext();
			sb.append('[');
			boolean flag = true;
			for (final Object o : v) {
				if (flag) {
					flag = false;
				} else {
					sb.append(',');
				}
				visitor.visit(o);
			}
			sb.append(']');
		}
	};

	/**
	 * {@link Visitor#getContext()}を{@link StringBuilder}にキャストして、
	 * v の{@link Object#toString()}をそのまま追加する{@link Acceptable}。
	 */
	public static final Acceptable<Object> OBJECT = new Acceptable<Object>() {
		public void accept(final Visitor visitor, final Object v) {
			final StringBuilder sb = (StringBuilder) visitor.getContext();
			sb.append(v);
		}
	};

	/**
	 * {@link Visitor#getContext()}を{@link Map}にキャストして、 {@link Map}を JS
	 * 表現に変換する{@link Acceptable}を返す。
	 */
	public static final Acceptable<Map> MAP = new Acceptable<Map>() {
		public void accept(final Visitor visitor, final Map v) {
			final StringBuilder sb = (StringBuilder) visitor.getContext();
			sb.append('{');
			boolean flag = true;
			for (final Object entryObject : v.entrySet()) {
				final Entry entry = (Entry) entryObject;
				if (flag) {
					flag = false;
				} else {
					sb.append(',');
				}
				visitor.visit(entry.getKey());
				sb.append(':');
				visitor.visit(entry.getValue());
			}
			sb.append('}');
		}
	};

	private JsVisitorConstants() {
	} // カバレージがここを通過してはいけない

	/**
	 * {@link Visitor#getContext()}を{@link StringBuilder}にキャストして、
	 * object の{@link Object#toString()}をそのまま追加する{@link Acceptable}を返す。
	 * 
	 * @param <V>
	 *            Element 役。
	 * @param object
	 *            設定する文字列。
	 * @return {@link Visitor#getContext()}を{@link StringBuilder}にキャストして s
	 *         設定する{@link Acceptable}。
	 */
	public static <V> Acceptable<V> objectAcceptable(final Object object) {
		return new Acceptable<V>() {
			public void accept(final Visitor visitor, final V v) {
				final StringBuilder sb = (StringBuilder) visitor.getContext();
				sb.append(object);
			}
		};
	}

	/**
	 * {@link Visitor#getContext()}を{@link StringBuilder}にキャストして、 v の JS
	 * クラス名を追加し委譲する{@link Acceptable}を返す。
	 * 
	 * @param <V>
	 *            Element 役。
	 * @param child
	 *            委譲先。
	 * @return JSクラス名を設定し委譲先に委譲する{@link Acceptable}。
	 */
	public static <V> Acceptable<V> classNameAcceptable(
			final Acceptable<V> child) {
		return new Acceptable<V>() {
			public void accept(final Visitor visitor, final V v) {
				final StringBuilder sb = (StringBuilder) visitor.getContext();
				sb.append("new ");
				sb.append(getLocalClassName(v.getClass()));
				sb.append('(');
				child.accept(visitor, v);
				sb.append(')');
			}
		};
	}

	/**
	 * {@link Visitor#getContext()}を{@link StringBuilder}にキャストして、
	 * {@link Switchable}を JS 表現に変換する{@link Acceptable}を返す。
	 * 
	 * @param <K>
	 *            {@link Switchable}のキーの型。
	 * @param <V>
	 *            {@link Switchable}の値の型。
	 * @param defaultKey
	 *            {@link Switchable#getDefault()}のキー。
	 * @return {@link Switchable}のキーを順次取り出してキーと値を
	 * {@link Visitor}に委譲する{@link Acceptable}。
	 */
	public static <K, V> Acceptable<Switchable<K, V>> switchableAcceptable(
			final Object defaultKey) {
		return new Acceptable<Switchable<K, V>>() {
			public void accept(
					final Visitor visitor,
					final Switchable<K, V> v) {
				final StringBuilder sb = (StringBuilder) visitor.getContext();
				sb.append('{');
				boolean flag = true;
				for (final K key : v.keys()) {
					if (flag) {
						flag = false;
					} else {
						sb.append(',');
					}
					visitor.visit(key);
					sb.append(':');
					visitor.visit(v.get(key));
				}
				if (defaultKey != null && v.getDefault() != null) {
					if (flag) {
						flag = false;
					} else {
						sb.append(',');
					}
					visitor.visit(defaultKey);
					sb.append(':');
					visitor.visit(v.getDefault());
				}
				sb.append('}');
			}
		};
	}

	/**
	 * パッケージ名を取り除いたクラス名を返す。 JavaScriptコンストラクタを生成する際に使用する。
	 * 
	 * @param clazz
	 *            変換対象のクラス。
	 * @return パッケージ名を取り除いたクラス名。
	 * @throws NullPointerException
	 *             引数が null の場合。
	 */
	public static String getLocalClassName(final Class clazz) {
		final Matcher m = LOCAL_CLASS_NAME.matcher(clazz.getName());
		m.matches();
		return m.group(2);
	}

	/**
	 * オブジェクトの文字列表現をプログラムで記載する形式に変換して委譲する{@link Acceptable}を返す。
	 * 
	 * @param child
	 *            委譲先。
	 * @return オブジェクトの文字列表現をプログラムで記載する形式に変換する{@link Acceptable}。
	 */
	public static Acceptable<Object> escapeStringAcceptable(
			final Acceptable<Object> child) {
		return new Acceptable<Object>() {
			private final Pattern patternT = Pattern.compile("\\t");

			private final Pattern patternN = Pattern.compile("\\n");

			private final Pattern patternR = Pattern.compile("\\r");

			private final Pattern patternF = Pattern.compile("\\f");

			private final Pattern patternA = Pattern.compile("\\a");

			private final Pattern patternE = Pattern.compile("\\e");

			public void accept(final Visitor visitor, final Object v) {
				String s = v.toString();
				s = patternT.matcher(s).replaceAll("\\t");
				s = patternN.matcher(s).replaceAll("\\n");
				s = patternR.matcher(s).replaceAll("\\r");
				s = patternF.matcher(s).replaceAll("\\f");
				s = patternA.matcher(s).replaceAll("\\a");
				s = patternE.matcher(s).replaceAll("\\e");
				child.accept(visitor, '\"' + s + '\"');
			}
		};
	}
}
