/*
 * Copyright 2015 JBoss Inc
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
*/

package org.jbpm.services.task.jaxb;

import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.fail;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Queue;

public class ComparePair {

    private Object orig;
    private Object copy;
    private Class<?> objInterface;

    private String[] nullFields = null;
    private boolean useGetMethods = true;

    public static void compareOrig(Object origObj, Object newObj, Class objClass) {
        ComparePair compare = new ComparePair(origObj, newObj, objClass);
        Queue<ComparePair> compares = new LinkedList<ComparePair>();
        compares.add(compare);
        while (!compares.isEmpty()) {
            compares.addAll(compares.poll().compare());
        }
    }

    public ComparePair(Object a, Object b, Class<?> c) {
        this.orig = a;
        this.copy = b;
        this.objInterface = c;
    }

    public List<ComparePair> compare() {
        if (useGetMethods) {
            return compareObjectsViaGetMethods(orig, copy, objInterface);
        } else {
            compareObjectsViaFields(orig, copy, nullFields);
            return null;
        }
    }

    public void recursiveCompare() {
        Queue<ComparePair> compares = new LinkedList<ComparePair>();
        compares.add(this);
        while (!compares.isEmpty()) {
            compares.addAll(compares.poll().compare());
        }
    }

    public void addNullFields(String... fieldNames) {
        if (fieldNames == null) {
            return;
        }
        if (nullFields == null) {
            nullFields = new String[0];
        }
        String[] newNullFields = new String[nullFields.length + fieldNames.length];
        for (int i = 0; i < nullFields.length; ++i) {
            newNullFields[i] = nullFields[i];
        }
        for (int i = 0; i < fieldNames.length; ++i) {
            newNullFields[i + nullFields.length] = fieldNames[i];
        }
        this.nullFields = newNullFields;
    }

    private List<ComparePair> compareObjectsViaGetMethods(Object orig, Object copy, Class<?> objInterface) {
        List<ComparePair> cantCompare = new ArrayList<ComparePair>();
        for (Method getIsMethod : objInterface.getDeclaredMethods()) {
            String methodName = getIsMethod.getName();
            String fieldName;
            if (methodName.startsWith("get")) {
                fieldName = methodName.substring(3);
            } else if (methodName.startsWith("is")) {
                fieldName = methodName.substring(2);
            } else {
                continue;
            }
            // getField -> field (lowercase f)
            fieldName = fieldName.substring(0, 1).toLowerCase() + fieldName.substring(1);
            try {
                Object origField = getIsMethod.invoke(orig, new Object[0]);
                Object copyField = getIsMethod.invoke(copy, new Object[0]);
                boolean skip = false;
                if (origField == null) {
                    if (nullFields != null) {
                        for (String nullField : nullFields) {
                            if (fieldName.equals(nullField)) {
                                skip = true;
                                break;
                            }
                        }
                    }
                    if (!skip) {
                        fail("Please fill in the " + fieldName + " field in the " + objInterface.getSimpleName() + "!");
                    }
                }
                if (skip) {
                    continue;
                }
                if (origField != null && !(origField instanceof Enum) && origField.getClass().getPackage().getName().startsWith("org.")) {
                    ComparePair newComPair = new ComparePair(origField, copyField, getInterface(origField));
                    newComPair.addNullFields(this.nullFields);
                    cantCompare.add(newComPair);
                    continue;
                } else if (origField instanceof List<?>) {
                    List<?> origList = (List) origField;
                    List<?> copyList = (List) copyField;
                    for (int i = 0; i < origList.size(); ++i) {
                        Class<?> newInterface = origField.getClass();
                        while (newInterface.getInterfaces().length > 0) {
                            newInterface = newInterface.getInterfaces()[0];
                        }
                        ComparePair newCompair = new ComparePair(origList.get(i), copyList.get(i), getInterface(origList.get(i)));
                        newCompair.addNullFields(this.nullFields);
                        cantCompare.add(newCompair);
                    }
                    continue;
                }
                assertEquals(fieldName, origField, copyField);
            } catch (Exception e) {
                throw new RuntimeException("Unable to compare " + fieldName, e);
            }
        }
        return cantCompare;
    }

    private Class<?> getInterface(Object obj) {
        Class<?> newInterface = obj.getClass();
        Class<?> parent = newInterface;
        while (parent != null) {
            parent = null;
            if (newInterface.getInterfaces().length > 0) {
                Class<?> newParent = newInterface.getInterfaces()[0];
                if (newParent.getPackage().getName().startsWith("org.")) {
                    parent = newInterface = newParent;
                }
            }
        }
        return newInterface;
    }

    public static void compareObjectsViaFields(Object orig, Object copy) {
        compareObjectsViaFields(orig, copy, new String[] {});
    }

    public static void compareObjectsViaFields(Object orig, Object copy, String... skipFields) {
        Class<?> origClass = orig.getClass();
        assertEquals("copy is not an instance of " + origClass + " (" + copy.getClass().getSimpleName() + ")", origClass,
                copy.getClass());
        for (Field field : orig.getClass().getDeclaredFields()) {
            try {
                field.setAccessible(true);
                Object origFieldVal = field.get(orig);
                Object copyFieldVal = field.get(copy);
                String fieldName = field.getName();

                boolean skip = false;
                for (String skipFieldName : skipFields) {
                    if (skipFieldName.matches(fieldName)) {
                        skip = true;
                        break;
                    }
                }
                if (skip) {
                    continue;
                }

                boolean nullFound = false;
                if (origFieldVal == null || copyFieldVal == null) {
                    nullFound = true;
                    for (String nullFieldName : skipFields) {
                        if (nullFieldName.matches(fieldName)) {
                            nullFound = false;
                        }
                    }
                }
                String failMsg = origClass.getSimpleName() + "." + field.getName() + " is null";
                assertFalse(failMsg + "!", nullFound);

                if (copyFieldVal != origFieldVal) {
                    assertNotNull(failMsg + "in copy!", copyFieldVal);
                    assertNotNull(failMsg + "in original!", origFieldVal);
                    Package pkg = origFieldVal.getClass().getPackage();
                    if (pkg == null || pkg.getName().startsWith("java.")) {
                        if( origFieldVal.getClass().isArray() ) { 
                            if( origFieldVal instanceof byte[] ) { 
                                assertArrayEquals(origClass.getSimpleName() + "." + field.getName(), (byte []) origFieldVal, (byte []) copyFieldVal);
                            } 
                        } else if( origFieldVal instanceof Map<?, ?> && copyFieldVal instanceof Map<?, ?> ) { 
                            org.apache.commons.collections.CollectionUtils.disjunction(
                                    ((Map) origFieldVal).values(),
                                    ((Map) copyFieldVal).values());
                        } else {
                            assertEquals(origClass.getSimpleName() + "." + field.getName(), origFieldVal, copyFieldVal);
                        }
                    } else {
                        compareObjectsViaFields(origFieldVal, copyFieldVal, skipFields);
                    }
                }
            } catch (Exception e) {
                throw new RuntimeException("Unable to access " + field.getName() + " when testing " + origClass.getSimpleName()
                        + ".", e);
            }
        }
    }
    
}