/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   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.apache.aries.versioning.utils;

import java.io.IOException;
import java.lang.reflect.Modifier;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.objectweb.asm.ClassReader;
import org.objectweb.asm.Type;

public class ClassDeclaration extends GenericDeclaration {

    // Binary Compatibility - deletion of package-level access field/method/constructors of classes and interfaces in the package
    // will not break binary compatibility when an entire package is updated.

    // Assumptions:
    // 1.  This tool assumes that the deletion of package-level fields/methods/constructors is not break binary compatibility
    // based on the assumption of the entire package is updated.
    //


    private final String superName;
    private final String[] interfaces;
    private final Map<String, FieldDeclaration> fields;
    private final Map<String, Set<MethodDeclaration>> methods;

    private final Map<String, Set<MethodDeclaration>> methodsInUpperChain = new HashMap<String, Set<MethodDeclaration>>();
    private final Map<String, FieldDeclaration> fieldsInUpperChain = new HashMap<String, FieldDeclaration>();
    private final Collection<String> supers = new ArrayList<String>();


    private final URLClassLoader jarsLoader;

    private final SerialVersionClassVisitor serialVisitor;

    public Map<String, FieldDeclaration> getFields() {
        return fields;
    }

    public Map<String, FieldDeclaration> getAllFields() {
        Map<String, FieldDeclaration> allFields = new HashMap<String, FieldDeclaration>(getFields());
        Map<String, FieldDeclaration> fieldsFromSupers = getFieldsInUpperChain();
        putIfAbsent(allFields, fieldsFromSupers);


        return allFields;
    }

    private void putIfAbsent(Map<String, FieldDeclaration> allFields,
                             Map<String, FieldDeclaration> fieldsFromSupers) {
        for (Map.Entry<String, FieldDeclaration> superFieldEntry : fieldsFromSupers.entrySet()) {
            String fieldName = superFieldEntry.getKey();
            FieldDeclaration fd = superFieldEntry.getValue();
            if (allFields.get(fieldName) == null) {
                allFields.put(fieldName, fd);
            }

        }
    }

    /**
     * Get the methods in the current class plus the methods in the upper chain
     *
     * @return map of method name to set of method declarations
     */
    public Map<String, Set<MethodDeclaration>> getAllMethods() {

        Map<String, Set<MethodDeclaration>> methods = new HashMap<String, Set<MethodDeclaration>>(getMethods());
        Map<String, Set<MethodDeclaration>> methodsFromSupers = getMethodsInUpperChain();
        for (Map.Entry<String, Set<MethodDeclaration>> superMethodsEntry : methodsFromSupers.entrySet()) {
            Set<MethodDeclaration> overloadingMethods = methods.get(superMethodsEntry.getKey());
            if (overloadingMethods != null) {
                overloadingMethods.addAll(superMethodsEntry.getValue());
            } else {
                methods.put(superMethodsEntry.getKey(), superMethodsEntry.getValue());
            }

        }


        return methods;
    }

    public Map<String, Set<MethodDeclaration>> getMethods() {
        return methods;
    }


//    public ClassDeclaration(int access, String name, String signature, String superName,
//                            String[] interfaces, URLClassLoader loader) {
//        super(access, name, signature);
//        this.superName = superName;
//        this.interfaces = interfaces;
//        this.fields = new HashMap<String, FieldDeclaration>();
//        this.methods = new HashMap<String, Set<MethodDeclaration>>();
//        this.jarsLoader = loader;
//        this.serialVisitor = null;
//    }

    public ClassDeclaration(int access, String name, String signature, String superName,
                            String[] interfaces, URLClassLoader loader, SerialVersionClassVisitor cv) {
        super(access, name, signature);
        this.superName = superName;
        this.interfaces = interfaces;
        this.fields = new HashMap<String, FieldDeclaration>();
        this.methods = new HashMap<String, Set<MethodDeclaration>>();
        this.jarsLoader = loader;
        this.serialVisitor = cv;
    }

    private void getFieldsRecursively(String superClass) {

        if ((superClass != null)) {
            // load the super class of the cd
            try {
                SerialVersionClassVisitor cv = new SerialVersionClassVisitor(null);
                SemanticVersioningClassVisitor svc = new SemanticVersioningClassVisitor(jarsLoader, cv);
                ClassReader cr = new ClassReader(jarsLoader.getResourceAsStream(superClass + SemanticVersioningUtils.classExt));
                cr.accept(svc, 0);
                ClassDeclaration cd = svc.getClassDeclaration();
                if (cd != null) {
                    addFieldInUpperChain(cd.getFields());
                    getFieldsRecursively(cd.getSuperName());
                    for (String iface : cd.getInterfaces()) {
                        getFieldsRecursively(iface);
                    }
                }
            } catch (IOException ioe) {
                // not a problem
            }
        }
    }

    private void getMethodsRecursively(String superClass) {
        if ((superClass != null)) {
            // load the super class of the cd
            SerialVersionClassVisitor cv = new SerialVersionClassVisitor(null);

            SemanticVersioningClassVisitor svc = new SemanticVersioningClassVisitor(jarsLoader, cv);
            // use URLClassLoader to load the class
            try {
                ClassReader cr = new ClassReader(jarsLoader.getResourceAsStream(superClass + SemanticVersioningUtils.classExt));
                cr.accept(svc, 0);
                ClassDeclaration cd = svc.getClassDeclaration();
                if (cd != null) {
                    addMethodsInUpperChain(cd.getMethods());
                    getMethodsRecursively(cd.getSuperName());
                    for (String iface : cd.getInterfaces()) {
                        getMethodsRecursively(iface);
                    }
                }
            } catch (IOException ioe) {
                // not a deal
            }
        }
    }


    public Map<String, FieldDeclaration> getFieldsInUpperChain() {
        if (fieldsInUpperChain.isEmpty()) {
            getFieldsRecursively(getSuperName());
            for (String ifs : getInterfaces()) {
                getFieldsRecursively(ifs);
            }
        }
        return fieldsInUpperChain;
    }

    private void addFieldInUpperChain(Map<String, FieldDeclaration> fields) {
        putIfAbsent(fieldsInUpperChain, fields);

    }

    public Map<String, Set<MethodDeclaration>> getMethodsInUpperChain() {
        if (methodsInUpperChain.isEmpty()) {
            getMethodsRecursively(getSuperName());
            for (String ifs : getInterfaces()) {
                getMethodsRecursively(ifs);
            }
        }
        return methodsInUpperChain;
    }

    private void addMethodsInUpperChain(Map<String, Set<MethodDeclaration>> methods) {
        for (Map.Entry<String, Set<MethodDeclaration>> method : methods.entrySet()) {
            String methodName = method.getKey();
            Set<MethodDeclaration> mds = new HashSet<MethodDeclaration>();
            if (methodsInUpperChain.get(methodName) != null) {
                mds.addAll(methodsInUpperChain.get(methodName));
            }
            mds.addAll(method.getValue());

            methodsInUpperChain.put(methodName, mds);
        }
    }

    public Collection<String> getUpperChainRecursively(String className) {
        Collection<String> clazz = new HashSet<String>();

        if (className != null) {
            // load the super class of the cd
            SerialVersionClassVisitor cv = new SerialVersionClassVisitor(null);

            SemanticVersioningClassVisitor svc = new SemanticVersioningClassVisitor(jarsLoader, cv);
            try {
                ClassReader cr = new ClassReader(jarsLoader.getResourceAsStream(className + SemanticVersioningUtils.classExt));
                cr.accept(svc, 0);
                clazz.add(className);
                if (svc.getClassDeclaration() != null) {
                    String superName = svc.getClassDeclaration().getSuperName();
                    clazz.addAll(getUpperChainRecursively(superName));
                    if (svc.getClassDeclaration().getInterfaces() != null) {
                        for (String iface : svc.getClassDeclaration().getInterfaces()) {
                            clazz.addAll(getUpperChainRecursively(iface));
                        }
                    }
                }
            } catch (IOException ioe) {
                // not to worry about this. terminate.
            }
        }
        return clazz;
    }

    public Collection<String> getAllSupers() {
        if (supers.isEmpty()) {
            supers.addAll(getUpperChainRecursively(getSuperName()));
            for (String iface : getInterfaces()) {
                supers.addAll(getUpperChainRecursively(iface));
            }
        }
        return supers;
    }

    public String getSuperName() {
        return superName;
    }

    public String[] getInterfaces() {
        return interfaces;
    }

    public void addFields(FieldDeclaration fd) {
        fields.put(fd.getName(), fd);
    }


    public void addMethods(MethodDeclaration md) {
        String key = md.getName();
        Set<MethodDeclaration> overloaddingMethods = methods.get(key);
        if (overloaddingMethods != null) {
            overloaddingMethods.add(md);
            methods.put(key, overloaddingMethods);
        } else {
            Set<MethodDeclaration> mds = new HashSet<MethodDeclaration>();
            mds.add(md);
            methods.put(key, mds);
        }
    }


    public BinaryCompatibilityStatus getBinaryCompatibleStatus(ClassDeclaration old) {
        // check class signature, fields, methods

        BinaryCompatibilityStatus reasons = new BinaryCompatibilityStatus();
        if (old == null) {
            return reasons;
        }
        getClassSignatureBinaryCompatibleStatus(old, reasons);
        getAllMethodsBinaryCompatibleStatus(old, reasons);
        getAllFieldsBinaryCompatibleStatus(old, reasons);
        getAllSuperPresentStatus(old, reasons);
        getSerializableBackCompatable(old, reasons);
        return reasons;
    }


    public boolean isAbstract() {
        return Modifier.isAbstract(getAccess());
    }

    private void getClassSignatureBinaryCompatibleStatus(ClassDeclaration originalClass, List<String> reasons) {
        // if a class was not abstract but changed to abstract
        // not final changed to final
        // public changed to non-public
        String prefix = " The class " + getName();
        if (!originalClass.isAbstract() && isAbstract()) {
            reasons.add(prefix + " was not abstract but is changed to be abstract.");
        }
        if (!originalClass.isFinal() && isFinal()) {
            reasons.add(prefix + " was not final but is changed to be final.");
        }
        if (originalClass.isPublic() && !isPublic()) {
            reasons.add(prefix + " was public but is changed to be non-public.");
        }
    }

    private void getAllFieldsBinaryCompatibleStatus(ClassDeclaration originalClass, List<String> reasons) {
        // for each field to see whether the same field has changed
        // not final -> final
        // static <-> nonstatic
        Map<String, FieldDeclaration> oldFields = originalClass.getAllFields();
        Map<String, FieldDeclaration> newFields = getAllFields();
        areFieldsBinaryCompatible(oldFields, newFields, reasons);
    }

    private void areFieldsBinaryCompatible(Map<String, FieldDeclaration> oldFields, Map<String, FieldDeclaration> currentFields, List<String> reasons) {

        for (Map.Entry<String, FieldDeclaration> entry : oldFields.entrySet()) {
            FieldDeclaration bef_fd = entry.getValue();
            FieldDeclaration cur_fd = currentFields.get(entry.getKey());

            isFieldBinaryCompatible(reasons, bef_fd, cur_fd);
        }
    }

    private boolean isFieldBinaryCompatible(List<String> reasons,
                                            FieldDeclaration bef_fd, FieldDeclaration cur_fd) {
        String fieldName = bef_fd.getName();
        //only interested in the public or protected fields

        boolean compatible = true;

        if (bef_fd.isPublic() || bef_fd.isProtected()) {
            String prefix = "The " + (bef_fd.isPublic() ? "public" : "protected") + " field " + fieldName;


            if (cur_fd == null) {
                reasons.add(prefix + " has been deleted.");
                compatible = false;
            } else {

                if ((!bef_fd.isFinal()) && (cur_fd.isFinal())) {
                    // make sure it has not been changed to final
                    reasons.add(prefix + " was not final but has been changed to be final.");
                    compatible = false;

                }
                if (bef_fd.isStatic() != cur_fd.isStatic()) {
                    // make sure it the static signature has not been changed
                    reasons.add(prefix + " was static but is changed to be non static or vice versa.");
                    compatible = false;
                }
                // check to see the field type is the same
                if (!isFieldTypeSame(bef_fd, cur_fd)) {
                    reasons.add(prefix + " has changed its type.");
                    compatible = false;

                }
                if (SemanticVersioningUtils.isLessAccessible(bef_fd, cur_fd)) {
                    // check whether the new field is less accessible than the old one
                    reasons.add(prefix + " becomes less accessible.");
                    compatible = false;
                }

            }
        }
        return compatible;
    }

    /**
     * Return whether the serializable class is binary compatible. The serial verison uid change breaks binary compatibility.
     *
     *
     * @param old Old class declaration
     * @param reasons list of binary compatibility problems
     */
    private void getSerializableBackCompatable(ClassDeclaration old, List<String> reasons) {
        // It does not matter one of them is not serializable.
        if ((getAllSupers().contains(SemanticVersioningUtils.SERIALIZABLE_CLASS_IDENTIFIER)) && (old.getAllSupers().contains(SemanticVersioningUtils.SERIALIZABLE_CLASS_IDENTIFIER))) {
            // check to see whether the serializable id is the same
            //ignore if it is enum
            if ((!getAllSupers().contains(SemanticVersioningUtils.ENUM_CLASS) && (!old.getAllSupers().contains(SemanticVersioningUtils.ENUM_CLASS)))) {
                long oldValue = getSerialVersionUID(old);
                long curValue = getSerialVersionUID(this);
                if ((oldValue != curValue)) {
                    reasons.add("The serializable class is no longer back compatible as the value of SerialVersionUID has changed from " + oldValue + " to " + curValue + ".");
                }
            }
        }

    }

    private long getSerialVersionUID(ClassDeclaration cd) {
      FieldDeclaration serialID = cd.getAllFields().get(SemanticVersioningUtils.SERIAL_VERSION_UTD);
      if (serialID != null) {
        if (serialID.isFinal() && serialID.isStatic() && Type.LONG_TYPE.equals(Type.getType(serialID.getDesc()))) {
          if (serialID.getValue() != null) {
            return (Long)serialID.getValue();

          } else {
            return 0;
          }
        }
      }
        // get the generated value
        return cd.getSerialVisitor().getComputeSerialVersionUID();
    }

    private boolean isFieldTypeSame(FieldDeclaration bef_fd, FieldDeclaration cur_fd) {
        boolean descSame = bef_fd.getDesc().equals(cur_fd.getDesc());
        if (descSame) {
            return true;
        }
        return false;

    }

    private void getAllMethodsBinaryCompatibleStatus(ClassDeclaration originalClass, List<String> reasons) {
        //  for all methods
        // no methods should have deleted
        // method return type has not changed
        // method changed from not abstract -> abstract
        Map<String, Set<MethodDeclaration>> oldMethods = originalClass.getAllMethods();
        Map<String, Set<MethodDeclaration>> newMethods = getAllMethods();
        areMethodsBinaryCompatible(oldMethods, newMethods, reasons);
    }

    private void areMethodsBinaryCompatible(
            Map<String, Set<MethodDeclaration>> oldMethods, Map<String, Set<MethodDeclaration>> newMethods, List<String> reasons) {

        boolean compatible = true;
        Map<String, Collection<MethodDeclaration>> extraMethods = new HashMap<String, Collection<MethodDeclaration>>();

        for (Map.Entry<String, Set<MethodDeclaration>> me : newMethods.entrySet()) {
            Collection<MethodDeclaration> mds = new ArrayList<MethodDeclaration>(me.getValue());
            extraMethods.put(me.getKey(), mds);
        }

        for (Map.Entry<String, Set<MethodDeclaration>> methods : oldMethods.entrySet()) {
            // all overloading methods, check against the current class
            String methodName = methods.getKey();
            Collection<MethodDeclaration> oldMDSigs = methods.getValue();
            // If the method cannot be found in the current class, it means that it has been deleted.
            Collection<MethodDeclaration> newMDSigs = newMethods.get(methodName);
            // for each overloading methods
            outer:
            for (MethodDeclaration md : oldMDSigs) {
                String mdName = md.getName();

                String prefix = "The " + SemanticVersioningUtils.getReadableMethodSignature(mdName, md.getDesc());
                if (md.isProtected() || md.isPublic()) {
                    boolean found = false;
                    if (newMDSigs != null) {
                        // try to find it in the current class
                        for (MethodDeclaration new_md : newMDSigs) {
                            // find the method with the same return type, parameter list
                            if ((md.equals(new_md))) {
                                found = true;
                                // If the old method is final but the new one is not or vice versa
                                // If the old method is static but the new one is non static
                                // If the old method is not abstract but the new is

                                if (!Modifier.isFinal(md.getAccess()) && !Modifier.isStatic(md.getAccess()) && Modifier.isFinal(new_md.getAccess())) {
                                    compatible = false;
                                    reasons.add(prefix + " was not final but has been changed to be final.");
                                }
                                if (Modifier.isStatic(md.getAccess()) != Modifier.isStatic(new_md.getAccess())) {
                                    compatible = false;
                                    reasons.add(prefix + " has changed from static to non-static or vice versa.");
                                }
                                if ((Modifier.isAbstract(new_md.getAccess())) && (!Modifier.isAbstract(md.getAccess()))) {
                                    compatible = false;
                                    reasons.add(prefix + " has changed from non abstract to abstract.");
                                }
                                if (SemanticVersioningUtils.isLessAccessible(md, new_md)) {
                                    compatible = false;
                                    reasons.add(prefix + " is less accessible.");
                                }

                                if (compatible) {
                                    // remove from the extra map
                                    Collection<MethodDeclaration> mds = extraMethods.get(methodName);
                                    mds.remove(new_md);
                                    extraMethods.put(methodName, mds);
                                    continue outer;
                                }
                            }
                        }
                    }

                    //
                    // if we are here, it means that we have not found the method with the same description and signature
                    // which means that the method has been deleted. Let's make sure it is not moved to its upper chain.
                    if (!found) {
                        if (!isMethodInSuperClass(md)) {

                            compatible = false;
                            reasons.add(prefix + " has been deleted or its return type or parameter list has changed.");
                        } else {
                            if (newMDSigs != null) {
                                for (MethodDeclaration new_md : newMDSigs) {
                                    // find the method with the same return type, parameter list
                                    if ((md.equals(new_md))) {
                                        Collection<MethodDeclaration> mds = extraMethods.get(methodName);
                                        mds.remove(new_md);
                                        extraMethods.put(methodName, mds);
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }

        // Check the newly added method has not caused binary incompatibility
        for (Map.Entry<String, Collection<MethodDeclaration>> extraMethodSet : extraMethods.entrySet()) {
            for (MethodDeclaration md : extraMethodSet.getValue()) {
                String head = "The " + SemanticVersioningUtils.getReadableMethodSignature(md.getName(), md.getDesc());
                isNewMethodSpecialCase(md, head, reasons);
            }
        }
    }

    /**
     * Return the newly added fields
     *
     * @param old old class declaration
     * @return FieldDeclarations for fields added to new class
     */
    public Collection<FieldDeclaration> getExtraFields(ClassDeclaration old) {
        Map<String, FieldDeclaration> oldFields = old.getAllFields();
        Map<String, FieldDeclaration> newFields = getAllFields();
        Map<String, FieldDeclaration> extraFields = new HashMap<String, FieldDeclaration>(newFields);
        for (String key : oldFields.keySet()) {
            extraFields.remove(key);
        }
        return extraFields.values();
    }

    /**
     * Return the extra non-private methods
     *
     * @param old old class declaration
     * @return method declarations for methods added to new class
     */
    public Collection<MethodDeclaration> getExtraMethods(ClassDeclaration old) {
        // Need to find whether there are new methods added.
        Collection<MethodDeclaration> extraMethods = new HashSet<MethodDeclaration>();
        Map<String, Set<MethodDeclaration>> currMethodsMap = getAllMethods();
        Map<String, Set<MethodDeclaration>> oldMethodsMap = old.getAllMethods();

        for (Map.Entry<String, Set<MethodDeclaration>> currMethod : currMethodsMap.entrySet()) {
            String methodName = currMethod.getKey();
            Collection<MethodDeclaration> newMethods = currMethod.getValue();

            // for each  method, we look for whether it exists in the old class
            Collection<MethodDeclaration> oldMethods = oldMethodsMap.get(methodName);
            for (MethodDeclaration new_md : newMethods) {
                if (!new_md.isPrivate()) {
                    if (oldMethods == null) {
                        extraMethods.add(new_md);
                    } else {
                        if (!oldMethods.contains(new_md)) {
                            extraMethods.add(new_md);
                        }
                    }
                }
            }
        }
        return extraMethods;
    }

    public boolean isMethodInSuperClass(MethodDeclaration md) {
        // scan the super class and interfaces
        String methodName = md.getName();
        Collection<MethodDeclaration> overloaddingMethods = getMethodsInUpperChain().get(methodName);
        if (overloaddingMethods != null) {
            for (MethodDeclaration value : overloaddingMethods) {
                // method signature and name same and also the method should not be less accessible
                if (md.equals(value) && (!SemanticVersioningUtils.isLessAccessible(md, value)) && (value.isStatic() == md.isStatic())) {
                    return true;
                }
            }
        }
        return false;
    }


    /**
     * The newly added method is less accessible than the old one in the super or is a static (respectively instance) method.
     *
     *
     * @param md method declaration
     * @param prefix beginning of incompatibility message
     * @param reasons list of binary incompatibility reasons
     * @return whether new method is less accessible or changed static-ness compared to old class
     */
    private boolean isNewMethodSpecialCase(MethodDeclaration md, String prefix, List<String> reasons) {
        // scan the super class and interfaces
        String methodName = md.getName();
        boolean special = false;
        Collection<MethodDeclaration> overloaddingMethods = getMethodsInUpperChain().get(methodName);
        if (overloaddingMethods != null) {
            for (MethodDeclaration value : overloaddingMethods) {
                // method signature and name same and also the method should not be less accessible
                if (!SemanticVersioningUtils.CONSTRUTOR.equals(md.getName())) {
                    if (md.equals(value)) {
                        if (SemanticVersioningUtils.isLessAccessible(value, md)) {
                            special = true;
                            reasons.add(prefix + " is less accessible than the same method in its parent.");
                        }
                        if (value.isStatic()) {
                            if (!md.isStatic()) {
                                special = true;
                                reasons.add(prefix + " is non-static but the same method in its parent is static.");
                            }
                        } else {
                            if (md.isStatic()) {
                                special = true;
                                reasons.add(prefix + " is static but the same method is its parent is not static.");
                            }
                        }
                    }
                }
            }
        }
        return special;
    }

    private void getAllSuperPresentStatus(ClassDeclaration old, List<String> reasons) {
        Collection<String> oldSupers = old.getAllSupers();
        boolean containsAll = getAllSupers().containsAll(oldSupers);
        if (!containsAll) {
            oldSupers.removeAll(getAllSupers());
            reasons.add("The superclasses or superinterfaces have stopped being super: " + oldSupers.toString() + ".");
        }
    }

    public SerialVersionClassVisitor getSerialVisitor() {
        return serialVisitor;
    }

}
