package com.github.jrcodeza.schema.generator;

import com.github.jrcodeza.schema.generator.filters.SchemaFieldFilter;
import com.github.jrcodeza.schema.generator.interceptors.SchemaFieldInterceptor;
import com.github.jrcodeza.schema.generator.model.CustomComposedSchema;
import com.github.jrcodeza.schema.generator.model.InheritanceInfo;
import com.github.jrcodeza.schema.generator.util.SchemaGeneratorHelper;
import io.swagger.v3.oas.models.media.Discriminator;
import io.swagger.v3.oas.models.media.ObjectSchema;
import io.swagger.v3.oas.models.media.Schema;
import io.swagger.v3.oas.models.media.StringSchema;
import org.apache.commons.lang3.StringUtils;
import org.springframework.util.ReflectionUtils;

import javax.validation.constraints.NotNull;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.ParameterizedType;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import static com.github.jrcodeza.schema.generator.util.CommonConstants.COMPONENT_REF_PREFIX;
import static com.github.jrcodeza.schema.generator.util.GeneratorUtils.shouldBeIgnored;


public class ComponentSchemaTransformer {

    private final List<SchemaFieldInterceptor> schemaFieldInterceptors;
    private AtomicReference<SchemaFieldFilter> schemaFieldFilter;
    private final SchemaGeneratorHelper schemaGeneratorHelper;

    public ComponentSchemaTransformer(List<SchemaFieldInterceptor> schemaFieldInterceptors,
                                      AtomicReference<SchemaFieldFilter> schemaFieldFilter,
                                      SchemaGeneratorHelper schemaGeneratorHelper) {
        this.schemaFieldInterceptors = schemaFieldInterceptors;
        this.schemaFieldFilter = schemaFieldFilter;
        this.schemaGeneratorHelper = schemaGeneratorHelper;
    }

    public Schema transformSimpleSchema(Class<?> clazz, Map<String, InheritanceInfo> inheritanceMap) {
        if (clazz.isEnum()) {
            return schemaGeneratorHelper.createEnumSchema(clazz.getEnumConstants());
        }
        List<String> requiredFields = new ArrayList<>();

        Schema<?> schema = new Schema<>();
        schema.setType("object");
        schema.setProperties(getClassProperties(clazz, requiredFields));
		schemaGeneratorHelper.enrichWithTypeAnnotations(schema, clazz.getDeclaredAnnotations());

        updateRequiredFields(schema, requiredFields);

        if (inheritanceMap.containsKey(clazz.getName())) {
            Discriminator discriminator = createDiscriminator(inheritanceMap.get(clazz.getName()));
            schema.setDiscriminator(discriminator);
            enrichWithDiscriminatorProperty(schema, discriminator);
        }
        if (clazz.getSuperclass() != null) {
            return traverseAndAddProperties(schema, inheritanceMap, clazz.getSuperclass(), clazz);
        }
        return schema;
    }

    private Discriminator createDiscriminator(InheritanceInfo inheritanceInfo) {
        Map<String, String> discriminatorTypeMapping = inheritanceInfo.getDiscriminatorClassMap().entrySet()
                .stream()
                .collect(Collectors.toMap(Map.Entry::getValue, Map.Entry::getKey));

        Discriminator discriminator = new Discriminator();
        discriminator.setPropertyName(inheritanceInfo.getDiscriminatorFieldName());
        discriminator.setMapping(discriminatorTypeMapping);
        return discriminator;
    }

    private void updateRequiredFields(Schema schema, List<String> requiredFields) {
        if (requiredFields == null || requiredFields.isEmpty()) {
            return;
        }
        if (schema.getRequired() == null) {
            schema.setRequired(requiredFields);
            return;
        }
        schema.getRequired().addAll(requiredFields);
    }

    private void updateSchemaProperties(Schema schema, String propertyName, Schema propertyValue) {
        if (StringUtils.isBlank(propertyName) || propertyValue == null) {
            return;
        }
        if (schema.getProperties() == null) {
            schema.setProperties(new HashMap<>());
        }
        schema.getProperties().put(propertyName, propertyValue);
    }

    private void enrichWithDiscriminatorProperty(Schema schema, Discriminator discriminator) {
        if (schema != null && !schema.getProperties().containsKey(discriminator.getPropertyName())) {
            List<String> discriminatorTypeRequiredProperty = new ArrayList<>();
            discriminatorTypeRequiredProperty.add(discriminator.getPropertyName());

            updateSchemaProperties(schema, discriminator.getPropertyName(), new StringSchema());
            updateRequiredFields(schema, discriminatorTypeRequiredProperty);
        }
    }

    private Schema<?> traverseAndAddProperties(Schema<?> schema, Map<String, InheritanceInfo> inheritanceMap, Class<?> superclass, Class<?> actualClass) {
        if (!schemaGeneratorHelper.isInPackagesToBeScanned(superclass)) {
            // adding properties from parent classes is present due to swagger ui bug, after using different ui
            // this becomes relevant only for third party packages
            List<String> requiredFields = new ArrayList<>();
            schema.getProperties().putAll(getClassProperties(superclass, requiredFields));
            updateRequiredFields(schema, requiredFields);
            if (superclass.getSuperclass() != null && !"java.lang".equals(superclass.getSuperclass().getPackage().getName())) {
                return traverseAndAddProperties(schema, inheritanceMap, superclass.getSuperclass(), superclass);
            }
            return schema;
        } else {
            Schema<?> parentClassSchema = new Schema<>();
            parentClassSchema.set$ref(COMPONENT_REF_PREFIX + superclass.getSimpleName());

            CustomComposedSchema composedSchema = new CustomComposedSchema();
            enrichWithAdditionalProperties(composedSchema, inheritanceMap, superclass.getName(), actualClass.getSimpleName());
            composedSchema.setAllOf(Arrays.asList(parentClassSchema, schema));
            composedSchema.setDescription(schema.getDescription());
            return composedSchema;
        }
    }

    private void enrichWithAdditionalProperties(CustomComposedSchema customComposedSchema, Map<String, InheritanceInfo> inheritanceInfoMap, String superClassName,
                                                String actualClassName) {
        if (inheritanceInfoMap.containsKey(superClassName)) {
            Map<String, String> discriminatorClassMap = inheritanceInfoMap.get(superClassName).getDiscriminatorClassMap();
            if (discriminatorClassMap.containsKey(actualClassName)) {
                customComposedSchema.setDiscriminatorValue(discriminatorClassMap.get(actualClassName));
            }
        }
    }

    private Map<String, Schema> getClassProperties(Class<?> clazz, List<String> requiredFields) {
        Map<String, Schema> classPropertyMap = new HashMap<>();
        ReflectionUtils.doWithLocalFields(clazz,
                field -> getFieldSchema(clazz, field, requiredFields).ifPresent(schema -> {
                    schemaFieldInterceptors.forEach(modelClassFieldInterceptor -> modelClassFieldInterceptor.intercept(clazz, field, schema));
                    classPropertyMap.put(field.getName(), schema);
                })
        );
        return classPropertyMap;
    }

    private Optional<Schema> getFieldSchema(Class<?> clazz, Field field, List<String> requiredFields) {
        if (shouldIgnoreField(clazz, field)) {
            return Optional.empty();
        }

        Class<?> typeSignature = field.getType();
        Annotation[] annotations = field.getAnnotations();
        if (isRequired(annotations)) {
            requiredFields.add(field.getName());
        }

        if (typeSignature.isPrimitive()) {
            return createBaseTypeSchema(field, requiredFields, annotations);
        } else if (typeSignature.isArray()) {
            return createArrayTypeSchema(typeSignature, annotations);
        } else if (StringUtils.equalsIgnoreCase(typeSignature.getName(), "java.lang.Object")) {
            ObjectSchema objectSchema = new ObjectSchema();
            objectSchema.setName(field.getName());
            return Optional.of(objectSchema);
        } else if (typeSignature.isAssignableFrom(List.class)) {
            if (field.getGenericType() instanceof ParameterizedType) {
                Class<?> listGenericParameter = (Class<?>) ((ParameterizedType) field.getGenericType()).getActualTypeArguments()[0];
                return Optional.of(schemaGeneratorHelper.parseArraySignature(listGenericParameter, annotations));
            }
            return Optional.empty();
        } else {
            return createClassRefSchema(typeSignature, annotations);
        }
    }

    private boolean shouldIgnoreField(Class<?> clazz, Field field) {
        if (shouldBeIgnored(field)) {
            return true;
        }

        return schemaFieldFilter.get() != null && schemaFieldFilter.get().shouldIgnore(clazz, field);
    }

    private Optional<Schema> createClassRefSchema(Class<?> typeClass, Annotation[] annotations) {
        Schema<?> schema = schemaGeneratorHelper.parseClassRefTypeSignature(typeClass, annotations);
        schemaGeneratorHelper.enrichWithTypeAnnotations(schema, annotations);
        return Optional.ofNullable(schema);
    }

    private Optional<Schema> createArrayTypeSchema(Class<?> typeSignature, Annotation[] annotations) {
        Class<?> arrayComponentType = typeSignature.getComponentType();
        Schema<?> schema = schemaGeneratorHelper.parseArraySignature(arrayComponentType, annotations);
        schemaGeneratorHelper.enrichWithTypeAnnotations(schema, annotations);
        return Optional.ofNullable(schema);
    }

    private Optional<Schema> createBaseTypeSchema(Field field, List<String> requiredFields, Annotation[] annotations) {
        if (!requiredFields.contains(field.getName())) {
            requiredFields.add(field.getName());
        }
        Schema<?> schema = schemaGeneratorHelper.parseBaseTypeSignature(field.getType(), annotations);
        schemaGeneratorHelper.enrichWithTypeAnnotations(schema, annotations);
        return Optional.ofNullable(schema);
    }

    private boolean isRequired(Annotation[] annotations) {
        return Stream.of(annotations).anyMatch(annotation -> annotation instanceof NotNull);
    }

}