/*
 * 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
 *
 *   https://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.bcel.generic;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Objects;

import org.apache.bcel.Const;
import org.apache.bcel.classfile.AccessFlags;
import org.apache.bcel.classfile.Annotations;
import org.apache.bcel.classfile.Attribute;
import org.apache.bcel.classfile.ConstantPool;
import org.apache.bcel.classfile.Field;
import org.apache.bcel.classfile.JavaClass;
import org.apache.bcel.classfile.Method;
import org.apache.bcel.classfile.RuntimeInvisibleAnnotations;
import org.apache.bcel.classfile.RuntimeVisibleAnnotations;
import org.apache.bcel.classfile.SourceFile;
import org.apache.bcel.classfile.Utility;
import org.apache.bcel.util.BCELComparator;
import org.apache.commons.lang3.ArrayUtils;

/**
 * Template class for building up a java class. May be initialized with an existing Java class (file).
 *
 * @see JavaClass
 */
public class ClassGen extends AccessFlags implements Cloneable {

    private static BCELComparator<ClassGen> bcelComparator = new BCELComparator<ClassGen>() {

        @Override
        public boolean equals(final ClassGen a, final ClassGen b) {
            return a == b || a != null && b != null && Objects.equals(a.getClassName(), b.getClassName());
        }

        @Override
        public int hashCode(final ClassGen o) {
            return o != null ? Objects.hashCode(o.getClassName()) : 0;
        }
    };

    /**
     * Gets the comparison strategy object.
     *
     * @return Comparison strategy object.
     */
    public static BCELComparator<ClassGen> getComparator() {
        return bcelComparator;
    }

    /**
     * Sets the comparison strategy object.
     *
     * @param comparator Comparison strategy object.
     */
    public static void setComparator(final BCELComparator<ClassGen> comparator) {
        bcelComparator = comparator;
    }

    /*
     * Corresponds to the fields found in a JavaClass object.
     */
    private String className;
    private String superClassName;
    private final String fileName;
    private int classNameIndex = -1;
    private int superclassNameIndex = -1;
    private int major = Const.MAJOR_1_1;
    private int minor = Const.MINOR_1_1;
    private ConstantPoolGen cp; // Template for building up constant pool
    // ArrayLists instead of arrays to gather fields, methods, etc.
    private final List<Field> fieldList = new ArrayList<>();
    private final List<Method> methodList = new ArrayList<>();

    private final List<Attribute> attributeList = new ArrayList<>();

    private final List<String> interfaceList = new ArrayList<>();

    private final List<AnnotationEntryGen> annotationList = new ArrayList<>();

    private List<ClassObserver> observers;

    /**
     * Constructs a new instance from an existing class.
     *
     * @param clazz JavaClass object (for example read from file).
     */
    public ClassGen(final JavaClass clazz) {
        super(clazz.getAccessFlags());
        classNameIndex = clazz.getClassNameIndex();
        superclassNameIndex = clazz.getSuperclassNameIndex();
        className = clazz.getClassName();
        superClassName = clazz.getSuperclassName();
        fileName = clazz.getSourceFileName();
        cp = new ConstantPoolGen(clazz.getConstantPool());
        major = clazz.getMajor();
        minor = clazz.getMinor();
        final Attribute[] attributes = clazz.getAttributes();
        // J5TODO: Could make unpacking lazy, done on first reference
        final AnnotationEntryGen[] annotations = unpackAnnotations(attributes);
        final String[] interfaceNames = clazz.getInterfaceNames();
        if (interfaceNames != null) {
            Collections.addAll(interfaceList, interfaceNames);
        }
        if (attributes != null) {
            for (final Attribute attribute : attributes) {
                if (!(attribute instanceof Annotations)) {
                    addAttribute(attribute);
                }
            }
        }
        Collections.addAll(annotationList, annotations);
        final Method[] methods = clazz.getMethods();
        if (methods != null) {
            Collections.addAll(methodList, methods);
        }
        final Field[] fields = clazz.getFields();
        if (fields != null) {
            Collections.addAll(fieldList, fields);
        }
    }

    /**
     * Convenience constructor to set up some important values initially.
     *
     * @param className fully qualified class name.
     * @param superClassName fully qualified superclass name.
     * @param fileName source file name.
     * @param accessFlags access qualifiers.
     * @param interfaces implemented interfaces.
     */
    public ClassGen(final String className, final String superClassName, final String fileName, final int accessFlags, final String[] interfaces) {
        this(className, superClassName, fileName, accessFlags, interfaces, new ConstantPoolGen());
    }

    /**
     * Convenience constructor to set up some important values initially.
     *
     * @param className fully qualified class name.
     * @param superClassName fully qualified superclass name.
     * @param fileName source file name.
     * @param accessFlags access qualifiers.
     * @param interfaces implemented interfaces.
     * @param cp constant pool to use.
     */
    public ClassGen(final String className, final String superClassName, final String fileName, final int accessFlags, final String[] interfaces,
        final ConstantPoolGen cp) {
        super(accessFlags);
        this.className = className;
        this.superClassName = superClassName;
        this.fileName = fileName;
        this.cp = cp;
        // Put everything needed by default into the constant pool and the vectors
        if (fileName != null) {
            addAttribute(new SourceFile(cp.addUtf8("SourceFile"), 2, cp.addUtf8(fileName), cp.getConstantPool()));
        }
        classNameIndex = cp.addClass(className);
        superclassNameIndex = cp.addClass(superClassName);
        if (interfaces != null) {
            Collections.addAll(interfaceList, interfaces);
        }
    }

    /**
     * Adds an annotation entry to this class.
     *
     * @param a the annotation entry to add.
     */
    public void addAnnotationEntry(final AnnotationEntryGen a) {
        annotationList.add(a);
    }

    /**
     * Add an attribute to this class.
     *
     * @param a attribute to add.
     */
    public void addAttribute(final Attribute a) {
        attributeList.add(a);
    }

    /**
     * Convenience method.
     *
     * Add an empty constructor to this class that does nothing but calling super().
     *
     * @param accessFlags rights for constructor.
     */
    public void addEmptyConstructor(final int accessFlags) {
        final InstructionList il = new InstructionList();
        il.append(InstructionConst.THIS); // Push 'this'
        il.append(new INVOKESPECIAL(cp.addMethodref(superClassName, Const.CONSTRUCTOR_NAME, "()V")));
        il.append(InstructionConst.RETURN);
        final MethodGen mg = new MethodGen(accessFlags, Type.VOID, Type.NO_ARGS, null, Const.CONSTRUCTOR_NAME, className, il, cp);
        mg.setMaxStack(1);
        addMethod(mg.getMethod());
    }

    /**
     * Add a field to this class.
     *
     * @param f field to add.
     */
    public void addField(final Field f) {
        fieldList.add(f);
    }

    /**
     * Add an interface to this class, that is, this class has to implement it.
     *
     * @param name interface to implement (fully qualified class name).
     */
    public void addInterface(final String name) {
        interfaceList.add(name);
    }

    /**
     * Add a method to this class.
     *
     * @param m method to add.
     */
    public void addMethod(final Method m) {
        methodList.add(m);
    }

    /**
     * Add observer for this object.
     *
     * @param o the observer.
     */
    public void addObserver(final ClassObserver o) {
        if (observers == null) {
            observers = new ArrayList<>();
        }
        observers.add(o);
    }

    @Override
    public Object clone() {
        try {
            return super.clone();
        } catch (final CloneNotSupportedException e) {
            throw new UnsupportedOperationException("Clone Not Supported", e); // never happens
        }
    }

    /**
     * Checks if this class contains the given field.
     *
     * @param f the field to check.
     * @return true if this class contains the field.
     */
    public boolean containsField(final Field f) {
        return fieldList.contains(f);
    }

    /**
     * Gets the field object with given name, or null.
     *
     * @param name the field name.
     * @return field object with given name, or null.
     */
    public Field containsField(final String name) {
        for (final Field f : fieldList) {
            if (f.getName().equals(name)) {
                return f;
            }
        }
        return null;
    }

    /**
     * Gets the method object with given name and signature, or null.
     *
     * @param name the method name.
     * @param signature the method signature.
     * @return method object with given name and signature, or null.
     */
    public Method containsMethod(final String name, final String signature) {
        for (final Method m : methodList) {
            if (m.getName().equals(name) && m.getSignature().equals(signature)) {
                return m;
            }
        }
        return null;
    }

    /**
     * Return value as defined by given BCELComparator strategy. By default two ClassGen objects are said to be equal when
     * their class names are equal.
     *
     * @see Object#equals(Object)
     */
    @Override
    public boolean equals(final Object obj) {
        return obj instanceof ClassGen && bcelComparator.equals(this, (ClassGen) obj);
    }

    /**
     * Gets the annotation entries.
     *
     * @return the annotation entries.
     */
    // J5TODO: Should we make calling unpackAnnotations() lazy and put it in here?
    public AnnotationEntryGen[] getAnnotationEntries() {
        return annotationList.toArray(AnnotationEntryGen.EMPTY_ARRAY);
    }

    /**
     * Gets the attributes.
     *
     * @return the attributes.
     */
    public Attribute[] getAttributes() {
        return attributeList.toArray(Attribute.EMPTY_ARRAY);
    }

    /**
     * Gets the class name.
     *
     * @return the class name.
     */
    public String getClassName() {
        return className;
    }

    /**
     * Gets the class name index.
     *
     * @return the class name index.
     */
    public int getClassNameIndex() {
        return classNameIndex;
    }

    /**
     * Gets the constant pool.
     *
     * @return the constant pool.
     */
    public ConstantPoolGen getConstantPool() {
        return cp;
    }

    /**
     * Gets the fields.
     *
     * @return the fields.
     */
    public Field[] getFields() {
        return fieldList.toArray(Field.EMPTY_ARRAY);
    }

    /**
     * Gets the file name.
     *
     * @return the file name.
     */
    public String getFileName() {
        return fileName;
    }

    /**
     * Gets the interface names.
     *
     * @return the interface names.
     */
    public String[] getInterfaceNames() {
        return interfaceList.toArray(ArrayUtils.EMPTY_STRING_ARRAY);
    }

    /**
     * Gets the interfaces.
     *
     * @return the interfaces.
     */
    public int[] getInterfaces() {
        final int size = interfaceList.size();
        final int[] interfaces = new int[size];
        Arrays.setAll(interfaces, i -> cp.addClass(interfaceList.get(i)));
        return interfaces;
    }

    /**
     * Gets the (finally) built up Java class object.
     *
     * @return the (finally) built up Java class object.
     */
    public JavaClass getJavaClass() {
        final int[] interfaces = getInterfaces();
        final Field[] fields = getFields();
        final Method[] methods = getMethods();
        Attribute[] attributes = null;
        if (annotationList.isEmpty()) {
            attributes = getAttributes();
        } else {
            // TODO: Sometime later, trash any attributes called 'RuntimeVisibleAnnotations' or 'RuntimeInvisibleAnnotations'
            final Attribute[] annAttributes = AnnotationEntryGen.getAnnotationAttributes(cp, getAnnotationEntries());
            attributes = new Attribute[attributeList.size() + annAttributes.length];
            attributeList.toArray(attributes);
            System.arraycopy(annAttributes, 0, attributes, attributeList.size(), annAttributes.length);
        }
        // Must be last since the above calls may still add something to it
        final ConstantPool cp = this.cp.getFinalConstantPool();
        return new JavaClass(classNameIndex, superclassNameIndex, fileName, major, minor, super.getAccessFlags(), cp, interfaces, fields, methods,
            attributes);
    }

    /**
     * Gets major version number of class file.
     *
     * @return major version number of class file.
     */
    public int getMajor() {
        return major;
    }

    /**
     * Gets the method at the given position.
     *
     * @param pos the position.
     * @return the method at the given position.
     */
    public Method getMethodAt(final int pos) {
        return methodList.get(pos);
    }

    /**
     * Gets the methods.
     *
     * @return the methods.
     */
    public Method[] getMethods() {
        return methodList.toArray(Method.EMPTY_ARRAY);
    }

    /**
     * Gets minor version number of class file.
     *
     * @return minor version number of class file.
     */
    public int getMinor() {
        return minor;
    }

    /**
     * Gets the superclass name.
     *
     * @return the superclass name.
     */
    public String getSuperclassName() {
        return superClassName;
    }

    /**
     * Gets the superclass name index.
     *
     * @return the superclass name index.
     */
    public int getSuperclassNameIndex() {
        return superclassNameIndex;
    }

    /**
     * Return value as defined by given BCELComparator strategy. By default return the hash code of the class name.
     *
     * @see Object#hashCode()
     */
    @Override
    public int hashCode() {
        return bcelComparator.hashCode(this);
    }

    /**
     * Remove an attribute from this class.
     *
     * @param a attribute to remove.
     */
    public void removeAttribute(final Attribute a) {
        attributeList.remove(a);
    }

    /**
     * Remove a field to this class.
     *
     * @param f field to remove.
     */
    public void removeField(final Field f) {
        fieldList.remove(f);
    }

    /**
     * Remove an interface from this class.
     *
     * @param name interface to remove (fully qualified name).
     */
    public void removeInterface(final String name) {
        interfaceList.remove(name);
    }

    /**
     * Remove a method from this class.
     *
     * @param m method to remove.
     */
    public void removeMethod(final Method m) {
        methodList.remove(m);
    }

    /**
     * Remove observer for this object.
     *
     * @param o the observer to remove.
     */
    public void removeObserver(final ClassObserver o) {
        if (observers != null) {
            observers.remove(o);
        }
    }

    /**
     * Replace given field with new one. If the old one does not exist add the new_ field to the class anyway.
     *
     * @param old the old field.
     * @param newField the new field.
     */
    public void replaceField(final Field old, final Field newField) {
        if (newField == null) {
            throw new ClassGenException("Replacement method must not be null");
        }
        final int i = fieldList.indexOf(old);
        if (i < 0) {
            fieldList.add(newField);
        } else {
            fieldList.set(i, newField);
        }
    }

    /**
     * Replace given method with new one. If the old one does not exist add the newMethod method to the class anyway.
     *
     * @param old the old method.
     * @param newMethod the new method.
     */
    public void replaceMethod(final Method old, final Method newMethod) {
        if (newMethod == null) {
            throw new ClassGenException("Replacement method must not be null");
        }
        final int i = methodList.indexOf(old);
        if (i < 0) {
            methodList.add(newMethod);
        } else {
            methodList.set(i, newMethod);
        }
    }

    /**
     * Sets the class name.
     *
     * @param name the class name.
     */
    public void setClassName(final String name) {
        className = Utility.pathToPackage(name);
        classNameIndex = cp.addClass(name);
    }

    /**
     * Sets the class name index.
     *
     * @param classNameIndex the class name index.
     */
    public void setClassNameIndex(final int classNameIndex) {
        this.classNameIndex = classNameIndex;
        this.className = Utility.pathToPackage(cp.getConstantPool().getConstantString(classNameIndex, Const.CONSTANT_Class));
    }

    /**
     * Sets the constant pool.
     *
     * @param constantPool the constant pool.
     */
    public void setConstantPool(final ConstantPoolGen constantPool) {
        cp = constantPool;
    }

    /**
     * Sets major version number of class file, default value is 45 (JDK 1.1).
     *
     * @param major major version number.
     */
    public void setMajor(final int major) { // TODO could be package-protected - only called by test code
        this.major = major;
    }

    /**
     * Sets the method at the given position.
     *
     * @param method the method.
     * @param pos the position.
     */
    public void setMethodAt(final Method method, final int pos) {
        methodList.set(pos, method);
    }

    /**
     * Sets the methods.
     *
     * @param methods the methods.
     */
    public void setMethods(final Method[] methods) {
        methodList.clear();
        if (methods != null) {
            Collections.addAll(methodList, methods);
        }
    }

    /**
     * Sets minor version number of class file, default value is 3 (JDK 1.1).
     *
     * @param minor minor version number.
     */
    public void setMinor(final int minor) { // TODO could be package-protected - only called by test code
        this.minor = minor;
    }

    /**
     * Sets the superclass name.
     *
     * @param name the superclass name.
     */
    public void setSuperclassName(final String name) {
        superClassName = Utility.pathToPackage(name);
        superclassNameIndex = cp.addClass(name);
    }

    /**
     * Sets the superclass name index.
     *
     * @param superclassNameIndex the superclass name index.
     */
    public void setSuperclassNameIndex(final int superclassNameIndex) {
        this.superclassNameIndex = superclassNameIndex;
        superClassName = Utility.pathToPackage(cp.getConstantPool().getConstantString(superclassNameIndex, Const.CONSTANT_Class));
    }

    /**
     * Unpacks attributes representing annotations.
     */
    private AnnotationEntryGen[] unpackAnnotations(final Attribute[] attributes) {
        final List<AnnotationEntryGen> annotationGenObjs = new ArrayList<>();
        if (attributes != null) {
            for (final Attribute attr : attributes) {
                if (attr instanceof RuntimeVisibleAnnotations) {
                    final RuntimeVisibleAnnotations rva = (RuntimeVisibleAnnotations) attr;
                    rva.forEach(a -> annotationGenObjs.add(new AnnotationEntryGen(a, getConstantPool(), false)));
                } else if (attr instanceof RuntimeInvisibleAnnotations) {
                    final RuntimeInvisibleAnnotations ria = (RuntimeInvisibleAnnotations) attr;
                    ria.forEach(a -> annotationGenObjs.add(new AnnotationEntryGen(a, getConstantPool(), false)));
                }
            }
        }
        return annotationGenObjs.toArray(AnnotationEntryGen.EMPTY_ARRAY);
    }

    /**
     * Call notify() method on all observers. This method is not called automatically whenever the state has changed, but
     * has to be called by the user after they have finished editing the object.
     */
    public void update() {
        if (observers != null) {
            for (final ClassObserver observer : observers) {
                observer.notify(this);
            }
        }
    }
}
