/*
 * NOTE: This copyright does *not* cover user programs that use HQ
 * program services by normal system calls through the application
 * program interfaces provided as part of the Hyperic Plug-in Development
 * Kit or the Hyperic Client Development Kit - this is merely considered
 * normal use of the program, and does *not* fall under the heading of
 * "derived work".
 * 
 * Copyright (C) [2004, 2005, 2006], Hyperic, Inc.
 * This file is part of HQ.
 * 
 * HQ is free software; you can redistribute it and/or modify
 * it under the terms version 2 of the GNU General Public License as
 * published by the Free Software Foundation. This program is distributed
 * in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
 * even the implied warranty of MERCHANTABILITY or FITNESS FOR A
 * PARTICULAR PURPOSE. See the GNU General Public License for more
 * details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
 * USA.
 */

package org.hyperic.util.xmlparser;

import org.hyperic.util.StringUtil;
import org.jdom.Attribute;
import org.jdom.Document;
import org.jdom.Element;
import org.jdom.JDOMException;
import org.jdom.input.SAXBuilder;

import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.File;
import java.io.PrintStream;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;

import org.xml.sax.EntityResolver;

/**
 * The main entry point && bulk of XmlParser.  The parsing routine
 * takes an entry-point tag, which provides information about subtags,
 * attributes it takes, etc.  Tags can implement various interfaces
 * to tell the parser to call back when certain conditions are met.  
 * This class takes the role of both a minimal validator as well as
 * a traversal mechanism for building data objects out of XML.
 */

public class XmlParser {
    private XmlParser(){}

    private static void checkAttributes(Element elem, XmlTagHandler tag,
                                        XmlFilterHandler filter)
        throws XmlAttrException
    {
        boolean handlesAttrs = tag instanceof XmlAttrHandler;
        XmlAttr[] attrs;

        if(handlesAttrs)
            attrs = ((XmlAttrHandler)tag).getAttributes();
        else
            attrs = new XmlAttr[0];

        // Ensure out all the required && optional attributes
        for(int i=0; i<attrs.length; i++){
            Attribute a = null;
            boolean found = false;

            for(Iterator j=elem.getAttributes().iterator(); j.hasNext(); ){
                a = (Attribute)j.next();

                if(a.getName().equalsIgnoreCase(attrs[i].getName())){
                    found = true;
                    break;
                }
            }

            if(!found && attrs[i].getType() == XmlAttr.REQUIRED){
                throw new XmlRequiredAttrException(elem, 
                                                        attrs[i].getName());
            }
            
            if(found && handlesAttrs){
                String val;

                val = filter.filterAttrValue(tag, a.getName(), a.getValue());
                
                ((XmlAttrHandler)tag).handleAttribute(i, val);
            }
        }

        // Second loop to handle unknown attributes
        for(Iterator i=elem.getAttributes().iterator(); i.hasNext(); ){
            Attribute a = (Attribute)i.next();
            boolean found = false;

            for(int j=0; j<attrs.length; j++){
                if(a.getName().equalsIgnoreCase(attrs[j].getName())){
                    found = true;
                    break;
                }
            }

            if(found)
                continue;

            if(tag instanceof XmlUnAttrHandler){
                XmlUnAttrHandler handler;
                String val;

                val = filter.filterAttrValue(tag, a.getName(), a.getValue());

                handler = (XmlUnAttrHandler)tag;
                handler.handleUnknownAttribute(a.getName(), val);
            } else {
                throw new XmlUnknownAttrException(elem, a.getName());
            }
        }

        if(tag instanceof XmlEndAttrHandler){
            ((XmlEndAttrHandler)tag).endAttributes();
        }
    }

    private static void checkSubNodes(Element elem, XmlTagHandler tag,
                                      XmlFilterHandler filter)
        throws XmlAttrException, XmlTagException
    {
        XmlTagInfo[] subTags = tag.getSubTags();
        Map hash;

        hash = new HashMap();
        
        // First, count how many times each sub-tag is referenced
        for(Iterator i=elem.getChildren().iterator(); i.hasNext(); ){
            Element e = (Element)i.next();
            String name;
            Integer val;

            name = e.getName().toLowerCase();
            if((val = (Integer)hash.get(name)) == null){
                val = new Integer(0);
            }
            
            val = new Integer(val.intValue() + 1);
            hash.put(name, val);
        }

        for(int i=0; i<subTags.length; i++){
            String name = subTags[i].getTag().getName().toLowerCase();
            Integer iVal = (Integer)hash.get(name);
            int threshold = 0, val;

            val = iVal == null ? 0 : iVal.intValue();

            switch(subTags[i].getType()){
            case XmlTagInfo.REQUIRED:
                if(val == 0){
                    throw new XmlMissingTagException(elem, name);
                } else if(val != 1){
                    throw new XmlTooManyTagException(elem, name);
                }
                break;
            case XmlTagInfo.OPTIONAL:
                if(val > 1){
                    throw new XmlTooManyTagException(elem, name);
                }
                break;
            case XmlTagInfo.ONE_OR_MORE:
                threshold++;
            case XmlTagInfo.ZERO_OR_MORE:
                if(val  < threshold){
                    throw new XmlMissingTagException(elem, name);
                }
                break;
            }
            
            hash.remove(name);
        }

        // Now check for excess sub-tags
        if(hash.size() != 0){
            Set keys = hash.keySet();
            
            throw new XmlTooManyTagException(elem, 
                                             (String)keys.iterator().next());
        }

        // Recurse to all sub-tags 
        for(Iterator i=elem.getChildren().iterator(); i.hasNext(); ){
            Element child = (Element)i.next();

            for(int j=0; j<subTags.length; j++){
                XmlTagHandler subTag = subTags[j].getTag();
                String subName = subTag.getName();

                if(child.getName().equalsIgnoreCase(subName)){
                    XmlParser.processNode(child, subTag, filter);
                    break;
                }
            }
        }
    }

    private static void processNode(Element elem, XmlTagHandler tag,
                                    XmlFilterHandler filter)
        throws XmlAttrException, XmlTagException
    {
        if(tag instanceof XmlTagEntryHandler){
            ((XmlTagEntryHandler)tag).enter();
        }

        if(tag instanceof XmlFilterHandler){
            filter = (XmlFilterHandler)tag;
        }

        XmlParser.checkAttributes(elem, tag, filter);
        if(tag instanceof XmlTextHandler) {
            ((XmlTextHandler)tag).handleText(elem.getText());
        }
        XmlParser.checkSubNodes(elem, tag, filter);

        if(tag instanceof XmlTagExitHandler){
            ((XmlTagExitHandler)tag).exit();
        }
    }
    
    private static class DummyFilter
        implements XmlFilterHandler
    {
        public String filterAttrValue(XmlTagHandler tag, String attrName,
                                      String attrValue)
        {
            return attrValue;
        }
    }

    /**
     * Parse an input stream, otherwise the same as parsing a file
     */
    public static void parse(InputStream is, XmlTagHandler tag)
        throws XmlParseException
    {
        parse(is, tag, null);
    }

    public static void parse(InputStream is, XmlTagHandler tag,
                             EntityResolver resolver)
        throws XmlParseException
    {
        SAXBuilder builder;
        Document doc;

        builder = new SAXBuilder();

        if (resolver != null) {
            builder.setEntityResolver(resolver);
        }

        try {
            if (resolver != null) {
                //WTF?  seems relative entity URIs are allowed
                //by certain xerces impls.  but fully qualified
                //file://... URLs trigger a NullPointerException
                //in others.  setting base here worksaround
                doc = builder.build(is, "");
            }
            else {
                doc = builder.build(is);
            }
        } catch(JDOMException exc){
            XmlParseException toThrow = new XmlParseException(exc.getMessage());
            toThrow.initCause(exc);
            throw toThrow;
        } catch (IOException exc) {
            XmlParseException toThrow = new XmlParseException(exc.getMessage());
            toThrow.initCause(exc);
            throw toThrow;
        }

        generalParse(tag, doc);
    }

    /**
     * Parse a file, which should have a root which is the associated tag.
     *
     * @param in   File to parse
     * @param tag  Root tag which the parsed file should contain
     */
    public static void parse(File in, XmlTagHandler tag)
        throws XmlParseException
    {
        SAXBuilder builder;
        Document doc;

        builder = new SAXBuilder();
        InputStream is = null;

        //open the file ourselves.  the builder(File)
        //method escapes " " -> "%20" and bombs
        try {
            is = new FileInputStream(in);
            doc = builder.build(is);
        } catch (IOException exc) {
            throw new XmlParseException(exc.getMessage());
        } catch (JDOMException exc) {
            throw new XmlParseException(exc.getMessage());
        } finally {
            if (is != null) {
                try {
                    is.close();
                } catch (IOException e) {}
            }
        }
        generalParse(tag, doc);
    }

    /** General parsing used by both parse methods above */
    private static void generalParse(XmlTagHandler tag, Document doc) 
        throws XmlParseException
    {

        Element root = doc.getRootElement();
        if(!root.getName().equalsIgnoreCase(tag.getName())){
            throw new XmlParseException("Incorrect root tag.  Expected <"+
                                             tag.getName() + "> but got <" +
                                             root.getName() + ">");
        }
        XmlParser.processNode(root, tag, new DummyFilter());
    }

    private static void dumpAttrs(XmlAttr[] attrs, String typeName,
                                  int type, PrintStream out, int indent)
    {
        String printMsg;
        boolean printed = false;
        int lineBase, lineLen;

        if(attrs.length == 0)
            return;
            
        lineLen = 0;
        printMsg = "- Has " + typeName + " attributes: ";
        lineBase = indent + printMsg.length();
        // Required attributes
        for(int i=0; i<attrs.length; i++){
            String toPrint;
            
            if(attrs[i].getType() != type)
                continue;
            
            if(!printed){
                toPrint = StringUtil.repeatChars(' ', indent) +
                    "- Has " + typeName + " attributes: ";
                out.print(toPrint);
                lineLen = toPrint.length();
                printed = true;
            }
            
            toPrint = attrs[i].getName() + ", ";
            lineLen += toPrint.length();
            out.print(toPrint);
            if(lineLen > 70){
                out.println();
                out.print(StringUtil.repeatChars(' ', lineBase));
                lineLen = lineBase;
            }                        
        }
        if(printed)
            out.println();
    }

    private static void dumpNode(XmlTagHandler tag, PrintStream out,
                                 int indent)
        throws XmlTagException
    {
        XmlTagInfo[] subTags = tag.getSubTags();

        out.println(StringUtil.repeatChars(' ', indent) +
                    "Tag <" + tag.getName() + ">:");
        if(tag instanceof XmlAttrHandler){
            XmlAttr[] attrs;

            attrs = ((XmlAttrHandler)tag).getAttributes();
            if(attrs.length == 0)
                out.println(StringUtil.repeatChars(' ', indent) +
                            "- has no required or optional attributes");

            XmlParser.dumpAttrs(attrs, "REQUIRED", XmlAttr.REQUIRED,
                               out, indent);
            XmlParser.dumpAttrs(attrs, "OPTIONAL", XmlAttr.OPTIONAL,
                               out, indent);
        } else {
            out.println(StringUtil.repeatChars(' ', indent) +
                        "- has no required or optional attributes");
        }
        
        if(tag instanceof XmlUnAttrHandler)
            out.println(StringUtil.repeatChars(' ', indent) +
                        "- handles arbitrary attributes");

        subTags = tag.getSubTags();
        if(subTags.length == 0){
            out.println(StringUtil.repeatChars(' ', indent) + 
                        "- has no subtags");
        } else {
            for(int i=0; i<subTags.length; i++){
                String name = subTags[i].getTag().getName();
                int type = subTags[i].getType();

                out.print(StringUtil.repeatChars(' ', indent) +
                          "- has subtag <" + name + ">, which ");
                switch(type){
                case XmlTagInfo.REQUIRED:
                    out.println("is REQUIRED");
                    break;
                case XmlTagInfo.OPTIONAL:
                    out.println("is OPTIONAL");
                    break;
                case XmlTagInfo.ONE_OR_MORE:
                    out.println("is REQUIRED at least ONCE");
                    break;
                case XmlTagInfo.ZERO_OR_MORE:
                    out.println("can be specified any # of times");
                    break;
                }
                
                XmlParser.dumpNode(subTags[i].getTag(), out, indent + 4);
            }
        }
    }

    public static void dump(XmlTagHandler root, PrintStream out){
        try {
            XmlParser.dumpNode(root, out, 0);
        } catch(XmlTagException exc){
            out.println("Error traversing tags: " + exc.getMessage());
        }
    }

    private static String bold(String text) {
        return "<emphasis role=\"bold\">" + text + "</emphasis>";
    }

    private static String tag(String name) {
        return bold("&lt;" + name + "&gt;");
    }

    private static String listitem(String name, String desc) {
        String item =
            "<listitem><para>" + name + "</para>";
        if (desc != null) {
            item += "<para>" + desc + "</para>";
        }
        return item;
    }

    private static void dumpAttrsWiki(XmlAttr[] attrs, String typeName,
                                      int type, PrintStream out, int indent)
    {
        boolean printed = false;
        if (attrs.length == 0) {
            return;
        }

        // Required attributes
        for (int i=0; i<attrs.length; i++) {
            if (attrs[i].getType() != type) {
                continue;
            }

            if (!printed) {
                out.println(StringUtil.repeatChars('*', indent) +
                            " " + typeName + " attributes: ");
                printed = true;
            }

            out.println(StringUtil.repeatChars('*', indent) +
                        " " + attrs[i].getName());
        }
    }

    private static void dumpNodeWiki(XmlTagHandler tag, PrintStream out,
                                     int indent)
        throws XmlTagException
    {
        XmlTagInfo[] subTags = tag.getSubTags();

        if (indent == 1) {
            out.println(StringUtil.repeatChars('*', indent) +
                        " Tag *<" + tag.getName() + ">*: ");
        }

        if (tag instanceof XmlAttrHandler) {
            XmlAttr[] attrs;

            attrs = ((XmlAttrHandler)tag).getAttributes();

            dumpAttrsWiki(attrs, "*REQUIRED*", XmlAttr.REQUIRED,
                          out, indent+1);
            dumpAttrsWiki(attrs, "*OPTIONAL*", XmlAttr.OPTIONAL,
                          out, indent+1);
        }
        
        subTags = tag.getSubTags();
        if (subTags.length != 0) {

            for (int i=0; i<subTags.length; i++) {
                String name = subTags[i].getTag().getName();
                int type = subTags[i].getType();
                String desc = "";

                switch(type){
                  case XmlTagInfo.REQUIRED:
                    desc = "REQUIRED";
                    break;
                  case XmlTagInfo.OPTIONAL:
                    desc = "OPTIONAL";
                    break;
                  case XmlTagInfo.ONE_OR_MORE:
                    desc = "REQUIRED at least ONCE";
                    break;
                  case XmlTagInfo.ZERO_OR_MORE:
                    desc = "can be specified any # of times";
                    break;
                }

                out.println(StringUtil.repeatChars('*', indent+1) +
                            " Sub Tag *<" + name + ">* " + desc);
                dumpNodeWiki(subTags[i].getTag(), out, indent+1);
            }
        }
    }

    public static void dumpWiki(XmlTagHandler root, PrintStream out){
        try {
            dumpNodeWiki(root, out, 1);
        } catch(XmlTagException exc){
            out.println("Error traversing tags: " + exc.getMessage());
        }
    }
}
