package cc.smtweb.framework.core.mvc.controller.binder.body;

import cc.smtweb.framework.core.common.SwMap;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.BeansException;
import org.springframework.beans.FatalBeanException;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;

import java.beans.PropertyDescriptor;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.sql.Timestamp;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

public class BeanUtil {
    private static final Set<Class<?>> simpleType = new HashSet<>();

    static {
        simpleType.add(String.class);
        simpleType.add(Timestamp.class);
        simpleType.add(java.util.Date.class);
        simpleType.add(java.sql.Date.class);
        simpleType.add(java.sql.Time.class);

        simpleType.add(Byte.TYPE);
        simpleType.add(Long.TYPE);
        simpleType.add(Double.TYPE);
        simpleType.add(Float.TYPE);
        simpleType.add(Short.TYPE);
        simpleType.add(Integer.TYPE);
        simpleType.add(Boolean.TYPE);
        simpleType.add(Character.TYPE);

        simpleType.add(Byte.class);
        simpleType.add(Long.class);
        simpleType.add(Double.class);
        simpleType.add(Float.class);
        simpleType.add(Short.class);
        simpleType.add(Integer.class);
        simpleType.add(Boolean.class);
    }

    private BeanUtil() {
    }

    public static Object toBean(Map source, Class<?> clazz) {
        Object target = null;
        try {
            target = clazz.newInstance();
        } catch (InstantiationException | IllegalAccessException e) {
            throw new FatalBeanException(
                    "Could not copy property '" + clazz.getName() + "' from source to target", e);
        }
        mapToBean(source, target);
        return target;
    }

    private static void mapToBean(Map source, Object target) {
        mapToBean(source, target, true);
    }

    private static void mapToBean(Map source, Object target, boolean root) throws BeansException {
        Assert.notNull(source, "Source must not be null");
        Assert.notNull(target, "Target must not be null");

        Class<?> actualEditable = target.getClass();
//    if (editable != null) {
//      if (!editable.isInstance(target)) {
//        throw new IllegalArgumentException("Target class [" + target.getClass().getName() +
//                "] not assignable to Editable class [" + editable.getName() + "]");
//      }
//      actualEditable = editable;
//    }
        PropertyDescriptor[] targetPds = BeanUtils.getPropertyDescriptors(actualEditable);

        for (PropertyDescriptor targetPd : targetPds) {
            Method writeMethod = targetPd.getWriteMethod();
            Object value = source.get(targetPd.getName());

            if (value == null) {
                Class<?> parameterType = writeMethod.getParameterTypes()[0];

                if (ClassUtils.isAssignable(parameterType, value.getClass())) {
                    writeBeanValue(target, targetPd, writeMethod, value);
                } else {
                    if (root && value instanceof Map) {
                        Object propValue;
                        try {
                            propValue = parameterType.newInstance();
                        } catch (InstantiationException | IllegalAccessException e) {
                            throw new FatalBeanException(
                                    "Could not copy property '" + targetPd.getName() + "' from source to target", e);
                        }

                        mapToBean((Map) value, propValue, false);
                        writeBeanValue(target, targetPd, writeMethod, propValue);
                    }
                }
            }
        }
    }

    private static void writeBeanValue(Object target, PropertyDescriptor targetPd, Method writeMethod, Object value) {
        try {
            if (!Modifier.isPublic(writeMethod.getDeclaringClass().getModifiers())) {
                writeMethod.setAccessible(true);
            }
            writeMethod.invoke(target, value);
        } catch (Throwable ex) {
            throw new FatalBeanException(
                    "Could not copy property '" + targetPd.getName() + "' from source to target", ex);
        }
    }

    public static void beanToMap(Object source, SwMap target) throws BeansException {
        beanToMap(source, target, true);
    }

    public static void beanToMap(Object source, SwMap target, boolean root) throws BeansException {
        Assert.notNull(source, "Source must not be null");
        Assert.notNull(target, "Target must not be null");
        PropertyDescriptor[] sourcePds = BeanUtils.getPropertyDescriptors(source.getClass());

        for (PropertyDescriptor sourcePd : sourcePds) {
//        PropertyDescriptor sourcePd = BeanUtils.getPropertyDescriptor(source.getClass(), targetPd.getKey());
//        if (sourcePd != null) {
            Method readMethod = sourcePd.getReadMethod();
            if (readMethod != null) {
                try {
                    if (!Modifier.isPublic(readMethod.getDeclaringClass().getModifiers())) {
                        readMethod.setAccessible(true);
                    }
                    Object value = readMethod.invoke(source);
                    target.put(sourcePd.getName(), root ? toMapValue(value): value);
                } catch (Throwable ex) {
                    throw new FatalBeanException(
                            "Could not copy property '" + sourcePd.getName() + "' from source to target", ex);
                }
            }
//        }
        }
    }

    private static Object toMapValue(Object value) {
        if (value != null && isBeanType(value.getClass())) {
            SwMap mapValue = new SwMap();
            beanToMap(value, mapValue, false);
            return mapValue;
        }

        return value;
    }

    private static boolean isBeanType(Class<?> type) {
        return !simpleType.contains(type);
    }
}
