/*
 * ProGuard -- shrinking, optimization, obfuscation, and preverification
 *             of Java bytecode.
 *
 * Copyright (c) 2002-2017 Eric Lafortune @ GuardSquare
 *
 * This program is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License as published by the Free
 * Software Foundation; either version 2 of the License, or (at your option)
 * any later version.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
 * more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with this program; if not, write to the Free Software Foundation, Inc.,
 * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
 */
package proguard.classfile.editor;

import proguard.classfile.*;
import proguard.classfile.attribute.*;

/**
 * This class can add and delete attributes to and from classes, fields,
 * methods, and code attributes. Attributes to be added must be filled out
 * beforehand, including their references to the constant pool. Existing
 * attributes of the same type are always replaced.
 *
 * @author Eric Lafortune
 */
public class AttributesEditor
{
    private final ProgramClass  targetClass;
    private final ProgramMember targetMember;
    private final CodeAttribute targetAttribute;
    private final boolean       replaceAttributes;


    /**
     * Creates a new AttributeAdder that will edit attributes in the given
     * target class.
     */
    public AttributesEditor(ProgramClass targetClass,
                            boolean      replaceAttributes)
    {
        this(targetClass, null, null, replaceAttributes);
    }


    /**
     * Creates a new AttributeAdder that will edit attributes in the given
     * target class member.
     */
    public AttributesEditor(ProgramClass  targetClass,
                            ProgramMember targetMember,
                            boolean       replaceAttributes)
    {
        this(targetClass, targetMember, null, replaceAttributes);
    }


    /**
     * Creates a new AttributeAdder that will edit attributes in the given
     * target code attribute.
     */
    public AttributesEditor(ProgramClass  targetClass,
                            ProgramMember targetMember,
                            CodeAttribute targetAttribute,
                            boolean       replaceAttributes)
    {
        this.targetClass       = targetClass;
        this.targetMember      = targetMember;
        this.targetAttribute   = targetAttribute;
        this.replaceAttributes = replaceAttributes;
    }


    /**
     * Finds the specified attribute in the target.
     */
    public Attribute findAttribute(String attributeName)
    {
        // What's the target?
        return
            targetAttribute != null ?
                findAttribute(targetAttribute.u2attributesCount,
                              targetAttribute.attributes,
                              attributeName) :
            targetMember != null ?
                findAttribute(targetMember.u2attributesCount,
                              targetMember.attributes,
                              attributeName) :
                findAttribute(targetClass.u2attributesCount,
                              targetClass.attributes,
                              attributeName);
    }


    /**
     * Adds the given attribute to the target.
     */
    public void addAttribute(Attribute attribute)
    {
        // What's the target?
        if (targetAttribute != null)
        {
            // Try to replace an existing attribute.
            if (!replaceAttributes ||
                !replaceAttribute(targetAttribute.u2attributesCount,
                                  targetAttribute.attributes,
                                  attribute))
            {
                // Otherwise append the attribute.
                targetAttribute.attributes =
                    addAttribute(targetAttribute.u2attributesCount,
                                 targetAttribute.attributes,
                                 attribute);

                targetAttribute.u2attributesCount++;
            }
        }
        else if (targetMember != null)
        {
            // Try to replace an existing attribute.
            if (!replaceAttributes ||
                !replaceAttribute(targetMember.u2attributesCount,
                                  targetMember.attributes,
                                  attribute))
            {
                // Otherwise append the attribute.
                targetMember.attributes =
                    addAttribute(targetMember.u2attributesCount,
                                 targetMember.attributes,
                                 attribute);

                targetMember.u2attributesCount++;
            }
        }
        else
        {
            // Try to replace an existing attribute.
            if (!replaceAttributes ||
                !replaceAttribute(targetClass.u2attributesCount,
                                  targetClass.attributes,
                                  attribute))
            {
                // Otherwise append the attribute.
                targetClass.attributes =
                    addAttribute(targetClass.u2attributesCount,
                                 targetClass.attributes,
                                 attribute);

                targetClass.u2attributesCount++;
            }
        }
    }


    /**
     * Deletes the specified attribute from the target.
     */
    public void deleteAttribute(String attributeName)
    {
        // What's the target?
        if (targetAttribute != null)
        {
            targetAttribute.u2attributesCount =
                deleteAttribute(targetAttribute.u2attributesCount,
                                targetAttribute.attributes,
                                attributeName);
        }
        else if (targetMember != null)
        {
            targetMember.u2attributesCount =
                deleteAttribute(targetMember.u2attributesCount,
                                targetMember.attributes,
                                attributeName);
        }
        else
        {
            targetClass.u2attributesCount =
                deleteAttribute(targetClass.u2attributesCount,
                                targetClass.attributes,
                                attributeName);
        }
    }


    // Small utility methods.

    /**
     * Tries to put the given attribute in place of an existing attribute of
     * the same name, returning whether it was present.
     */
    private boolean replaceAttribute(int         attributesCount,
                                     Attribute[] attributes,
                                     Attribute   attribute)
    {
        // Find the attribute with the same name.
        int index = findAttributeIndex(attributesCount,
                                       attributes,
                                       attribute.getAttributeName(targetClass));
        if (index < 0)
        {
            return false;
        }

        attributes[index] = attribute;

        return true;
    }


    /**
     * Appends the given attribute to the given array of attributes, creating a
     * new array if necessary.
     */
    private Attribute[] addAttribute(int         attributesCount,
                                     Attribute[] attributes,
                                     Attribute   attribute)
    {
        // Is the array too small to contain the additional attribute?
        if (attributes.length <= attributesCount)
        {
            // Create a new array and copy the attributes into it.
            Attribute[] newAttributes = new Attribute[attributesCount + 1];
            System.arraycopy(attributes, 0,
                             newAttributes, 0,
                             attributesCount);
            attributes = newAttributes;
        }

        // Append the attribute.
        attributes[attributesCount] = attribute;

        return attributes;
    }


    /**
     * Deletes the attributes with the given name from the given array of
     * attributes, returning the new number of attributes.
     */
    private int deleteAttribute(int         attributesCount,
                                Attribute[] attributes,
                                String      attributeName)
    {
        // Find the attribute.
        int index = findAttributeIndex(attributesCount,
                                       attributes,
                                       attributeName);
        if (index < 0)
        {
            return attributesCount;
        }

        // Shift the other attributes in the array.
        System.arraycopy(attributes, index + 1,
                         attributes, index,
                         attributesCount - index - 1);

        // Clear the last entry in the array.
        attributes[--attributesCount] = null;

        return attributesCount;
    }


    /**
     * Finds the index of the attribute with the given name in the given
     * array of attributes.
     */
    private int findAttributeIndex(int         attributesCount,
                                   Attribute[] attributes,
                                   String      attributeName)
    {
        for (int index = 0; index < attributesCount; index++)
        {
            Attribute attribute = attributes[index];

            if (attribute.getAttributeName(targetClass).equals(attributeName))
            {
                return index;
            }
        }

        return -1;
    }


    /**
     * Finds the attribute with the given name in the given
     * array of attributes.
     */
    private Attribute findAttribute(int         attributesCount,
                                    Attribute[] attributes,
                                    String      attributeName)
    {
        for (int index = 0; index < attributesCount; index++)
        {
            Attribute attribute = attributes[index];

            if (attribute.getAttributeName(targetClass).equals(attributeName))
            {
                return attribute;
            }
        }

        return null;
    }
}
