/**
 * This file is part of the Meeds project (https://meeds.io/).
 *
 * Copyright (C) 2020 - 2025 Meeds Association contact@meeds.io
 *
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 3 of the License, or (at your option) any later version.
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 */
package org.exoplatform.commons.serialization.model;

import java.io.Serializable;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType;
import java.util.HashMap;
import java.util.Map;
import java.util.TreeMap;

import org.exoplatform.commons.serialization.api.TypeConverter;
import org.exoplatform.commons.serialization.api.annotations.Converted;
import org.exoplatform.commons.serialization.api.annotations.Serialized;
import org.exoplatform.commons.serialization.model.metadata.ClassTypeMetaData;
import org.exoplatform.commons.serialization.model.metadata.ConvertedTypeMetaData;
import org.exoplatform.commons.serialization.model.metadata.DomainMetaData;
import org.exoplatform.commons.serialization.model.metadata.TypeMetaData;
import org.exoplatform.services.log.ExoLogger;
import org.exoplatform.services.log.Log;

class TypeModelBuilder {

    /** . */
    private static final Log                   log                   = ExoLogger.getLogger(TypeModelBuilder.class);

    /** . */
    private static final Map<Class<?>, Class<?>> primitiveToWrapperMap = new HashMap<Class<?>, Class<?>>();

    static {
        primitiveToWrapperMap.put(byte.class, Byte.class);
        primitiveToWrapperMap.put(short.class, Short.class);
        primitiveToWrapperMap.put(int.class, Integer.class);
        primitiveToWrapperMap.put(long.class, Long.class);
        primitiveToWrapperMap.put(float.class, Float.class);
        primitiveToWrapperMap.put(double.class, Double.class);
        primitiveToWrapperMap.put(boolean.class, Boolean.class);
        primitiveToWrapperMap.put(char.class, Character.class);
    }

    // Build the missing types required to have knowledge about the
    // provided java type
    private final Map<String, TypeModel<?>> addedTypeModels = new HashMap<String, TypeModel<?>>();

    /** . */
    private final DomainMetaData metaData;

    /** . */
    private final Map<String, TypeModel<?>> existingTypeModels;

    public TypeModelBuilder(DomainMetaData metaData, Map<String, TypeModel<?>> existingTypeModels) {
        this.metaData = metaData;
        this.existingTypeModels = existingTypeModels;
    }

    Map<String, TypeModel<?>> getAddedTypeModels() {
        return addedTypeModels;
    }

    <O> TypeModel<O> build(Class<O> javaType) {
        if (javaType.isPrimitive()) {
            throw new IllegalArgumentException("No primitive type accepted");
        }

        //
        TypeModel<O> typeModel = get(javaType);

        //
        if (typeModel != null) {
            log.debug("Found type model " + typeModel + "  for java type " + javaType);
            return typeModel;
        }

        //
        log.debug("About to build type model for java type " + javaType);
        TypeMetaData typeMetaData = metaData.getTypeMetaData(javaType);

        //
        if (typeMetaData == null) {
            log.debug("No meta data found for java type " + javaType + " about to build it");
            boolean serialized = javaType.getAnnotation(Serialized.class) != null;
            Converted converted = javaType.getAnnotation(Converted.class);
            if (serialized) {
                if (converted != null) {
                    throw new TypeException();
                }
                typeMetaData = new ClassTypeMetaData(javaType.getName(), true);
            } else if (converted != null) {
                typeMetaData = new ConvertedTypeMetaData(javaType.getName(), converted.value());
            } else {
                typeMetaData = new ClassTypeMetaData(javaType.getName(), false);
            }
        }
        log.debug("Built type meta data " + typeMetaData + " for java type " + javaType);

        //
        return build(javaType, typeMetaData);
    }

    private <O> TypeModel<O> build(Class<O> javaType, TypeMetaData typeMetaData) {
        if (typeMetaData instanceof ClassTypeMetaData) {
            return buildClassType(javaType, (ClassTypeMetaData) typeMetaData);
        } else {
            return buildConvertedType(javaType, (ConvertedTypeMetaData) typeMetaData);
        }
    }

    private <O> ConvertedTypeModel<O, ?> buildConvertedType(Class<O> javaType, ConvertedTypeMetaData typeMetaData) {
        log.debug("About to build type model from type type metadata " + typeMetaData);

        //
        Class<? extends TypeConverter<?, ?>> converterClass = typeMetaData.getConverterClass();
        ParameterizedType converterParameterizedType = (ParameterizedType) converterClass.getGenericSuperclass();
        if (!converterParameterizedType.getActualTypeArguments()[0].equals(javaType)) {
            throw new TypeException("The declared type parameter in the converter " + converterClass.getName()
                    + " does not match the type it is related to " + javaType.getName());
        }

        //
        Class<? extends TypeConverter<O, ?>> converterJavaType = (Class<TypeConverter<O, ?>>) converterClass;

        //
        return buildConvertedType(javaType, converterJavaType);
    }

    private <O, T> ConvertedTypeModel<O, T> buildConvertedType(Class<O> javaType,
            Class<? extends TypeConverter<O, ? /* This is a bit funky and nasty, need to investigate */>> converterJavaType) {
        Class<T> outputClass = (Class<T>) ((ParameterizedType) converterJavaType.getGenericSuperclass())
                .getActualTypeArguments()[1];

        //
        ClassTypeModel<T> targetType = (ClassTypeModel<T>) build(outputClass);

        //
        TypeModel<? super O> superType = null;
        Class<? super O> superJavaType = javaType.getSuperclass();
        if (superJavaType != null) {
            superType = build(superJavaType);
        }

        //
        ConvertedTypeModel<O, T> typeModel = new ConvertedTypeModel<O, T>(javaType, superType, targetType,
                (Class<TypeConverter<O, T>>) converterJavaType);

        //
        addedTypeModels.put(typeModel.getName(), typeModel);

        //
        return typeModel;
    }

    private <O> ClassTypeModel<O> buildClassType(Class<O> javaType, ClassTypeMetaData typeMetaData) {
        ClassTypeModel<? super O> superTypeModel = null;
        if (javaType.getSuperclass() != null) {
            TypeModel<? super O> builtType = build(javaType.getSuperclass());
            if (builtType instanceof ClassTypeModel) {
                superTypeModel = (ClassTypeModel<? super O>) builtType;
            } else {
                throw new TypeException();
            }
        }

        //
        TreeMap<String, FieldModel<O, ?>> fieldModels = new TreeMap<String, FieldModel<O, ?>>();

        //
        SerializationMode serializationMode;
        if (typeMetaData.isSerialized()) {
            serializationMode = SerializationMode.SERIALIZED;
        } else if (Serializable.class.isAssignableFrom(javaType)) {
            serializationMode = SerializationMode.SERIALIZABLE;
        } else {
            serializationMode = SerializationMode.NONE;
        }

        //
        ClassTypeModel<O> typeModel = new ClassTypeModel<O>(javaType, superTypeModel, fieldModels, serializationMode);

        //
        addedTypeModels.put(javaType.getName(), typeModel);

        // Now build fields
        for (Field field : javaType.getDeclaredFields()) {
            if (!Modifier.isStatic(field.getModifiers())) {
                field.setAccessible(true);
                Class<?> fieldJavaType = field.getType();

                // Replace if a primitive
                if (fieldJavaType.isPrimitive()) {
                    fieldJavaType = primitiveToWrapperMap.get(fieldJavaType);
                }

                TypeModel<?> fieldTypeModel = build(fieldJavaType);
                if (fieldTypeModel != null) {
                    fieldModels.put(field.getName(), createField(typeModel, field, fieldTypeModel));
                }
            }
        }

        //
        return typeModel;
    }

    private <O, V> FieldModel<O, V> createField(TypeModel<O> owner, Field field, TypeModel<V> fieldTypeModel) {
        return new FieldModel<O, V>(owner, field, fieldTypeModel);
    }

    private <O> TypeModel<O> get(Class<O> javaType) {
        TypeModel<?> typeModel = existingTypeModels.get(javaType.getName());
        if (typeModel == null) {
            typeModel = addedTypeModels.get(javaType.getName());
        }

        // Cast OK
        return (TypeModel<O>) typeModel;
    }
}
