/**
 * JOnAS: Java(TM) Open Application Server
 * Copyright (C) 2005 Bull S.A.
 * Contact: jonas-team@ow2.org
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or any later version.
 *
 * This library 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
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307
 * USA
 *
 * --------------------------------------------------------------------------
 * $Id: XMLToFormUtil.java 15428 2008-10-07 11:20:29Z sauthieg $
 * --------------------------------------------------------------------------
 */

package org.ow2.jonas.webapp.jonasadmin.xml;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.Map;

import org.ow2.jonas.webapp.jonasadmin.xml.xs.ElementRestrictions;
import org.ow2.jonas.webapp.jonasadmin.xml.xs.SchemaRestrictions;
import org.ow2.jonas.webapp.jonasadmin.xml.xs.SchemaRestrictionsFactory;
import org.w3c.dom.Document;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

/**
 * Convert a DOM tree into an HTML form for display to the user.
 *
 * @author Gregory Lapouchnina
 * @author Patrick Smith
 */
public class XMLToFormUtil {

    /**
     * Used to keep track when giving out unique IDs to tree nodes.
     */
    private int currentID;

    /**
     * Maximum length of an input field.
     */
    private static int MAX_INPUT_FIELD_SIZE = 50;

    /**
     * Create a new XMLToFormUtil with the current ID being 0.
     *
     */
    public XMLToFormUtil() {
        this.currentID = 0;
    }

    /**
     * Convert a DOM tree into a form to use on the configuration page.
     *
     * @param doc
     *            the document to display
     * @param mapping
     *            Map that will store the relationship between IDs (which are
     *            also the IDs of input fields) nodes in the tree.
     * @return a string containing the HTML for the form
     */
    public String documentToForm(Document doc, Map mapping) {
        StringBuffer sb = new StringBuffer();

        SchemaRestrictions restrictions = (new SchemaRestrictionsFactory())
                .getSchemaRestrictions(SchemaRestrictions.RAR_TYPE);

        Iterator restIter = restrictions.getComplexElements().iterator();
        while (restIter.hasNext()) {
            sb.append("<input type=\"hidden\" id=\"");
            sb.append((String) restIter.next());
            sb.append("\" value=\"complex\">");
        }

        NodeList children = doc.getChildNodes();
        for (int i = 0; i < children.getLength(); i++) {
            if (children.item(i).getNodeType() == Node.ELEMENT_NODE) {
                sb.append(childToForm(doc, children.item(i), mapping,
                        restrictions));
            }
        }
        return sb.toString();
    }

    /**
     * The last ID used when building the form.
     *
     * @return last ID used to build the form
     */
    public int getLastId() {
        return currentID;
    }

    /**
     * Build the HTML for a single node within the DOM tree.
     *
     * @param doc
     *            the DOM tree
     * @param child
     *            the Node within the tree for which the HTML is constructed
     * @param mapping
     *            Map of IDs to Nodes within the tree
     * @param restrictions
     *            schema based restrictions to be used when constructing HTML
     * @return HTML to display this single Node of the tree and its children
     *         (along with attributes)
     */
    private String childToForm(Document doc, Node child, Map mapping,
            SchemaRestrictions restrictions) {
        StringBuffer sb = new StringBuffer();
        int thisChildID = getNextID();

        // save this node in the mapping
        mapping.put(String.valueOf(thisChildID), child);
        
        // print a hidden field for use in JavaScript mapping from ID to node name
        sb.append("<input type=\"hidden\" id=\"" + thisChildID + "\" value=\"" + child.getNodeName() + "\">");

        // is it possible to add children or attributes to this element?
        ElementRestrictions er;

        if ((er = restrictions.getElementRestrictions(child.getNodeName())) != null
                && er.isComplex()) {
            sb.append("<div id=\"" + thisChildID
                    + "-element\" class=\"element1\">");
            // add/remove buttons
            sb.append("<div id=\"");
            sb.append(thisChildID);
            sb.append("-buttons\" class=\"buttons\">");

            sb.append("<select class=\"elementSelect\" id=\"" + thisChildID
                    + "-select\">");
            sb.append("<option/>");

            // TODO print a list of attributes that can be added to this element
            //sb.append("<optgroup label=\"Attributes\">");
            //sb.append("</optgroup>");
            sb.append("<optgroup label=\"Elements\">");
            if (restrictions.getElementRestrictions(child.getNodeName()) != null) {

                // Print all the names of all the allowed children for this
                // element. If the children are specified in a sequence, or in
                // an another way where the order is important then we add the
                // value of the previous element into the <option> tag. This is
                // used by the JavaScript on the client side to find the proper
                // place to insert the new element.
                Iterator i = er.getChildren().iterator();
                String prev = null;
                while (i.hasNext()) {
                    String current = (String) i.next();

                    if (prev != null && er.isSequence()) {
                        sb.append("<option value=\"" + prev + "\">" + current
                                + "</option>");
                    } else if (!er.isSequence()) {
                        sb.append("<option value=\"-1\">" + current
                                + "</option>");
                    } else {
                        sb.append("<option value=\"0\">" + current
                                + "</option>");
                    }
                    prev = current;
                }
            }
            sb.append("</optgroup>");
            sb.append("</select>");

            sb.append("<input type=\"button\" class=\"actionButton\" onClick=\"addElement('"
                            + thisChildID + "', new Array(");
            // provide a list of allowed children for this addElement function
            Iterator iter = er.getChildren().iterator();
            String childrenToAdd = "";
            while (iter.hasNext()) { 
                if (childrenToAdd.equals("")) {
                    childrenToAdd = "'" + (String)iter.next() + "'";
                } else {
                    childrenToAdd += ", '" + iter.next() + "'";
                }
            }
            sb.append(childrenToAdd);
            sb.append("))\", value=\"Add\" /> -");
            
            // cannot remove the top element
            if (!doc.getDocumentElement().equals(child)) {
                sb.append("<input type=\"button\" class=\"actionButton\" onClick=\"removeElement('"
                                + thisChildID + "')\"value=\"Remove\" />");
            }
            
            sb.append("</div>");

            NamedNodeMap attrs = child.getAttributes();

            // opened and closed element names

            // if there element does not have any attributes
            if (attrs == null || attrs.getLength() == 0) {
                sb.append("<span onClick=\"showHide('" + thisChildID + "')\" id=\""
                        + thisChildID + "-top\" class=\"elementName\">- &lt;"
                        + child.getNodeName() + "&gt;</span>");
            } else {
                sb.append("<span id=\"" + thisChildID + "-top\">");
                sb.append("<table border=\"0\" cellspacing=\"2\" cellpadding=\"2\">");

                // print each attribute
                for (int i = 0; i < attrs.getLength(); i++) {
                    sb.append(attributesToForm(child.getNodeName(), thisChildID,
                            attrs, i, mapping));
                }

                sb.append("</table></span>");

            }

            sb.append("<span onClick=\"showHide('"
                            + thisChildID
                            + "')\" id=\""
                            + thisChildID
                            + "-closed\" class=\"elementName\" style=\"display: none\">+ &lt;"
                            + child.getNodeName() + "&gt; ...</span>");
        }


        sb.append("<div id=\"" + thisChildID + "-content\">");

        // if this child has children that we need to display
        if (child.hasChildNodes()) {
            NodeList children = child.getChildNodes();
            for (int i = 0; i < children.getLength(); i++) {
                Node thisChild = children.item(i);
                if (thisChild.getNodeType() == Node.ELEMENT_NODE
                        && thisChild.hasChildNodes()
                        && thisChild.getFirstChild().getNodeType() == Node.TEXT_NODE
                        && thisChild.getChildNodes().getLength() == 1) {
                    int newID = getNextID();
                    
                    mapping.put(String.valueOf(newID), thisChild);
                    sb.append(textNodeToForm(thisChild.getFirstChild(),
                            thisChild.getNodeName(), mapping, newID));
                } else if (children.item(i).getNodeType() == Node.ELEMENT_NODE) {
                    sb.append(childToForm(doc, thisChild, mapping, restrictions));
                } else if (children.item(i).getNodeType() == Node.CDATA_SECTION_NODE) {
                    sb.append(cdataNodeToForm(children.item(i), mapping));
                }
            }
        } else {
           
            if ((er = restrictions.getElementRestrictions(child.getNodeName())) != null
                    && er.isComplex()) {
                
                sb.append("<span id=\""
                        + thisChildID
                        + "-empty\">Empty. Add a child from the dropdown.</span>");
            } else {
                // we know that this element cannot contain any children, so we will
                // display an input field even if it does not currently have any
                // child nodes of type TEXT
                // we need to put an empty text node inside so we can save the
                // value if one is entered by the user
                child.appendChild(doc.createTextNode(""));
                sb.append(textNodeToForm(child.getFirstChild(), child.getNodeName(), mapping,
                        thisChildID));
            }
        }

        if ((er = restrictions.getElementRestrictions(child.getNodeName())) != null
                && er.isComplex()) {
             sb.append("</div>\n");
             sb.append("&lt;/" + child.getNodeName() + "&gt;");
             
        }
        sb.append("</div>\n");
        return sb.toString();
    }

    /**
     * Create the HTML to display a CDATA node.
     *
     * @param node
     *            CDATA node
     * @param mapping
     *            map of IDs to Nodes within the DOM tree
     * @return HTML to display a CDATA node as an input field
     */
    private String cdataNodeToForm(Node node, Map mapping) {
        StringBuffer sb = new StringBuffer();
        String value = node.getNodeValue();
        int id = getNextID();
        if (value.trim().length() != 0) {
            sb.append("<div id=\"" + id + "\" class=\"element2\">");
            sb.append("&lt;![CDATA[<input name=\"values(");
            sb.append(id);
            mapping.put(String.valueOf(id), node);
            sb.append(")\" type=\"text\" size=\"" + getSize(value.length())
                    + "\" class=\"textElement\" value=\"");
            sb.append(node.getNodeValue());
            sb.append("\">]]&gt;\n");
            sb
                    .append("<input type=\"button\" class=\"removeButton\" onClick=\"removeElement('"
                            + id + "')\"value=\" X \" />");
            sb.append("</div>");
        }
        return sb.toString();
    }

    /**
     * Create the HTML to display a text node.
     *
     * @param node
     *            the text node
     * @param name
     *            the name of the parent element
     * @param mapping
     *            map of IDs to Nodes within the tree
     * @return HTML to display the input field for this text node
     */
    private String textNodeToForm(Node node, String name, Map mapping,
            int parentID) {
        StringBuffer sb = new StringBuffer();
        String value = (node.getNodeValue() == null) ? "" : node.getNodeValue();
        int id = getNextID();
        
        // print a hidden field for use in JavaScript mapping from ID to node name
        sb.append("<input type=\"hidden\" id=\"" + parentID + "\" value=\"" + name + "\">");

        sb.append("<div id=\"" + parentID + "-element\" class=\"element2\">");
        sb.append("&lt;" + name + "&gt;");

        mapping.put(String.valueOf(id), node);

        if (value.indexOf("\n") == -1) {
            sb.append("<input name=\"values(");
            sb.append(id);

            sb.append(")\" type=\"text\" size=\"" + getSize(value.length())
                    + "\" class=\"textElement\" value=\"");
            sb.append(value);
            sb.append("\">\n");
        } else {
            String[] stringLines = value.split("\n");
            int numLines = stringLines.length;
            sb.append("<br/><textArea name=\"values(");
            sb.append(id);
            sb.append(")\" rows=\"");
            sb.append(numLines);
            sb.append("\" cols=\"");
            sb.append(getSize(value.length()));
            sb.append("\">");
            sb.append(value);
            sb.append("</textArea><br/>");
        }
        sb.append("&lt;/" + name + "&gt;");
        sb
                .append("<input type=\"button\" class=\"removeButton\" onClick=\"removeElement('"
                        + parentID + "')\"value=\" X \" />");
        sb.append("</div>");

        return sb.toString();
    }

    /**
     * Determine what size of input field to use for the given value of a ndoe.
     *
     * @param lengthOfValue
     *            the length of the Node's value
     * @return the length of the value if it is less than MAX_INPUT_FIELD_SIZE
     *         or MAX_INPUT_FIELD_SIZE otherwise
     */
    private int getSize(int lengthOfValue) {
        if (lengthOfValue < MAX_INPUT_FIELD_SIZE) {
            return lengthOfValue;
        } else {
            return MAX_INPUT_FIELD_SIZE;
        }
    }

    /**
     * Create the HTML to display the attributes of a Node.
     *
     * @param nodeName
     *            the name of the node
     * @param nodeID
     *            the ID of the node (from the mapping)
     * @param attrs
     *            the set attributes
     * @param i
     *            the index into the set of attributes
     * @param mapping
     *            the mapping of IDs to Nodes within the tree
     * @return HTML to display attributes for this node
     */
    private String attributesToForm(String nodeName, int nodeID,
            NamedNodeMap attrs, int i, Map mapping) {
        StringBuffer sb = new StringBuffer();

        sb.append("<tr><td align=\"right\">");
        if (i == 0) {
            sb.append("- <span onClick=\"showHide('" + nodeID
                    + "')\" class=\"elementName\"> &lt;" + nodeName
                    + "</span> ");
        }
        int id = getNextID();

        sb.append(attrs.item(i).getNodeName());
        sb.append(" = \"</td><td>\n");
        sb.append("<input name=\"values(");
        sb.append(id);
        mapping.put(String.valueOf(id), attrs.item(i));
        sb.append(")\" class=\"textElement\" type=\"text\" size=\""
                + getSize(attrs.item(i).getNodeValue().length())
                + ")\" value=\"");
        sb.append(attrs.item(i).getNodeValue());
        sb.append("\">\"");

        // if this is the last attribute
        if (i == attrs.getLength() - 1) {
            sb.append("&gt;");
        }

        sb.append("</td></tr>\n");
        return sb.toString();
    }

    /**
     * Get the next ID to use for the node.
     *
     * @return the next ID
     */
    private int getNextID() {
        int result = currentID;
        currentID++;
        return result;
    }

}
