package ru.ulstu.odin.model;

import com.fasterxml.jackson.annotation.JsonProperty;
import org.hibernate.validator.constraints.NotBlank;
import org.hibernate.validator.constraints.NotEmpty;
import ru.ulstu.core.error.OdinException;
import ru.ulstu.odin.model.annotation.OdinCaption;
import ru.ulstu.odin.model.annotation.OdinReadOnly;
import ru.ulstu.odin.model.annotation.OdinVisible;

import javax.validation.constraints.NotNull;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Objects;
import java.util.Optional;

public abstract class OdinField implements Comparable {
    public enum OdinFieldType {
        BOOLEAN,
        DATE,
        NUMERIC,
        STRING,
        COLLECTION,
        OBJECT,
        UNKNOWN;

        @Override
        public String toString() {
            return this.name().toLowerCase();
        }
    }

    private Field field;
    protected final OdinFieldType fieldType;
    protected final String fieldName;
    protected final String caption;
    protected final OdinVisible.OdinVisibleType visible;
    protected final boolean readOnly;
    protected final boolean notEmpty;

    public OdinField(Field field, OdinFieldType fieldType) {
        this.field = field;
        this.fieldName = getFieldName(field);
        this.caption = Optional.ofNullable(getValueFromAnnotation(OdinCaption.class, "value"))
                .map(value -> cast(value, String.class))
                .orElse(fieldName);
        this.visible = getValue(OdinVisible.class, "type", OdinVisible.OdinVisibleType.class);
        this.readOnly = field.isAnnotationPresent(OdinReadOnly.class);
        this.notEmpty = field.isAnnotationPresent(NotBlank.class)
                || field.isAnnotationPresent(NotEmpty.class)
                || field.isAnnotationPresent(NotNull.class);
        this.fieldType = fieldType;
    }

    private String getFieldName(Field field) {
        if (field.isAnnotationPresent(JsonProperty.class)) {
            final String fieldName = cast(getValueFromAnnotation(JsonProperty.class, "value"), String.class);
            return Optional.ofNullable(fieldName).filter(value -> !value.isEmpty()).orElse(field.getName());
        }
        return field.getName();
    }

    private Object getDefaultValueFromAnnotation(Class<? extends Annotation> annotationClass, String valueName) {
        try {
            final Method method = annotationClass.getDeclaredMethod(valueName);
            return method.getDefaultValue();
        } catch (NoSuchMethodException e) {
            throw new OdinException(String.format("Default value %s is not found in annotation %s",
                    valueName, annotationClass.getSimpleName()));
        }
    }

    private Object getValueFromAnnotation(Class<? extends Annotation> annotationClass, String valueName) {
        if (!field.isAnnotationPresent(annotationClass)) {
            return null;
        }
        try {
            final Method method = annotationClass.getDeclaredMethod(valueName);
            return method.invoke(field.getAnnotation(annotationClass));
        } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
            throw new OdinException(String.format("Value %s is not found in annotation %s",
                    valueName, annotationClass.getSimpleName()));
        }
    }

    private <T> T cast(Object value, Class<T> clazz) {
        try {
            return clazz.cast(value);
        } catch (ClassCastException e) {
            return null;
        }
    }

    protected <T> T getValue(Class<? extends Annotation> annotationClass, String valueName, Class<T> clazz) {
        if (field.isAnnotationPresent(annotationClass)) {
            return cast(getValueFromAnnotation(annotationClass, valueName), clazz);
        }
        return cast(getDefaultValueFromAnnotation(annotationClass, valueName), clazz);
    }

    public String getFieldType() {
        return fieldType.toString();
    }

    public String getFieldName() {
        return fieldName;
    }

    public String getCaption() {
        return caption;
    }

    public String getVisible() {
        return visible.toString();
    }

    public boolean isReadOnly() {
        return readOnly;
    }

    public boolean isNotEmpty() {
        return notEmpty;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        OdinField odinField = (OdinField) o;
        return Objects.equals(fieldName, odinField.fieldName);
    }

    @Override
    public int hashCode() {
        return Objects.hash(fieldName);
    }

    @Override
    public String toString() {
        return getClass().getSimpleName() + " {" +
                "fieldName='" + fieldName + '\'' +
                ", fieldType='" + fieldType + '\'' +
                '}';
    }

    @Override
    public int compareTo(Object o) {
        final String thisName = Optional.ofNullable(fieldName).orElse("");
        final String oName = Optional.ofNullable(o).map(obj -> ((OdinField) obj).fieldName).orElse("");
        return thisName.compareTo(oName);
    }
}
