package com.alibaba.fastjson2.writer;

import com.alibaba.fastjson2.*;
import com.alibaba.fastjson2.codec.FieldInfo;
import com.alibaba.fastjson2.filter.*;
import com.alibaba.fastjson2.util.BeanUtils;
import com.alibaba.fastjson2.util.Fnv;
import com.alibaba.fastjson2.util.TypeUtils;

import java.lang.reflect.Field;
import java.lang.reflect.Type;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Map;

import static com.alibaba.fastjson2.JSONB.Constants.BC_TYPED_ANY;
import static com.alibaba.fastjson2.JSONWriter.Feature.*;

public class ObjectWriterAdapter<T>
        implements ObjectWriter<T> {
    boolean hasFilter;
    PropertyPreFilter propertyPreFilter;
    PropertyFilter propertyFilter;
    NameFilter nameFilter;
    ValueFilter valueFilter;

    static final String TYPE = "@type";

    final Class objectClass;
    final List<FieldWriter> fieldWriters;
    protected final FieldWriter[] fieldWriterArray;

    final String typeKey;
    byte[] typeKeyJSONB;
    private String typeName;
    protected long typeNameHash;
    protected long typeNameSymbolCache;
    protected byte[] typeNameJSONB;

    byte[] nameWithColonUTF8;
    char[] nameWithColonUTF16;

    final long features;

    final long[] hashCodes;
    final short[] mapping;

    final boolean hasValueField;
    final boolean serializable;
    final boolean containsNoneFieldGetter;
    final boolean googleCollection;

    public ObjectWriterAdapter(Class<T> objectClass, List<FieldWriter> fieldWriters) {
        this(objectClass, null, null, 0, fieldWriters);
    }

    public ObjectWriterAdapter(
            Class<T> objectClass,
            String typeKey,
            String typeName,
            long features,
            List<FieldWriter> fieldWriters
    ) {
        this.objectClass = objectClass;
        this.typeName = typeName;
        this.typeKey = typeKey == null || typeKey.isEmpty() ? TYPE : typeKey;
        this.features = features;
        this.fieldWriters = fieldWriters;
        this.serializable = objectClass == null || java.io.Serializable.class.isAssignableFrom(objectClass);
        this.googleCollection =
                "com.google.common.collect.AbstractMapBasedMultimap$RandomAccessWrappedList".equals(typeName)
                || "com.google.common.collect.AbstractMapBasedMultimap$WrappedSet".equals(typeName);

        this.fieldWriterArray = new FieldWriter[fieldWriters.size()];
        fieldWriters.toArray(fieldWriterArray);

        this.hasValueField = fieldWriterArray.length == 1 && (fieldWriterArray[0].features & FieldInfo.VALUE_MASK) != 0;

        boolean containsNoneFieldGetter = false;
        long[] hashCodes = new long[fieldWriterArray.length];
        for (int i = 0; i < fieldWriterArray.length; i++) {
            FieldWriter fieldWriter = fieldWriterArray[i];
            long hashCode = Fnv.hashCode64(fieldWriter.fieldName);
            hashCodes[i] = hashCode;

            if (fieldWriter.method != null && (fieldWriter.features & FieldInfo.FIELD_MASK) == 0) {
                containsNoneFieldGetter = true;
            }
        }
        this.containsNoneFieldGetter = containsNoneFieldGetter;

        this.hashCodes = Arrays.copyOf(hashCodes, hashCodes.length);
        Arrays.sort(this.hashCodes);

        mapping = new short[this.hashCodes.length];
        for (int i = 0; i < hashCodes.length; i++) {
            long hashCode = hashCodes[i];
            int index = Arrays.binarySearch(this.hashCodes, hashCode);
            mapping[index] = (short) i;
        }
    }

    protected String getTypeName() {
        if (typeName == null && objectClass != null) {
            if (Enum.class.isAssignableFrom(objectClass) && !objectClass.isEnum()) {
                typeName = objectClass.getSuperclass().getName();
            } else {
                typeName = TypeUtils.getTypeName(objectClass);
            }
        }
        return typeName;
    }

    protected byte[] getTypeNameJSONB() {
        if (this.typeNameJSONB == null) {
            String typeName = this.getTypeName();
            if (typeName != null) {
                typeNameJSONB = JSONB.toBytes(typeName);
            }
        }
        return typeNameJSONB;
    }

    protected long getTypeNameHash() {
        String typeName;
        if (typeNameHash == 0 && (typeName = getTypeName()) != null) {
            this.typeNameHash = Fnv.hashCode64(typeName);
        }
        return this.typeNameHash;
    }

    @Override
    public long getFeatures() {
        return features;
    }

    @Override
    public FieldWriter getFieldWriter(long hashCode) {
        int m = Arrays.binarySearch(hashCodes, hashCode);
        if (m < 0) {
            return null;
        }

        int index = this.mapping[m];
        return fieldWriterArray[index];
    }

    @Override
    public final boolean hasFilter(JSONWriter jsonWriter) {
        return hasFilter || jsonWriter.hasFilter(containsNoneFieldGetter);
    }

    public void setPropertyFilter(PropertyFilter propertyFilter) {
        this.propertyFilter = propertyFilter;
        if (propertyFilter != null) {
            hasFilter = true;
        }
    }

    public void setValueFilter(ValueFilter valueFilter) {
        this.valueFilter = valueFilter;
        if (valueFilter != null) {
            hasFilter = true;
        }
    }

    public void setNameFilter(NameFilter nameFilter) {
        this.nameFilter = nameFilter;
        if (nameFilter != null) {
            hasFilter = true;
        }
    }

    public void setPropertyPreFilter(PropertyPreFilter propertyPreFilter) {
        this.propertyPreFilter = propertyPreFilter;
        if (propertyPreFilter != null) {
            hasFilter = true;
        }
    }

    @Override
    public void writeArrayMappingJSONB(JSONWriter jsonWriter, Object object, Object fieldName, Type fieldType, long features) {
        if (jsonWriter.isWriteTypeInfo(object, fieldType, features)) {
            writeClassInfo(jsonWriter);
        }

        int size = fieldWriters.size();
        jsonWriter.startArray(size);
        for (int i = 0; i < size; ++i) {
            FieldWriter fieldWriter = fieldWriters.get(i);
            fieldWriter.writeValue(jsonWriter, object);
        }
    }

    @Override
    public void writeJSONB(JSONWriter jsonWriter, Object object, Object fieldName, Type fieldType, long features) {
        long featuresAll = features | this.features | jsonWriter.getFeatures();

        if (!serializable) {
            if ((featuresAll & JSONWriter.Feature.ErrorOnNoneSerializable.mask) != 0) {
                errorOnNoneSerializable();
                return;
            }

            if ((featuresAll & JSONWriter.Feature.IgnoreNoneSerializable.mask) != 0) {
                jsonWriter.writeNull();
                return;
            }
        }

        if ((featuresAll & JSONWriter.Feature.IgnoreNoneSerializable.mask) != 0) {
            writeWithFilter(jsonWriter, object, fieldName, fieldType, features);
            return;
        }

        int size = fieldWriterArray.length;
        if (jsonWriter.isWriteTypeInfo(object, fieldType, features)) {
            writeClassInfo(jsonWriter);
        }
        jsonWriter.startObject();
        for (int i = 0; i < size; ++i) {
            FieldWriter fieldWriter = fieldWriters.get(i);
            fieldWriter.write(jsonWriter, object);
        }
        jsonWriter.endObject();
    }

    protected final void writeClassInfo(JSONWriter jsonWriter) {
        SymbolTable symbolTable = jsonWriter.symbolTable;
        if (symbolTable != null) {
            if (writeClassInfoSymbol(jsonWriter, symbolTable)) {
                return;
            }
        }

        jsonWriter.writeTypeName(getTypeNameJSONB(), getTypeNameHash());
    }

    private boolean writeClassInfoSymbol(JSONWriter jsonWriter, SymbolTable symbolTable) {
        int symbolTableIdentity = System.identityHashCode(symbolTable);

        int symbol;
        if (typeNameSymbolCache == 0) {
            symbol = symbolTable.getOrdinalByHashCode(getTypeNameHash());
            if (symbol != -1) {
                typeNameSymbolCache = ((long) symbol << 32) | symbolTableIdentity;
            }
        } else {
            int identity = (int) typeNameSymbolCache;
            if (identity == symbolTableIdentity) {
                symbol = (int) (typeNameSymbolCache >> 32);
            } else {
                symbol = symbolTable.getOrdinalByHashCode(getTypeNameHash());
                if (symbol != -1) {
                    typeNameSymbolCache = ((long) symbol << 32) | symbolTableIdentity;
                }
            }
        }

        if (symbol != -1) {
            jsonWriter.writeRaw(BC_TYPED_ANY);
            jsonWriter.writeInt32(-symbol);
            return true;
        }
        return false;
    }

    @Override
    public void write(JSONWriter jsonWriter, Object object, Object fieldName, Type fieldType, long features) {
        if (hasValueField) {
            FieldWriter fieldWriter = fieldWriterArray[0];
            fieldWriter.writeValue(jsonWriter, object);
            return;
        }

        long featuresAll = features | this.features | jsonWriter.getFeatures();
        boolean beanToArray = (featuresAll & BeanToArray.mask) != 0;

        if (jsonWriter.jsonb) {
            if (beanToArray) {
                writeArrayMappingJSONB(jsonWriter, object, fieldName, fieldType, features);
                return;
            }

            writeJSONB(jsonWriter, object, fieldName, fieldType, features);
            return;
        }

        if (googleCollection) {
            Collection collection = (Collection) object;
            ObjectWriterImplCollection.INSTANCE.write(jsonWriter, collection, fieldName, fieldType, features);
            return;
        }

        if (beanToArray) {
            writeArrayMapping(jsonWriter, object, fieldName, fieldType, features);
            return;
        }

        if (!serializable) {
            if ((featuresAll & JSONWriter.Feature.ErrorOnNoneSerializable.mask) != 0) {
                errorOnNoneSerializable();
                return;
            }

            if ((featuresAll & JSONWriter.Feature.IgnoreNoneSerializable.mask) != 0) {
                jsonWriter.writeNull();
                return;
            }
        }

        if (hasFilter(jsonWriter)) {
            writeWithFilter(jsonWriter, object, fieldName, fieldType, features);
            return;
        }

        jsonWriter.startObject();

        if (((features | this.features) & WriteClassName.mask) != 0 || jsonWriter.isWriteTypeInfo(object, features)) {
            writeTypeInfo(jsonWriter);
        }

        for (int i = 0, size = fieldWriters.size(); i < size; ++i) {
            FieldWriter fieldWriter = fieldWriters.get(i);
            fieldWriter.write(jsonWriter, object);
        }

        jsonWriter.endObject();
    }

    public Map<String, Object> toMap(Object object) {
        JSONObject map = new JSONObject(fieldWriters.size());
        for (int i = 0; i < fieldWriters.size(); i++) {
            FieldWriter fieldWriter = fieldWriters.get(i);
            map.put(
                    fieldWriter.fieldName,
                    fieldWriter.getFieldValue(object)
            );
        }
        return map;
    }

    @Override
    public List<FieldWriter> getFieldWriters() {
        return fieldWriters;
    }

    @Override
    public boolean writeTypeInfo(JSONWriter jsonWriter) {
        String typeName = this.getTypeName();
        if (jsonWriter.utf8) {
            if (nameWithColonUTF8 == null) {
                int typeKeyLength = typeKey.length();
                int typeNameLength = typeName.length();
                byte[] chars = new byte[typeKeyLength + typeNameLength + 5];
                chars[0] = '"';
                typeKey.getBytes(0, typeKeyLength, chars, 1);
                chars[typeKeyLength + 1] = '"';
                chars[typeKeyLength + 2] = ':';
                chars[typeKeyLength + 3] = '"';
                typeName.getBytes(0, typeNameLength, chars, typeKeyLength + 4);
                chars[typeKeyLength + typeNameLength + 4] = '"';

                nameWithColonUTF8 = chars;
            }
            jsonWriter.writeNameRaw(nameWithColonUTF8);
            return true;
        } else if (jsonWriter.utf16) {
            if (nameWithColonUTF16 == null) {
                int typeKeyLength = typeKey.length();
                int typeNameLength = typeName.length();
                char[] chars = new char[typeKeyLength + typeNameLength + 5];
                chars[0] = '"';
                typeKey.getChars(0, typeKeyLength, chars, 1);
                chars[typeKeyLength + 1] = '"';
                chars[typeKeyLength + 2] = ':';
                chars[typeKeyLength + 3] = '"';
                typeName.getChars(0, typeNameLength, chars, typeKeyLength + 4);
                chars[typeKeyLength + typeNameLength + 4] = '"';

                nameWithColonUTF16 = chars;
            }
            jsonWriter.writeNameRaw(nameWithColonUTF16);
            return true;
        } else if (jsonWriter.jsonb) {
            if (typeKeyJSONB == null) {
                typeKeyJSONB = JSONB.toBytes(typeKey);
            }
            jsonWriter.writeRaw(typeKeyJSONB);
            jsonWriter.writeRaw(typeNameJSONB);
            return true;
        }

        jsonWriter.writeString(typeKey);
        jsonWriter.writeColon();
        jsonWriter.writeString(typeName);
        return true;
    }

    @Override
    public void writeWithFilter(JSONWriter jsonWriter, Object object, Object fieldName, Type fieldType, long features) {
        if (object == null) {
            jsonWriter.writeNull();
            return;
        }

        if (jsonWriter.isWriteTypeInfo(object, fieldType, features)) {
            if (jsonWriter.jsonb) {
                writeClassInfo(jsonWriter);
                jsonWriter.startObject();
            } else {
                jsonWriter.startObject();
                writeTypeInfo(jsonWriter);
            }
        } else {
            jsonWriter.startObject();
        }

        JSONWriter.Context context = jsonWriter.context;
        boolean ignoreNonFieldGetter = ((context.getFeatures() | features) & IgnoreNonFieldGetter.mask) != 0;

        BeforeFilter beforeFilter = context.getBeforeFilter();
        if (beforeFilter != null) {
            beforeFilter.writeBefore(jsonWriter, object);
        }

        PropertyPreFilter propertyPreFilter = context.getPropertyPreFilter();
        if (propertyPreFilter == null) {
            propertyPreFilter = this.propertyPreFilter;
        }

        NameFilter nameFilter = context.getNameFilter();
        if (nameFilter == null) {
            nameFilter = this.nameFilter;
        } else {
            if (this.nameFilter != null) {
                nameFilter = NameFilter.compose(this.nameFilter, nameFilter);
            }
        }

        ContextNameFilter contextNameFilter = context.getContextNameFilter();

        ValueFilter valueFilter = context.getValueFilter();
        if (valueFilter == null) {
            valueFilter = this.valueFilter;
        } else {
            if (this.valueFilter != null) {
                valueFilter = ValueFilter.compose(this.valueFilter, valueFilter);
            }
        }

        ContextValueFilter contextValueFilter = context.getContextValueFilter();

        PropertyFilter propertyFilter = context.getPropertyFilter();
        if (propertyFilter == null) {
            propertyFilter = this.propertyFilter;
        }

        LabelFilter labelFilter = context.getLabelFilter();

        for (int i = 0, size = fieldWriters.size(); i < size; ++i) {
            FieldWriter fieldWriter = fieldWriters.get(i);

            Field field = fieldWriter.field;

            if (ignoreNonFieldGetter
                    && fieldWriter.method != null
                    && (fieldWriter.features & FieldInfo.FIELD_MASK) == 0) {
                continue;
            }

            // pre property filter
            final String fieldWriterFieldName = fieldWriter.fieldName;
            if (propertyPreFilter != null
                    && !propertyPreFilter.process(jsonWriter, object, fieldWriterFieldName)) {
                continue;
            }

            if (labelFilter != null) {
                String label = fieldWriter.label;
                if (label != null && !label.isEmpty()) {
                    if (!labelFilter.apply(label)) {
                        continue;
                    }
                }
            }

            // fast return
            if (nameFilter == null && propertyFilter == null && valueFilter == null && contextValueFilter == null && contextNameFilter == null
            ) {
                fieldWriter.write(jsonWriter, object);
                continue;
            }

            Object fieldValue;
            try {
                fieldValue = fieldWriter.getFieldValue(object);
            } catch (Throwable e) {
                if ((context.getFeatures() & JSONWriter.Feature.IgnoreErrorGetter.mask) != 0) {
                    continue;
                }
                throw e;
            }

            if (fieldValue == null && !jsonWriter.isWriteNulls()) {
                continue;
            }

            BeanContext beanContext = null;

            // name filter
            String filteredName = fieldWriterFieldName;
            if (nameFilter != null) {
                filteredName = nameFilter.process(object, filteredName, fieldValue);
            }

            if (contextNameFilter != null) {
                beanContext = new BeanContext(
                        objectClass,
                        fieldWriter.method,
                        field,
                        fieldWriter.fieldName,
                        fieldWriter.label,
                        fieldWriter.fieldClass,
                        fieldWriter.fieldType,
                        fieldWriter.features,
                        fieldWriter.format
                );
                filteredName = contextNameFilter.process(beanContext, object, filteredName, fieldValue);
            }

            // property filter
            if (propertyFilter != null
                    && !propertyFilter.apply(object, fieldWriterFieldName, fieldValue)) {
                continue;
            }

            boolean nameChanged = filteredName != null && filteredName != fieldWriterFieldName;

            Object filteredValue = fieldValue;
            if (valueFilter != null) {
                filteredValue = valueFilter.apply(object, fieldWriterFieldName, fieldValue);
            }
            if (contextValueFilter != null) {
                if (beanContext == null) {
                    if (field == null && fieldWriter.method != null) {
                        field = BeanUtils.getDeclaredField(objectClass, fieldWriter.fieldName);
                    }

                    beanContext = new BeanContext(
                            objectClass,
                            fieldWriter.method,
                            field,
                            fieldWriter.fieldName,
                            fieldWriter.label,
                            fieldWriter.fieldClass,
                            fieldWriter.fieldType,
                            fieldWriter.features,
                            fieldWriter.format
                    );
                }
                filteredValue = contextValueFilter.process(beanContext, object, filteredName, filteredValue);
            }

            if (filteredValue != fieldValue) {
                if (nameChanged) {
                    jsonWriter.writeName(filteredName);
                    jsonWriter.writeColon();
                } else {
                    fieldWriter.writeFieldName(jsonWriter);
                }

                if (filteredValue == null) {
                    jsonWriter.writeNull();
                } else {
                    ObjectWriter fieldValueWriter = fieldWriter.getObjectWriter(jsonWriter, filteredValue.getClass());
                    fieldValueWriter.write(jsonWriter, filteredValue, fieldName, fieldType, features);
                }
            } else {
                if (!nameChanged) {
                    fieldWriter.write(jsonWriter, object);
                } else {
                    jsonWriter.writeName(filteredName);
                    jsonWriter.writeColon();

                    if (fieldValue == null) {
                        ObjectWriter fieldValueWriter = fieldWriter.getObjectWriter(jsonWriter, fieldWriter.fieldClass);
                        fieldValueWriter.write(jsonWriter, null, fieldName, fieldType, features);
                    } else {
                        ObjectWriter fieldValueWriter = fieldWriter.getObjectWriter(jsonWriter, fieldValue.getClass());
                        fieldValueWriter.write(jsonWriter, fieldValue, fieldName, fieldType, features);
                    }
                }
            }
        }

        AfterFilter afterFilter = context.getAfterFilter();
        if (afterFilter != null) {
            afterFilter.writeAfter(jsonWriter, object);
        }

        jsonWriter.endObject();
    }

    public JSONObject toJSONObject(T object) {
        return toJSONObject(object, 0);
    }

    public JSONObject toJSONObject(T object, long features) {
        JSONObject jsonObject = new JSONObject();

        for (FieldWriter fieldWriter : fieldWriters) {
            Object fieldValue = fieldWriter.getFieldValue(object);
            Class fieldClass = fieldWriter.fieldClass;

            long fieldFeatures = fieldWriter.features;
            if ((fieldFeatures & FieldInfo.UNWRAPPED_MASK) != 0) {
                if (fieldValue instanceof Map) {
                    jsonObject.putAll((Map) fieldValue);
                    continue;
                }

                ObjectWriter fieldObjectWriter = fieldWriter.getInitWriter();
                if (fieldObjectWriter == null) {
                    fieldObjectWriter = JSONFactory.defaultObjectWriterProvider.getObjectWriter(fieldWriter.fieldClass);
                }
                List<FieldWriter> unwrappedFieldWriters = fieldObjectWriter.getFieldWriters();
                for (FieldWriter unwrappedFieldWriter : unwrappedFieldWriters) {
                    Object unwrappedFieldValue = unwrappedFieldWriter.getFieldValue(fieldValue);
                    jsonObject.put(unwrappedFieldWriter.fieldName, unwrappedFieldValue);
                }
                continue;
            }

            if (fieldValue != null) {
                String fieldValueClassName = fieldValue.getClass().getName();
                if (Collection.class.isAssignableFrom(fieldClass)
                        && fieldValue.getClass() != JSONObject.class
                        && !fieldValueClassName.equals("com.alibaba.fastjson.JSONObject")
                ) {
                    Collection collection = (Collection) fieldValue;
                    JSONArray array = new JSONArray(collection.size());
                    for (Object item : collection) {
                        Object itemJSON = item == object ? jsonObject : JSON.toJSON(item);
                        array.add(itemJSON);
                    }
                    fieldValue = array;
                }
            }

            if (fieldValue == null && ((this.features | features) & WriteNulls.mask) == 0) {
                continue;
            }

            if (fieldValue == object) {
                fieldValue = jsonObject;
            }
            if (fieldValue instanceof Enum) {
                if ((features & WriteEnumsUsingName.mask) != 0) {
                    fieldValue = ((Enum) fieldValue).name();
                }
            }
            if (fieldWriter instanceof FieldWriterObject && fieldValue != null && !(fieldValue instanceof Map)) {
                ObjectWriter valueWriter = fieldWriter.getInitWriter();
                if (valueWriter == null) {
                    valueWriter = JSONFactory.getObjectWriter(fieldWriter.fieldType, this.features | features);
                }
                if (valueWriter instanceof ObjectWriterAdapter) {
                    ObjectWriterAdapter objectWriterAdapter = (ObjectWriterAdapter) valueWriter;
                    if (!objectWriterAdapter.getFieldWriters().isEmpty()) {
                        fieldValue = objectWriterAdapter.toJSONObject(fieldValue);
                    } else {
                        fieldValue = JSON.toJSON(fieldValue);
                    }
                }
            }
            jsonObject.put(fieldWriter.fieldName, fieldValue);
        }

        return jsonObject;
    }

    @Override
    public String toString() {
        return objectClass.getName();
    }

    protected void errorOnNoneSerializable() {
        throw new JSONException("not support none serializable class " + objectClass.getName());
    }
}
