/*
 * Licensed Materials - Property of Perforce Software, Inc. 
 * © Copyright Perforce Software, Inc. 2014, 2021 
 * © Copyright IBM Corp. 2009, 2014
 * © Copyright ILOG 1996, 2009
 * All Rights Reserved.
 *
 * Note to U.S. Government Users Restricted Rights:
 * The Software and Documentation were developed at private expense and
 * are "Commercial Items" as that term is defined at 48 CFR 2.101,
 * consisting of "Commercial Computer Software" and
 * "Commercial Computer Software Documentation", as such terms are
 * used in 48 CFR 12.212 or 48 CFR 227.7202-1 through 227.7202-4,
 * as applicable.
 */
package xml;

import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.GridLayout;
import java.awt.Toolkit;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.BufferedReader;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.StringReader;
import java.io.StringWriter;
import java.io.Writer;
import java.net.URL;

import javax.swing.ImageIcon;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.JTextField;
import javax.swing.JTree;
import javax.swing.SwingUtilities;
import javax.swing.border.TitledBorder;
import javax.swing.event.TreeModelEvent;
import javax.swing.event.TreeModelListener;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.DefaultTreeCellEditor;
import javax.swing.tree.DefaultTreeCellRenderer;
import javax.swing.tree.DefaultTreeModel;
import javax.swing.tree.TreePath;

import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.xml.sax.InputSource;

import ilog.views.chart.IlvChart;
import ilog.views.chart.IlvChartLayout;
import ilog.views.chart.IlvChartRenderer;
import ilog.views.chart.IlvLegend;
import ilog.views.chart.data.IlvDataSet;
import ilog.views.chart.data.xml.IlvXMLDataReader;
import ilog.views.chart.data.xml.IlvXMLDataSource;
import ilog.views.chart.graphic.IlvMarkerFactory;
import ilog.views.chart.renderer.IlvPolylineChartRenderer;
import ilog.views.chart.renderer.IlvSinglePolylineRenderer;
import ilog.views.util.IlvImageUtil;
import ilog.views.util.xml.XMLUtil;
import shared.AbstractChartExample;

/**
 * An example that shows how to import data contained in an XML document. The
 * document is displayed in three different graphic representations:
 * <ul>
 * <li>As a chart of polylines</li>
 * <li>As a tree</li>
 * <li>As text</li>
 * </ul>
 */
public class XMLDemo extends AbstractChartExample {
  /** The name of the default file. */
  static final String DEFAULT_FILE = "data.xml";
  private ImageIcon attrIcon;
  private ImageIcon textIcon;
  /** The chart data source containing the data. */
  IlvXMLDataSource xmlDS;
  /** The data reader. */
  IlvXMLDataReader xmlReader;
  /** The TreeModel used to visualize the DOM in a tree. */
  DefaultTreeModel treeModel;
  /** The TextArea used to visualize the document as text. */
  JTextArea textArea;
  /** The document ID. */
  String documentId;
  /** The document currently being viewed in the tree. */
  Document document;

  private boolean internalUpdate = false;

  /**
   * A <code>TreeModelListener</code> used to track any modification performed
   * on the tree DOM.
   */
  TreeModelListener treeModelListener = new TreeModelListener() {
    Override
    public void treeNodesChanged(TreeModelEvent evt) {
      handleTreeModelEvent(evt);
    }

    Override
    public void treeNodesInserted(TreeModelEvent evt) {
      treeNodesChanged(evt);
    }

    Override
    public void treeNodesRemoved(TreeModelEvent evt) {
      treeNodesChanged(evt);
    }

    Override
    public void treeStructureChanged(TreeModelEvent evt) {
      treeNodesChanged(evt);
    }
  };

  /**
   * Initializes a new <code>XMLSample</code>.
   */
  Override
  public void init(Container container) {
    super.init(container);
    container.setLayout(new GridLayout(2, 1));

    try {
      attrIcon = new ImageIcon(IlvImageUtil.loadImageFromFile(getClass(), "attr.gif"));
      textIcon = new ImageIcon(IlvImageUtil.loadImageFromFile(getClass(), "text.gif"));
    } catch (Exception e) {
      System.err.println("Cannot load images. " + e.getMessage());
      e.printStackTrace();
    }

    // The absolute path of the default file is memorized to be able to
    // set a systemId when a new InputSource is created from a stream.
    URL documentURL = getClass().getResource(DEFAULT_FILE);
    documentId = (documentURL == null) ? null : documentURL.toString();

    // ======================= Chart initialization =======================

    IlvChart chart = new IlvChart();
    chart.setAntiAliasing(true);
    // The chart data source. An IlvXMLDataSource is a specialized data source
    // used in association with an IlvXMLDataReader to load data from an
    // XML document. The document must be conform to the Perforce JViews
    // Charts DTD
    // (more information in the Reference Manual).
    xmlDS = new IlvXMLDataSource();
    xmlReader = new IlvXMLDataReader();
    xmlReader.setValidating(true);
    // Load the data contained in the data.xml file in the data source.
    loadDefaultFile();
    // Each data set contained in the XML file is rendered as a polyline.
    IlvPolylineChartRenderer renderer = new IlvPolylineChartRenderer() {
      Override
      protected IlvChartRenderer createChild(IlvDataSet dataSet) {
        IlvSinglePolylineRenderer child = new IlvSinglePolylineRenderer();
        child.setMarker(IlvMarkerFactory.getSquareMarker());
        return child;
      }
    };
    renderer.setDataSource(xmlDS);
    chart.addRenderer(renderer);
    // Add a legend to the chart on top of the chart area.
    IlvLegend legend = new IlvLegend();
    chart.addLegend(legend, IlvChartLayout.NORTH_BOTTOM);

    // ======================== UI Initialization =========================

    JPanel panel = new JPanel(new GridLayout(1, 2));
    JPanel left = new JPanel(new BorderLayout());

    // The text area is used to display the XML document as text.
    textArea = new JTextArea();
    try {
      BufferedReader in = new BufferedReader(new InputStreamReader(((new URL(documentId)).openStream())));
      textArea.read(in, null);
      in.close();
    } catch (IOException e) {
      e.printStackTrace();
    }
    left.add(new JScrollPane(textArea), BorderLayout.CENTER);
    JButton button = new JButton("Send to chart");
    button.addActionListener(new ActionListener() {
      Override
      public void actionPerformed(ActionEvent evt) {
        sendToChart();
      }
    });
    left.add(button, BorderLayout.SOUTH);
    left.setBorder(new TitledBorder(null, "Text view (Edit text + \"Send\" to update)", TitledBorder.CENTER,
        TitledBorder.DEFAULT_POSITION));

    // The JTree is used to display the DOM as a tree.
    treeModel = new DefaultTreeModel(new DefaultMutableTreeNode("<No document>"));
    Document document = xmlReader.getDocument();
    fillTreeModel(document);
    treeModel.addTreeModelListener(treeModelListener);
    JTree tree = new JTree(treeModel);
    configureTree(tree);
    panel.add(left);
    JPanel right = new JPanel(new BorderLayout());
    right.setBorder(new TitledBorder(null, "Tree view (Click on a node to edit)", TitledBorder.CENTER,
        TitledBorder.DEFAULT_POSITION));
    right.add(new JScrollPane(tree), BorderLayout.CENTER);
    panel.add(right);
    // A button to reload the original document.
    button = new JButton("Reset to default data");
    button.addActionListener(new ActionListener() {
      Override
      public void actionPerformed(ActionEvent evt) {
        loadDefaultFile();
        fillTreeModel(xmlReader.getDocument());
      }
    });
    // Add the button to the chart as its footer. The footer is a JComponent
    // that can be added at the bottom of the chart area.
    chart.setFooter(button);
    // The header is a JComponent that can be added on the top of the chart.
    // As a convenience, the setHeaderText method offers an easy way to
    // have just a label as header: a JLabel initialized with the specified
    // string is automatically created and added to the chart.
    chart.setHeaderText("Chart view");

    container.add(chart);
    container.add(panel);
  }

  /**
   * Loads the document edited in the text area in the chart. This method also
   * refreshes the tree view.
   */
  protected void sendToChart() {
    // The contents of the text area is first saved in a StringWriter. Then
    // an InputSource that will read the writer buffer is created. Finally,
    // The InputSource is read by the data source reader to initialize the
    // data sets.
    Writer writer = new StringWriter();
    try {
      textArea.write(writer);
      InputSource src = new InputSource(new StringReader(writer.toString()));
      src.setSystemId(documentId);
      xmlDS.load(src, xmlReader);
      // Disable the internal TreeModel listener.
      internalUpdate = true;
      // Refresh the tree according to the new document that has just been
      // read.
      fillTreeModel(xmlReader.getDocument());
      internalUpdate = false;
    } catch (Exception e) {
      JOptionPane.showMessageDialog(null, "Not a valid JViews Charts XML document.", "Cannot load data !",
          JOptionPane.ERROR_MESSAGE);
    }
  }

  /**
   * Load the contents of the default <i>data.xml</i> file.
   */
  protected void loadDefaultFile() {
    try {
      // Load the chart data contained in the given document. This method
      // parses the XML document (calling the IlvXMLDataReader.read() method)
      // and initializes its data sets.
      xmlDS.load(new InputSource(documentId), xmlReader);
      xmlReader.getDocument().getDoctype();
    } catch (Exception e) {
      e.printStackTrace();
    }
  }

  // ==================== JTree handling ===============================

  /**
   * Returns a string representation of the current DOM viewed in the tree. May
   * return <code>null</code> if the transformation failed.
   */
  private String documentToString() {
    String doc = null;
    try {
      ByteArrayOutputStream writer = new ByteArrayOutputStream();
      XMLUtil.WriteDocument(document, writer, document.getDoctype().getPublicId(), document.getDoctype().getSystemId(),
          null);
      doc = writer.toString();
      writer.close();
    } catch (IOException e) {
      e.printStackTrace();
    }
    // System.err.println("doc : \n" + doc);
    return doc;
  }

  /**
   * Called when an event occurs on the <code>TreeModel</code>. This methods
   * loads the new DOM in the chart and text views.
   */
  protected void handleTreeModelEvent(TreeModelEvent evt) {
    if (internalUpdate)
      return;
    // The new DOM is saved as a string that is used to create a reader
    // needed to initialize a new InputSource. This InputSource is then
    // loaded by the XML data source using the XML reader.
    String doc = documentToString();
    StringReader reader = new StringReader(doc);
    try {
      InputSource src = new InputSource(reader);
      // Set the system ID to find the DTD.
      src.setSystemId(documentId.toString());
      xmlDS.load(src, xmlReader);
    } catch (Exception e) {
      JOptionPane.showMessageDialog(null, "Not a valid JViews Charts XML document.", "Cannot load data !",
          JOptionPane.ERROR_MESSAGE);
    } finally {
      reader.close();
    }
    // Refresh the text area contents.
    textArea.setText(doc);
  }

  /**
   * Configures the specified tree to use a custom <code>CellRenderer</code> and
   * <code>CellEditor</code> able to handle <code>org.w3c.dom.Node</code> user
   * objects.
   */
  protected void configureTree(JTree tree) {
    tree.setEditable(true);
    // Create a custom cell renderer.
    // The text of a tree node is initialized to:
    // - the Node value for a Node of type TEXT
    // - the Node name + the Node value for a Node of type ATTRIBUTE
    // - the Node name for a Node of another type
    DefaultTreeCellRenderer cellRenderer = new DefaultTreeCellRenderer() {
      Override
      public Component getTreeCellRendererComponent(JTree tree, Object value, boolean selected, boolean expanded,
          boolean leaf, int row, boolean hasFocus) {
        super.getTreeCellRendererComponent(tree, value, selected, expanded, leaf, row, hasFocus);
        Node node = (Node) ((DefaultMutableTreeNode) value).getUserObject();
        if (node.getNodeType() == Node.TEXT_NODE) {
          setText(node.getNodeValue());
          if (textIcon != null)
            setIcon(textIcon);
        } else if (node.getNodeType() == Node.ATTRIBUTE_NODE) {
          setText(node.getNodeName() + " : " + node.getNodeValue());
          if (attrIcon != null)
            setIcon(attrIcon);
        } else {
          setText(node.getNodeName());
        }
        return this;
      }

    };
    tree.setCellRenderer(cellRenderer);
    // Create a custom editor able to handle org.w3c.dom.Node object.
    tree.setCellEditor(new DefaultTreeCellEditor(tree, cellRenderer) {
      // During editing, the text field text is initialized to the
      // Node value for a Node of type TEXT or ATTRIBUTE or the Node name
      // for other types.
      Override
      public Component getTreeCellEditorComponent(JTree tree, Object value, boolean isSelected, boolean expanded,
          boolean leaf, int row) {
        Component comp = super.getTreeCellEditorComponent(tree, value, isSelected, expanded, leaf, row);
        Node node = (Node) ((DefaultMutableTreeNode) value).getUserObject();
        if ((node.getNodeType() == Node.TEXT_NODE) || (node.getNodeType() == Node.ATTRIBUTE_NODE))
          ((JTextField) editingComponent).setText(node.getNodeValue());
        else
          ((JTextField) editingComponent).setText(node.getNodeName());

        return comp;
      }

      // After a cell has been edited, set the value of the corresponding
      // Node to the text field text.
      Override
      public Object getCellEditorValue() {
        String value = (String) super.getCellEditorValue();
        TreePath path = this.tree.getPathForRow(lastRow);
        if (path != null) {
          DefaultMutableTreeNode treeNode = (DefaultMutableTreeNode) path.getLastPathComponent();
          Node node = (Node) treeNode.getUserObject();
          if ((node.getNodeType() == Node.TEXT_NODE) || (node.getNodeType() == Node.ATTRIBUTE_NODE))
            node.setNodeValue(value);
          return node;
        }
        return value;
      }
    });
  }

  /**
   * Populates the tree model according to the specified Document.
   */
  protected void fillTreeModel(Document doc) {
    document = doc;
    Element root = document.getDocumentElement();
    DefaultMutableTreeNode treeRoot = createTreeNode(root);
    treeModel.setRoot(treeRoot);
    addToTree(treeRoot, root);
  }

  private void addToTree(DefaultMutableTreeNode parent, Node node) {
    // Every node is associated with its corresponding DOM element.
    String value = null;
    Node child = node.getFirstChild();
    while (child != null) {
      if (child.getNodeType() == Node.ELEMENT_NODE) {
        DefaultMutableTreeNode childTreeNode = createTreeNode(child);
        parent.add(childTreeNode);
        NamedNodeMap attributes = child.getAttributes();
        for (int i = 0; i < attributes.getLength(); ++i) {
          Node attr = attributes.item(i);
          DefaultMutableTreeNode attrNode = createTreeNode(attr);
          childTreeNode.add(attrNode);
        }
        addToTree(childTreeNode, child);
      } else if ((child.getNodeType() == Node.TEXT_NODE)
          && ((value = child.getNodeValue()).length() > 0 && Character.isJavaIdentifierPart(value.charAt(0)))) {
        parent.add(createTreeNode(child));
      }
      child = child.getNextSibling();
    }
  }

  /**
   * Factory method that returns a <code>DefaultMutableTreeNode</code> instance.
   */
  protected DefaultMutableTreeNode createTreeNode(Node node) {
    return new DefaultMutableTreeNode(node);
  }

  public static void main(String[] args) {
    SwingUtilities.invokeLater(new Runnable() {
      Override
      public void run() {
        JFrame frame = new JFrame("XMLDemo");
        XMLDemo sample = new XMLDemo();
        sample.init(frame.getContentPane());
        frame.setSize(800, 600);
        Dimension d = Toolkit.getDefaultToolkit().getScreenSize();
        frame.setLocation(d.width / 2 - frame.getWidth() / 2, d.height / 2 - frame.getHeight() / 2);
        frame.setVisible(true);
      }
    });
  }
}