/*
 * ProGuard -- shrinking, optimization, obfuscation, and preverification
 *             of Java bytecode.
 *
 * Copyright (c) 2002-2018 GuardSquare NV
 *
 * 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.ant;

import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.types.DataType;
import proguard.*;
import proguard.classfile.*;
import proguard.classfile.util.ClassUtil;

import java.util.*;

/**
 * This DataType represents a class specification in Ant.
 *
 * @author Eric Lafortune
 */
public class ClassSpecificationElement extends DataType
{
    private static final String ANY_CLASS_KEYWORD  = "*";

    private String access;
    private String annotation;
    private String type;
    private String name;
    private String extendsAnnotation;
    private String extends_;
    private List   fieldSpecifications  = new ArrayList();
    private List   methodSpecifications = new ArrayList();


    /**
     * Adds the contents of this class specification element to the given list.
     * @param classSpecifications the class specifications to be extended.
     */
    public void appendTo(List classSpecifications)
    {
        // Get the referenced file set, or else this one.
        ClassSpecificationElement classSpecificationElement = isReference() ?
            (ClassSpecificationElement)getCheckedRef(this.getClass(),
                                                     this.getClass().getName()) :
            this;

        ClassSpecification classSpecification =
            createClassSpecification(classSpecificationElement);

        // Add it to the list.
        classSpecifications.add(classSpecification);
    }


    /**
     * Creates a new class specification corresponding to the contents of this
     * class specification element.
     */
    protected ClassSpecification createClassSpecification(ClassSpecificationElement classSpecificationElement)
    {
        String access            = classSpecificationElement.access;
        String annotation        = classSpecificationElement.annotation;
        String type              = classSpecificationElement.type;
        String name              = classSpecificationElement.name;
        String extendsAnnotation = classSpecificationElement.extendsAnnotation;
        String extends_          = classSpecificationElement.extends_;

        // For backward compatibility, allow a single "*" wildcard to match
        // any class.
        if (name != null &&
            name.equals(ANY_CLASS_KEYWORD))
        {
            name = null;
        }

        ClassSpecification classSpecification =
            new ClassSpecification(null,
                                   requiredAccessFlags(true,  access, type),
                                   requiredAccessFlags(false, access, type),
                                   annotation        != null ? ClassUtil.internalType(annotation)        : null,
                                   name              != null ? ClassUtil.internalClassName(name)         : null,
                                   extendsAnnotation != null ? ClassUtil.internalType(extendsAnnotation) : null,
                                   extends_          != null ? ClassUtil.internalClassName(extends_)     : null);

        for (int index = 0; index < fieldSpecifications.size(); index++)
        {
            classSpecification.addField((MemberSpecification)fieldSpecifications.get(index));
        }

        for (int index = 0; index < methodSpecifications.size(); index++)
        {
            classSpecification.addMethod((MemberSpecification)methodSpecifications.get(index));
        }

        return classSpecification;
    }


    // Ant task attributes.

    public void setAccess(String access)
    {
        this.access = access;
    }


    public void setAnnotation(String annotation)
    {
        this.annotation = annotation;
    }


    public void setType(String type)
    {
        this.type = type;
    }


    public void setName(String name)
    {
        this.name = name;
    }


    public void setExtendsannotation(String extendsAnnotation)
    {
        this.extendsAnnotation = extendsAnnotation;
    }


    public void setExtends(String extends_)
    {
        this.extends_ = extends_;
    }


    public void setImplements(String implements_)
    {
        this.extends_ = implements_;
    }


    // Ant task nested elements.

    public void addConfiguredField(MemberSpecificationElement memberSpecificationElement)
    {
        if (fieldSpecifications == null)
        {
            fieldSpecifications = new ArrayList();
        }

        memberSpecificationElement.appendTo(fieldSpecifications,
                                            false,
                                            false);
    }


    public void addConfiguredMethod(MemberSpecificationElement memberSpecificationElement)
    {
        if (methodSpecifications == null)
        {
            methodSpecifications = new ArrayList();
        }

        memberSpecificationElement.appendTo(methodSpecifications,
                                            true,
                                            false);
    }


    public void addConfiguredConstructor(MemberSpecificationElement memberSpecificationElement)
    {
        if (methodSpecifications == null)
        {
            methodSpecifications = new ArrayList();
        }

        memberSpecificationElement.appendTo(methodSpecifications,
                                            true,
                                            true);
    }


    // Small utility methods.

    private int requiredAccessFlags(boolean set,
                                    String  access,
                                    String  type)
    throws BuildException
    {
        int accessFlags = 0;

        if (access != null)
        {
            StringTokenizer tokenizer = new StringTokenizer(access, " ,");
            while (tokenizer.hasMoreTokens())
            {
                String token = tokenizer.nextToken();

                if (token.startsWith("!") ^ set)
                {
                    String strippedToken = token.startsWith("!") ?
                        token.substring(1) :
                        token;

                    int accessFlag =
                        strippedToken.equals(JavaConstants.ACC_PUBLIC)     ? ClassConstants.ACC_PUBLIC     :
                        strippedToken.equals(JavaConstants.ACC_FINAL)      ? ClassConstants.ACC_FINAL      :
                        strippedToken.equals(JavaConstants.ACC_ABSTRACT)   ? ClassConstants.ACC_ABSTRACT   :
                        strippedToken.equals(JavaConstants.ACC_SYNTHETIC)  ? ClassConstants.ACC_SYNTHETIC  :
                        strippedToken.equals(JavaConstants.ACC_ANNOTATION) ? ClassConstants.ACC_ANNOTATION :
                                                                             0;

                    if (accessFlag == 0)
                    {
                        throw new BuildException("Incorrect class access modifier ["+strippedToken+"]");
                    }

                    accessFlags |= accessFlag;
                }
            }
        }

        if (type != null && (type.startsWith("!") ^ set))
        {
            int accessFlag =
                type.equals("class")                           ? 0                            :
                type.equals(      JavaConstants.ACC_INTERFACE) ||
                type.equals("!" + JavaConstants.ACC_INTERFACE) ? ClassConstants.ACC_INTERFACE :
                type.equals(      JavaConstants.ACC_ENUM)      ||
                type.equals("!" + JavaConstants.ACC_ENUM)      ? ClassConstants.ACC_ENUM      :
                                                                 -1;

            if (accessFlag == -1)
            {
                throw new BuildException("Incorrect class type ["+type+"]");
            }

            accessFlags |= accessFlag;
        }

        return accessFlags;
    }
}
