package charactermanaj.util;

import java.awt.Color;
import java.beans.BeanInfo;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.lang.annotation.Annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.Method;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.AbstractMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.TreeMap;
import java.util.logging.Level;
import java.util.logging.Logger;


/**
 * Setter/Getterのペアをもつビーンのプロパティを文字列化してプロパティに設定するか、
 * プロパティからビーンに値を設定するためのユーテリティクラス.<br>
 * @author seraphy
 */
public final class BeanPropertiesUtilities {

	private static final Logger logger = Logger.getLogger(BeanPropertiesUtilities.class.getName());

	private BeanPropertiesUtilities() {
		throw new RuntimeException("utilities class.");
	}

	/**
	 * プロパティ値を文字列として変換する
	 */
	public interface StringConverter {
		Object valueOf(String text);
		String toString(Object obj);
	}

	/**
	 * int値をunsigned 32bit integerとして#つき16進数として文字列化し、
	 * それをint値として受け取れるようにするための文字列変換クラス.
	 */
	public static class UnsignedHexStringConverter implements StringConverter {
		@Override
		public Object valueOf(String text) {
			text = (text != null) ? text.trim() : null;
			if (text != null && text.length() > 0) {
				return (int)(long) Long.decode(text); // 符号無し32ビット値を受け取るためにlongで受け取ってintにする。
			}
			return 0;
		}

		@Override
		public String toString(Object obj) {
			if (obj != null) {
				return "#" + Long.toString(((Number) obj).longValue() & 0xffffffffL, 16);
			}
			return "";
		}
	}

	/**
	 * プロパティのgetter(またはsetter)のメソッドに付与して、
	 * プロパティファイルへの読み書きや編集時の文字列変換を行うクラスを明示的に指定できるようにする。
	 */
	@Target({ ElementType.METHOD })
	@Retention(RetentionPolicy.RUNTIME)
	public @interface StringConverterSpec {

		Class<? extends StringConverter> value();
	}

	/**
	 * プロパティへのアクセッサをまとめたもの。
	 * 実際にビーンにアクセスするためには、{@link #setBean(Object)}でビーンを設定する必要がある。
	 */
	public static class PropertyAccessorMap<T> extends AbstractMap<String, PropertyAccessor> {

		private final BeanHolder<T> beanHolder;

		private final Map<String, PropertyAccessor> accessorMap;

		public PropertyAccessorMap(Map<String, PropertyAccessor> accessorMap, BeanHolder<T> beanHolder) {
			this.accessorMap = accessorMap;
			this.beanHolder = beanHolder;
		}

		public T getBean() {
			return beanHolder.getBean();
		}

		public void setBean(T bean) {
			beanHolder.setBean(bean);
		}

		@Override
		public Set<Entry<String, PropertyAccessor>> entrySet() {
			return accessorMap.entrySet();
		}
	}

	/**
	 * アクセッサからビーンを間接参照するためのホルダ。
	 * (実際にビーンにアクセスするまでビーンの設定を遅延させるため。)
	 */
	public static class BeanHolder<T> {

		private T bean;

		public T getBean() {
			return bean;
		}

		public void setBean(T bean) {
			this.bean = bean;
		}
	}

	/**
	 * プロパティへのアクセッサ
	 */
	public interface PropertyAccessor {

		/**
		 * プロパティのタイプ
		 * @return
		 */
		Class<?> getPropertyType();

		/**
		 * ビーンからプロパティを取得する。
		 * @return
		 */
		Object getValue();

		/**
		 * ビーンのプロパティを設定する。
		 * @param value
		 */
		void setValue(Object value);

		/**
		 * プロパティがもつアノテーションを取得する。
		 * @param annotationClass
		 * @return アノテーション、なければnull
		 */
		<T extends Annotation> T getAnnotation(Class<T> annotationClass);
	}

	/**
	 * クラスを指定してプロパティアクセッサのマップを生成して返す。
	 * @param beanClass
	 * @return
	 * @throws IntrospectionException
	 */
	public static <T> PropertyAccessorMap<T> getPropertyAccessorMap(final Class<T> beanClass) {
		if (beanClass == null) {
			throw new NullPointerException("beanClass");
		}
		Map<String, PropertyAccessor> accessorMap = new TreeMap<String, PropertyAccessor>();
		final BeanHolder<T> beanHolder = new BeanHolder<T>();

		BeanInfo beanInfo;
		try {
			beanInfo = Introspector.getBeanInfo(beanClass);
		} catch (IntrospectionException ex) {
			throw new RuntimeException("bean intorospector failed. :" + beanClass, ex);
		}
		for (PropertyDescriptor propDesc : beanInfo
				.getPropertyDescriptors()) {
			String name = propDesc.getName();
			final Class<?> typ = propDesc.getPropertyType();
			final Method mtdReader = propDesc.getReadMethod();
			final Method mtdWriter = propDesc.getWriteMethod();

			if (mtdReader != null && mtdWriter != null) {
				// 読み書き双方が可能なもののみ対象とする.
				PropertyAccessor accessor = new PropertyAccessor() {
					private Object getBean() {
						Object bean = beanHolder.getBean();
						if (bean == null) {
							throw new IllegalStateException("bean not set.");
						}
						return bean;
					}

					@Override
					public Class<?> getPropertyType() {
						return typ;
					}

					@Override
					public Object getValue() {
						try {
							return mtdReader.invoke(getBean());
						} catch (RuntimeException ex) {
							throw ex;
						} catch (Exception ex) {
							if (ex.getCause() instanceof RuntimeException) {
								throw (RuntimeException) ex.getCause();
							}
							throw new RuntimeException(ex);
						}
					}

					@Override
					public void setValue(Object value) {
						try {
							mtdWriter.invoke(getBean(), value);
						} catch (RuntimeException ex) {
							throw ex;
						} catch (Exception ex) {
							if (ex.getCause() instanceof RuntimeException) {
								throw (RuntimeException) ex.getCause();
							}
							throw new RuntimeException(ex);
						}
					}

					@Override
					public <E extends Annotation> E getAnnotation(Class<E> annotationClass) {
						E annt = mtdReader.getAnnotation(annotationClass);
						if (annt == null) {
							annt = mtdWriter.getAnnotation(annotationClass);
						}
						return annt;
					}
				};
				accessorMap.put(name, accessor);
			}
		}
		return new PropertyAccessorMap<T>(accessorMap, beanHolder);
	}

	/**
	 * ビーンのSetter/Getterのペアをもつプロパティに対して、Propertiesより該当するプロパティの値を
	 * 読み取り、プロパティに設定します.<br>
	 * Propertiesに該当するプロパティ名が設定されていなければスキップされます.<br>
	 * Propertiesにビーンにないプロパティ名があった場合、それは単に無視されます.<br>
	 * Propertyの値が空文字の場合、Beanのプロパティの型が文字列以外であればnullが設定されます.<br>
	 * (文字列の場合、空文字のまま設定されます.書き込み時、nullは空文字にされるため、文字列についてはnullを表現することはできません。)<br>
	 * @param bean 設定されるビーン
	 * @param props プロパティソース
	 * @return 値の設定を拒否されたプロパティの名前、エラーがなければ空
	 */
	public static Set<String> loadFromProperties(Object bean, Properties props) {
		if (bean == null || props == null) {
			throw new IllegalArgumentException();
		}
		HashSet<String> rejectNames = new HashSet<String>();

		@SuppressWarnings("unchecked")
		PropertyAccessorMap<Object> accessorMap = (PropertyAccessorMap<Object>)getPropertyAccessorMap(bean.getClass());
		accessorMap.setBean(bean);

		for (Map.Entry<String, PropertyAccessor> accessorEntry : accessorMap.entrySet()) {
			String name = accessorEntry.getKey();
			PropertyAccessor accessor = accessorEntry.getValue();
			Class<?> typ = accessor.getPropertyType();
			// プロパティのStringConverterSpecアノテーションがあれば取得する
			StringConverterSpec anntStringConverter = accessor.getAnnotation(StringConverterSpec.class);

			String strVal = props.getProperty(name);
			if (strVal == null) {
				// 設定値がないのでスキップ
				continue;
			}


			Object val;
			Throwable reject = null;
			try {
				if (anntStringConverter != null) {
					Class<? extends StringConverter> convCls = anntStringConverter.value();
					StringConverter conv = convCls.getConstructor().newInstance();
					val = conv.valueOf(strVal);
				} else if (String.class.equals(typ)) {
					val = strVal;
				} else if (strVal.length() == 0) {
					val = null;
				} else {
					if (Boolean.class.equals(typ) || boolean.class.equals(typ)) {
						val = Boolean.valueOf(strVal);
					} else if (Integer.class.equals(typ) || int.class.equals(typ)) {
						val = Integer.valueOf(strVal);
					} else if (Long.class.equals(typ) || long.class.equals(typ)) {
						val = Long.valueOf(strVal);
					} else if (Float.class.equals(typ) || float.class.equals(typ)) {
						val = Float.valueOf(strVal);
					} else if (Double.class.equals(typ) || double.class.equals(typ)) {
						val = Double.valueOf(strVal);
					} else if (BigInteger.class.equals(typ)) {
						val = new BigInteger(strVal);
					} else if (BigDecimal.class.equals(typ)) {
						val = new BigDecimal(strVal);
					} else if (Color.class.equals(typ)) {
						long decode = Long.decode(strVal).longValue();
						if ((decode & 0xff000000) != 0) {
							// アルファの指定あり
							val = new Color((int) decode, true);
						} else {
							// アルファの指定なし
							// (仕組み上、アルファ0の設定値は受け取れないが実用性に問題ないと思われる。)
							val = new Color((int) decode, false);
						}
					} else {
						rejectNames.add(name);
						logger.log(Level.WARNING,
							"unsupported propery type: " + typ
							+ "/beanClass="	+ bean.getClass() + " #" + name);
						continue;
					}
				}
				accessor.setValue(val);
				reject = null;

			} catch (Exception ex) {
				reject = ex;
			}

			if (reject != null) {
				rejectNames.add(name);
				logger.log(Level.WARNING, "invalid propery: "
						+ typ + "/beanClass="
						+ bean.getClass() + " #" + name + " /val=" + strVal
						, reject);
			}
		}
		return rejectNames;
	}

	/**
	 * ビーンのSetter/Getterのペアをもつプロパティの各値をPropertiesに文字列として登録します.<br>
	 * nullの場合は空文字が設定されます.<br>
	 * @param bean プロパティに転送する元情報となるビーン
	 * @param props 設定されるプロパティ
	 */
	public static void saveToProperties(Object bean, Properties props) {
		if (bean == null || props == null) {
			throw new IllegalArgumentException();
		}

		try {
			@SuppressWarnings("unchecked")
			PropertyAccessorMap<Object> accessorMap = (PropertyAccessorMap<Object>) getPropertyAccessorMap(
					bean.getClass());
			accessorMap.setBean(bean);

			for (Map.Entry<String, PropertyAccessor> accessorEntry : accessorMap.entrySet()) {
				String name = accessorEntry.getKey();
				PropertyAccessor accessor = accessorEntry.getValue();

				// プロパティのStringConverterSpecアノテーションがあれば取得する
				StringConverterSpec anntStringConverter = accessor.getAnnotation(StringConverterSpec.class);

				Object val = accessor.getValue();

				String strVal;
				if (anntStringConverter != null) {
					StringConverter conv = anntStringConverter.value().getConstructor().newInstance();
					strVal = conv.toString(val);

				} else if (val == null) {
					strVal = "";
				} else if (val instanceof String) {
					strVal = (String) val;
				} else if (val instanceof Number) {
					strVal = ((Number) val).toString();
				} else if (val instanceof Boolean) {
					strVal = ((Boolean) val).booleanValue() ? "true" : "false";
				} else if (val instanceof Color) {
					Color color = (Color) val;
					int alpha = color.getAlpha();
					if (alpha == 255) {
						strVal = "#" + Integer.toHexString(color.getRGB() & 0xffffff);
					} else {
						strVal = "#" + Long.toHexString(color.getRGB() & 0xffffffffL);
					}
				} else {
					logger.log(Level.WARNING, "unsupported propery type: "
							+ val.getClass() + "/beanClass="
							+ bean.getClass() + " #" + name);
					continue;
				}

				props.setProperty(name, strVal);
			}

		} catch (Exception ex) {
			throw new RuntimeException("bean property read failed. :" + bean.getClass(), ex);
		}
	}
}
