package org.hildan.livedoc.core.builders.merger;

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

import org.hildan.livedoc.core.pojo.ApiStage;
import org.hildan.livedoc.core.pojo.ApiVerb;
import org.hildan.livedoc.core.pojo.ApiVisibility;
import org.hildan.livedoc.core.pojo.SpecialDefaultIntValue;
import org.hildan.livedoc.core.pojo.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 overridingSource
     *         the object to read the property values from
     * @param baseTarget
     *         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 overridingSource, T baseTarget) {
        assert overridingSource != null;
        assert baseTarget != null;
        List<Property> properties = propScanner.getProperties(overridingSource.getClass());
        for (Property prop : properties) {
            Field field = (Field) prop.getAccessibleObject();
            mergeField(field, overridingSource, baseTarget);
        }
    }

    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) {
        return targetValue == null || !sameClass(sourceValue, targetValue) || (isOfBasicType(sourceValue)
                && !isDefaultValue(field, sourceValue));
    }

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

    private static boolean isOfBasicType(Object obj) {
        Class<?> clazz = obj.getClass();
        return TypePredicates.isBasicType(clazz) || TypePredicates.isContainer(clazz);
    }

    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 instanceof ApiVerb) {
            return value == ApiVerb.UNDEFINED;
        }
        if (value instanceof ApiStage) {
            return value == ApiStage.UNDEFINED;
        }
        if (value instanceof ApiVisibility) {
            return value == ApiVisibility.UNDEFINED;
        }
        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);
        }
    }
}

@@ -124,20 +124,71 @@
 va.util.
-List
+Collection;%0Aimport java.util.List;%0Aimport java.util.Map
 ;%0A%0Aimpor
@@ -2642,23 +2642,20 @@
         
-return 
+if (
 targetVa
@@ -2669,57 +2669,147 @@
 null
- %7C%7C !sameClass(sourceValue, targetValue) %7C%7C (isOf
+) %7B%0A            return true;%0A        %7D%0A        if (sourceValue == null) %7B%0A            return false;%0A        %7D%0A        if (TypePredicates.is
 Basi
@@ -2821,25 +2821,39 @@
 (sourceValue
-)
+.getClass())) %7B
 %0A           
@@ -2857,14 +2857,14 @@
     
-    &&
+return
  !is
@@ -2899,45 +2899,163 @@
 lue)
-)
 ;%0A    
+   
+ 
 %7D%0A
-%0A    private static boolea
+        if (TypePredicates.isContainer(sourceValue.getClass())) %7B%0A            return !isEmptyContainer(sourceValue);%0A        %7D%0A        retur
 n 
+!
 same
@@ -3060,23 +3060,16 @@
 meClass(
-Object 
 sourceVa
@@ -3064,39 +3064,32 @@
 ass(sourceValue,
- Object
  targetValue) %7B%0A
@@ -3089,314 +3089,669 @@
 lue)
- %7B
+;
 %0A    
+%7D%0A%0A
     
-return sourceValue == null %7C%7C targetValue == null %7C%7C sourceValue.getClass().equals(targetValue.getClass());%0A    %7D%0A%0A    private static boolean isOfBasicType(Object obj) %7B%0A        Class%3C?%3E clazz = obj.getClass();%0A        return TypePredicates.isBasicType(clazz) %7C%7C TypePredicates.isContainer(clazz
+private static boolean isEmptyContainer(Object nonNullContainer) %7B%0A        if (nonNullContainer instanceof Collection) %7B%0A            return ((Collection)nonNullContainer).isEmpty();%0A        %7D%0A        if (nonNullContainer instanceof Map) %7B%0A            return ((Map)nonNullContainer).isEmpty();%0A        %7D%0A        if (nonNullContainer.getClass().isArray()) %7B%0A            return ((Object%5B%5D)nonNullContainer).length == 0;%0A        %7D%0A        return false;%0A    %7D%0A%0A    private static boolean sameClass(Object sourceValue, Object targetValue) %7B%0A        return sourceValue == null %7C%7C targetValue == null %7C%7C sourceValue.getClass().equals(targetValue.getClass()
 );%0A 

Fix DocMerger regarding collections