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

import java.math.BigDecimal;
import java.math.BigInteger;
import java.text.DateFormat;
import java.text.DecimalFormat;
import java.text.NumberFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Calendar;

import woolpack.convert.ConvertUtils;
import woolpack.fn.Fn;
import woolpack.fn.FnUtils;
import woolpack.utils.Utils;

/**
 * 型変換のユーティリティです。
 * 型推論で表記を簡略するためのスタティックメソッドと変数を含みます。
 * 
 * @author nakamura
 * 
 */
public final class TypeConvertUtils {
	/**
	 * {@link ConvertContext#getToType()}を返す関数です。
	 */
	public static final Fn<ConvertContext, Class, RuntimeException> GET_TO_TYPE = new Fn<ConvertContext, Class, RuntimeException>() {
		public Class exec(final ConvertContext c) {
			return c.getToType();
		}
	};

	/**
	 * {@link ConvertContext#getValue()}の{@link Object#getClass()}を返す関数です。
	 */
	public static final Fn<ConvertContext, Class, RuntimeException> GET_FROM_TYPE = new Fn<ConvertContext, Class, RuntimeException>() {
		public Class exec(final ConvertContext c) {
			return c.getValue().getClass();
		}
	};
	
	private static final NumberFormat NUMBER_FORMAT = new DecimalFormat();
	private static final DateFormat DATE_FORMAT = new SimpleDateFormat("yyyyMMdd");

	/**
	 * デフォルト単純型変換の関数です。
	 */
	public static final Fn<ConvertContext, Void, RuntimeException> SIMPLE_CONVERTER = getSimpleConverter(NUMBER_FORMAT, DATE_FORMAT, FnUtils.<ConvertContext, Void>fix(null));

	/**
	 * デフォルトの一覧型変換の関数です。
	 */
	public static final Fn<ConvertContext, Void, RuntimeException> COLLECTION_CONVERTER = getCollectionConverter(NUMBER_FORMAT, DATE_FORMAT);
	
	/**
	 * {@link ConvertContext#getPropertyName()}を返す関数です。
	 */
	public static final Fn<ConvertContext, String, RuntimeException> GET_PROPERTY_NAME = new PropertyNameGetter();
	
	private TypeConvertUtils() {
	}
	
	private static Fn<ConvertContext, Void, RuntimeException> getSimpleConverterPrivate(
			final NumberFormat numberFormat,
			final DateFormat dateFormat,
			final Fn<ConvertContext, Void, RuntimeException> successorFn) {
		final Fn<Object, java.util.Date, RuntimeException> dateParser = new Fn<Object, java.util.Date, RuntimeException>() {
			public java.util.Date exec(final Object c) {
				if (c instanceof java.util.Date) {
					return (java.util.Date) c;
				} else if (c instanceof Calendar) {
					return new java.util.Date(((Calendar) c).getTimeInMillis());
				} else if (c instanceof Long) {
					return new java.util.Date((Long) c);
				} else {
					try {
						return ((DateFormat) dateFormat.clone()).parse(c.toString());
					} catch (final ParseException e) {
						throw new IllegalArgumentException(e);
					}
				}
			}
		};
		final Fn<Object, Number, RuntimeException> numberParser = new Fn<Object, Number, RuntimeException>() {
			public Number exec(final Object c) {
				if (Number.class.isAssignableFrom(c.getClass())) {
					return (Number) c;
				} else if (c instanceof Boolean) {
					return ((Boolean) c) ? 1 : 0;
				} else {
					try {
						return ((NumberFormat) numberFormat.clone()).parse(c.toString());
					} catch (final ParseException e) {
						throw new IllegalArgumentException(e);
					}
				}
			}
		};
		return FnUtils.exec(FnUtils.join(GET_TO_TYPE, FnUtils.switching(Utils.<Class, Fn<ConvertContext, Void, RuntimeException>>
			map(String.class, new Fn<ConvertContext, Void, RuntimeException>() {
				private final Fn<ConvertContext, Void, RuntimeException> dateConverter = new Converter(ConvertUtils.format(ConvertUtils.formatFactory(dateFormat)));
				private final Fn<ConvertContext, Void, RuntimeException> numberConverter = new Converter(ConvertUtils.format(ConvertUtils.formatFactory(numberFormat)));
				private final Fn<ConvertContext, Void, RuntimeException> defaultConverter = new Converter(ConvertUtils.TO_STRING);
				public Void exec(final ConvertContext c) {
					if (c.getValue() instanceof java.util.Date) {
						dateConverter.exec(c);
					} else if (c.getValue() instanceof Number) {
						numberConverter.exec(c);
					} else {
						defaultConverter.exec(c);
					}
					return null;
				}
			})
			.map(Boolean.class, new Converter(new Fn<Object, Boolean, RuntimeException>() {
				public Boolean exec(final Object c) {
					return c != null && !"false".equals(c) && !Boolean.FALSE.equals(c);
				}
			}))
			.map(Character.class, new Converter(new Fn<Object, Character, RuntimeException>() {
				public Character exec(final Object c) {
					final String s = c.toString();
					if (s.length() == 0) {
						throw new IllegalArgumentException();
					}
					return s.charAt(0);
				}
			}))
			.map(java.util.Date.class, new Converter(dateParser))
			.map(Calendar.class, new Converter(FnUtils.join(dateParser, ConvertUtils.TO_CALENDAR)))
			.map(java.sql.Date.class, new Converter(FnUtils.join(dateParser, ConvertUtils.TO_SQL_DATE)))
			.map(java.sql.Time.class, new Converter(FnUtils.join(dateParser, ConvertUtils.TO_TIME)))
			.map(java.sql.Timestamp.class, new Converter(FnUtils.join(dateParser, ConvertUtils.TO_TIMESTAMP)))
			.map(Byte.class, new Converter(FnUtils.join(numberParser, ConvertUtils.TO_BYTE)))
			.map(Short.class, new Converter(FnUtils.join(numberParser, ConvertUtils.TO_SHORT)))
			.map(Integer.class, new Converter(FnUtils.join(numberParser, ConvertUtils.TO_INTEGER)))
			.map(Long.class, new Converter(new Fn<Object, Long, RuntimeException>() {
				private final Fn<Object, Long, RuntimeException> converter = FnUtils.join(numberParser, ConvertUtils.TO_LONG);
				public Long exec(final Object c) {
					if (c instanceof java.util.Date) {
						return ((java.util.Date) c).getTime();
					} else if (c instanceof Calendar) {
						return ((Calendar) c).getTimeInMillis();
					} else {
						return converter.exec(c);
					}
				}
			}))
			.map(Float.class, new Converter(FnUtils.join(numberParser, ConvertUtils.TO_FLOAT)))
			.map(Double.class, new Converter(FnUtils.join(numberParser, ConvertUtils.TO_DOUBLE)))
			.map(BigInteger.class, new Converter(FnUtils.join(numberParser, ConvertUtils.TO_BIG_INTEGER)))
			.map(BigDecimal.class, new Converter(FnUtils.join(numberParser, ConvertUtils.TO_BIG_DECIMAL)))
			, successorFn)));
	}

	/**
	 * 単純型変換の関数を生成します。
	 * @param numberFormat 数値フォーマット。
	 * @param dateFormat 日付フォーマット。
	 * @param successorFn この変換器で変換できない場合の委譲先。
	 * @return 関数。
	 */
	public static Fn<ConvertContext, Void, RuntimeException> getSimpleConverter(
			final NumberFormat numberFormat,
			final DateFormat dateFormat,
			final Fn<ConvertContext, Void, RuntimeException> successorFn) {
		return
		new ToPrimitiveConverter(
		new DelegationIfNecessityConverter(
		getSimpleConverterPrivate(numberFormat, dateFormat, successorFn)));
	}

	/**
	 * 一覧型変換の関数を生成します。
	 * @param numberFormat 数値フォーマット。
	 * @param dateFormat 日付フォーマット。
	 * @return 関数。
	 */
	public static Fn<ConvertContext, Void, RuntimeException> getCollectionConverter(
			final NumberFormat numberFormat,
			final DateFormat dateFormat) {
		return 
		new SettingFnConverter(
		new ToPrimitiveConverter(
		new RuntimeExceptionToNullConverter(
		new DelegationIfNecessityConverter(
		new ToArrayConverter(
		new ToCollectionViewConverter(
		new ToCollectionDecompositionConverter(
		new ToMapViewConverter(
		getSimpleConverterPrivate(
				numberFormat,
				dateFormat,
				new ToBeanConverter())))))))));
	}
	
	/**
	 * BeanをMapに変換する関数を生成します。
	 * @return 関数。
	 */
	public static Fn<Object, BeanMap, RuntimeException> toMap() {
		return new Fn<Object, BeanMap, RuntimeException>() {
			public BeanMap exec(final Object c) {
				return new BeanMap(c);
			}
		};
	}
	
	/**
	 * BeanをMapに変換する関数を生成します。
	 * @param fn 型を変換する関数。
	 * @return 関数。
	 */
	public static Fn<Object, BeanMap, RuntimeException> toMap(
			final Fn<ConvertContext, Void, RuntimeException> fn) {
		return new Fn<Object, BeanMap, RuntimeException>() {
			public BeanMap exec(final Object c) {
				return new BeanMap(c, fn);
			}
		};
	}
}
