/*

   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

       http://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.batik.apps.svgbrowser;

import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.Frame;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.awt.event.ActionEvent;
import java.awt.event.FocusAdapter;
import java.awt.event.FocusEvent;
import java.io.IOException;
import java.io.StringReader;
import java.util.EventListener;
import java.util.EventObject;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.ResourceBundle;
import java.util.Vector;

import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.JButton;
import javax.swing.JDialog;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.JTextField;
import javax.swing.SwingUtilities;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.event.EventListenerList;
import javax.swing.event.TableModelEvent;
import javax.swing.event.TableModelListener;
import javax.swing.table.DefaultTableModel;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

import org.apache.batik.dom.AbstractNode;
import org.apache.batik.dom.util.DOMUtilities;
import org.apache.batik.util.SVGConstants;
import org.apache.batik.util.gui.resource.ActionMap;
import org.apache.batik.util.gui.resource.ButtonFactory;
import org.apache.batik.util.gui.resource.MissingListenerException;
import org.apache.batik.util.gui.xmleditor.XMLTextEditor;
import org.apache.batik.constants.XMLConstants;
import org.apache.batik.util.resources.ResourceManager;

import org.w3c.dom.Attr;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;

import org.xml.sax.ErrorHandler;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;

/**
 * Used to preview and edit nodes.
 */
public class NodePickerPanel extends JPanel implements ActionMap {


     // The NodePickerPanel modes of work
    /**
     * View only. Inspects the associated node.
     */
    private static final int VIEW_MODE = 1;

    /**
     * Edit mode. Used for editing the associated node.
     */
    private static final int EDIT_MODE = 2;

    /**
     * Creates new element while in this mode.
     */
    private static final int ADD_NEW_ELEMENT = 3;

    /**
     * The resource file name.
     */
    private static final String RESOURCES =
        "org.apache.batik.apps.svgbrowser.resources.NodePickerPanelMessages";

    /**
     * The resource bundle.
     */
    private static ResourceBundle bundle;

    /**
     * The resource manager.
     */
    private static ResourceManager resources;
    static {
        bundle = ResourceBundle.getBundle(RESOURCES, Locale.getDefault());
        resources = new ResourceManager(bundle);
    }

    /**
     * The attributes table - the table that consists of attribute name and
     * attribute value columns. Shows the element's attributes.
     */
    private JTable attributesTable;

    /**
     * The Attribute table model listener.
     */
    private TableModelListener tableModelListener;

    /**
     * The Attributes table ScrollPane.
     */
    private JScrollPane attributePane;

    /**
     * The Attributes table and buttons Panel.
     */
    private JPanel attributesPanel;

    /**
     * The Button factory.
     */
    private ButtonFactory buttonFactory;

    /**
     * The Add button.
     */
    private JButton addButton;

    /**
     * The Remove button.
     */
    private JButton removeButton;

    /**
     * The Attributes table label.
     */
    private JLabel attributesLabel;

    /**
     * The Apply button.
     */
    private JButton applyButton;

    /**
     * The Reset button.
     */
    private JButton resetButton;

    /**
     * The OK and Cancel button Panel.
     */
    private JPanel choosePanel;

    /**
     * The svg input panel.
     */
    private SVGInputPanel svgInputPanel;

    /**
     * The isWellFormed label.
     */
    private JLabel isWellFormedLabel;

    /**
     * The svgInputPanel name label.
     */
    private JLabel svgInputPanelNameLabel;

    /**
     * If the attribute table listener should process the update event and
     * update node picker after an update on the table had triggered. Used
     * instead of removing and adding the table listener.
     */
    private boolean shouldProcessUpdate = true;

    /**
     * The element that is being previewed or edited it's content (xml
     * representation).
     */
    private Element previewElement;

    /**
     * The copy of the original (preview) element. Used to synchronize svginput
     * area and the attributes table, since the original elements attributes
     * shouldn't be changed while previewing or editing it.
     */
    private Element clonedElement;

    /**
     * The parent Element for the element to be added. It is used when adding
     * the new element, to get the information on where to be appended.
     */
    private Node parentElement;

    /**
     * The panel mode.
     */
    private int mode;

    /**
     * If the element being edited is actually changed.
     */
    private boolean isDirty;

    /**
     * Listeners list.
     */
    private EventListenerList eventListeners =
        new EventListenerList();

    /**
     * The controller for this panel.
     */
    private NodePickerController controller;

    /**
     * The map that contains the listeners
     */
    private Map listeners = new HashMap(10);

    /**
     * Constructor.
     *
     * @param controller
     *            The node picker panel controller
     */
    public NodePickerPanel(NodePickerController controller) {
        super(new GridBagLayout());
        this.controller = controller;
        initialize();
    }

    /**
     * Initalizes this panel.
     */
    private void initialize() {
        // Associate buttons with the actions
        addButtonActions();

        // Add components
        GridBagConstraints grid = new GridBagConstraints();
        grid.gridx = 1;
        grid.gridy = 1;
        grid.anchor = GridBagConstraints.NORTHWEST;
        grid.fill = GridBagConstraints.NONE;
        grid.insets = new Insets(5, 5, 0, 5);
        attributesLabel = new JLabel();
        String attributesLabelValue = resources
                .getString("AttributesTable.name");
        attributesLabel.setText(attributesLabelValue);
        this.add(attributesLabel, grid);

        grid.gridx = 1;
        grid.gridy = 2;
        grid.gridwidth = 2;
        grid.weightx = 1.0;
        grid.weighty = 0.3;
        grid.fill = GridBagConstraints.BOTH;
        grid.anchor = GridBagConstraints.CENTER;
        grid.insets = new Insets(0, 0, 0, 5);
        this.add(getAttributesPanel(), grid);

        grid.weightx = 0;
        grid.weighty = 0;
        grid.gridwidth = 1;
        grid.gridx = 1;
        grid.gridy = 3;
        grid.anchor = GridBagConstraints.NORTHWEST;
        grid.fill = GridBagConstraints.NONE;
        grid.insets = new Insets(0, 5, 0, 5);
        svgInputPanelNameLabel = new JLabel();
        String svgInputLabelValue = resources.getString("InputPanelLabel.name");
        svgInputPanelNameLabel.setText(svgInputLabelValue);
        this.add(svgInputPanelNameLabel, grid);

        grid.gridx = 1;
        grid.gridy = 4;
        grid.gridwidth = 2;
        grid.weightx = 1.0;
        grid.weighty = 1.0;
        grid.fill = GridBagConstraints.BOTH;
        grid.anchor = GridBagConstraints.CENTER;
        grid.insets = new Insets(0, 5, 0, 10);
        this.add(getSvgInputPanel(), grid);

        grid.weightx = 0;
        grid.weighty = 0;
        grid.gridwidth = 1;
        grid.gridx = 1;
        grid.gridy = 5;
        grid.anchor = GridBagConstraints.NORTHWEST;
        grid.fill = GridBagConstraints.NONE;
        grid.insets = new Insets(5, 5, 0, 5);
        isWellFormedLabel = new JLabel();
        String isWellFormedLabelVal =
            resources.getString("IsWellFormedLabel.wellFormed");
        isWellFormedLabel.setText(isWellFormedLabelVal);
        this.add(isWellFormedLabel, grid);

        grid.weightx = 0;
        grid.weighty = 0;
        grid.gridwidth = 1;
        grid.gridx = 2;
        grid.gridy = 5;
        grid.anchor = GridBagConstraints.EAST;
        grid.insets = new Insets(0, 0, 0, 5);
        this.add(getChoosePanel(), grid);

        // Set the default mode
        enterViewMode();
    }

    /**
     * Gets buttonFactory.
     */
    private ButtonFactory getButtonFactory() {
        if (buttonFactory == null) {
            buttonFactory = new ButtonFactory(bundle, this);
        }
        return buttonFactory;
    }

    /**
     * Adds button actions.
     */
    private void addButtonActions() {
        listeners.put("ApplyButtonAction", new ApplyButtonAction());
        listeners.put("ResetButtonAction", new ResetButtonAction());
        listeners.put("AddButtonAction", new AddButtonAction());
        listeners.put("RemoveButtonAction", new RemoveButtonAction());
    }

    /**
     * Gets the Add button.
     */
    private JButton getAddButton() {
        if (addButton == null) {
            addButton = getButtonFactory().createJButton("AddButton");
            addButton.addFocusListener(new NodePickerEditListener());
        }
        return addButton;
    }

    /**
     * Gets the Remove button.
     */
    private JButton getRemoveButton() {
        if (removeButton == null) {
            removeButton = getButtonFactory().createJButton("RemoveButton");
            removeButton.addFocusListener(new NodePickerEditListener());
        }
        return removeButton;
    }

    /**
     * Gets the Apply button.
     */
    private JButton getApplyButton() {
        if (applyButton == null) {
            applyButton = getButtonFactory().createJButton("ApplyButton");
        }
        return applyButton;
    }

    /**
     * Gets the Reset sbutton.
     */
    private JButton getResetButton() {
        if (resetButton == null) {
            resetButton = getButtonFactory().createJButton("ResetButton");
        }
        return resetButton;
    }

    /**
     * Gets the attributesPanel.
     */
    private JPanel getAttributesPanel() {
        if (attributesPanel == null) {
            attributesPanel = new JPanel(new GridBagLayout());

            GridBagConstraints g11 = new GridBagConstraints();
            g11.gridx = 1;
            g11.gridy = 1;
            g11.fill = GridBagConstraints.BOTH;
            g11.anchor = GridBagConstraints.CENTER;
            g11.weightx = 4.0;
            g11.weighty = 1.0;
            g11.gridheight = 5;
            g11.gridwidth = 2;
            g11.insets = new Insets(5, 5, 5, 0);

            GridBagConstraints g12 = new GridBagConstraints();
            g12.gridx = 3;
            g12.gridy = 1;
            g12.fill = GridBagConstraints.HORIZONTAL;
            g12.anchor = GridBagConstraints.NORTH;
            g12.insets = new Insets(5, 20, 0, 5);
            g12.weightx = 1.0;

            GridBagConstraints g32 = new GridBagConstraints();
            g32.gridx = 3;
            g32.gridy = 3;
            g32.fill = GridBagConstraints.HORIZONTAL;
            g32.anchor = GridBagConstraints.NORTH;
            g32.insets = new Insets(5, 20, 0, 5);
            g32.weightx = 1.0;

            attributesTable = new JTable();
            attributesTable.setModel(new AttributesTableModel(10, 2));
            tableModelListener = new AttributesTableModelListener();
            attributesTable.getModel()
                    .addTableModelListener(tableModelListener);
            attributesTable.addFocusListener(new NodePickerEditListener());
            attributePane = new JScrollPane();
            attributePane.getViewport().add(attributesTable);

            attributesPanel.add(attributePane, g11);
            attributesPanel.add(getAddButton(), g12);
            attributesPanel.add(getRemoveButton(), g32);
        }
        return attributesPanel;
    }

    /**
     * Gets the svgInputPanel.
     */
    private SVGInputPanel getSvgInputPanel() {
        if (svgInputPanel == null) {
            svgInputPanel = new SVGInputPanel();
            svgInputPanel.getNodeXmlArea().getDocument().addDocumentListener
                (new XMLAreaListener());
            svgInputPanel.getNodeXmlArea().addFocusListener
                (new NodePickerEditListener());
        }
        return svgInputPanel;
    }

    /**
     * Gets the choosePanel.
     */
    private JPanel getChoosePanel() {
        if (choosePanel == null) {
            choosePanel = new JPanel(new GridBagLayout());

            GridBagConstraints g11 = new GridBagConstraints();
            g11.gridx = 1;
            g11.gridy = 1;
            g11.weightx = 0.5;
            g11.anchor = GridBagConstraints.WEST;
            g11.fill = GridBagConstraints.HORIZONTAL;
            g11.insets = new Insets(5, 5, 5, 5);

            GridBagConstraints g12 = new GridBagConstraints();
            g12.gridx = 2;
            g12.gridy = 1;
            g12.weightx = 0.5;
            g12.anchor = GridBagConstraints.EAST;
            g12.fill = GridBagConstraints.HORIZONTAL;
            g12.insets = new Insets(5, 5, 5, 5);

            choosePanel.add(getApplyButton(), g11);
            choosePanel.add(getResetButton(), g12);
        }
        return choosePanel;
    }

    /**
     * Gets the results of this node picker panel - gets the contents of the xml
     * text area.
     */
    public String getResults() {
        return getSvgInputPanel().getNodeXmlArea().getText();
    }

    /**
     * Update the components and the element after text is being inputted in the
     * xml text area.
     *
     * @param referentElement
     *            The updated element, referent element
     * @param elementToUpdate
     *            The element to update.
     */
    private void updateViewAfterSvgInput(Element referentElement,
            Element elementToUpdate) {
        if (referentElement != null) {
            String isWellFormedLabelVal =
                resources.getString("IsWellFormedLabel.wellFormed");
            isWellFormedLabel.setText(isWellFormedLabelVal);
            getApplyButton().setEnabled(true);
            attributesTable.setEnabled(true);
            updateElementAttributes(elementToUpdate, referentElement);
            shouldProcessUpdate = false;
            updateAttributesTable(elementToUpdate);
            shouldProcessUpdate = true;
        } else {
            String isWellFormedLabelVal =
                resources.getString("IsWellFormedLabel.notWellFormed");
            isWellFormedLabel.setText(isWellFormedLabelVal);
            getApplyButton().setEnabled(false);
            attributesTable.setEnabled(false);
        }
    }

    /**
     * Replaces all of the attributes of the given element with the referent
     * element's attributes.
     *
     * @param elem
     *            The element whose attributes should be replaced
     * @param referentElement
     *            The referentElement to copy the attributes from
     */
    private void updateElementAttributes(Element elem, Element referentElement) {
        // Remove all element attributes
        removeAttributes(elem);

        // Copy all attributes from the referent element to the given element
        NamedNodeMap newNodeMap = referentElement.getAttributes();
        for (int i = newNodeMap.getLength() - 1; i >= 0; i--) {
            Node newAttr = newNodeMap.item(i);
            String qualifiedName = newAttr.getNodeName();
            String attributeValue = newAttr.getNodeValue();
            String prefix = DOMUtilities.getPrefix(qualifiedName);
            String namespaceURI = getNamespaceURI(prefix);
            elem.setAttributeNS(namespaceURI, qualifiedName, attributeValue);
        }
    }

    /**
     * Replaces all of the atributes of a given element with the values from the
     * given table model.
     *
     * @param element
     *            The node whose attributes should update
     * @param tableModel
     *            The tableModel from which to get attributes
     */
    private void updateElementAttributes
            (Element element, AttributesTableModel tableModel) {

        // Remove all element attributes
        removeAttributes(element);

        // Copy all the attribute name - value pairs from the table model to
        // the given element
        for (int i = 0; i < tableModel.getRowCount(); i++) {
            String newAttrName = (String) tableModel.getAttrNameAt(i);
            String newAttrValue = (String) tableModel.getAttrValueAt(i);
            if (newAttrName != null && newAttrName.length() > 0) {
                String namespaceURI;
                if (newAttrName.equals(XMLConstants.XMLNS_PREFIX)) {
                    namespaceURI = XMLConstants.XMLNS_NAMESPACE_URI;
                } else {
                    String prefix = DOMUtilities.getPrefix(newAttrName);
                    namespaceURI = getNamespaceURI(prefix);
                }
                if (newAttrValue != null) {
                    element.setAttributeNS
                        (namespaceURI, newAttrName, newAttrValue);
                } else {
                    element.setAttributeNS(namespaceURI, newAttrName, "");
                }
            }
        }
    }

    /**
     * Removes all the attributes from an element.
     *
     * @param element
     *            The given element
     */
    private void removeAttributes(Element element) {
        NamedNodeMap oldNodeMap = element.getAttributes();
        int n = oldNodeMap.getLength();
        for (int i = n - 1; i >= 0; i--) {
            element.removeAttributeNode((Attr) oldNodeMap.item(i));
        }
    }

    /**
     * Looks up for the namespaceURI based on the given prefix. Uses the
     * Node.lookupNamespaceURI method, starting from the parent element of
     * the element being edited / created.
     *
     * @param prefix
     *            The given prefix
     * @return namespaceURI or null
     */
    private String getNamespaceURI(String prefix) {
        String namespaceURI = null;
        if (prefix != null) {
            if (prefix.equals(SVGConstants.XMLNS_PREFIX)) {
                namespaceURI = SVGConstants.XMLNS_NAMESPACE_URI;
            } else {
                AbstractNode n;
                if (mode == EDIT_MODE) {
                    n = (AbstractNode) previewElement;
                    namespaceURI = n.lookupNamespaceURI(prefix);
                } else if (mode == ADD_NEW_ELEMENT) {
                    n = (AbstractNode) parentElement;
                    namespaceURI = n.lookupNamespaceURI(prefix);
                }

            }
        }
        return namespaceURI;
    }

    /**
     * Fills the attributesTable with the given element attribute name - value
     * pairs.
     *
     * @param elem
     *            The given element
     */
    private void updateAttributesTable(Element elem) {
        NamedNodeMap map = elem.getAttributes();
        AttributesTableModel tableModel =
            (AttributesTableModel) attributesTable.getModel();
        // Remove and update rows from the table if needed...
        for (int i = tableModel.getRowCount() - 1; i >= 0; i--) {
            String attrName = (String) tableModel.getValueAt(i, 0);
            String newAttrValue = "";
            if (attrName != null) {
                newAttrValue = elem.getAttributeNS(null, attrName);
            }
            if (attrName == null || newAttrValue.length() == 0) {
                tableModel.removeRow(i);
            }
            if (newAttrValue.length() > 0) {
                tableModel.setValueAt(newAttrValue, i, 1);
            }
        }

        // Add rows
        for (int i = 0; i < map.getLength(); i++) {
            Node attr = map.item(i);
            String attrName = attr.getNodeName();
            String attrValue = attr.getNodeValue();
            if (tableModel.getValueForName(attrName) == null) {
                Vector rowData = new Vector();
                rowData.add(attrName);
                rowData.add(attrValue);
                tableModel.addRow(rowData);
            }
        }
    }

    /**
     * Shows node's String representation in svgInputPanel
     *
     * @param node
     *            The given node
     */
    private void updateNodeXmlArea(Node node) {
        getSvgInputPanel().getNodeXmlArea().setText(DOMUtilities.getXML(node));
    }

    /**
     * Getter for the preivewElement.
     *
     * @return the preivewElement
     */
    private Element getPreviewElement() {
        return previewElement;
    }

    /**
     * Sets the preview element. Enters the view mode and updates the associated
     * components.
     *
     * @param elem
     *            the element to set
     */
    public void setPreviewElement(Element elem) {
        if (previewElement != elem && isDirty) {
            if (!promptForChanges()) {
                return;
            }
        }

        this.previewElement = elem;
        enterViewMode();

        updateNodeXmlArea(elem);
        updateAttributesTable(elem);
    }

    /**
     * Invoked by the {@link DOMViewer} to inform the
     * <code>NodePickerPanel</code> that it is being hidden.
     */
    boolean panelHiding() {
        return !isDirty || promptForChanges();
    }

    /**
     * Gets the current working mode.
     *
     * @return the mode
     */
    private int getMode() {
        return mode;
    }

    /**
     * Enters the view mode.
     */
    public void enterViewMode() {
        if (mode != VIEW_MODE) {
            mode = VIEW_MODE;
            // Disable appropriate buttons
            getApplyButton().setEnabled(false);
            getResetButton().setEnabled(false);
            // Enable the remove and add buttons
            getRemoveButton().setEnabled(true);
            getAddButton().setEnabled(true);
            // Update the isWellFormed label
            String isWellFormedLabelVal =
                resources.getString("IsWellFormedLabel.wellFormed");
            isWellFormedLabel.setText(isWellFormedLabelVal);
        }
    }

    /**
     * Enters the edit mode.
     */
    public void enterEditMode() {
        if (mode != EDIT_MODE) {
            mode = EDIT_MODE;
            clonedElement = (Element) previewElement.cloneNode(true);

            // Enable appropriate buttons
            getApplyButton().setEnabled(true);
            getResetButton().setEnabled(true);
        }
    }

    /**
     * Enters the add new element mode.
     *
     * @param newElement
     *            The element to be added
     * @param parent
     *            The parent node of the element to be added
     */
    public void enterAddNewElementMode(Element newElement, Node parent) {
        if (mode != ADD_NEW_ELEMENT) {
            mode = ADD_NEW_ELEMENT;
            previewElement = newElement;
            clonedElement = (Element) newElement.cloneNode(true);
            parentElement = parent;
            // Update the appropriate areas
            updateNodeXmlArea(newElement);
            // Enable appropriate buttons
            getApplyButton().setEnabled(true);
            getResetButton().setEnabled(true);
//          // Request focus
//          getSvgInputPanel().getNodeXmlArea().requestFocusInWindow();
        }
    }

    /**
     * Updates the panel when DOM Mutation event occures.
     */
    public void updateOnDocumentChange(String mutationEventType, Node targetNode) {
        if (mode == VIEW_MODE) {
            if (this.isShowing() &&
                    shouldUpdate(mutationEventType,
                                 targetNode,
                                 getPreviewElement())) {
                setPreviewElement(getPreviewElement());
            }
        }
    }

    /**
     * If the panel should update its components after dom mutation event.
     * Checks whether any node that is the child node of the node currently
     * being previewed has changed. If true, updates the xml text area of this
     * NodePicker. In case of DOMAttrModiefied mutation event, the additional
     * condition is added - to check whether the attributes of an element that
     * is being previewed are changed. If true, the xml text area is refreshed.
     *
     * @return True if should update
     */
    private boolean shouldUpdate(String mutationEventType, Node affectedNode,
            Node currentNode) {
        if (mutationEventType.equals("DOMNodeInserted")) {
            if (DOMUtilities.isAncestorOf(currentNode, affectedNode)) {
                return true;
            }
        } else if (mutationEventType.equals("DOMNodeRemoved")) {
            if (DOMUtilities.isAncestorOf(currentNode, affectedNode)) {
                return true;
            }
        } else if (mutationEventType.equals("DOMAttrModified")) {
            if (DOMUtilities.isAncestorOf(currentNode, affectedNode)
                    || currentNode == affectedNode) {
                return true;
            }
        } else if (mutationEventType.equals("DOMCharDataModified")) {
            if (DOMUtilities.isAncestorOf(currentNode, affectedNode)) {
                return true;
            }
        }
        return false;
    }

    /**
     * Parses the given xml and return parsed document's root element.
     * Used to check whether the given xml is well formed.
     *
     * @param xmlString
     *            Xml as a String
     * @return Element
     */
    private Element parseXml(String xmlString) {
        Document doc = null;
        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
        try {
            factory.setFeature("http://xml.org/sax/features/external-general-entities", false);
            factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
            factory.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
            javax.xml.parsers.DocumentBuilder parser = factory.newDocumentBuilder();
            parser.setErrorHandler(new ErrorHandler() {
                public void error(SAXParseException exception)
                        throws SAXException {
                }

                public void fatalError(SAXParseException exception)
                        throws SAXException {
                }

                public void warning(SAXParseException exception)
                        throws SAXException {
                }
            });
            doc = parser.parse(new InputSource(new StringReader(xmlString)));
        } catch (ParserConfigurationException e1) {
        } catch (SAXException e1) {
        } catch (IOException e1) {
        }
        if (doc != null) {
            return doc.getDocumentElement();
        }
        return null;
    }

    /**
     * Sets the node picker components to be editable / uneditable.
     *
     * @param editable
     *            Whether to enable or disable edit
     */
    public void setEditable(boolean editable) {
        getSvgInputPanel().getNodeXmlArea().setEditable(editable);
        getResetButton().setEnabled(editable);
        getApplyButton().setEnabled(editable);
        getAddButton().setEnabled(editable);
        getRemoveButton().setEnabled(editable);
        attributesTable.setEnabled(editable);
    }

    /**
     * Checks whether the given component is a part component of the this node
     * picker.
     *
     * @param component
     *            The given component
     * @return True if the given component is a part of the this NodePicker
     */
    private boolean isANodePickerComponent(Component component) {
        return SwingUtilities.getAncestorOfClass(NodePickerPanel.class,
                                                 component) != null;
    }

    /**
     * Shows a dialog to save changes.
     */
    public boolean promptForChanges() {
        // If the xml is well formed
        if (getApplyButton().isEnabled() && isElementModified()) {
            String confirmString = resources.getString("ConfirmDialog.message");
            int option = JOptionPane.showConfirmDialog(getSvgInputPanel(),
                                                       confirmString);
            if (option == JOptionPane.YES_OPTION) {
                getApplyButton().doClick();
            } else if (option == JOptionPane.CANCEL_OPTION) {
                return false;
            } else {
                getResetButton().doClick();
            }
        } else {
            getResetButton().doClick();
        }
        isDirty = false;
        return true;
    }

    /**
     * Whether the element being edit is changed.
     *
     * @return True if the element being edit is changed
     */
    private boolean isElementModified() {
        if (getMode() == EDIT_MODE) {
            return !DOMUtilities.getXML(previewElement).equals
                (getSvgInputPanel().getNodeXmlArea().getText());
        } else if (getMode() == ADD_NEW_ELEMENT) {
            return true;
        }
        return false;
    }

    /**
     * Manages the edits on focus events.
     */
    protected class NodePickerEditListener extends FocusAdapter {

        public void focusGained(FocusEvent e) {
            if (getMode() == VIEW_MODE) {
                enterEditMode();
            }
            setEditable(controller.isEditable()
                    && controller.canEdit(previewElement));
            isDirty = isElementModified();
        }

        // XXX Java 1.3 does not have getOppositeComponent()
        /*public void focusLost(FocusEvent e) {
            // Prompts the user to save changes that he made for an element,
            // when the NodePicker loses focus
            if (!isANodePickerComponent(e.getOppositeComponent())
                    && !e.isTemporary() && isDirty) {
                promptForChanges();
            }
        }*/
    }

    /**
     * Listens for the changes in the xml text area and updates this node picker
     * panel if needed.
     */
    protected class XMLAreaListener implements DocumentListener {
        public void changedUpdate(DocumentEvent e) {
            isDirty = isElementModified();
        }

        public void insertUpdate(DocumentEvent e) {
            updateNodePicker(e);
            isDirty = isElementModified();
        }

        public void removeUpdate(DocumentEvent e) {
            updateNodePicker(e);
            isDirty = isElementModified();
        }

        /**
         * Updates the node picker panel after document changes.
         *
         * @param e
         *            The document event
         */
        private void updateNodePicker(DocumentEvent e) {
            if (getMode() == EDIT_MODE) {
                updateViewAfterSvgInput
                    (parseXml(svgInputPanel.getNodeXmlArea().getText()),
                     clonedElement);
            } else if (getMode() == ADD_NEW_ELEMENT) {
                updateViewAfterSvgInput
                    (parseXml(svgInputPanel.getNodeXmlArea().getText()),
                     previewElement);
            }
        }
    }

    /**
     * Listens for the changes in the table and updates this node picker panel
     * if needed.
     */
    protected class AttributesTableModelListener implements TableModelListener {
        public void tableChanged(TableModelEvent e) {
            if (e.getType() == TableModelEvent.UPDATE && shouldProcessUpdate) {
                updateNodePicker(e);
            }
        }

        /**
         * Updates the node picker panel after document changes.
         *
         * @param e
         *            The document event
         */
        private void updateNodePicker(TableModelEvent e) {
            if (getMode() == EDIT_MODE) {
                updateElementAttributes
                    (clonedElement, (AttributesTableModel) (e.getSource()));
                updateNodeXmlArea(clonedElement);
            } else if (getMode() == ADD_NEW_ELEMENT) {
                updateElementAttributes
                    (previewElement, (AttributesTableModel) (e.getSource()));
                updateNodeXmlArea(previewElement);
            }
        }
    }

    /**
     * The action associated with the 'Apply' button.
     */
    protected class ApplyButtonAction extends AbstractAction {
        public void actionPerformed(ActionEvent e) {
            isDirty = false;
            String xmlAreaText = getResults();
            if (getMode() == EDIT_MODE) {
                fireUpdateElement
                    (new NodePickerEvent
                        (NodePickerPanel.this,
                         xmlAreaText,
                         previewElement,
                         NodePickerEvent.EDIT_ELEMENT));
            } else if (getMode() == ADD_NEW_ELEMENT) {
                fireAddNewElement
                    (new NodePickerEvent
                        (NodePickerPanel.this,
                         xmlAreaText,
                         parentElement,
                         NodePickerEvent.ADD_NEW_ELEMENT));
            }
            enterViewMode();
        }
    }

    /**
     * The action associated with the 'Reset' button.
     */
    protected class ResetButtonAction extends AbstractAction {
        public void actionPerformed(ActionEvent e) {
            isDirty = false;
            setPreviewElement(getPreviewElement());
        }
    }

    /**
     * The action associated with the 'Add' button.
     */
    protected class AddButtonAction extends AbstractAction {
        public void actionPerformed(ActionEvent e) {
            if (getMode() == VIEW_MODE) {
                enterEditMode();
            }
            DefaultTableModel model =
                (DefaultTableModel) attributesTable.getModel();
            shouldProcessUpdate = false;
            model.addRow((Vector) null);
            shouldProcessUpdate = true;
        }
    }

    /**
     * The action associated with the 'Remove' button.
     */
    protected class RemoveButtonAction extends AbstractAction {
        public void actionPerformed(ActionEvent e) {
            if (getMode() == VIEW_MODE) {
                enterEditMode();
            }
            // Find the contextElement
            Element contextElement = clonedElement;
            if (getMode() == ADD_NEW_ELEMENT) {
                contextElement = previewElement;
            }
            DefaultTableModel model =
                (DefaultTableModel) attributesTable.getModel();
            int[] selectedRows = attributesTable.getSelectedRows();
            for (int selectedRow : selectedRows) {
                String attrName = (String) model.getValueAt(selectedRow, 0);
                if (attrName != null) {
                    String prefix = DOMUtilities.getPrefix(attrName);
                    String localName = DOMUtilities.getLocalName(attrName);
                    String namespaceURI = getNamespaceURI(prefix);
                    contextElement.removeAttributeNS(namespaceURI, localName);
                }
            }
            shouldProcessUpdate = false;
            updateAttributesTable(contextElement);
            shouldProcessUpdate = true;
            updateNodeXmlArea(contextElement);
        }
    }

    /**
     * Returns the action associated with the given string or null on error
     *
     * @param key
     *            the key mapped with the action to get
     * @throws MissingListenerException
     *             if the action is not found
     */
    public Action getAction(String key) throws MissingListenerException {
        return (Action) listeners.get(key);
    }

    /**
     * The attributesTable model.
     */
    public static class AttributesTableModel extends DefaultTableModel {
        public AttributesTableModel(int rowCount, int columnCount) {
            super(rowCount, columnCount);
        }

        public String getColumnName(int column) {
            if (column == 0) {
                return resources.getString("AttributesTable.column1");
            } else {
                return resources.getString("AttributesTable.column2");
            }
        }

        /**
         * Gets the value of the attribute with the given attribute name.
         *
         * @param attrName
         *            The given attribute name
         */
        public Object getValueForName(Object attrName) {
            for (int i = 0; i < getRowCount(); i++) {
                if (getValueAt(i, 0) != null
                        && getValueAt(i, 0).equals(attrName)) {
                    return getValueAt(i, 1);
                }
            }
            return null;
        }

        /**
         * Gets the name of the attribute with the table row.
         */
        public Object getAttrNameAt(int i) {
            return getValueAt(i, 0);
        }

        /**
         * Gets the value of the attribute with the table row.
         */
        public Object getAttrValueAt(int i) {
            return getValueAt(i, 1);
        }

        /**
         * Gets the first row where the given attribute name appears.
         * @param attrName    The given attribute name
         */
        public int getRow(Object attrName) {
            for (int i = 0; i < getRowCount(); i++) {
                if (getValueAt(i, 0) != null
                        && getValueAt(i, 0).equals(attrName)) {
                    return i;
                }
            }
            return -1;
        }
    }

    // Custom events support
    /**
     * Fires the updateElement event.
     *
     * @param event
     *            The associated NodePickerEvent event
     */
    public void fireUpdateElement(NodePickerEvent event) {
        Object[] listeners = eventListeners.getListenerList();

        int length = listeners.length;
        for (int i = 0; i < length; i += 2) {
            if (listeners[i] == NodePickerListener.class) {
                ((NodePickerListener) listeners[i + 1])
                        .updateElement(event);
            }
        }
    }

    /**
     * Fires the AddNewElement event.
     *
     * @param event
     *            The associated NodePickerEvent event
     */
    public void fireAddNewElement(NodePickerEvent event) {
        Object[] listeners = eventListeners.getListenerList();
        int length = listeners.length;
        for (int i = 0; i < length; i += 2) {
            if (listeners[i] == NodePickerListener.class) {
                ((NodePickerListener) listeners[i + 1])
                        .addNewElement(event);
            }
        }
    }

    /**
     * Adds the listener to the listener list.
     *
     * @param listener
     *            The listener to add
     */
    public void addListener(NodePickerListener listener) {
        eventListeners.add(NodePickerListener.class, listener);
    }

    /**
     * Event to pass to listener.
     */
    public static class NodePickerEvent extends EventObject {

        // The event types
        public static final int EDIT_ELEMENT = 1;

        public static final int ADD_NEW_ELEMENT = 2;

        /**
         * The type of this event.
         */
        private int type;

        /**
         * The string that is to be parsed.
         */
        private String result;

        /**
         * The context node associated with this event.
         */
        private Node contextNode;

        /**
         * Creates the NodePickerEvent.
         *
         * @param source
         *            The NodePicker that initiated the event
         * @param result
         *            the NodePicker result
         * @param contextNode
         *            the associated context node
         */
        public NodePickerEvent(Object source, String result, Node contextNode,
                               int type) {
            super(source);
            this.result = result;
            this.contextNode = contextNode;
        }

        /**
         * Gets the NodePickerPanel result.
         *
         * @return the result
         */
        public String getResult() {
            return result;
        }

        /**
         * Gets the context node.
         * 'EDIT_ELEMENT' event type - the context node is the original element
         * being previewed.
         * 'ADD_NEW_ELEMENT' event type - the context node is the parent node of
         * the element being added
         *
         * @return the context node
         */
        public Node getContextNode() {
            return contextNode;
        }

        /**
         * Gets the type of this event.
         *
         * @return the type
         */
        public int getType() {
            return type;
        }
    }

    /**
     * Node picker listener.
     */
    public interface NodePickerListener extends EventListener {
        /**
         * Updates the element from the data contained in the NodePickerEvent.
         */
        void updateElement(NodePickerEvent event);

        /**
         * Adds the element from the data contained in the NodePickerEvent.
         */
        void addNewElement(NodePickerEvent event);
    }

    /**
     * The adapter for the NodePicker listener.
     */
    public static class NodePickerAdapter implements NodePickerListener {

        public void addNewElement(NodePickerEvent event) {
        }

        public void updateElement(NodePickerEvent event) {
        }
    }

    /**
     * The panel to view and edit the elements xml representation.
     */
    protected static class SVGInputPanel extends JPanel {

        /**
         * The text area.
         */
        protected XMLTextEditor nodeXmlArea;

        /**
         * Constructor.
         */
        public SVGInputPanel() {
            super(new BorderLayout());
            add(new JScrollPane(getNodeXmlArea()));
        }

        /**
         * Gets the nodeXmlArea.
         *
         * @return    the nodeXmlArea
         */
        protected XMLTextEditor getNodeXmlArea() {
            if (nodeXmlArea == null) {
                // Create syntax-highlighted text area
                nodeXmlArea = new XMLTextEditor();
                nodeXmlArea.setEditable(true);
            }
            return nodeXmlArea;
        }
    }

    /**
     * Dialog for choosing element name.
     */
    public static class NameEditorDialog extends JDialog implements ActionMap {

        /**
         * The return value if 'OK' is chosen.
         */
        public static final int OK_OPTION = 0;

        /**
         * The return value if 'Cancel' is chosen.
         */
        public static final int CANCEL_OPTION = 1;

        /**
         * The resource file name.
         */
        protected static final String RESOURCES =
            "org.apache.batik.apps.svgbrowser.resources.NameEditorDialogMessages";

        /**
         * The resource bundle.
         */
        protected static ResourceBundle bundle;

        /**
         * The resource manager.
         */
        protected static ResourceManager resources;
        static {
            bundle = ResourceBundle.getBundle(RESOURCES, Locale.getDefault());
            resources = new ResourceManager(bundle);
        }

        /**
         * The Dialog results.
         */
        protected int returnCode;

        /**
         * The Dialog main panel.
         */
        protected JPanel mainPanel;

        /**
         * The Button factory.
         */
        protected ButtonFactory buttonFactory;

        /**
         * The node name label.
         */
        protected JLabel nodeNameLabel;

        /**
         * The node name field.
         */
        protected JTextField nodeNameField;

        /**
         * The OK button.
         */
        protected JButton okButton;

        /**
         * The Cancel button.
         */
        protected JButton cancelButton;

        /**
         * The map that contains the listeners
         */
        protected Map listeners = new HashMap(10);

        /**
         * Constructor.
         *
         * @param frame
         *            Parent frame
         */
        public NameEditorDialog(Frame frame) {
            super(frame, true);
            this.setResizable(false);
            this.setModal(true);
            initialize();
        }

        /**
         * Initializes the dialog.
         */
        protected void initialize() {
            this.setSize(resources.getInteger("Dialog.width"),
                         resources.getInteger("Dialog.height"));
            this.setTitle(resources.getString("Dialog.title"));
            addButtonActions();
            this.setContentPane(getMainPanel());
        }

        /**
         * Gets buttonFactory.
         */
        protected ButtonFactory getButtonFactory() {
            if (buttonFactory == null) {
                buttonFactory = new ButtonFactory(bundle, this);
            }
            return buttonFactory;
        }

        /**
         * Adds button actions.
         */
        protected void addButtonActions() {
            listeners.put("OKButtonAction", new OKButtonAction());
            listeners.put("CancelButtonAction", new CancelButtonAction());
        }

        /**
         * Shows the dialog.
         *
         * @return OK_OPTION or CANCEL_OPTION.
         */
        public int showDialog() {
            setVisible(true);
            return returnCode;
        }

        /**
         * Gets the Ok button.
         *
         * @return the okButton
         */
        protected JButton getOkButton() {
            if (okButton == null) {
                okButton = getButtonFactory().createJButton("OKButton");
                this.getRootPane().setDefaultButton(okButton);
            }
            return okButton;
        }

        /**
         * Gets the Cancel button.
         *
         * @return the cancelButton
         */
        protected JButton getCancelButton() {
            if (cancelButton == null) {
                cancelButton = getButtonFactory().createJButton("CancelButton");
            }
            return cancelButton;
        }

        /**
         * Gets dialog's main panel.
         *
         * @return the mainPanel
         */
        protected JPanel getMainPanel() {
            if (mainPanel == null) {
                mainPanel = new JPanel(new GridBagLayout());

                GridBagConstraints gridBag = new GridBagConstraints();
                gridBag.gridx = 1;
                gridBag.gridy = 1;
                gridBag.fill = GridBagConstraints.NONE;
                gridBag.insets = new Insets(5, 5, 5, 5);
                mainPanel.add(getNodeNameLabel(), gridBag);

                gridBag.gridx = 2;
                gridBag.weightx = 1.0;
                gridBag.weighty = 1.0;
                gridBag.fill = GridBagConstraints.HORIZONTAL;
                gridBag.anchor = GridBagConstraints.CENTER;
                mainPanel.add(getNodeNameField(), gridBag);

                gridBag.gridx = 1;
                gridBag.gridy = 2;
                gridBag.weightx = 0;
                gridBag.weighty = 0;
                gridBag.anchor = GridBagConstraints.EAST;
                gridBag.fill = GridBagConstraints.HORIZONTAL;
                mainPanel.add(getOkButton(), gridBag);

                gridBag.gridx = 2;
                gridBag.gridy = 2;
                gridBag.anchor = GridBagConstraints.EAST;
                mainPanel.add(getCancelButton(), gridBag);
            }
            return mainPanel;
        }

        /**
         * Gets the node name label.
         *
         * @return the nodeNameLabel
         */
        public JLabel getNodeNameLabel() {
            if (nodeNameLabel == null) {
                nodeNameLabel = new JLabel();
                nodeNameLabel.setText(resources.getString("Dialog.label"));
            }
            return nodeNameLabel;
        }

        /**
         * Gets the text field for node name.
         *
         * @return the nodeNameField
         */
        protected JTextField getNodeNameField() {
            if (nodeNameField == null) {
                nodeNameField = new JTextField();
            }
            return nodeNameField;
        }

        /**
         * Gets the dialog results.
         *
         * @return the element name
         */
        public String getResults() {
            return nodeNameField.getText();
        }

        /**
         * The action associated with the 'OK' button of Attribute Adder Dialog
         */
        protected class OKButtonAction extends AbstractAction {
            public void actionPerformed(ActionEvent e) {
                returnCode = OK_OPTION;
                dispose();
            }
        }

        /**
         * The action associated with the 'Cancel' button of Attribute Adder
         * Dialog
         */
        protected class CancelButtonAction extends AbstractAction {
            public void actionPerformed(ActionEvent e) {
                returnCode = CANCEL_OPTION;
                dispose();
            }
        }

        /**
         * Returns the action associated with the given string or null on error
         *
         * @param key
         *            the key mapped with the action to get
         * @throws MissingListenerException
         *             if the action is not found
         */
        public Action getAction(String key) throws MissingListenerException {
            return (Action) listeners.get(key);
        }
    }
}

