package org.hildan.livedoc.core;

import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.util.Collection;
import java.util.List;
import java.util.Map;

import org.hildan.livedoc.core.model.doc.SpecialDefaultIntValue;
import org.hildan.livedoc.core.model.doc.SpecialDefaultStringValue;
import org.hildan.livedoc.core.scanners.properties.Property;
import org.hildan.livedoc.core.scanners.properties.PropertyScanner;
import org.hildan.livedoc.core.scanners.types.predicates.TypePredicates;

public class DocMerger {

    private final PropertyScanner propScanner;

    public DocMerger(PropertyScanner propScanner) {
        this.propScanner = propScanner;
    }

    /**
     * Overrides each property of the given target if the given source has a meaningful (non default) value for the
     * property.
     *
     * @param source
     *         the object to read the property values from
     * @param target
     *         the object to set properties on (if there is something meaningful to put in place of the previous value)
     * @param <T>
     *         the type of objects to manipulate
     */
    public <T> void merge(T source, T target) {
        assert source != null;
        assert target != null;
        List<Property> properties = propScanner.getProperties(source.getClass());
        for (Property prop : properties) {
            Field field = (Field) prop.getAccessibleObject();
            mergeField(field, source, target);
        }
    }

    private <T> void mergeField(Field field, T source, T target) {
        try {
            field.setAccessible(true);
            Object sourceValue = field.get(source);
            Object targetValue = field.get(target);
            if (sourceValue != null) {
                if (shouldReplaceWithoutMerge(field, sourceValue, targetValue)) {
                    field.set(target, sourceValue);
                } else if (!isDefaultValue(field, sourceValue)) {
                    merge(sourceValue, targetValue);
                }
            }
        } catch (IllegalAccessException e) {
            String msg = String.format("Could not copy field '%s' of the given object of type %s", field.getName(),
                    source.getClass().getSimpleName());
            throw new FieldCopyException(msg, e);
        }
    }

    private static boolean shouldReplaceWithoutMerge(Field field, Object sourceValue, Object targetValue) {
        if (targetValue == null) {
            return true;
        }
        if (sourceValue == null) {
            return false;
        }
        if (TypePredicates.isBasicType(sourceValue.getClass())) {
            return !isDefaultValue(field, sourceValue);
        }
        if (TypePredicates.isContainer(sourceValue.getClass())) {
            return !isEmptyContainer(sourceValue);
        }
        return !sameClass(sourceValue, targetValue);
    }

    private static boolean isEmptyContainer(Object nonNullContainer) {
        if (nonNullContainer instanceof Collection) {
            return ((Collection) nonNullContainer).isEmpty();
        }
        if (nonNullContainer instanceof Map) {
            return ((Map) nonNullContainer).isEmpty();
        }
        if (nonNullContainer.getClass().isArray()) {
            return ((Object[]) nonNullContainer).length == 0;
        }
        return false;
    }

    private static boolean sameClass(Object sourceValue, Object targetValue) {
        return sourceValue == null || targetValue == null || sourceValue.getClass().equals(targetValue.getClass());
    }

    private static boolean isDefaultValue(Field field, Object value) {
        SpecialDefaultStringValue specialDefaultStr = field.getAnnotation(SpecialDefaultStringValue.class);
        if (specialDefaultStr != null) {
            return specialDefaultStr.value().equals(value);
        }
        SpecialDefaultIntValue specialDefaultInt = field.getAnnotation(SpecialDefaultIntValue.class);
        if (specialDefaultInt != null) {
            return Integer.valueOf(specialDefaultInt.value()).equals(value);
        }
        return isDefaultValueOfItsType(value);
    }

    private static boolean isDefaultValueOfItsType(Object value) {
        if (value instanceof CharSequence) {
            return ((CharSequence) value).length() == 0;
        }
        if (value instanceof Boolean) {
            return Boolean.FALSE.equals(value);
        }
        if (value.getClass().isArray()) {
            return Array.getLength(value) == 0;
        }
        return false;
    }

    public static class FieldCopyException extends RuntimeException {
        FieldCopyException(String msg, Throwable cause) {
            super(msg, cause);
        }
    }
}

@@ -500,16 +500,101 @@
 dicates;
+%0Aimport org.jetbrains.annotations.NotNull;%0Aimport org.jetbrains.annotations.Nullable;
 %0A%0Apublic
@@ -2454,32 +2454,42 @@
 rge(Field field,
+ @Nullable
  Object sourceVa
@@ -2484,32 +2484,54 @@
 ect sourceValue,
+%0A            @Nullable
  Object targetVa
@@ -3045,23 +3045,25 @@
 ner(
-Object nonNullC
+@NotNull Object c
 onta
@@ -3078,32 +3078,25 @@
         if (
-nonNullC
+c
 ontainer ins
@@ -3150,24 +3150,17 @@
 ection) 
-nonNullC
+c
 ontainer
@@ -3190,32 +3190,25 @@
         if (
-nonNullC
+c
 ontainer ins
@@ -3248,24 +3248,17 @@
  ((Map) 
-nonNullC
+c
 ontainer
@@ -3292,24 +3292,17 @@
     if (
-nonNullC
+c
 ontainer
@@ -3361,16 +3361,9 @@
 %5B%5D) 
-nonNullC
+c
 onta
@@ -3457,16 +3457,25 @@
 meClass(
+@NotNull 
 Object s
@@ -3485,16 +3485,25 @@
 ceValue,
+ @NotNull
  Object 
@@ -3535,54 +3535,8 @@
 turn
- sourceValue == null %7C%7C targetValue == null %7C%7C
  sou
@@ -3648,16 +3648,25 @@
 d field,
+ @NotNull
  Object 
@@ -4228,16 +4228,25 @@
 ItsType(
+@NotNull 
 Object v

Add null analysis to DocMerger