package sharin.sql.runner.processor;

import java.math.BigDecimal;
import java.sql.Date;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.Time;
import java.sql.Timestamp;
import java.util.HashMap;
import java.util.Map;

import sharin.sql.runner.ColumnConverter;
import sharin.sql.runner.ResultSetProcessor;
import sharin.sql.runner.converter.BigDecimalColumnConverter;
import sharin.sql.runner.converter.BooleanColumnConverter;
import sharin.sql.runner.converter.ByteColumnConverter;
import sharin.sql.runner.converter.BytesColumnConverter;
import sharin.sql.runner.converter.DateColumnConverter;
import sharin.sql.runner.converter.DoubleColumnConverter;
import sharin.sql.runner.converter.FloatColumnConverter;
import sharin.sql.runner.converter.IntegerColumnConverter;
import sharin.sql.runner.converter.LongColumnConverter;
import sharin.sql.runner.converter.ObjectColumnConverter;
import sharin.sql.runner.converter.PrimitiveBooleanColumnConverter;
import sharin.sql.runner.converter.PrimitiveByteColumnConverter;
import sharin.sql.runner.converter.PrimitiveDoubleColumnConverter;
import sharin.sql.runner.converter.PrimitiveFloatColumnConverter;
import sharin.sql.runner.converter.PrimitiveIntColumnConverter;
import sharin.sql.runner.converter.PrimitiveLongColumnConverter;
import sharin.sql.runner.converter.PrimitiveShortColumnConverter;
import sharin.sql.runner.converter.ShortColumnConverter;
import sharin.sql.runner.converter.StringColumnConverter;
import sharin.sql.runner.converter.TimeColumnConverter;
import sharin.sql.runner.converter.TimestampColumnConverter;
import sharin.util.PropertyUtils;
import sharin.util.ReflectUtils;
import sharin.util.SqlUtils;

public class BeanResultSetProcessor implements ResultSetProcessor {

    private static final Map<Class<?>, ColumnConverter> defaultColumnConverterMap;

    static {
        Map<Class<?>, ColumnConverter> map = new HashMap<Class<?>, ColumnConverter>();

        map.put(boolean.class, new PrimitiveBooleanColumnConverter());
        map.put(byte.class, new PrimitiveByteColumnConverter());
        map.put(double.class, new PrimitiveDoubleColumnConverter());
        map.put(float.class, new PrimitiveFloatColumnConverter());
        map.put(int.class, new PrimitiveIntColumnConverter());
        map.put(long.class, new PrimitiveLongColumnConverter());
        map.put(short.class, new PrimitiveShortColumnConverter());

        map.put(BigDecimal.class, new BigDecimalColumnConverter());
        map.put(Boolean.class, new BooleanColumnConverter());
        map.put(Byte.class, new ByteColumnConverter());
        map.put(byte[].class, new BytesColumnConverter());
        map.put(Date.class, new DateColumnConverter());
        map.put(Double.class, new DoubleColumnConverter());
        map.put(Float.class, new FloatColumnConverter());
        map.put(Integer.class, new IntegerColumnConverter());
        map.put(Long.class, new LongColumnConverter());
        map.put(Object.class, new ObjectColumnConverter());
        map.put(Short.class, new ShortColumnConverter());
        map.put(String.class, new StringColumnConverter());
        map.put(Time.class, new TimeColumnConverter());
        map.put(Timestamp.class, new TimestampColumnConverter());

        defaultColumnConverterMap = map;
    }

    private final Class<?> beanClass;

    private final Map<Class<?>, ColumnConverter> columnConverterMap;

    private final ColumnConverter objectColumnConverter;

    public BeanResultSetProcessor(Class<?> beanClass) {
        this(beanClass, null);
    }

    public BeanResultSetProcessor(Class<?> beanClass,
            Map<Class<?>, ColumnConverter> columnConverterMap) {

        this.beanClass = beanClass;

        Map<Class<?>, ColumnConverter> map = new HashMap<Class<?>, ColumnConverter>();
        map.putAll(defaultColumnConverterMap);

        if (columnConverterMap != null) {
            map.putAll(columnConverterMap);
        }

        this.columnConverterMap = map;
        this.objectColumnConverter = map.get(Object.class);
    }

    public Object prepare(ResultSet rs) {
        ResultSetMetaData metaData = SqlUtils.getMetaData(rs);
        int columnCount = SqlUtils.getColumnCount(metaData);
        String[] columnLabels = new String[columnCount];
        ColumnConverter[] columnConverters = new ColumnConverter[columnCount];

        for (int i = 0; i < columnCount; i++) {
            columnLabels[i] = SqlUtils.getColumnLabel(metaData, i + 1);
            Class<?> type = PropertyUtils.getNestedPropertyType(beanClass,
                    columnLabels[i]);

            if (type != null) {
                columnConverters[i] = columnConverterMap.get(type);
            }

            if (columnConverters[i] == null) {
                columnConverters[i] = objectColumnConverter;
            }
        }

        Map<String, Object> mapContext = new HashMap<String, Object>();
        mapContext.put("columnLabels", columnLabels);
        mapContext.put("columnConverters", columnConverters);
        return mapContext;
    }

    @SuppressWarnings("unchecked")
    public Object process(ResultSet rs, Object context) {
        Map<String, Object> mapContext = (Map<String, Object>) context;
        String[] columnLabels = (String[]) mapContext.get("columnLabels");
        ColumnConverter[] columnConverters = (ColumnConverter[]) mapContext
                .get("columnConverters");
        Object result = ReflectUtils.newInstance(beanClass);

        for (int i = 0; i < columnLabels.length; i++) {
            Object value = columnConverters[i].convert(rs, i + 1);
            PropertyUtils
                    .setNestedPropertyValue(result, columnLabels[i], value);
        }

        return result;
    }
}
