package com.yihu.hos.web.framework.util.sql;

import org.hibernate.transform.ResultTransformer;

import javax.persistence.Column;
import java.beans.BeanInfo;
import java.beans.IntrospectionException;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * @created Airhead 2015/12/26.
 */
public class BeanTransformer<T> implements ResultTransformer {
    private final Class<T> mapClass;
    public Map<String, String> columnToProperty;
    public Map<String, String> propertyToColumn;
    public BeanTransformer(Class<T> mapClass) {
        this.mapClass = mapClass;
        this.initColumnsToProperties();
        this.initPropertiesToColumns();
    }

    public Object transformTuple(Object[] tuple, String[] aliases) {
        try {
            PropertyDescriptor[] props = this.propertyDescriptors();
            int[] col2Pro = this.mapColumnsToProperties(aliases, props);

            return this.createBean(tuple, mapClass, props, col2Pro);

        } catch (Exception e) {
            e.printStackTrace();
        }

        return null;
    }

    @Override
    public List transformList(List list) {
        return list;
    }

    protected T newInstance(Class<T> c) throws Exception {
        try {
            return c.newInstance();
        } catch (InstantiationException | IllegalAccessException e) {
            throw new Exception("Cannot create " + c.getName() + ": " + e.getMessage());
        }
    }


    protected int[] mapColumnsToProperties(String[] aliases, PropertyDescriptor[] props) throws SQLException {
        int[] col2Pro = new int[aliases.length];
        Arrays.fill(col2Pro, -1);

        for (int col = 0; col < aliases.length; ++col) {
            String columnName = aliases[col];
            String propertyName = this.columnToProperty.get(columnName.toUpperCase());
            for (int i = 0; i < props.length; ++i) {
                if (propertyName.equals(props[i].getName())) {
                    col2Pro[col] = i;
                    break;
                }
            }
        }

        return col2Pro;
    }


    private T createBean(Object[] tuple, Class<T> type, PropertyDescriptor[] props, int[] col2Pro) throws Exception {
        T bean = this.newInstance(type);

        for (int i = 0; i < col2Pro.length; ++i) {
            int realIndex = col2Pro[i];
            if (realIndex != -1) {
                Object value = tuple[i];
                PropertyDescriptor prop = props[realIndex];
                this.callSetter(bean, prop, value);
            }
        }

        return bean;
    }

    private void callSetter(Object target, PropertyDescriptor prop, Object value) throws Exception {
        Method setter = prop.getWriteMethod();

        if (setter == null) {
            return;
        }

        Class<?>[] params = setter.getParameterTypes();
        try {
            // convert types for some popular ones
            if (value instanceof java.util.Date) {
                final String targetType = params[0].getName();
                if ("java.sql.Date".equals(targetType)) {
                    value = new java.sql.Date(((java.util.Date) value).getTime());
                } else if ("java.sql.Time".equals(targetType)) {
                    value = new java.sql.Time(((java.util.Date) value).getTime());
                } else if ("java.sql.Timestamp".equals(targetType)) {
                    Timestamp tsValue = (Timestamp) value;
                    int nanos = tsValue.getNanos();
                    value = new Timestamp(tsValue.getTime());
                    ((Timestamp) value).setNanos(nanos);
                }
            } else {
//                if (value instanceof String && params[0].isEnum()) {
//                    value = Enum.valueOf(params[0].asSubclass(Enum.class), (String) value);
//                }
            }


            // Don't call setter if the value object isn't the right type
            if (this.isCompatibleType(value, params[0])) {
                setter.invoke(target, new Object[]{value});
            } else {
                throw new Exception(
                        "Cannot set " + prop.getName() + ": incompatible types, cannot convert "
                                + value.getClass().getName() + " to " + params[0].getName());
                // value cannot be null here because isCompatibleType allows null
            }

        } catch (IllegalArgumentException | IllegalAccessException | InvocationTargetException e) {
            throw new SQLException(
                    "Cannot set " + prop.getName() + ": " + e.getMessage());

        }
    }

    private void initColumnsToProperties() {
        columnToProperty = new HashMap<>();
        Field[] fields = mapClass.getDeclaredFields();
        for (Field f : fields) {
            Column column = f.getAnnotation(Column.class);
            if (column == null) {
                continue;
            }

            String columnName = column.name();
            String propertyName = f.getName();
            columnToProperty.put(columnName.toUpperCase(), propertyName);
        }
    }

    private void initPropertiesToColumns() {
        propertyToColumn = new HashMap<>();
        Field[] fields = mapClass.getDeclaredFields();
        for (Field f : fields) {
            Column column = f.getAnnotation(Column.class);
            if (column == null) {
                continue;
            }

            String columnName = column.name();
            String propertyName = f.getName();
            propertyToColumn.put(propertyName,columnName.toUpperCase());
        }
    }

    private PropertyDescriptor[] propertyDescriptors() throws Exception {
        BeanInfo beanInfo = null;

        try {
            beanInfo = Introspector.getBeanInfo(mapClass);
        } catch (IntrospectionException e) {
            throw new Exception("Bean introspection failed: " + e.getMessage());
        }

        return beanInfo.getPropertyDescriptors();
    }

    private boolean isCompatibleType(Object value, Class<?> type) {
        // Do object check first, then primitives
        if (value == null || type.isInstance(value)) {
            return true;

        } else if (type.equals(Integer.TYPE) && value instanceof Integer) {
            return true;

        } else if (type.equals(Integer.TYPE) && value instanceof Long) {
            return true;
        } else if (type.equals(Long.TYPE) && value instanceof Long) {
            return true;

        } else if (type.equals(Double.TYPE) && value instanceof Double) {
            return true;

        } else if (type.equals(Float.TYPE) && value instanceof Float) {
            return true;

        } else if (type.equals(Short.TYPE) && value instanceof Short) {
            return true;

        } else if (type.equals(Byte.TYPE) && value instanceof Byte) {
            return true;

        } else if (type.equals(Character.TYPE) && value instanceof Character) {
            return true;

        } else if (type.equals(Boolean.TYPE) && value instanceof Boolean) {
            return true;

        }
        return false;

    }
}