package com.alibaba.fastjson2.reader;

import com.alibaba.fastjson2.*;
import com.alibaba.fastjson2.function.Function;
import com.alibaba.fastjson2.util.Fnv;
import com.alibaba.fastjson2.util.TypeUtils;

import java.lang.reflect.Modifier;
import java.lang.reflect.Type;
import java.util.*;

import static com.alibaba.fastjson2.JSONB.Constants.BC_NULL;
import static com.alibaba.fastjson2.JSONB.Constants.BC_TYPED_ANY;

public class ObjectReaderNoneDefaultConstructor<T>
        extends ObjectReaderAdapter<T> {
    final String[] paramNames;
    final FieldReader[] setterFieldReaders;
    private final Function<Map<Long, Object>, T> creator;

    public ObjectReaderNoneDefaultConstructor(
            Class objectClass,
            String typeKey,
            String typeName,
            long features,
            Function<Map<Long, Object>, T> creator,
            String[] paramNames,
            FieldReader[] paramFieldReaders,
            FieldReader[] setterFieldReaders,
            Class[] seeAlso,
            String[] seeAlsoNames
    ) {
        super(
                objectClass,
                typeKey,
                typeName,
                features,
                null,
                null,
                seeAlso,
                seeAlsoNames,
                null,
                concat(paramFieldReaders, setterFieldReaders)
        );

        this.paramNames = paramNames;
        this.creator = creator;
        this.setterFieldReaders = setterFieldReaders;
    }

    static FieldReader[] concat(FieldReader[] a, FieldReader[] b) {
        if (b == null) {
            return a;
        }
        int alen = a.length;
        a = Arrays.copyOf(a, alen + b.length);
        System.arraycopy(b, 0, a, alen, b.length);
        return a;
    }

    @Override
    public T createInstanceNoneDefaultConstructor(Map<Long, Object> values) {
        return creator.apply(values);
    }

    @Override
    public T readJSONBObject(JSONReader jsonReader, Type fieldType, Object fieldName, long features) {
        if (!serializable) {
            jsonReader.errorOnNoneSerializable(objectClass);
        }

        byte type = jsonReader.getType();
        if (type == BC_NULL) {
            jsonReader.next();
            return null;
        }

        if (type == BC_TYPED_ANY) {
            ObjectReader objectReader = jsonReader.checkAutoType(this.objectClass, getTypeNameHash(), this.features | features);
            if (objectReader != null && objectReader != this) {
                return (T) objectReader.readJSONBObject(jsonReader, fieldType, fieldName, features);
            }
        }

        LinkedHashMap<Long, Object> valueMap = null;
        Map<Long, String> references = null;

        if (jsonReader.isArray()) {
            if (jsonReader.isSupportBeanArray()) {
                int entryCnt = jsonReader.startArray();
                for (int i = 0; i < entryCnt; ++i) {
                    FieldReader fieldReader = fieldReaders[i];
                    Object fieldValue = fieldReader.readFieldValue(jsonReader);
                    if (valueMap == null) {
                        valueMap = new LinkedHashMap<>();
                    }
                    valueMap.put(fieldReader.fieldNameHash, fieldValue);
                }
            } else {
                throw new JSONException(jsonReader.info("expect object, but " + JSONB.typeName(jsonReader.getType())));
            }
        } else {
            jsonReader.nextIfObjectStart();
            for (int i = 0; ; ++i) {
                if (jsonReader.nextIfObjectEnd()) {
                    break;
                }

                long hashCode = jsonReader.readFieldNameHashCode();
                if (hashCode == 0) {
                    continue;
                }

                if (hashCode == HASH_TYPE && i == 0) {
                    long typeHash = jsonReader.readTypeHashCode();
                    JSONReader.Context context = jsonReader.context;
                    ObjectReader autoTypeObjectReader = context.getObjectReaderAutoType(typeHash);
                    if (autoTypeObjectReader == null) {
                        String typeName = jsonReader.getString();
                        autoTypeObjectReader = context.getObjectReaderAutoType(typeName, objectClass);

                        if (autoTypeObjectReader == null) {
                            throw new JSONException(jsonReader.info("auotype not support : " + typeName));
                        }
                    }

                    T object = (T) autoTypeObjectReader.readJSONBObject(jsonReader, fieldType, fieldName, features);
                    jsonReader.nextIfComma();
                    return object;
                }

                FieldReader fieldReader = getFieldReader(hashCode);
                if (fieldReader == null) {
                    processExtra(jsonReader, null, features);
                    continue;
                }

                Object fieldValue;
                if (jsonReader.isReference()) {
                    jsonReader.next();
                    String reference = jsonReader.readString();
                    if (references == null) {
                        references = new HashMap<>();
                    }
                    references.put(hashCode, reference);
                    continue;
                }

                fieldValue = fieldReader.readFieldValue(jsonReader);
                if (valueMap == null) {
                    valueMap = new LinkedHashMap<>();
                }
                valueMap.put(fieldReader.fieldNameHash, fieldValue);
            }
        }

        Map<Long, Object> args
                = valueMap == null
                ? Collections.emptyMap()
                : valueMap;
        T object = createInstanceNoneDefaultConstructor(args);
        if (setterFieldReaders != null) {
            for (int i = 0; i < setterFieldReaders.length; i++) {
                FieldReader fieldReader = setterFieldReaders[i];
                Object fieldValue = args.get(fieldReader.fieldNameHash);
                fieldReader.accept(object, fieldValue);
            }
        }

        if (references != null) {
            for (Map.Entry<Long, String> entry : references.entrySet()) {
                Long hashCode = entry.getKey();
                String reference = entry.getValue();
                FieldReader fieldReader = getFieldReader(hashCode);
                if ("..".equals(reference)) {
                    fieldReader.accept(object, object);
                    continue;
                }
                fieldReader.addResolveTask(jsonReader, object, reference);
            }
        }

        return object;
    }

    @Override
    public T readObject(JSONReader jsonReader, Type fieldType, Object fieldName, long features) {
        if (!serializable) {
            jsonReader.errorOnNoneSerializable(objectClass);
        }

        if (jsonReader.jsonb) {
            return readJSONBObject(jsonReader, fieldType, fieldName, 0);
        }

        if (jsonReader.isSupportBeanArray(features | this.features)
                && jsonReader.nextIfArrayStart()) {
            LinkedHashMap<Long, Object> valueMap = null;
            for (int i = 0; i < fieldReaders.length; i++) {
                Object fieldValue = fieldReaders[i].readFieldValue(jsonReader);
                if (valueMap == null) {
                    valueMap = new LinkedHashMap<>();
                }
                long hash = fieldReaders[i].fieldNameHash;
                valueMap.put(hash, fieldValue);
            }

            if (!jsonReader.nextIfArrayEnd()) {
                throw new JSONException(jsonReader.info("array not end, " + jsonReader.current()));
            }

            jsonReader.nextIfComma();
            return createInstanceNoneDefaultConstructor(
                    valueMap == null
                            ? Collections.emptyMap()
                            : valueMap);
        }

        boolean objectStart = jsonReader.nextIfObjectStart();
        if (!objectStart) {
            if (jsonReader.isTypeRedirect()) {
                jsonReader.setTypeRedirect(false);
            } else if (jsonReader.nextIfNullOrEmptyString()) {
                return null;
            }
        }

        JSONReader.Context context = jsonReader.context;
        long featuresAll = this.features | features | context.features;

        LinkedHashMap<Long, Object> valueMap = null;
        for (int i = 0; ; i++) {
            if (jsonReader.nextIfObjectEnd()) {
                break;
            }

            long hashCode = jsonReader.readFieldNameHashCode();
            if (hashCode == 0) {
                continue;
            }

            if (hashCode == typeKeyHashCode && i == 0) {
                long typeHash = jsonReader.readTypeHashCode();
                if (typeHash == getTypeNameHash()) {
                    continue;
                }

                boolean supportAutoType = (featuresAll & JSONReader.Feature.SupportAutoType.mask) != 0;

                ObjectReader autoTypeObjectReader;

                if (supportAutoType) {
                    autoTypeObjectReader = jsonReader.getObjectReaderAutoType(typeHash, objectClass, this.features);
                } else {
                    String typeName = jsonReader.getString();
                    autoTypeObjectReader = context.getObjectReaderAutoType(typeName, objectClass);
                }

                if (autoTypeObjectReader == null) {
                    String typeName = jsonReader.getString();
                    autoTypeObjectReader = context.getObjectReaderAutoType(typeName, objectClass, this.features);
                }

                if (autoTypeObjectReader != null) {
                    Object object = autoTypeObjectReader.readObject(jsonReader, fieldType, fieldName, 0);
                    jsonReader.nextIfComma();
                    return (T) object;
                }
                continue;
            }

            FieldReader fieldReader = getFieldReader(hashCode);

            if (fieldReader == null && (featuresAll & JSONReader.Feature.SupportSmartMatch.mask) != 0) {
                long hashCodeLCase = jsonReader.getNameHashCodeLCase();
                fieldReader = getFieldReaderLCase(hashCodeLCase);

                if (fieldReader == null) {
                    String name = jsonReader.getFieldName();
                    if (name.startsWith("is")) {
                        String fieldName1 = name.substring(2);
                        long hashCode64LCase = Fnv.hashCode64LCase(fieldName1);
                        FieldReader fieldReader1 = getFieldReaderLCase(hashCode64LCase);
                        if (fieldReader1 != null) {
                            Class fieldClass = fieldReader1.fieldClass;
                            if (fieldClass == Boolean.class || fieldClass == boolean.class) {
                                fieldReader = fieldReader1;
                            }
                        }
                    }
                }
            }

            if (fieldReader == null) {
                processExtra(jsonReader, null, features);
                continue;
            }

            Object fieldValue = fieldReader.readFieldValue(jsonReader);
            if (valueMap == null) {
                valueMap = new LinkedHashMap<>();
            }

            long hash;
            if (fieldReader instanceof FieldReaderObjectParam) {
                hash = ((FieldReaderObjectParam<?>) fieldReader).paramNameHash;
            } else {
                hash = fieldReader.fieldNameHash;
            }
            valueMap.put(hash, fieldValue);
        }

        if (hasDefaultValue) {
            if (valueMap == null) {
                valueMap = new LinkedHashMap<>();
            }
            for (FieldReader fieldReader : fieldReaders) {
                if (fieldReader.defaultValue != null) {
                    Object fieldValue = valueMap.get(fieldReader.fieldNameHash);
                    if (fieldValue == null) {
                        valueMap.put(fieldReader.fieldNameHash, fieldReader.defaultValue);
                    }
                }
            }
        }

        Map<Long, Object> argsMap = valueMap == null ? Collections.emptyMap() : valueMap;
        T object = creator.apply(argsMap);

        if (setterFieldReaders != null && valueMap != null) {
            for (int i = 0; i < setterFieldReaders.length; i++) {
                FieldReader fieldReader = setterFieldReaders[i];
                Object fieldValue = valueMap.get(fieldReader.fieldNameHash);
                if (fieldValue != null) {
                    fieldReader.accept(object, fieldValue);
                }
            }
        }

        jsonReader.nextIfComma();

        return object;
    }

    public T createInstance(Collection collection, long features) {
        int index = 0;

        ObjectReaderProvider provider = JSONFactory.defaultObjectReaderProvider;

        LinkedHashMap<Long, Object> valueMap = new LinkedHashMap<>();
        for (Iterator it = collection.iterator(); it.hasNext(); ) {
            Object fieldValue = it.next();
            if (index >= fieldReaders.length) {
                break;
            }
            FieldReader fieldReader = fieldReaders[index];

            if (fieldValue != null) {
                Class<?> valueClass = fieldValue.getClass();
                Class fieldClass = fieldReader.fieldClass;
                Type fieldType = fieldReader.fieldType;
                if (!(fieldType instanceof Class)) {
                    fieldValue = TypeUtils.cast(fieldValue, fieldType, provider);
                } else if (valueClass != fieldClass) {
                    Function typeConvert = provider.getTypeConvert(valueClass, fieldClass);
                    if (typeConvert != null) {
                        fieldValue = typeConvert.apply(fieldValue);
                    }
                }
            }

            long hash;
            if (fieldReader instanceof FieldReaderObjectParam) {
                hash = ((FieldReaderObjectParam<?>) fieldReader).paramNameHash;
            } else {
                hash = fieldReader.fieldNameHash;
            }
            valueMap.put(hash, fieldValue);

            index++;
        }

        return createInstanceNoneDefaultConstructor(
                valueMap
        );
    }

    @Override
    public T createInstance(Map map, long features) {
        ObjectReaderProvider provider = JSONFactory.defaultObjectReaderProvider;
        Object typeKey = map.get(getTypeKey());

        if (typeKey instanceof String) {
            String typeName = (String) typeKey;
            long typeHash = Fnv.hashCode64(typeName);
            ObjectReader<T> reader = null;
            if ((features & JSONReader.Feature.SupportAutoType.mask) != 0) {
                reader = autoType(provider, typeHash);
            }

            if (reader == null) {
                reader = provider.getObjectReader(
                        typeName, getObjectClass(), features | getFeatures()
                );
            }

            if (reader != this && reader != null) {
                return reader.createInstance(map, features);
            }
        }

        LinkedHashMap<Long, Object> valueMap = null;

        for (Iterator<Map.Entry> it = map.entrySet().iterator(); it.hasNext(); ) {
            Map.Entry entry = it.next();
            String fieldName = entry.getKey().toString();
            Object fieldValue = entry.getValue();

            FieldReader fieldReader = getFieldReader(fieldName);
            if (fieldReader != null) {
                if (fieldValue != null) {
                    Class<?> valueClass = fieldValue.getClass();
                    Class fieldClass = fieldReader.fieldClass;
                    if (valueClass != fieldClass) {
                        if (fieldValue instanceof JSONObject) {
                            ObjectReader fieldObjectReader = provider.getObjectReader(fieldReader.fieldType);
                            fieldValue = fieldObjectReader.createInstance((Map) fieldValue, features);
                        } else if (fieldValue instanceof JSONArray) {
                            fieldValue = ((JSONArray) fieldValue).to(fieldReader.fieldType, features);
                        } else {
                            Function typeConvert = provider.getTypeConvert(valueClass, fieldClass);
                            if (typeConvert != null) {
                                fieldValue = typeConvert.apply(fieldValue);
                            }
                        }
                    }
                }

                if (valueMap == null) {
                    valueMap = new LinkedHashMap<>();
                }

                long hash;
                if (fieldReader instanceof FieldReaderObjectParam) {
                    hash = ((FieldReaderObjectParam<?>) fieldReader).paramNameHash;
                } else {
                    hash = fieldReader.fieldNameHash;
                }
                valueMap.put(hash, fieldValue);
            }
        }

        T object = createInstanceNoneDefaultConstructor(
                valueMap == null
                        ? Collections.emptyMap()
                        : valueMap
        );

        for (int i = 0; i < setterFieldReaders.length; i++) {
            FieldReader fieldReader = setterFieldReaders[i];
            Object fieldValue = map.get(fieldReader.fieldName);
            if (fieldValue == null) {
                continue;
            }

            if (fieldReader.field != null && Modifier.isFinal(fieldReader.field.getModifiers())) {
                try {
                    Object value = fieldReader.method.invoke(object);
                    if (value instanceof Collection && !((Collection) value).isEmpty()) {
                        continue;
                    }
                } catch (Exception e) {
                    // just ignore
                }
            }

            Class<?> valueClass = fieldValue.getClass();
            Class fieldClass = fieldReader.fieldClass;
            Type fieldType = fieldReader.fieldType;
            if (valueClass != fieldClass) {
                Function typeConvert = provider.getTypeConvert(valueClass, fieldClass);
                if (!(fieldType instanceof Class)) {
                    fieldValue = TypeUtils.cast(fieldValue, fieldType, provider);
                } else if (typeConvert != null) {
                    fieldValue = typeConvert.apply(fieldValue);
                } else if (fieldValue instanceof Map) {
                    ObjectReader objectReader = fieldReader.getObjectReader(JSONFactory.createReadContext(provider));
                    fieldValue = objectReader.createInstance((Map) fieldValue, features | fieldReader.features);
                }
            }
            fieldReader.accept(object, fieldValue);
        }

        return object;
    }
}
