package ru.ulstu.odin.service;

import com.fasterxml.jackson.annotation.JsonIgnore;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import ru.ulstu.core.error.OdinException;
import ru.ulstu.odin.model.*;
import ru.ulstu.odin.model.*;

import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.*;
import java.util.stream.Collectors;

@Service
public class OdinService<L, E extends OdinDto> {
    private final Logger log = LoggerFactory.getLogger(OdinService.class);

    private final Map<String, OdinField.OdinFieldType> aliases;

    public OdinService() {
        aliases = new HashMap<>();
        aliases.put("boolean", OdinField.OdinFieldType.BOOLEAN);
        aliases.put("date", OdinField.OdinFieldType.DATE);
        aliases.put("instant", OdinField.OdinFieldType.DATE);
        aliases.put("localdate", OdinField.OdinFieldType.DATE);
        aliases.put("localtime", OdinField.OdinFieldType.DATE);
        aliases.put("localdatetime", OdinField.OdinFieldType.DATE);
        aliases.put("int", OdinField.OdinFieldType.NUMERIC);
        aliases.put("integer", OdinField.OdinFieldType.NUMERIC);
        aliases.put("double", OdinField.OdinFieldType.NUMERIC);
        aliases.put("float", OdinField.OdinFieldType.NUMERIC);
        aliases.put("long", OdinField.OdinFieldType.NUMERIC);
        aliases.put("string", OdinField.OdinFieldType.STRING);
        aliases.put("collection", OdinField.OdinFieldType.COLLECTION);
        aliases.put("object", OdinField.OdinFieldType.OBJECT);
    }

    private OdinField.OdinFieldType getFieldTypeByTypeAlias(String fieldType) {
        return Optional.ofNullable(aliases.get(fieldType)).orElse(OdinField.OdinFieldType.UNKNOWN);
    }

    private String getTypeAlias(String fieldType) {
        return fieldType.replaceAll(".*\\.", "").toLowerCase().trim();
    }

    private OdinField.OdinFieldType getFieldType(Field field) {
        final String typeAlias;
        if (Collection.class.isAssignableFrom(field.getType())) {
            typeAlias = "collection";
        } else if (OdinDto.class.isAssignableFrom(field.getType())) {
            typeAlias = "object";
        } else {
            typeAlias = getTypeAlias(field.getGenericType().getTypeName());
        }
        return getFieldTypeByTypeAlias(typeAlias);
    }

    private <T> List<OdinField> getDtoMetaModel(Class<T> dtoClass) {
        return Arrays.stream(dtoClass.getDeclaredFields())
                .filter(field -> !field.isAnnotationPresent(JsonIgnore.class)
                        && !Modifier.isStatic(field.getModifiers()))
                .map(field -> {
                    OdinField.OdinFieldType fieldType = getFieldType(field);
                    switch (fieldType) {
                        case BOOLEAN:
                            return new OdinBooleanField(field);
                        case DATE:
                            return new OdinDateField(field);
                        case NUMERIC:
                            return new OdinNumericField(field);
                        case STRING:
                            return new OdinStringField(field);
                        case COLLECTION:
                            return new OdinCollectionField(field);
                        case OBJECT:
                            return new OdinObjectField(field);
                        default:
                            log.debug("Unknown type {}. Skip field {} of DTO {}.",
                                    field.getGenericType().getTypeName(), field.getName(), dtoClass.getSimpleName());
                    }
                    return null;
                })
                .filter(Objects::nonNull)
                .collect(Collectors.toList());
    }

    public OdinMetadata getListModel(Class<L> listDtoClass) {
        if (listDtoClass == null) {
            throw new OdinException("List DTO class is null");
        }
        return new OdinMetadata(OdinDto.class.isAssignableFrom(listDtoClass), getDtoMetaModel(listDtoClass));
    }

    public OdinMetadata getElementModel(Class<E> elementDtoClass) {
        return elementDtoClass == null
                ? null :
                new OdinMetadata(true, getDtoMetaModel(elementDtoClass));
    }
}
