package jdiff;

import java.util.*;

/**
 * Convert some remove and add operations into change operations.
 *
 * Once the numbers of members removed and added are known
 * we can deduce more information about changes. For instance, if there are
 * two methods with the same name, and one or more of them has a 
 * parameter type change, then this can only be reported as removing 
 * the old version(s) and adding the new version(s), because there are 
 * multiple methods with the same name. 
 *
 * However, if only <i>one</i> method with a given name is removed, and  
 * only <i>one</i> method with the same name is added, we can convert these
 * operations to a change operation. For constructors, this is true if 
 * the types are the same. For fields, the field names have to be the same, 
 * though this should never occur, since field names are unique.
 *
 * Another merge which can be made is if two or more methods with the same name
 * were marked as removed and added because of changes other than signature.
 *
 * See the file LICENSE.txt for copyright details.
 * @author Matthew Doar, mdoar@pobox.com
 */
class MergeChanges {

    /**
     * Convert some remove and add operations into change operations.
     *
     * Note that if a single thread modifies a collection directly while it is 
     * iterating over the collection with a fail-fast iterator, the iterator 
     * will throw java.util.ConcurrentModificationException   
     */
    public static void mergeRemoveAdd(APIDiff apiDiff) {
        // Go through all the ClassDiff objects searching for the above cases.
        Iterator iter = apiDiff.packagesChanged.iterator();
        while (iter.hasNext()) {
            PackageDiff pkgDiff = (PackageDiff)(iter.next());
            Iterator iter2 = pkgDiff.classesChanged.iterator();
            while (iter2.hasNext()) {
                ClassDiff classDiff = (ClassDiff)(iter2.next());
                // Note: using iterators to step through the members gives a
                // ConcurrentModificationException exception with large files.
                // Constructors
                ConstructorAPI[] ctorArr = new ConstructorAPI[classDiff.ctorsRemoved.size()];
                ctorArr = (ConstructorAPI[])classDiff.ctorsRemoved.toArray(ctorArr);
                for (int ctorIdx = 0; ctorIdx < ctorArr.length; ctorIdx++) {
                    ConstructorAPI removedCtor = ctorArr[ctorIdx];
                    mergeRemoveAddCtor(removedCtor, classDiff, pkgDiff);
                }
                // Methods
                MethodAPI[] methodArr = new MethodAPI[classDiff.methodsRemoved.size()];
                methodArr = (MethodAPI[])classDiff.methodsRemoved.toArray(methodArr);
                for (int methodIdx = 0; methodIdx < methodArr.length; methodIdx++) {
                    MethodAPI removedMethod = methodArr[methodIdx];
                    // Only merge locally defined methods
                    if (removedMethod.inheritedFrom_ == null)
                        mergeRemoveAddMethod(removedMethod, classDiff, pkgDiff);
                }
                // Fields
                FieldAPI[] fieldArr = new FieldAPI[classDiff.fieldsRemoved.size()];
                fieldArr = (FieldAPI[])classDiff.fieldsRemoved.toArray(fieldArr);
                for (int fieldIdx = 0; fieldIdx < fieldArr.length; fieldIdx++) {
                    FieldAPI removedField = fieldArr[fieldIdx]; 
                    // Only merge locally defined fields
                    if (removedField.inheritedFrom_ == null)
                        mergeRemoveAddField(removedField, classDiff, pkgDiff);
                }
            }
        }        
    }

    /**
     * Convert some removed and added constructors into changed constructors.
     */
    public static void mergeRemoveAddCtor(ConstructorAPI removedCtor, ClassDiff classDiff, PackageDiff pkgDiff) {
        // Search on the type of the constructor
        int startRemoved = classDiff.ctorsRemoved.indexOf(removedCtor);
        int endRemoved = classDiff.ctorsRemoved.lastIndexOf(removedCtor);
        int startAdded = classDiff.ctorsAdded.indexOf(removedCtor);
        int endAdded = classDiff.ctorsAdded.lastIndexOf(removedCtor);
        if (startRemoved != -1 && startRemoved == endRemoved && 
            startAdded != -1 && startAdded == endAdded) {
            // There is only one constructor with the type of the
            // removedCtor in both the removed and added constructors.
            ConstructorAPI addedCtor = (ConstructorAPI)(classDiff.ctorsAdded.get(startAdded));
            // Create a MemberDiff for this change
            MemberDiff ctorDiff = new MemberDiff(classDiff.name_);
            ctorDiff.oldType_ = removedCtor.getSignature();
            ctorDiff.newType_ = addedCtor.getSignature(); // Should be the same as removedCtor.type
            ctorDiff.oldExceptions_ = removedCtor.exceptions_;
            ctorDiff.newExceptions_ = addedCtor.exceptions_;
            ctorDiff.addModifiersChange(removedCtor.modifiers_.diff(addedCtor.modifiers_));
            // Track changes in documentation
            if (APIComparator.docChanged(removedCtor.doc_, addedCtor.doc_)) {
                String type = ctorDiff.newType_;
                if (type.compareTo("void") == 0)
                    type = "";
                String fqName = pkgDiff.name_ + "." + classDiff.name_;
                String link1 = "<a href=\"" + fqName + HTMLReportGenerator.reportFileExt + "\" class=\"hiddenlink\">";
                String link2 = "<a href=\"" + fqName + HTMLReportGenerator.reportFileExt + "#" + fqName + ".ctor_changed(" + type + ")\" class=\"hiddenlink\">";
                String id = pkgDiff.name_ + "." + classDiff.name_ + ".ctor(" + HTMLReportGenerator.simpleName(type) + ")";
                String title = link1 + "Class <b>" + classDiff.name_ + 
                    "</b></a>, " + link2 + "constructor <b>" + classDiff.name_ + "(" + HTMLReportGenerator.simpleName(type) + ")</b></a>";
                ctorDiff.documentationChange_ = Diff.saveDocDiffs(pkgDiff.name_, classDiff.name_, removedCtor.doc_, addedCtor.doc_, id, title);
            }
            classDiff.ctorsChanged.add(ctorDiff);
            // Now remove the entries from the remove and add lists
            classDiff.ctorsRemoved.remove(startRemoved);
            classDiff.ctorsAdded.remove(startAdded);
            if (trace && ctorDiff.modifiersChange_ != null)
                System.out.println("Merged the removal and addition of constructor into one change: " + ctorDiff.modifiersChange_);
        }
    }

    /**
     * Convert some removed and added methods into changed methods.
     */
    public static void mergeRemoveAddMethod(MethodAPI removedMethod, 
                                            ClassDiff classDiff, 
                                            PackageDiff pkgDiff) {
        mergeSingleMethods(removedMethod, classDiff, pkgDiff);
        mergeMultipleMethods(removedMethod, classDiff, pkgDiff);
    }

    /**
     * Convert single removed and added methods into a changed method.
     */
    public static void mergeSingleMethods(MethodAPI removedMethod, ClassDiff classDiff, PackageDiff pkgDiff) {
        // Search on the name of the method
        int startRemoved = classDiff.methodsRemoved.indexOf(removedMethod);
        int endRemoved = classDiff.methodsRemoved.lastIndexOf(removedMethod);
        int startAdded = classDiff.methodsAdded.indexOf(removedMethod);
        int endAdded = classDiff.methodsAdded.lastIndexOf(removedMethod);
        if (startRemoved != -1 && startRemoved == endRemoved && 
            startAdded != -1 && startAdded == endAdded) {
            // There is only one method with the name of the
            // removedMethod in both the removed and added methods.
            MethodAPI addedMethod = (MethodAPI)(classDiff.methodsAdded.get(startAdded));
            if (addedMethod.inheritedFrom_ == null) {
                // Create a MemberDiff for this change
                MemberDiff methodDiff = new MemberDiff(removedMethod.name_);
                methodDiff.oldType_ = removedMethod.returnType_;
                methodDiff.newType_ = addedMethod.returnType_;
                methodDiff.oldSignature_ = removedMethod.getSignature();
                methodDiff.newSignature_ = addedMethod.getSignature();
                methodDiff.oldExceptions_ = removedMethod.exceptions_;
                methodDiff.newExceptions_ = addedMethod.exceptions_;
                // The addModifiersChange field may not have been
                // initialized yet if there were multiple methods of the same
                // name.
                diffMethods(methodDiff, removedMethod, addedMethod);
                methodDiff.addModifiersChange(removedMethod.modifiers_.diff(addedMethod.modifiers_));
                // Track changes in documentation
                if (APIComparator.docChanged(removedMethod.doc_, addedMethod.doc_)) {
                    String sig = methodDiff.newSignature_;
                    if (sig.compareTo("void") == 0)
                        sig = "";
                    String fqName = pkgDiff.name_ + "." + classDiff.name_;
                    String link1 = "<a href=\"" + fqName + HTMLReportGenerator.reportFileExt + "\" class=\"hiddenlink\">";
                    String link2 = "<a href=\"" + fqName + HTMLReportGenerator.reportFileExt + "#" + fqName + "." + addedMethod.name_ + "_changed(" + sig + ")\" class=\"hiddenlink\">";
                    String id = pkgDiff.name_ + "." + classDiff.name_ + ".dmethod." + addedMethod.name_ + "(" + HTMLReportGenerator.simpleName(sig) + ")";
                    String title = link1 + "Class <b>" + classDiff.name_ + "</b></a>, " +
                        link2 +  HTMLReportGenerator.simpleName(methodDiff.newType_) + " <b>" + addedMethod.name_ + "(" + HTMLReportGenerator.simpleName(sig) + ")</b></a>";
                    methodDiff.documentationChange_ = Diff.saveDocDiffs(pkgDiff.name_, classDiff.name_, removedMethod.doc_, addedMethod.doc_, id, title);
                }
                classDiff.methodsChanged.add(methodDiff);
                // Now remove the entries from the remove and add lists
                classDiff.methodsRemoved.remove(startRemoved);
                classDiff.methodsAdded.remove(startAdded);
                if (trace) {
                    System.out.println("Merged the removal and addition of method " + 
                                       removedMethod.name_ + 
                                       " into one change");
                }
            } //if (addedMethod.inheritedFrom_ == null)
        }
    }

    /**
     * Convert multiple removed and added methods into changed methods.
     * This handles the case where the methods' signatures are unchanged, but
     * something else changed.
     */
    public static void mergeMultipleMethods(MethodAPI removedMethod, ClassDiff classDiff, PackageDiff pkgDiff) {
        // Search on the name and signature of the method
        int startRemoved = classDiff.methodsRemoved.indexOf(removedMethod);
        int endRemoved = classDiff.methodsRemoved.lastIndexOf(removedMethod);
        int startAdded = classDiff.methodsAdded.indexOf(removedMethod);
        int endAdded = classDiff.methodsAdded.lastIndexOf(removedMethod);
        if (startRemoved != -1 && endRemoved != -1 &&
            startAdded != -1 && endAdded != -1) {
            // Find the index of the current removed method
            int removedIdx = -1;
            for (int i = startRemoved; i <= endRemoved; i++) {
                if (removedMethod.equalSignatures(classDiff.methodsRemoved.get(i))) {
                    removedIdx = i;
                    break;
                }
            }
            if (removedIdx == -1) {
                System.out.println("Error: removed method index not found");
                System.exit(5);
            }
            // Find the index of the added method with the same signature, if
            // it exists, and make sure it is defined locally.
            int addedIdx = -1;
            for (int i = startAdded; i <= endAdded; i++) {
                MethodAPI addedMethod2 = (MethodAPI)(classDiff.methodsAdded.get(i));
                if (addedMethod2.inheritedFrom_ == null &&
                    removedMethod.equalSignatures(addedMethod2)) {
                    addedIdx = i;
                    break;
                }
            }
            if (addedIdx == -1)
                return;
            MethodAPI addedMethod = (MethodAPI)(classDiff.methodsAdded.get(addedIdx));
            // Create a MemberDiff for this change
            MemberDiff methodDiff = new MemberDiff(removedMethod.name_);
            methodDiff.oldType_ = removedMethod.returnType_;
            methodDiff.newType_ = addedMethod.returnType_;
            methodDiff.oldSignature_ = removedMethod.getSignature();
            methodDiff.newSignature_ = addedMethod.getSignature();
            methodDiff.oldExceptions_ = removedMethod.exceptions_;
            methodDiff.newExceptions_ = addedMethod.exceptions_;
                // The addModifiersChange field may not have been
                // initialized yet if there were multiple methods of the same
                // name.
                diffMethods(methodDiff, removedMethod, addedMethod);
            methodDiff.addModifiersChange(removedMethod.modifiers_.diff(addedMethod.modifiers_));
            // Track changes in documentation
            if (APIComparator.docChanged(removedMethod.doc_, addedMethod.doc_)) {
                String sig = methodDiff.newSignature_;
                if (sig.compareTo("void") == 0)
                    sig = "";
                String fqName = pkgDiff.name_ + "." + classDiff.name_;
                String link1 = "<a href=\"" + fqName + HTMLReportGenerator.reportFileExt + "\" class=\"hiddenlink\">";
                String link2 = "<a href=\"" + fqName + HTMLReportGenerator.reportFileExt + "#" + fqName + "." + addedMethod.name_ + "_changed(" + sig + ")\" class=\"hiddenlink\">";
                String id = pkgDiff.name_ + "." + classDiff.name_ + ".dmethod." + addedMethod.name_ + "(" + HTMLReportGenerator.simpleName(sig) + ")";
                String title = link1 + "Class <b>" + classDiff.name_ + "</b></a>, " +
                    link2 +  HTMLReportGenerator.simpleName(methodDiff.newType_) + " <b>" + addedMethod.name_ + "(" + HTMLReportGenerator.simpleName(sig) + ")</b></a>";
                methodDiff.documentationChange_ = Diff.saveDocDiffs(pkgDiff.name_, classDiff.name_, removedMethod.doc_, addedMethod.doc_, id, title);
            }
            classDiff.methodsChanged.add(methodDiff);
            // Now remove the entries from the remove and add lists
            classDiff.methodsRemoved.remove(removedIdx);
            classDiff.methodsAdded.remove(addedIdx);
            if (trace) {
                System.out.println("Merged the removal and addition of method " + 
                                   removedMethod.name_ + 
                                   " into one change. There were multiple methods of this name.");
            }
        }
    }

    /**
     * Track changes in methods related to abstract, native, and 
     * synchronized modifiers here.
     */
    public static void diffMethods(MemberDiff methodDiff, 
                                   MethodAPI oldMethod, 
                                   MethodAPI newMethod) {
        // Abstract or not
        if (oldMethod.isAbstract_ != newMethod.isAbstract_) {
            String changeText = "";
            if (oldMethod.isAbstract_)
                changeText += "Changed from abstract to non-abstract.";
            else
                changeText += "Changed from non-abstract to abstract.";
            methodDiff.addModifiersChange(changeText);
        }
        // Native or not
        if (Diff.showAllChanges && 
        oldMethod.isNative_ != newMethod.isNative_) {
            String changeText = "";
            if (oldMethod.isNative_)
                changeText += "Changed from native to non-native.";
            else
                changeText += "Changed from non-native to native.";
            methodDiff.addModifiersChange(changeText);
        }
        // Synchronized or not
        if (Diff.showAllChanges && 
        oldMethod.isSynchronized_ != newMethod.isSynchronized_) {
            String changeText = "";
            if (oldMethod.isSynchronized_)
                changeText += "Changed from synchronized to non-synchronized.";
            else
                changeText += "Changed from non-synchronized to synchronized.";
            methodDiff.addModifiersChange(changeText);
        }
    }

    /**
     * Convert some removed and added fields into changed fields.
     */
    public static void mergeRemoveAddField(FieldAPI removedField, ClassDiff classDiff, PackageDiff pkgDiff) {
        // Search on the name of the field
        int startRemoved = classDiff.fieldsRemoved.indexOf(removedField);
        int endRemoved = classDiff.fieldsRemoved.lastIndexOf(removedField);
        int startAdded = classDiff.fieldsAdded.indexOf(removedField);
        int endAdded = classDiff.fieldsAdded.lastIndexOf(removedField);
        if (startRemoved != -1 && startRemoved == endRemoved && 
            startAdded != -1 && startAdded == endAdded) {
            // There is only one field with the name of the
            // removedField in both the removed and added fields.
            FieldAPI addedField = (FieldAPI)(classDiff.fieldsAdded.get(startAdded));
            if (addedField.inheritedFrom_ == null) {
                // Create a MemberDiff for this change
                MemberDiff fieldDiff = new MemberDiff(removedField.name_);
                fieldDiff.oldType_ = removedField.type_;
                fieldDiff.newType_ = addedField.type_;
                fieldDiff.addModifiersChange(removedField.modifiers_.diff(addedField.modifiers_));
                // Track changes in documentation
                if (APIComparator.docChanged(removedField.doc_, addedField.doc_)) {
                    String fqName = pkgDiff.name_ + "." + classDiff.name_;
                    String link1 = "<a href=\"" + fqName + HTMLReportGenerator.reportFileExt + "\" class=\"hiddenlink\">";
                    String link2 = "<a href=\"" + fqName + HTMLReportGenerator.reportFileExt + "#" + fqName + "." + addedField.name_ + "\" class=\"hiddenlink\">";
                    String id = pkgDiff.name_ + "." + classDiff.name_ + ".field." + addedField.name_;
                    String title = link1 + "Class <b>" + classDiff.name_ + "</b></a>, " +
                        link2 + HTMLReportGenerator.simpleName(fieldDiff.newType_) + " <b>" + addedField.name_ + "</b></a>";
                    fieldDiff.documentationChange_ = Diff.saveDocDiffs(pkgDiff.name_, classDiff.name_, removedField.doc_, addedField.doc_, id, title);
                }
                classDiff.fieldsChanged.add(fieldDiff);
                // Now remove the entries from the remove and add lists
                classDiff.fieldsRemoved.remove(startRemoved);
                classDiff.fieldsAdded.remove(startAdded);
                if (trace) {
                    System.out.println("Merged the removal and addition of field " + 
                                       removedField.name_ + 
                                       " into one change");
                }
            } //if (addedField.inheritedFrom == null) 
        }
    }

    /** Set to enable increased logging verbosity for debugging. */
    private static boolean trace = false;

}