/*
 * Copyright 2004-2015 the Seasar Foundation and the Others.
 *
 * 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 org.seasar.framework.beans.util;

import java.sql.Time;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;

import org.seasar.framework.beans.BeanDesc;
import org.seasar.framework.beans.Converter;
import org.seasar.framework.beans.ConverterRuntimeException;
import org.seasar.framework.beans.PropertyDesc;
import org.seasar.framework.beans.converter.DateConverter;
import org.seasar.framework.beans.converter.NumberConverter;
import org.seasar.framework.beans.converter.SqlDateConverter;
import org.seasar.framework.beans.converter.TimeConverter;
import org.seasar.framework.beans.converter.TimestampConverter;
import org.seasar.framework.beans.factory.BeanDescFactory;
import org.seasar.framework.util.DateConversionUtil;
import org.seasar.framework.util.TimeConversionUtil;
import org.seasar.framework.util.TimestampConversionUtil;

/**
 * JavaBeansやMapに対して操作を行う抽象クラスです。
 * 
 * @author higa
 * @param <S>
 *            JavaBeansに対して操作を行うサブタイプです。
 * 
 */
public abstract class AbstractCopy<S extends AbstractCopy<S>> {

    /**
     * 空文字列の配列です。
     */
    protected static final String[] EMPTY_STRING_ARRAY = new String[0];

    /**
     * 日付用のデフォルトコンバータです。
     */
    protected static final Converter DEFAULT_DATE_CONVERTER = new DateConverter(
            DateConversionUtil.getY4Pattern(Locale.getDefault()));

    /**
     * 日時用のデフォルトコンバータです。
     */
    protected static final Converter DEFAULT_TIMESTAMP_CONVERTER = new DateConverter(
            TimestampConversionUtil.getPattern(Locale.getDefault()));

    /**
     * 時間用のデフォルトコンバータです。
     */
    protected static final Converter DEFAULT_TIME_CONVERTER = new DateConverter(
            TimeConversionUtil.getPattern(Locale.getDefault()));

    /**
     * 操作の対象に含めるプロパティ名の配列です。
     */
    protected String[] includePropertyNames = EMPTY_STRING_ARRAY;

    /**
     * 操作の対象に含めないプロパティ名の配列です。
     */
    protected String[] excludePropertyNames = EMPTY_STRING_ARRAY;

    /**
     * null値のプロパティを操作の対象外にするかどうかです。
     */
    protected boolean excludesNull = false;

    /**
     * 空白を操作の対象外にするかどうかです。
     */
    protected boolean excludesWhitespace = false;

    /**
     * プレフィックスです。
     */
    protected String prefix;

    /**
     * JavaBeanのデリミタです。
     */
    protected char beanDelimiter = '$';

    /**
     * Mapのデリミタです。
     */
    protected char mapDelimiter = '.';

    /**
     * 特定のプロパティに関連付けられたコンバータです。
     */
    protected Map<String, Converter> converterMap = new HashMap<String, Converter>();

    /**
     * 特定のプロパティに関連付けられていないコンバータです。
     */
    protected List<Converter> converters = new ArrayList<Converter>();

    /**
     * CharSequenceの配列をStringの配列に変換します。
     * 
     * @param charSequenceArray
     *            CharSequenceの配列
     * @return Stringの配列
     */
    protected String[] toStringArray(CharSequence[] charSequenceArray) {
        int length = charSequenceArray.length;
        String[] stringArray = new String[length];
        for (int index = 0; index < length; index++) {
            stringArray[index] = charSequenceArray[index].toString();
        }
        return stringArray;
    }

    /**
     * 操作の対象に含めるプロパティ名を指定します。
     * 
     * @param propertyNames
     *            プロパティ名の配列
     * @return このインスタンス自身
     */
    @SuppressWarnings("unchecked")
    public S includes(CharSequence... propertyNames) {
        this.includePropertyNames = toStringArray(propertyNames);
        return (S) this;
    }

    /**
     * 操作の対象に含めないプロパティ名を指定します。
     * 
     * @param propertyNames
     *            プロパティ名の配列
     * @return このインスタンス自身
     */
    @SuppressWarnings("unchecked")
    public S excludes(CharSequence... propertyNames) {
        this.excludePropertyNames = toStringArray(propertyNames);
        return (S) this;
    }

    /**
     * null値のプロパティを操作の対象外にします。
     * 
     * @return このインスタンス自身
     */
    @SuppressWarnings("unchecked")
    public S excludesNull() {
        this.excludesNull = true;
        return (S) this;
    }

    /**
     * 空白のプロパティを操作の対象外にします。
     * 
     * @return このインスタンス自身
     */
    @SuppressWarnings("unchecked")
    public S excludesWhitespace() {
        this.excludesWhitespace = true;
        return (S) this;
    }

    /**
     * プレフィックスを指定します。
     * 
     * @param prefix
     *            プレフィックス
     * @return このインスタンス自身
     * 
     */
    @SuppressWarnings("unchecked")
    public S prefix(CharSequence prefix) {
        this.prefix = prefix.toString();
        return (S) this;
    }

    /**
     * JavaBeansのデリミタを設定します。
     * 
     * @param beanDelimiter
     *            JavaBeansのデリミタ
     * @return このインスタンス自身
     */
    @SuppressWarnings("unchecked")
    public S beanDelimiter(char beanDelimiter) {
        this.beanDelimiter = beanDelimiter;
        return (S) this;
    }

    /**
     * Mapのデリミタを設定します。
     * 
     * @param mapDelimiter
     *            Mapのデリミタ
     * @return このインスタンス自身
     */
    @SuppressWarnings("unchecked")
    public S mapDelimiter(char mapDelimiter) {
        this.mapDelimiter = mapDelimiter;
        return (S) this;
    }

    /**
     * コンバータを設定します。
     * 
     * @param converter
     * @param propertyNames
     * @return このインスタンス自身
     */
    @SuppressWarnings("unchecked")
    public S converter(Converter converter, CharSequence... propertyNames) {
        if (propertyNames.length == 0) {
            converters.add(converter);
        } else {
            for (CharSequence name : propertyNames) {
                converterMap.put(name.toString(), converter);
            }
        }
        return (S) this;
    }

    /**
     * 日付のコンバータを設定します。
     * 
     * @param pattern
     *            日付のパターン
     * @param propertyNames
     *            プロパティ名の配列
     * @return このインスタンス自身
     */
    public S dateConverter(String pattern, CharSequence... propertyNames) {
        return converter(new DateConverter(pattern), propertyNames);
    }

    /**
     * SQL用日付のコンバータを設定します。
     * 
     * @param pattern
     *            日付のパターン
     * @param propertyNames
     *            プロパティ名の配列
     * @return このインスタンス自身
     */
    public S sqlDateConverter(String pattern, CharSequence... propertyNames) {
        return converter(new SqlDateConverter(pattern), propertyNames);
    }

    /**
     * 時間のコンバータを設定します。
     * 
     * @param pattern
     *            時間のパターン
     * @param propertyNames
     *            プロパティ名の配列
     * @return このインスタンス自身
     */
    public S timeConverter(String pattern, CharSequence... propertyNames) {
        return converter(new TimeConverter(pattern), propertyNames);
    }

    /**
     * 日時のコンバータを設定します。
     * 
     * @param pattern
     *            日時のパターン
     * @param propertyNames
     *            プロパティ名の配列
     * @return このインスタンス自身
     */
    public S timestampConverter(String pattern, CharSequence... propertyNames) {
        return converter(new TimestampConverter(pattern), propertyNames);
    }

    /**
     * 数値のコンバータを設定します。
     * 
     * @param pattern
     *            数値のパターン
     * @param propertyNames
     *            プロパティ名の配列
     * @return このインスタンス自身
     */
    public S numberConverter(String pattern, CharSequence... propertyNames) {
        return converter(new NumberConverter(pattern), propertyNames);
    }

    /**
     * 対象のプロパティかどうかを返します。
     * 
     * @param name
     *            プロパティ名
     * @return 対象のプロパティかどうか
     */
    protected boolean isTargetProperty(String name) {
        if (prefix != null && !name.startsWith(prefix)) {
            return false;
        }
        if (includePropertyNames.length > 0) {
            for (String s : includePropertyNames) {
                if (s.equals(name)) {
                    for (String s2 : excludePropertyNames) {
                        if (s2.equals(name)) {
                            return false;
                        }
                    }
                    return true;
                }
            }
            return false;
        }
        if (excludePropertyNames.length > 0) {
            for (String s : excludePropertyNames) {
                if (s.equals(name)) {
                    return false;
                }
            }
            return true;
        }
        return true;
    }

    /**
     * BeanからBeanにコピーを行います。
     * 
     * @param src
     *            コピー元
     * @param dest
     *            コピー先
     */
    protected void copyBeanToBean(Object src, Object dest) {
        BeanDesc srcBeanDesc = BeanDescFactory.getBeanDesc(src.getClass());
        BeanDesc destBeanDesc = BeanDescFactory.getBeanDesc(dest.getClass());
        int size = srcBeanDesc.getPropertyDescSize();
        for (int i = 0; i < size; i++) {
            PropertyDesc srcPropertyDesc = srcBeanDesc.getPropertyDesc(i);
            String srcPropertyName = srcPropertyDesc.getPropertyName();
            if (!srcPropertyDesc.isReadable()
                    || !isTargetProperty(srcPropertyName)) {
                continue;
            }
            String destPropertyName = trimPrefix(srcPropertyName);
            if (!destBeanDesc.hasPropertyDesc(destPropertyName)) {
                continue;
            }
            PropertyDesc destPropertyDesc = destBeanDesc
                    .getPropertyDesc(destPropertyName);
            if (!destPropertyDesc.isWritable()) {
                continue;
            }
            Object value = srcPropertyDesc.getValue(src);
            if (value instanceof String && excludesWhitespace
                    && ((String) value).trim().length() == 0) {
                continue;
            }
            if (value == null && excludesNull) {
                continue;
            }
            value = convertValue(value, destPropertyName, destPropertyDesc
                    .getPropertyType());
            destPropertyDesc.setValue(dest, value);
        }
    }

    /**
     * BeanからMapにコピーを行います。
     * 
     * @param src
     *            コピー元
     * @param dest
     *            コピー先
     */
    @SuppressWarnings("unchecked")
    protected void copyBeanToMap(Object src, Map dest) {
        BeanDesc srcBeanDesc = BeanDescFactory.getBeanDesc(src.getClass());
        int size = srcBeanDesc.getPropertyDescSize();
        for (int i = 0; i < size; i++) {
            PropertyDesc srcPropertyDesc = srcBeanDesc.getPropertyDesc(i);
            String srcPropertyName = srcPropertyDesc.getPropertyName();
            if (!srcPropertyDesc.isReadable()
                    || !isTargetProperty(srcPropertyName)) {
                continue;
            }
            Object value = srcPropertyDesc.getValue(src);
            if (value instanceof String && excludesWhitespace
                    && ((String) value).trim().length() == 0) {
                continue;
            }
            if (value == null && excludesNull) {
                continue;
            }
            String destPropertyName = trimPrefix(srcPropertyName.replace(
                    beanDelimiter, mapDelimiter));
            value = convertValue(value, destPropertyName, null);
            dest.put(destPropertyName, value);
        }
    }

    /**
     * MapからBeanにコピーを行います。
     * 
     * @param src
     *            コピー元
     * @param dest
     *            コピー先
     */
    protected void copyMapToBean(Map<String, Object> src, Object dest) {
        BeanDesc destBeanDesc = BeanDescFactory.getBeanDesc(dest.getClass());
        for (Iterator<String> i = src.keySet().iterator(); i.hasNext();) {
            String srcPropertyName = i.next();
            if (!isTargetProperty(srcPropertyName)) {
                continue;
            }
            String destPropertyName = trimPrefix(srcPropertyName.replace(
                    mapDelimiter, beanDelimiter));
            if (!destBeanDesc.hasPropertyDesc(destPropertyName)) {
                continue;
            }
            PropertyDesc destPropertyDesc = destBeanDesc
                    .getPropertyDesc(destPropertyName);
            if (!destPropertyDesc.isWritable()) {
                continue;
            }
            Object value = src.get(srcPropertyName);
            if (value instanceof String && excludesWhitespace
                    && ((String) value).trim().length() == 0) {
                continue;
            }
            if (value == null && excludesNull) {
                continue;
            }
            value = convertValue(value, destPropertyName, destPropertyDesc
                    .getPropertyType());
            destPropertyDesc.setValue(dest, value);
        }
    }

    /**
     * MapからMapにコピーを行います。
     * 
     * @param src
     *            コピー元
     * @param dest
     *            コピー先
     */
    protected void copyMapToMap(Map<String, Object> src,
            Map<String, Object> dest) {
        for (Iterator<String> i = src.keySet().iterator(); i.hasNext();) {
            String srcPropertyName = i.next();
            if (!isTargetProperty(srcPropertyName)) {
                continue;
            }
            String destPropertyName = trimPrefix(srcPropertyName);
            Object value = src.get(srcPropertyName);
            if (value instanceof String && excludesWhitespace
                    && ((String) value).trim().length() == 0) {
                continue;
            }
            if (value == null && excludesNull) {
                continue;
            }
            value = convertValue(value, destPropertyName, null);
            dest.put(destPropertyName, value);
        }
    }

    /**
     * プレフィックスを削ります。
     * 
     * @param propertyName
     *            プロパティ名
     * @return 削った結果
     */
    protected String trimPrefix(String propertyName) {
        if (prefix == null) {
            return propertyName;
        }
        return propertyName.substring(prefix.length());
    }

    /**
     * 値を変換します。
     * 
     * @param value
     *            値
     * @param destPropertyName
     *            コピー先のプロパティ名
     * @param destPropertyClass
     *            コピー先のプロパティクラス
     * @return 変換後の値
     */
    protected Object convertValue(Object value, String destPropertyName,
            Class<?> destPropertyClass) {
        if (value == null || value.getClass() != String.class
                && destPropertyClass != null
                && destPropertyClass != String.class) {
            return value;
        }
        Converter converter = converterMap.get(destPropertyName);
        if (converter == null) {
            Class<?> targetClass = value.getClass() != String.class ? value
                    .getClass() : destPropertyClass;
            if (targetClass == null) {
                return value;
            }
            for (Class<?> clazz = targetClass; clazz != Object.class
                    && clazz != null; clazz = clazz.getSuperclass()) {
                converter = findConverter(clazz);
                if (converter != null) {
                    break;
                }
            }
            if (converter == null && destPropertyClass != null) {
                converter = findDefaultConverter(targetClass);
            }
            if (converter == null) {
                return value;
            }
        }
        try {
            if (value.getClass() == String.class) {
                return converter.getAsObject((String) value);
            }
            return converter.getAsString(value);
        } catch (Throwable cause) {
            throw new ConverterRuntimeException(destPropertyName, value, cause);
        }
    }

    /**
     * クラスに対応するコンバータを探します。
     * 
     * @param clazz
     *            クラス
     * @return コンバータ
     */
    protected Converter findConverter(Class<?> clazz) {
        for (Converter c : converters) {
            if (c.isTarget(clazz)) {
                return c;
            }
        }
        return null;
    }

    /**
     * クラスに対応するデフォルトのコンバータを探します。
     * 
     * @param clazz
     *            クラス
     * @return コンバータ
     */
    protected Converter findDefaultConverter(Class<?> clazz) {
        if (clazz == java.sql.Date.class) {
            return DEFAULT_DATE_CONVERTER;
        }
        if (clazz == Time.class) {
            return DEFAULT_TIME_CONVERTER;
        }
        if (java.util.Date.class.isAssignableFrom(clazz)) {
            return DEFAULT_TIMESTAMP_CONVERTER;
        }
        return null;
    }
}