/*
 * JBoss, Home of Professional Open Source
 * Copyright 2015 Red Hat Inc. and/or its affiliates and other contributors
 * as indicated by the @author tags. All rights reserved.
 * See the copyright.txt in the distribution for a
 * full listing of individual contributors.
 *
 * This copyrighted material is made available to anyone wishing to use,
 * modify, copy, or redistribute it subject to the terms and conditions
 * of the GNU Lesser General Public License, v. 2.1.
 * This program is distributed in the hope that it will be useful, but WITHOUT A
 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
 * PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more details.
 * You should have received a copy of the GNU Lesser General Public License,
 * v.2.1 along with this distribution; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
 * MA  02110-1301, USA.
 */
package org.jboss.as.cli.gui;

import java.awt.BorderLayout;
import java.awt.Container;
import java.awt.Dialog;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.List;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.Vector;
import javax.swing.AbstractButton;
import javax.swing.Box;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JComboBox;
import javax.swing.JComponent;
import javax.swing.JDialog;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextField;
import javax.swing.text.JTextComponent;
import org.jboss.as.cli.gui.ManagementModelNode.UserObject;
import org.jboss.as.cli.gui.component.BrowsePanel;
import org.jboss.as.cli.gui.component.ListEditor;
import org.jboss.as.cli.gui.component.WordWrapLabel;
import org.jboss.dmr.ModelNode;
import org.jboss.dmr.ModelType;
import org.jboss.dmr.Property;

/**
 * JDialog that allows the user to specify the params for an operation.
 *
 * @author Stan Silvert ssilvert@redhat.com (C) 2012 Red Hat Inc.
 */
public class OperationDialog extends JDialog {

    private CliGuiContext cliGuiCtx;
    private ManagementModelNode node;
    private String opName;
    private SortedSet<RequestProp> props;

    public OperationDialog(CliGuiContext cliGuiCtx, ManagementModelNode node, String opName, String strDescription, ModelNode requestProperties) {
        super(cliGuiCtx.getMainWindow(), opName, Dialog.ModalityType.APPLICATION_MODAL);
        this.cliGuiCtx = cliGuiCtx;
        this.node = node;
        this.opName = opName;

        try {
            setProps(requestProperties);
        } catch (Exception e) {
            e.printStackTrace();
            return;
        }

        setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);

        Container contentPane = getContentPane();
        contentPane.setLayout(new BorderLayout(10, 10));

        // the html table allows word wrap and constant max width
        JLabel opDescription = new WordWrapLabel(strDescription, 400);
        JPanel opDescPanel = new JPanel();
        opDescPanel.add(opDescription);
        contentPane.add(opDescPanel, BorderLayout.NORTH);

        contentPane.add(makeInputPanel(), BorderLayout.CENTER);

        contentPane.add(makeButtonPanel(), BorderLayout.SOUTH);
        pack();
        setResizable(true);
    }

    @Override
    public void setVisible(boolean isVisible) {
        if (node.isLeaf()) {
            // "rightSide" field should have focus for write-attribute dialog
            // where "leftSide" field is already populated
            for (RequestProp prop : props) {
                if (prop.getName().equals("value")) {
                    prop.getValueComponent().requestFocus();
                }
            }
        }

        super.setVisible(isVisible);
    }

    /**
     * Set the value of the underlying component.  Note that this will
     * not work for ListEditor components.  Also, note that for a JComboBox,
     * The value object must have the same identity as an object in the drop-down.
     *
     * @param propName The DMR property name to set.
     * @param value  The value.
     */
    public void setValue(String propName, Object value) {
        for (RequestProp prop : props) {
            if (prop.getName().equals(propName)) {
                JComponent valComp = prop.getValueComponent();
                if (valComp instanceof JTextComponent) {
                    ((JTextComponent)valComp).setText(value.toString());
                }
                if (valComp instanceof AbstractButton) {
                    ((AbstractButton)valComp).setSelected((Boolean)value);
                }
                if (valComp instanceof JComboBox) {
                    ((JComboBox)valComp).setSelectedItem(value);
                }
                return;
            }
        }
    }

    private void setProps(ModelNode requestProperties) throws Exception {
        props = new TreeSet<RequestProp>();
        if (opName.equals("add")) {
            UserObject usrObj = (UserObject)node.getUserObject();
            props.add(new RequestProp("/" + usrObj.getName() + "=<name>/", "Resource name for the new " + usrObj.getName(), true, ModelType.STRING));
        }

        if (opName.equals("write-attribute") && node.isLeaf()) {
            ModelNode nameNode = requestProperties.get("name");
            nameNode.get("type").set(ModelType.UNDEFINED); // undefined type will display as uneditable String
            UserObject usrObj = (UserObject)node.getUserObject();
            ModelNode nameNodeValue = new ModelNode();
            nameNodeValue.set(usrObj.getName());
            props.add(new RequestProp("name", requestProperties.get("name"), nameNodeValue));

            ModelNode rscDesc = cliGuiCtx.getExecutor().doCommand(node.addressPath() + ":read-resource-description");
            ModelNode valueNode = rscDesc.get("result", "attributes", usrObj.getName());
            valueNode.get("required").set(false); // value is never required for write-attribute
            ModelNode valueNodeValue = usrObj.getBackingNode().get(usrObj.getName());
            props.add(new RequestProp("value", valueNode, valueNodeValue));
            return;
        }

        for (Property prop : requestProperties.asPropertyList()) {
            props.add(new RequestProp(prop.getName(), prop.getValue(), null));
        }
    }

    private JScrollPane makeInputPanel() {
        boolean hasRequiredFields = false;
        JPanel inputPanel = new JPanel(new GridBagLayout());
        GridBagConstraints gbConst = new GridBagConstraints();
        gbConst.anchor = GridBagConstraints.WEST;
        gbConst.insets = new Insets(5,5,5,5);

        for (RequestProp prop : props) {
            JLabel label = prop.getLabel();
            gbConst.gridwidth = 1;
            inputPanel.add(label, gbConst);

            inputPanel.add(Box.createHorizontalStrut(5));

            JComponent comp = prop.getValueComponent();
            gbConst.gridwidth = GridBagConstraints.REMAINDER;
            inputPanel.add(comp, gbConst);

            if (prop.isRequired) hasRequiredFields = true;
        }

        if (hasRequiredFields) {
            inputPanel.add(new JLabel(" * = Required Field"));
        }

        return new JScrollPane(inputPanel);
    }

    private JPanel makeButtonPanel() {
        JPanel buttonPanel = new JPanel();

        JButton ok = new JButton("OK");
        ok.addActionListener(new SetOperationActionListener());

        JButton cancel = new JButton("Cancel");
        cancel.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent ae) {
                OperationDialog.this.dispose();
            }
        });

        buttonPanel.add(ok);
        buttonPanel.add(cancel);
        return buttonPanel;
    }

    /**
     * Set the command line with address + operation + params.
     */
    private class SetOperationActionListener implements ActionListener {
        public void actionPerformed(ActionEvent ae) {

            String addressPath = OperationDialog.this.node.addressPath();
            if (OperationDialog.this.opName.equals("add")) {
                UserObject usrObj = (UserObject)OperationDialog.this.node.getUserObject();
                ManagementModelNode parent = (ManagementModelNode)OperationDialog.this.node.getParent();
                RequestProp resourceProp = OperationDialog.this.props.first();
                String value = resourceProp.getValueAsString();
                value = ManagementModelNode.escapeAddressElement(value);
                addressPath = parent.addressPath() + usrObj.getName() + "=" + value + "/";
                OperationDialog.this.props.remove(resourceProp);
            }

            StringBuilder command = new StringBuilder();
            command.append(addressPath);
            command.append(":");
            command.append(OperationDialog.this.opName);
            addRequestProps(command, OperationDialog.this.props);

            JTextComponent cmdText = cliGuiCtx.getCommandLine().getCmdText();
            cmdText.setText(command.toString());
            OperationDialog.this.dispose();
            cmdText.requestFocus();
        }

        private void addRequestProps(StringBuilder command, SortedSet<RequestProp> reqProps) {
            boolean addedProps = false;
            command.append("(");
            for (RequestProp prop : reqProps) {
                String submittedValue = prop.getValueAsString();

                if (submittedValue == null) continue;
                if (submittedValue.equals("")) continue;

                // Don't display boolean values that are already the default.
                // This only works if the default value is provided by read-operation-description.
                if (prop.type == ModelType.BOOLEAN && !prop.expressionsAllowed) {
                    ModelNode defaultValue = prop.getDefaultValue();
                    if ((defaultValue != null) && (defaultValue.asBoolean() == Boolean.parseBoolean(submittedValue))) continue;
                }

                addedProps = true;
                command.append(prop.getName());
                command.append("=");
                command.append(submittedValue);

                command.append(",");
            }

            if (addedProps) {
                // replace training comma with close paren
                command.replace(command.length()-1, command.length(), ")");
            } else {
                // remove opening paren
                command.deleteCharAt(command.length() - 1);
            }
        }

    }

    /**
     * Request property class.  This class contains all known information about each operation attribute.
     *
     * It is also responsible for building the input component for the attribute.
     */
    private class RequestProp implements Comparable {
        private final String name;
        private ModelNode props;
        private ModelType type;
        private String description;
        private boolean isRequired = false;
        private boolean nillable = false;
        private boolean expressionsAllowed = false;
        private ModelNode defaultValue = null;
        private ModelNode value = null;

        private JLabel label;
        private JComponent valueComponent;

        private boolean isResourceName = false;

        /**
         * Constructor used for resource name property.
         * @param name Property name
         * @param description Description for tool tip text.
         * @param required Is this an isRequired property?
         */
        public RequestProp(String name, String description, boolean required, ModelType type) {
            this.name = name;
            this.props = new ModelNode();
            this.description = description;
            this.type = type;
            this.isRequired = required;
            this.isResourceName = true;
            setInputComponent();
            setInputComponentValue();
        }

        public RequestProp(String name, ModelNode props, ModelNode value) {
            this.name = name;
            this.props = props;
            this.value = value;
            this.type = props.get("type").asType();

            if (props.hasDefined("description")) {
                this.description = props.get("description").asString();
            }

            if (props.hasDefined("required")) {
                this.isRequired = props.get("required").asBoolean();
            }

            if (props.hasDefined("nillable")) {
                this.nillable = props.get("nillable").asBoolean();
            }

            if (props.hasDefined("expressions-allowed")) {
                this.expressionsAllowed = props.get("expressions-allowed").asBoolean();
            }

            if (props.hasDefined("default")) {
                this.defaultValue = props.get("default");
            }

            setInputComponent();
            setInputComponentValue();
        }

        public String getName() {
            return name;
        }

        public JComponent getValueComponent() {
            return valueComponent;
        }

        public JLabel getLabel() {
            return this.label;
        }

        public ModelNode getDefaultValue() {
            return this.defaultValue;
        }

        public String getValueAsString() {
            if (valueComponent instanceof JLabel) {
                return ((JLabel)valueComponent).getText();
            }

            if (valueComponent instanceof JTextComponent) {
                return ((JTextComponent)valueComponent).getText();
            }

            if (valueComponent instanceof AbstractButton) {
                return Boolean.toString(((AbstractButton)valueComponent).isSelected());
            }

            if (valueComponent instanceof JComboBox) {
                return ((JComboBox)valueComponent).getSelectedItem().toString();
            }

            if (valueComponent instanceof ListEditor) {
                ModelNode list = ((ListEditor)valueComponent).getValue();
                if (list.isDefined()) return list.asString();
                return "";
            }

            if (valueComponent instanceof JLabel) {
                return ((JLabel)valueComponent).getText();
            }

            if (valueComponent instanceof BrowsePanel) {
                return ((BrowsePanel)valueComponent).getText();
            }

            return null;
        }

        private void setInputComponent() {
            this.label = makeLabel();
            if (type == ModelType.BOOLEAN && !expressionsAllowed) {
                this.valueComponent = new JCheckBox(makeLabelString(false));
                this.valueComponent.setToolTipText(description);
                this.label = new JLabel(); // checkbox doesn't need a label
            } else if (type == ModelType.UNDEFINED) {
                JLabel jLabel = new JLabel();
                this.valueComponent = jLabel;
            } else if (props.get("allowed").isDefined()) {
                JComboBox comboBox = makeJComboBox(props.get("allowed").asList());
                this.valueComponent = comboBox;
            } else if (type == ModelType.LIST) {
                ListEditor listEditor = new ListEditor(OperationDialog.this);
                this.valueComponent = listEditor;
            } else if (type == ModelType.BYTES) {
                this.valueComponent = new BrowsePanel(OperationDialog.this);
            } else {
                JTextField textField = new JTextField(30);
                this.valueComponent = textField;
            }
        }

        private void setInputComponentValue() {
            ModelNode valueToSet = defaultValue;
            if (value != null) valueToSet = value;
            if (valueToSet == null) return;

            if (valueComponent instanceof JLabel) {
                ((JLabel)valueComponent).setText(valueToSet.asString());
            }

            if (valueComponent instanceof ListEditor) {
                ((ListEditor)valueComponent).setValue(valueToSet);
            }

            if (!valueToSet.isDefined()) return;

            if (valueComponent instanceof JTextComponent) {
                ((JTextComponent)valueComponent).setText(valueToSet.asString());
            }

            if (valueComponent instanceof JCheckBox) {
                ((JCheckBox)valueComponent).setSelected(valueToSet.asBoolean());
            }

            if (valueComponent instanceof JComboBox) {
                ((JComboBox)valueComponent).setSelectedItem(valueToSet.asString());
            }

            if (valueComponent instanceof BrowsePanel) {
                ((BrowsePanel)valueComponent).setText(valueToSet.asString());
            }
        }

        private String makeLabelString(boolean addColon) {
            String labelString = name;
            if (addColon) labelString += ":";
            if (isRequired) labelString += " *";
            return labelString;
        }

        private JLabel makeLabel() {
            JLabel label = new JLabel(makeLabelString(true));
            label.setToolTipText(description);
            return label;
        }

        private JComboBox makeJComboBox(List<ModelNode> values) {
            Vector<String> valueVector = new Vector<String>(values.size());
            if (!isRequired) {
                valueVector.add("");
            }

            for (ModelNode node : values) {
                valueVector.add(node.asString());
            }
            return new JComboBox(valueVector);
        }

        // fill in form fields for write-attribute when an attribute node is selected.
        private void setWriteAttributeValues() {
            if (!OperationDialog.this.node.isLeaf()) return;
            if (!OperationDialog.this.opName.equals("write-attribute")) return;

            UserObject usrObj = (UserObject)OperationDialog.this.node.getUserObject();

            if (this.name.equals("name")) {
                ((JTextField)valueComponent).setText(usrObj.getName());
                return;
            }

            if (usrObj.getValue().equals("undefined")) return;

            if (this.name.equals("value")) ((JTextField)valueComponent).setText(usrObj.getValue());
        }

        @Override
        public int compareTo(Object t) {
            if (this.equals(t)) return 0;
            if (this.isResourceName) return -1;
            RequestProp compareTo = (RequestProp)t;
            if (this.isRequired && compareTo.isRequired) return 1;
            if (this.isRequired) return -1;
            return 1;
        }

        @Override
        public boolean equals(Object obj) {
            if (this == obj) return true;
            if (!(obj instanceof RequestProp)) return false;
            RequestProp compareTo = (RequestProp)obj;
            return this.name.equals(compareTo.name);
        }

        @Override
        public int hashCode() {
            return name.hashCode();
        }

    }

}
