package krpc.rpc.util;

import com.google.protobuf.ByteString;
import com.google.protobuf.Descriptors;
import com.google.protobuf.Message;
import com.google.protobuf.MessageOrBuilder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.lang.reflect.Field;
import java.math.BigInteger;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;

public class MessageToBean {

    static Logger log = LoggerFactory.getLogger(MessageToBean.class);

    static Object notFound = new Object();
    static ConcurrentHashMap<String,Object> fieldClsCache = new ConcurrentHashMap<>(); // notFound or Class
    static ConcurrentHashMap<String,Object> fieldCache = new ConcurrentHashMap<>(); // notFound or Field

    static public <T>  T toBean(Message message,Class<T> beanCls) {
        try {
            T obj = beanCls.newInstance();
            copyToBean(message,obj);
            return obj;
        } catch(Exception e) {
            log.error("toBean exception, e="+e.getMessage()+",beanCls="+beanCls.getName());
            return null;
        }
    }

    static public void copyToBean(Message message, Object bean) {
        copyProperties(message, bean);
    }

    static public void copyProperties(Message message, Object bean) {
        Map<Descriptors.FieldDescriptor, Object> fields = getFields(message,true);
        for (Map.Entry<Descriptors.FieldDescriptor, Object> i : fields.entrySet()) {

            Descriptors.FieldDescriptor field = i.getKey();
            Object value = i.getValue();

            if (field.isMapField()) {
                parseMapFieldValue(field, value, bean);
            } else if (field.isRepeated()) {
                for (Object element : (List<?>) value) {
                    parseSingleField(field, element, bean, true);
                }
            } else {
                parseSingleField(field, value, bean, false);
            }

        }
    }

    private static boolean isSimpleType(Descriptors.FieldDescriptor field) {
        switch( field.getType()) {
            case MESSAGE:
            case BYTES:
            case ENUM:
                return false;
            default:
                return true;
        }
    }

    private static void parseMapFieldValue(Descriptors.FieldDescriptor field, Object value, Object bean)   {
        Descriptors.Descriptor type = field.getMessageType();
        Descriptors.FieldDescriptor keyField = type.findFieldByName("key");
        Descriptors.FieldDescriptor valueField = type.findFieldByName("value");
        if (keyField != null && valueField != null) {

            Iterator listItem = ((List)value).iterator();

            Map map = new LinkedHashMap();
            while(listItem.hasNext()) {
                Object element = listItem.next();
                Message entry = (Message)element;
                Object entryKey = entry.getField(keyField);
                Object entryValue = entry.getField(valueField);

                if( !isSimpleType(keyField) ) {
                    throw new RuntimeException("not supported map field type, keyField="+keyField.getName());
                }
                if(  !isSimpleType(valueField) ) {
                    if( entryValue instanceof  Message ) {
                        Class cls = getMapValueCls(bean,field.getName());
                        if( cls == null ) continue;
                        entryValue = MessageToBean.toBean((Message)entryValue,cls);
                    } else {
                        continue;
//                        throw new RuntimeException("not supported map field type, valueField=" + valueField.getName());
                    }
                }

                map.put(entryKey,entryValue);
            }

            addToResults(field.getName(), map, bean, false);
        } else {
            throw new RuntimeException("Invalid map field");
        }
    }

    static Class getMapValueCls(Object bean, String field) {
        try {
            Field f = bean.getClass().getDeclaredField(field);
            String s = f.getGenericType().toString();
            int p1 = s.indexOf(",");
            int p2 = s.indexOf(">");
            String clsName = s.substring(p1+1,p2).trim();
            return Class.forName(clsName);
        }catch(Exception e) {
            return null;
        }
    }

    static void parseSingleField(Descriptors.FieldDescriptor field, Object value, Object bean, boolean isArray) {

        String name = field.getName();

        switch (field.getType()) {
            case INT32:
            case SINT32:
            case SFIXED32:
                addToResults(name, value, bean, isArray);
                break;

            case INT64:
            case SINT64:
            case SFIXED64:
                addToResults(name, value, bean, isArray);
                break;

            case BOOL:
                addToResults(name,value, bean, isArray);
                break;

            case FLOAT:
                addToResults(name,value, bean, isArray);
                break;

            case DOUBLE:
                addToResults(name,value, bean, isArray);
                break;

            case UINT32:
            case FIXED32:
                addToResults(name, unsignedToLong((Integer) value), bean, isArray);
                break;

            case UINT64:
            case FIXED64:
                addToResults(name, unsignedToBigInteger((Long) value), bean, isArray);
                break;

            case STRING:
                addToResults(name,value, bean, isArray);
                break;

            case BYTES: {
                if (value instanceof ByteString) {
                    addToResults(name, value, bean, isArray);
                }
                if (value instanceof String) {
                    byte[] bb = getBytes((String) value);
                    if (bb != null) {
                        addToResults(name, ByteString.copyFrom(bb), bean, isArray);
                    }
                }
                if (value instanceof byte[]) {
                    addToResults(name, ByteString.copyFrom((byte[]) value), bean, isArray);
                }
            }
            break;

            case ENUM:
                addToResults(name, ((Descriptors.EnumValueDescriptor) value).getNumber(), bean, isArray);
                break;

            case MESSAGE:
                Object sub = getFieldInstance(name,bean);
                copyProperties((Message) value, sub);
                addToResults(name, sub, bean, isArray);
                break;

            default:
                break;
        }
    }

    static void addToResults(String name, Object value, Object bean, boolean isArray) {
        try {
            Object fieldObj = getField(name,bean);
            if( fieldObj == notFound ) return;
            Field field = (Field)fieldObj;

            if (!isArray) {
                Object newValue = convertType(field.getType(), value);
                field.set(bean, newValue);
            } else {
                Object list = field.get(bean);
                if( list == null ) {
                    Class cls = field.getType();
                    if( cls.equals(List.class) || cls.equals(ArrayList.class) ) {
                        list = ArrayList.class.newInstance();
                        field.set(bean,list);
                    }
                    else if( cls.equals(LinkedList.class) ) {
                        list = LinkedList.class.newInstance();
                        field.set(bean,list);
                    } else {
                        return;
                    }
                }
                if( list instanceof List ) {
                    ((List)list).add(value);
                }
                else if( list instanceof ArrayList ) {
                    ((ArrayList)list).add(value);
                }
                else if( list instanceof LinkedList ) {
                    ((LinkedList)list).add(value);
                }
            }
        } catch(Exception e) {
        }
    }

    static Object convertType(Class fieldCls, Object v) {
        String type = fieldCls.getName();

        switch (type) {
            case "boolean":
            case "java.lang.Boolean":
                return TypeSafe.anyToBool(v);
            case "char":
            case "java.lang.Character":
                return (char) TypeSafe.anyToInt(v);
            case "byte":
            case "java.lang.Byte":
                return (byte) TypeSafe.anyToInt(v);
            case "short":
            case "java.lang.Short":
                return (short) TypeSafe.anyToInt(v);
            case "int":
            case "java.lang.Integer":
                return TypeSafe.anyToInt(v);
            case "long":
            case "java.lang.Long":
                return TypeSafe.anyToLong(TypeSafe.anyToString(v));
            case "float":
            case "java.lang.Float":
                return TypeSafe.anyToFloat(TypeSafe.anyToString(v));
            case "double":
            case "java.lang.Double":
                return TypeSafe.anyToDouble(TypeSafe.anyToString(v));
            case "java.lang.String":
                return TypeSafe.anyToString(v);
            case "java.util.Date":
                return TypeSafe.anyToDate(v);
            case "java.sql.Date": {
                Date d = TypeSafe.anyToDate(v);
                if( d == null ) return null;
                return new java.sql.Date(d.getTime());
            }
            case "java.sql.Timestamp": {
                Date d = TypeSafe.anyToDate(v);
                if (d == null) return null;
                return new java.sql.Timestamp(d.getTime());
            }
            case "java.util.Map":
            case "java.util.LinkedHashMap": {
                if (!(v instanceof Map)) return null;
                return v;
            }
            case "java.util.HashMap": {
                if (!(v instanceof Map)) return null;
                HashMap h = new HashMap();
                h.putAll((Map)v);
                return h;
            }
        }
        return null;
    }

    static Object getFieldCls(String name, Object bean) {
        String key = bean.getClass().getName()+":"+name;
        Object obj = fieldClsCache.get(key);
        if( obj != null ) return obj;

        try {
            Object fieldObj = getField(name,bean);
            if( fieldObj != notFound ) {
                Field field = (Field) fieldObj;
                Class cls = field.getType();
                if (cls.equals(List.class) || cls.equals(ArrayList.class) || cls.equals(LinkedList.class)) {
                    String typeName = field.getGenericType().getTypeName();
                    String itemClsName = getItemClsName(typeName);
                    if (itemClsName != null) {
                        cls = Class.forName(itemClsName);
                        fieldClsCache.put(key, cls);
                        return cls;
                    }
                } else {
                    fieldClsCache.put(key, cls);
                    return cls;
                }
            }
        } catch(Exception e) {
        }
        fieldClsCache.put(key,notFound);
        return notFound;
    }

    static Object getFieldInstance(String name, Object bean) {
        try {
            Object obj = getFieldCls(name,bean);
            if( obj == notFound ) return null;
            return ((Class)obj).newInstance();
        } catch(Exception e) {
            String key = bean.getClass().getName()+":"+name;
            fieldClsCache.put(key,notFound);
            return null;
        }
    }

    static String getItemClsName(String typeName) {
        int p1 = typeName.indexOf("<");
        int p2 = typeName.lastIndexOf(">");
        if (p1 >= 0 && p2 > p1) return typeName.substring(p1 + 1, p2);
        return null;
    }

    static Object getField(String name, Object bean) {
        String key = bean.getClass().getName()+":"+name;
        Object obj = fieldCache.get(key);
        if( obj != null ) return obj;

        Class cls = bean.getClass();
        while(!cls.equals(Object.class)) {
            try {
                Field field = cls.getDeclaredField(name);
                field.setAccessible(true);
                fieldCache.put(key, field);
                return field;
            } catch (Exception e) {
            }
            cls = cls.getSuperclass();
        }

        fieldCache.put(key,notFound);
        return notFound;
    }

    static public Map<Descriptors.FieldDescriptor, Object> getFields(MessageOrBuilder message, boolean withDefaultValue) {
        if (!withDefaultValue) {
            return message.getAllFields();
        }
        Map<Descriptors.FieldDescriptor, Object> fieldsToPrint = new LinkedHashMap<>();
        for (Descriptors.FieldDescriptor field : message.getDescriptorForType().getFields()) {
            if (field.isOptional()) {
                if (field.getJavaType() == Descriptors.FieldDescriptor.JavaType.MESSAGE && !message.hasField(field)) {
                    continue;
                }
//                if (field.getJavaType() == Descriptors.FieldDescriptor.JavaType.STRING && !message.hasField(field)) {
//                    continue;
//                }
            }
            fieldsToPrint.put(field, message.getField(field));
        }
        return fieldsToPrint;
    }

    static byte[] getBytes(String s) {
        if (s == null) return null;
        try {
            return s.getBytes("utf-8");
        } catch (Exception e) {
            return null;
        }
    }

    static public Long unsignedToLong(final int value) {
        if (value >= 0) {
            return Long.valueOf(value);
        } else {
            return value & 0x00000000FFFFFFFFL;
        }
    }

    static public BigInteger unsignedToBigInteger(final long value) {
        if (value >= 0) {
            return BigInteger.valueOf(value);
        } else {
            return BigInteger.valueOf(value & 0x7FFFFFFFFFFFFFFFL).setBit(63);
        }
    }

    public static String unsignedToString(final int value) {
        if (value >= 0) {
            return Integer.toString(value);
        } else {
            return String.valueOf(unsignedToLong(value));
        }
    }

    public static String unsignedToString(final long value) {
        if (value >= 0) {
            return Long.toString(value);
        } else {
            return String.valueOf(unsignedToBigInteger(value));
        }
    }

}