/*
 * 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.gui;

import proguard.*;
import proguard.classfile.ClassConstants;
import proguard.classfile.util.ClassUtil;

import javax.swing.*;
import javax.swing.border.*;
import java.awt.*;
import java.awt.event.*;
import java.util.List;

/**
 * This <code>JDialog</code> allows the user to enter a String.
 *
 * @author Eric Lafortune
 */
final class ClassSpecificationDialog extends JDialog
{
    /**
     * Return value if the dialog is canceled (with the Cancel button or by
     * closing the dialog window).
     */
    public static final int CANCEL_OPTION = 1;

    /**
     * Return value if the dialog is approved (with the Ok button).
     */
    public static final int APPROVE_OPTION = 0;


    private final JTextArea commentsTextArea = new JTextArea(4, 20);

    private final JRadioButton keepClassesAndMembersRadioButton  = new JRadioButton(msg("keep"));
    private final JRadioButton keepClassMembersRadioButton       = new JRadioButton(msg("keepClassMembers"));
    private final JRadioButton keepClassesWithMembersRadioButton = new JRadioButton(msg("keepClassesWithMembers"));

    private final JCheckBox keepDescriptorClassesCheckBox = new JCheckBox(msg("keepDescriptorClasses"));

    private final JCheckBox allowShrinkingCheckBox    = new JCheckBox(msg("allowShrinking"));
    private final JCheckBox allowOptimizationCheckBox = new JCheckBox(msg("allowOptimization"));
    private final JCheckBox allowObfuscationCheckBox  = new JCheckBox(msg("allowObfuscation"));


    private final JRadioButton[] publicRadioButtons;
    private final JRadioButton[] finalRadioButtons;
    private final JRadioButton[] abstractRadioButtons;
    private final JRadioButton[] interfaceRadioButtons;
    private final JRadioButton[] annotationRadioButtons;
    private final JRadioButton[] enumRadioButtons;
    private final JRadioButton[] syntheticRadioButtons;

    private final JTextField annotationTypeTextField        = new JTextField(20);
    private final JTextField classNameTextField             = new JTextField(20);
    private final JTextField extendsAnnotationTypeTextField = new JTextField(20);
    private final JTextField extendsClassNameTextField      = new JTextField(20);

    private final MemberSpecificationsPanel memberSpecificationsPanel;

    private int returnValue;


    public ClassSpecificationDialog(JFrame owner, boolean fullKeepOptions)
    {
        super(owner, msg("specifyClasses"), true);
        setResizable(true);

        // Create some constraints that can be reused.
        GridBagConstraints constraints = new GridBagConstraints();
        constraints.anchor = GridBagConstraints.WEST;
        constraints.insets = new Insets(1, 2, 1, 2);

        GridBagConstraints constraintsStretch = new GridBagConstraints();
        constraintsStretch.fill    = GridBagConstraints.HORIZONTAL;
        constraintsStretch.weightx = 1.0;
        constraintsStretch.anchor  = GridBagConstraints.WEST;
        constraintsStretch.insets  = constraints.insets;

        GridBagConstraints constraintsLast = new GridBagConstraints();
        constraintsLast.gridwidth = GridBagConstraints.REMAINDER;
        constraintsLast.anchor    = GridBagConstraints.WEST;
        constraintsLast.insets    = constraints.insets;

        GridBagConstraints constraintsLastStretch = new GridBagConstraints();
        constraintsLastStretch.gridwidth = GridBagConstraints.REMAINDER;
        constraintsLastStretch.fill      = GridBagConstraints.HORIZONTAL;
        constraintsLastStretch.weightx   = 1.0;
        constraintsLastStretch.anchor    = GridBagConstraints.WEST;
        constraintsLastStretch.insets    = constraints.insets;

        GridBagConstraints panelConstraints = new GridBagConstraints();
        panelConstraints.gridwidth = GridBagConstraints.REMAINDER;
        panelConstraints.fill      = GridBagConstraints.HORIZONTAL;
        panelConstraints.weightx   = 1.0;
        panelConstraints.weighty   = 0.0;
        panelConstraints.anchor    = GridBagConstraints.NORTHWEST;
        panelConstraints.insets    = constraints.insets;

        GridBagConstraints stretchPanelConstraints = new GridBagConstraints();
        stretchPanelConstraints.gridwidth = GridBagConstraints.REMAINDER;
        stretchPanelConstraints.fill      = GridBagConstraints.BOTH;
        stretchPanelConstraints.weightx   = 1.0;
        stretchPanelConstraints.weighty   = 1.0;
        stretchPanelConstraints.anchor    = GridBagConstraints.NORTHWEST;
        stretchPanelConstraints.insets    = constraints.insets;

        GridBagConstraints labelConstraints = new GridBagConstraints();
        labelConstraints.anchor = GridBagConstraints.CENTER;
        labelConstraints.insets = new Insets(2, 10, 2, 10);

        GridBagConstraints lastLabelConstraints = new GridBagConstraints();
        lastLabelConstraints.gridwidth = GridBagConstraints.REMAINDER;
        lastLabelConstraints.anchor    = GridBagConstraints.CENTER;
        lastLabelConstraints.insets    = labelConstraints.insets;

        GridBagConstraints advancedButtonConstraints = new GridBagConstraints();
        advancedButtonConstraints.weightx = 1.0;
        advancedButtonConstraints.weighty = 1.0;
        advancedButtonConstraints.anchor  = GridBagConstraints.SOUTHWEST;
        advancedButtonConstraints.insets  = new Insets(4, 4, 8, 4);

        GridBagConstraints okButtonConstraints = new GridBagConstraints();
        okButtonConstraints.weightx = 1.0;
        okButtonConstraints.weighty = 1.0;
        okButtonConstraints.anchor  = GridBagConstraints.SOUTHEAST;
        okButtonConstraints.insets  = advancedButtonConstraints.insets;

        GridBagConstraints cancelButtonConstraints = new GridBagConstraints();
        cancelButtonConstraints.gridwidth = GridBagConstraints.REMAINDER;
        cancelButtonConstraints.weighty   = 1.0;
        cancelButtonConstraints.anchor    = GridBagConstraints.SOUTHEAST;
        cancelButtonConstraints.insets    = advancedButtonConstraints.insets;

        GridBagLayout layout = new GridBagLayout();

        Border etchedBorder = BorderFactory.createEtchedBorder(EtchedBorder.RAISED);

        // Create the comments panel.
        JPanel commentsPanel = new JPanel(layout);
        commentsPanel.setBorder(BorderFactory.createTitledBorder(etchedBorder,
                                                                 msg("comments")));

        JScrollPane commentsScrollPane = new JScrollPane(commentsTextArea);
        commentsScrollPane.setBorder(classNameTextField.getBorder());

        commentsPanel.add(tip(commentsScrollPane, "commentsTip"), constraintsLastStretch);

        // Create the keep option panel.
        ButtonGroup keepButtonGroup = new ButtonGroup();
        keepButtonGroup.add(keepClassesAndMembersRadioButton);
        keepButtonGroup.add(keepClassMembersRadioButton);
        keepButtonGroup.add(keepClassesWithMembersRadioButton);

        JPanel keepOptionPanel = new JPanel(layout);
        keepOptionPanel.setBorder(BorderFactory.createTitledBorder(etchedBorder,
                                                                   msg("keepTitle")));

        keepOptionPanel.add(tip(keepClassesAndMembersRadioButton,  "keepTip"),                   constraintsLastStretch);
        keepOptionPanel.add(tip(keepClassMembersRadioButton,       "keepClassMembersTip"),       constraintsLastStretch);
        keepOptionPanel.add(tip(keepClassesWithMembersRadioButton, "keepClassesWithMembersTip"), constraintsLastStretch);
        keepOptionPanel.add(tip(keepDescriptorClassesCheckBox,  "keepDescriptorClassesTip"),  constraintsLastStretch);

        // Create the also keep panel.
        final JPanel alsoKeepOptionPanel = new JPanel(layout);
        alsoKeepOptionPanel.setBorder(BorderFactory.createTitledBorder(etchedBorder,
                                                                       msg("alsoKeepTitle")));

        alsoKeepOptionPanel.add(tip(keepDescriptorClassesCheckBox, "keepDescriptorClassesTip"),    constraintsLastStretch);

        // Create the allow option panel.
        final JPanel allowOptionPanel = new JPanel(layout);
        allowOptionPanel.setBorder(BorderFactory.createTitledBorder(etchedBorder,
                                                                    msg("allowTitle")));

        allowOptionPanel.add(tip(allowShrinkingCheckBox,    "allowShrinkingTip"),    constraintsLastStretch);
        allowOptionPanel.add(tip(allowOptimizationCheckBox, "allowOptimizationTip"), constraintsLastStretch);
        allowOptionPanel.add(tip(allowObfuscationCheckBox,  "allowObfuscationTip"),  constraintsLastStretch);

        // Create the access panel.
        JPanel accessPanel = new JPanel(layout);
        accessPanel.setBorder(BorderFactory.createTitledBorder(etchedBorder,
                                                               msg("access")));

        accessPanel.add(Box.createGlue(),                                labelConstraints);
        accessPanel.add(tip(new JLabel(msg("required")), "requiredTip"), labelConstraints);
        accessPanel.add(tip(new JLabel(msg("not")),      "notTip"),      labelConstraints);
        accessPanel.add(tip(new JLabel(msg("dontCare")), "dontCareTip"), labelConstraints);
        accessPanel.add(Box.createGlue(),                                constraintsLastStretch);

        publicRadioButtons     = addRadioButtonTriplet("Public",     accessPanel);
        finalRadioButtons      = addRadioButtonTriplet("Final",      accessPanel);
        abstractRadioButtons   = addRadioButtonTriplet("Abstract",   accessPanel);
        interfaceRadioButtons  = addRadioButtonTriplet("Interface",  accessPanel);
        annotationRadioButtons = addRadioButtonTriplet("Annotation", accessPanel);
        enumRadioButtons       = addRadioButtonTriplet("Enum",       accessPanel);
        syntheticRadioButtons  = addRadioButtonTriplet("Synthetic",  accessPanel);

        // Create the annotation type panel.
        final JPanel annotationTypePanel = new JPanel(layout);
        annotationTypePanel.setBorder(BorderFactory.createTitledBorder(etchedBorder,
                                                                       msg("annotation")));

        annotationTypePanel.add(tip(annotationTypeTextField, "classNameTip"), constraintsLastStretch);

        // Create the class name panel.
        JPanel classNamePanel = new JPanel(layout);
        classNamePanel.setBorder(BorderFactory.createTitledBorder(etchedBorder,
                                                                  msg("class")));

        classNamePanel.add(tip(classNameTextField, "classNameTip"), constraintsLastStretch);

        // Create the extends annotation type panel.
        final JPanel extendsAnnotationTypePanel = new JPanel(layout);
        extendsAnnotationTypePanel.setBorder(BorderFactory.createTitledBorder(etchedBorder,
                                                                              msg("extendsImplementsAnnotation")));

        extendsAnnotationTypePanel.add(tip(extendsAnnotationTypeTextField, "classNameTip"), constraintsLastStretch);

        // Create the extends class name panel.
        JPanel extendsClassNamePanel = new JPanel(layout);
        extendsClassNamePanel.setBorder(BorderFactory.createTitledBorder(etchedBorder,
                                                                         msg("extendsImplementsClass")));

        extendsClassNamePanel.add(tip(extendsClassNameTextField, "classNameTip"), constraintsLastStretch);


        // Create the class member list panel.
        memberSpecificationsPanel = new MemberSpecificationsPanel(this, fullKeepOptions);
        memberSpecificationsPanel.setBorder(BorderFactory.createTitledBorder(etchedBorder,
                                                                             msg("classMembers")));

        // Create the Advanced button.
        final JButton advancedButton = new JButton(msg("basic"));
        advancedButton.addActionListener(new ActionListener()
        {
            public void actionPerformed(ActionEvent e)
            {
                boolean visible = !alsoKeepOptionPanel.isVisible();

                alsoKeepOptionPanel       .setVisible(visible);
                allowOptionPanel          .setVisible(visible);
                annotationTypePanel       .setVisible(visible);
                extendsAnnotationTypePanel.setVisible(visible);

                advancedButton.setText(msg(visible ? "basic" : "advanced"));

                pack();
            }
        });
        advancedButton.doClick();

        // Create the Ok button.
        JButton okButton = new JButton(msg("ok"));
        okButton.addActionListener(new ActionListener()
        {
            public void actionPerformed(ActionEvent e)
            {
                returnValue = APPROVE_OPTION;
                hide();
            }
        });

        // Create the Cancel button.
        JButton cancelButton = new JButton(msg("cancel"));
        cancelButton.addActionListener(new ActionListener()
        {
            public void actionPerformed(ActionEvent e)
            {
                hide();
            }
        });

        // Add all panels to the main panel.
        JPanel mainPanel = new JPanel(layout);
        mainPanel.add(tip(commentsPanel,              "commentsTip"),                    panelConstraints);
        if (fullKeepOptions)
        {
            mainPanel.add(tip(keepOptionPanel,        "keepTitleTip"),                   panelConstraints);
            mainPanel.add(tip(alsoKeepOptionPanel,    "alsoKeepTitleTip"),               panelConstraints);
            mainPanel.add(tip(allowOptionPanel,       "allowTitleTip"),                  panelConstraints);
        }
        mainPanel.add(tip(accessPanel,                "accessTip"),                      panelConstraints);
        mainPanel.add(tip(annotationTypePanel,        "annotationTip"),                  panelConstraints);
        mainPanel.add(tip(classNamePanel,             "classTip"),                       panelConstraints);
        mainPanel.add(tip(extendsAnnotationTypePanel, "extendsImplementsAnnotationTip"), panelConstraints);
        mainPanel.add(tip(extendsClassNamePanel,      "extendsImplementsClassTip"),      panelConstraints);
        mainPanel.add(tip(memberSpecificationsPanel,  "classMembersTip"),                stretchPanelConstraints);

        mainPanel.add(tip(advancedButton, "advancedTip"), advancedButtonConstraints);
        mainPanel.add(okButton,                           okButtonConstraints);
        mainPanel.add(cancelButton,                       cancelButtonConstraints);

        getContentPane().add(new JScrollPane(mainPanel));
    }


    /**
     * Adds a JLabel and three JRadioButton instances in a ButtonGroup to the
     * given panel with a GridBagLayout, and returns the buttons in an array.
     */
    private JRadioButton[] addRadioButtonTriplet(String labelText,
                                                 JPanel panel)
    {
        GridBagConstraints labelConstraints = new GridBagConstraints();
        labelConstraints.anchor = GridBagConstraints.WEST;
        labelConstraints.insets = new Insets(2, 10, 2, 10);

        GridBagConstraints buttonConstraints = new GridBagConstraints();
        buttonConstraints.insets = labelConstraints.insets;

        GridBagConstraints lastGlueConstraints = new GridBagConstraints();
        lastGlueConstraints.gridwidth = GridBagConstraints.REMAINDER;
        lastGlueConstraints.weightx   = 1.0;

        // Create the radio buttons.
        JRadioButton radioButton0 = new JRadioButton();
        JRadioButton radioButton1 = new JRadioButton();
        JRadioButton radioButton2 = new JRadioButton();

        // Put them in a button group.
        ButtonGroup buttonGroup = new ButtonGroup();
        buttonGroup.add(radioButton0);
        buttonGroup.add(radioButton1);
        buttonGroup.add(radioButton2);

        // Add the label and the buttons to the panel.
        panel.add(new JLabel(labelText), labelConstraints);
        panel.add(radioButton0,          buttonConstraints);
        panel.add(radioButton1,          buttonConstraints);
        panel.add(radioButton2,          buttonConstraints);
        panel.add(Box.createGlue(),      lastGlueConstraints);

        return new JRadioButton[]
        {
             radioButton0,
             radioButton1,
             radioButton2
        };
    }


    /**
     * Sets the KeepClassSpecification to be represented in this dialog.
     */
    public void setKeepSpecification(KeepClassSpecification keepClassSpecification)
    {
        boolean markClasses           = keepClassSpecification.markClasses;
        boolean markConditionally     = keepClassSpecification.markConditionally;
        boolean markDescriptorClasses = keepClassSpecification.markDescriptorClasses;
        boolean allowShrinking        = keepClassSpecification.allowShrinking;
        boolean allowOptimization     = keepClassSpecification.allowOptimization;
        boolean allowObfuscation      = keepClassSpecification.allowObfuscation;

        // Figure out the proper keep radio button and set it.
        JRadioButton keepOptionRadioButton =
            markConditionally ? keepClassesWithMembersRadioButton :
            markClasses       ? keepClassesAndMembersRadioButton  :
                                keepClassMembersRadioButton;

        keepOptionRadioButton.setSelected(true);

        // Set the other check boxes.
        keepDescriptorClassesCheckBox.setSelected(markDescriptorClasses);
        allowShrinkingCheckBox       .setSelected(allowShrinking);
        allowOptimizationCheckBox    .setSelected(allowOptimization);
        allowObfuscationCheckBox     .setSelected(allowObfuscation);

        setClassSpecification(keepClassSpecification);
    }


    /**
     * Sets the ClassSpecification to be represented in this dialog.
     */
    public void setClassSpecification(ClassSpecification classSpecification)
    {
        String comments              = classSpecification.comments;
        String annotationType        = classSpecification.annotationType;
        String className             = classSpecification.className;
        String extendsAnnotationType = classSpecification.extendsAnnotationType;
        String extendsClassName      = classSpecification.extendsClassName;
        List   keepFieldOptions      = classSpecification.fieldSpecifications;
        List   keepMethodOptions     = classSpecification.methodSpecifications;

        // Set the comments text area.
        commentsTextArea.setText(comments == null ? "" : comments);

        // Set the access radio buttons.
        setClassSpecificationRadioButtons(classSpecification, ClassConstants.ACC_PUBLIC,      publicRadioButtons);
        setClassSpecificationRadioButtons(classSpecification, ClassConstants.ACC_FINAL,       finalRadioButtons);
        setClassSpecificationRadioButtons(classSpecification, ClassConstants.ACC_ABSTRACT,    abstractRadioButtons);
        setClassSpecificationRadioButtons(classSpecification, ClassConstants.ACC_INTERFACE,   interfaceRadioButtons);
        setClassSpecificationRadioButtons(classSpecification, ClassConstants.ACC_ANNOTATTION, annotationRadioButtons);
        setClassSpecificationRadioButtons(classSpecification, ClassConstants.ACC_ENUM,        enumRadioButtons);
        setClassSpecificationRadioButtons(classSpecification, ClassConstants.ACC_SYNTHETIC,   syntheticRadioButtons);

        // Set the class and annotation text fields.
        annotationTypeTextField       .setText(annotationType        == null ? ""  : ClassUtil.externalType(annotationType));
        classNameTextField            .setText(className             == null ? "*" : ClassUtil.externalClassName(className));
        extendsAnnotationTypeTextField.setText(extendsAnnotationType == null ? ""  : ClassUtil.externalType(extendsAnnotationType));
        extendsClassNameTextField     .setText(extendsClassName      == null ? ""  : ClassUtil.externalClassName(extendsClassName));

        // Set the keep class member option list.
        memberSpecificationsPanel.setMemberSpecifications(keepFieldOptions, keepMethodOptions);
    }


    /**
     * Returns the KeepClassSpecification currently represented in this dialog.
     */
    public KeepClassSpecification getKeepSpecification()
    {
        boolean markClasses           = !keepClassMembersRadioButton     .isSelected();
        boolean markConditionally     = keepClassesWithMembersRadioButton.isSelected();
        boolean markDescriptorClasses = keepDescriptorClassesCheckBox    .isSelected();
        boolean allowShrinking        = allowShrinkingCheckBox           .isSelected();
        boolean allowOptimization     = allowOptimizationCheckBox        .isSelected();
        boolean allowObfuscation      = allowObfuscationCheckBox         .isSelected();

        return new KeepClassSpecification(markClasses,
                                          markConditionally,
                                          markDescriptorClasses,
                                          allowShrinking,
                                          allowOptimization,
                                          allowObfuscation,
                                          getClassSpecification());
    }


    /**
     * Returns the ClassSpecification currently represented in this dialog.
     */
    public ClassSpecification getClassSpecification()
    {
        String comments              = commentsTextArea.getText();
        String annotationType        = annotationTypeTextField.getText();
        String className             = classNameTextField.getText();
        String extendsAnnotationType = extendsAnnotationTypeTextField.getText();
        String extendsClassName      = extendsClassNameTextField.getText();

        ClassSpecification classSpecification =
            new ClassSpecification(comments.equals("")              ? null : comments,
                                   0,
                                   0,
                                   annotationType.equals("")        ? null : ClassUtil.internalType(annotationType),
                                   className.equals("") ||
                                   className.equals("*")            ? null : ClassUtil.internalClassName(className),
                                   extendsAnnotationType.equals("") ? null : ClassUtil.internalType(extendsAnnotationType),
                                   extendsClassName.equals("")      ? null : ClassUtil.internalClassName(extendsClassName));

        // Also get the access radio button settings.
        getClassSpecificationRadioButtons(classSpecification, ClassConstants.ACC_PUBLIC,      publicRadioButtons);
        getClassSpecificationRadioButtons(classSpecification, ClassConstants.ACC_FINAL,       finalRadioButtons);
        getClassSpecificationRadioButtons(classSpecification, ClassConstants.ACC_ABSTRACT,    abstractRadioButtons);
        getClassSpecificationRadioButtons(classSpecification, ClassConstants.ACC_INTERFACE,   interfaceRadioButtons);
        getClassSpecificationRadioButtons(classSpecification, ClassConstants.ACC_ANNOTATTION, annotationRadioButtons);
        getClassSpecificationRadioButtons(classSpecification, ClassConstants.ACC_ENUM,        enumRadioButtons);
        getClassSpecificationRadioButtons(classSpecification, ClassConstants.ACC_SYNTHETIC,   syntheticRadioButtons);

        // Get the keep class member option lists.
        classSpecification.fieldSpecifications  = memberSpecificationsPanel.getMemberSpecifications(true);
        classSpecification.methodSpecifications = memberSpecificationsPanel.getMemberSpecifications(false);

        return classSpecification;
    }


    /**
     * Shows this dialog. This method only returns when the dialog is closed.
     *
     * @return <code>CANCEL_OPTION</code> or <code>APPROVE_OPTION</code>,
     *         depending on the choice of the user.
     */
    public int showDialog()
    {
        returnValue = CANCEL_OPTION;

        // Open the dialog in the right place, then wait for it to be closed,
        // one way or another.
        pack();
        setLocationRelativeTo(getOwner());
        show();

        return returnValue;
    }


    /**
     * Sets the appropriate radio button of a given triplet, based on the access
     * flags of the given keep option.
     */
    private void setClassSpecificationRadioButtons(ClassSpecification classSpecification,
                                                   int                flag,
                                                   JRadioButton[]     radioButtons)
    {
        int index = (classSpecification.requiredSetAccessFlags   & flag) != 0 ? 0 :
                    (classSpecification.requiredUnsetAccessFlags & flag) != 0 ? 1 :
                                                                                 2;
        radioButtons[index].setSelected(true);
    }


    /**
     * Updates the access flag of the given keep option, based on the given radio
     * button triplet.
     */
    private void getClassSpecificationRadioButtons(ClassSpecification classSpecification,
                                                   int                flag,
                                                   JRadioButton[]     radioButtons)
    {
        if      (radioButtons[0].isSelected())
        {
            classSpecification.requiredSetAccessFlags   |= flag;
        }
        else if (radioButtons[1].isSelected())
        {
            classSpecification.requiredUnsetAccessFlags |= flag;
        }
    }


    /**
     * Attaches the tool tip from the GUI resources that corresponds to the
     * given key, to the given component.
     */
    private static JComponent tip(JComponent component, String messageKey)
    {
        component.setToolTipText(msg(messageKey));

        return component;
    }


    /**
     * Returns the message from the GUI resources that corresponds to the given
     * key.
     */
    private static String msg(String messageKey)
    {
         return GUIResources.getMessage(messageKey);
    }
}
