/**
 * BSD-style license; for more info see http://pmd.sourceforge.net/license.html
*/
package net.sourceforge.pmd;

import net.sourceforge.pmd.util.ResourceLoader;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Properties;
import java.util.Set;
import java.util.StringTokenizer;

public class RuleSetFactory {
    private ClassLoader classLoader;

    /**
     * Returns an Iterator of RuleSet objects loaded from descriptions from
     * the "rulesets.properties" resource or from the "rulesets.filenames" property.
     * @return an iterator on RuleSet objects
     */
    public Iterator getRegisteredRuleSets() throws RuleSetNotFoundException {
        try {
            Properties props = new Properties();
            props.load(ResourceLoader.loadResourceAsStream("rulesets/rulesets.properties"));
            String rulesetFilenames = props.getProperty("rulesets.filenames");
            List ruleSets = new ArrayList();
            for (StringTokenizer st = new StringTokenizer(rulesetFilenames, ","); st.hasMoreTokens();) {
                ruleSets.add(createRuleSet(st.nextToken()));
            }
            return ruleSets.iterator();
        } catch (IOException ioe) {
            throw new RuntimeException("Couldn't find rulesets.properties; please ensure that the rulesets directory is on the classpath.  Here's the current classpath: " + System.getProperty("java.class.path"));
        }
    }

    /**
     * Create a ruleset from a name or from a list of name
     * @param name name of rule set file loaded as a resource
     * @param classLoader the classloader used to load the ruleset and subsequent rules
     * @return the new ruleset
     * @throws RuleSetNotFoundException
     */
    public RuleSet createRuleSet(String name, ClassLoader classLoader) throws RuleSetNotFoundException {
        RuleSet ruleSet = null;
        setClassLoader(classLoader);
        
        if (name.indexOf(',') == -1) {
            ruleSet = createRuleSet(tryToGetStreamTo(name, classLoader));
        } else {
            ruleSet = new RuleSet();
            for (StringTokenizer st = new StringTokenizer(name, ","); st.hasMoreTokens();) {
                String ruleSetName = st.nextToken().trim();
                RuleSet tmpRuleSet = createRuleSet(ruleSetName, classLoader);
                ruleSet.addRuleSet(tmpRuleSet);
            }
        }

        return ruleSet;
    }

    /**
     * Creates a ruleset.  If passed a comma-delimited string (rulesets/basic.xml,rulesets/unusedcode.xml)
     * it will parse that string and create a new ruleset for each item in the list.
     * Same as createRuleSet(name, ruleSetFactory.getClassLoader()).
     */
    public RuleSet createRuleSet(String name) throws RuleSetNotFoundException {
        return createRuleSet(name, getClass().getClassLoader());
    }

    /**
     * Create a ruleset from an inputsteam.
     * Same as createRuleSet(inputStream, ruleSetFactory.getClassLoader()).
     * @param inputStream an input stream  that contains a ruleset descripion
     * @return a new ruleset
     */
    public RuleSet createRuleSet(InputStream inputStream) {
        return createRuleSet(inputStream, getClass().getClassLoader());
    }

    /**
     * Create a ruleset from an input stream with a specified class loader
     * @param inputStream an input stream that contains a ruleset descripion
     * @param classLoader a class loader used to load rule classes
     * @return a new ruleset
     */
    public RuleSet createRuleSet(InputStream inputStream, ClassLoader classLoader) {
        try {
            setClassLoader(classLoader);
            DocumentBuilder builder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
            Document doc = builder.parse(inputStream);
            Element root = doc.getDocumentElement();

            RuleSet ruleSet = new RuleSet();
            ruleSet.setName(root.getAttribute("name"));

            NodeList nodeList = root.getChildNodes();
            for (int i = 0; i < nodeList.getLength(); i++) {
                Node node = nodeList.item(i);
                if (node.getNodeType() == Node.ELEMENT_NODE) {
                    if (node.getNodeName().equals("description")) {
                        parseDescriptionNode(ruleSet, node);
                    } else if (node.getNodeName().equals("rule")) {
                        parseRuleNode(ruleSet, node);
                    }
                }
            }

            return ruleSet;
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException("Couldn't read from that source: " + e.getMessage());
        }
    }

    /**
     * Return the class loader used to load ruleset resources and rules
     * @return
     */
    public ClassLoader getClassLoader() {
        return classLoader;
    }

    /**
     * Sets the class loader used to load ruleset resources and rules 
     * @param loader a class loader
     */
    public void setClassLoader(ClassLoader loader) {
        classLoader = loader;
    }

    /**
     * Try to load a resource with the specified class loader
     * @param name a resource name (contains a ruleset description)
     * @param loader a class loader used to load that rule set description
     * @return an inputstream to that resource
     * @throws RuleSetNotFoundException
     */
    private InputStream tryToGetStreamTo(String name, ClassLoader loader) throws RuleSetNotFoundException {
        InputStream in = ResourceLoader.loadResourceAsStream(name, loader);
        if (in == null) {
            throw new RuleSetNotFoundException("Can't find resource " + name + ".  Make sure the resource is a valid file or URL or is on the CLASSPATH");
        }
        
        return in;
    }
    
    /**
     * Parse a ruleset description node
     * @param ruleSet the ruleset being constructed
     * @param descriptionNode must be a description element node
     */
    private void parseDescriptionNode(RuleSet ruleSet, Node descriptionNode) {
        NodeList nodeList = descriptionNode.getChildNodes();
        StringBuffer buffer = new StringBuffer();
        for (int i = 0 ; i < nodeList.getLength(); i++) {
            Node node = nodeList.item(i);
            if (node.getNodeType() == Node.TEXT_NODE) {
                buffer.append(node.getNodeValue());
            } else if (node.getNodeType() == Node.CDATA_SECTION_NODE) {
                buffer.append(node.getNodeValue());
            }
        }
        ruleSet.setDescription(buffer.toString());
    }
    
    /**
     * Parse a rule node
     * @param ruleSet the ruleset being constructed
     * @param ruleElement must be a rule element node
     */
    private void parseRuleNode(RuleSet ruleSet, Node ruleNode) throws ClassNotFoundException, InstantiationException, IllegalAccessException, RuleSetNotFoundException {
        Element ruleElement = (Element) ruleNode;
        String ref = ruleElement.getAttribute("ref");
        if (ref.trim().length() == 0) {
            parseInternallyDefinedRuleNode(ruleSet, ruleNode);
        } else {
            parseExternallyDefinedRuleNode(ruleSet, ruleNode);
        }
    }
    
    /**
     * Process a rule definition node
     * @param ruleSet the ruleset being constructed
     * @param ruleNode must be a rule element node
     */
    private void parseInternallyDefinedRuleNode(RuleSet ruleSet, Node ruleNode) throws ClassNotFoundException, InstantiationException, IllegalAccessException {
        Element ruleElement = (Element) ruleNode;

        String className = ruleElement.getAttribute("class");
        String name = ruleElement.getAttribute("name");
        String message = ruleElement.getAttribute("message");
        Rule rule = (Rule) getClassLoader().loadClass(className).newInstance();
        rule.setName(name);
        rule.setMessage(message);
        
        NodeList nodeList = ruleElement.getChildNodes();
        for (int i = 0; i < nodeList.getLength(); i++) {
            Node node = nodeList.item(i);
            if (node.getNodeType() == Node.ELEMENT_NODE) {
                if (node.getNodeName().equals("description")) {
                    parseDescriptionNode(rule, node);
                } else if (node.getNodeName().equals("example")) {
                    parseExampleNode(rule, node);
                } else if (node.getNodeName().equals("priority")) {
                    parsePriorityNode(rule, node);
                } else if (node.getNodeName().equals("properties")) {
                    parsePropertiesNode(rule, node);
                }
            }
        }
        
        ruleSet.addRule(rule);
    }
    
    /**
     * Process a reference to a rule
     * @param ruleSet the ruleset being constructucted
     * @param ruleNode must be a ruke element node
     */
    private void parseExternallyDefinedRuleNode(RuleSet ruleSet, Node ruleNode) throws RuleSetNotFoundException {
        Element ruleElement = (Element) ruleNode;
        String ref = ruleElement.getAttribute("ref");
        if (ref.endsWith("xml")) {
            parseRuleNodeWithExclude(ruleSet, ruleElement, ref);
        } else {
            parseRuleNodeWithSimpleReference(ruleSet, ref);
        }
    }
    
    /**
     * Parse a rule node with a simple reference
     * @param ruleSet the ruleset being constructed
     * @param ref a reference to a rule
     */
    private void parseRuleNodeWithSimpleReference(RuleSet ruleSet, String ref) throws RuleSetNotFoundException {
        RuleSetFactory rsf = new RuleSetFactory();
        ExternalRuleID externalRuleID = new ExternalRuleID(ref);
        RuleSet externalRuleSet = rsf.createRuleSet(ResourceLoader.loadResourceAsStream(externalRuleID.getFilename()));
        ruleSet.addRule(externalRuleSet.getRuleByName(externalRuleID.getRuleName()));
    }

    /**
     * Parse a reference rule node with excludes
     * @param ruleSet the ruleset being constructed
     * @param ruleElement must be a rule element
     * @param ref the ruleset reference
     */    
    private void parseRuleNodeWithExclude(RuleSet ruleSet, Element ruleElement, String ref) throws RuleSetNotFoundException {
        NodeList excludeNodes = ruleElement.getChildNodes();
        Set excludes = new HashSet();
        for (int i=0; i<excludeNodes.getLength(); i++) {
            Node node = excludeNodes.item(i);
            if ((node.getNodeType() == Node.ELEMENT_NODE) && (node.getNodeName().equals("exclude"))) {
                Element excludeElement = (Element) node;
                excludes.add(excludeElement.getAttribute("name"));
            }
        }
        
        RuleSetFactory rsf = new RuleSetFactory();
        RuleSet externalRuleSet = rsf.createRuleSet(ResourceLoader.loadResourceAsStream(ref));
        for (Iterator i = externalRuleSet.getRules().iterator(); i.hasNext();) {
            Rule rule = (Rule) i.next();
            if (!excludes.contains(rule.getName())) {
                 ruleSet.addRule(rule);
            }
        }
    }

    /**
     * Process a rule descrtiprion node
     * @param rule the rule being constructed
     * @param descriptionNode must be a description element node
     */
    private void parseDescriptionNode(Rule rule, Node descriptionNode) {
        NodeList nodeList = descriptionNode.getChildNodes();
        StringBuffer buffer = new StringBuffer();
        for (int i = 0; i < nodeList.getLength(); i++) {
            Node node = nodeList.item(i);
            if (node.getNodeType() == Node.CDATA_SECTION_NODE) {
                buffer.append(node.getNodeValue());
            } else if (node.getNodeType() == Node.TEXT_NODE) {
                buffer.append(node.getNodeValue());
            }
        }
        rule.setDescription(buffer.toString());
    }

    /**
     * Process a rule example node
     * @param rule the rule being constructed
     * @param exampleNode must be a example element node
     */
    private void parseExampleNode(Rule rule, Node exampleNode) {
        NodeList nodeList = exampleNode.getChildNodes();
        StringBuffer buffer = new StringBuffer();
        for (int i = 0; i < nodeList.getLength(); i++) {
            Node node = nodeList.item(i);
            if (node.getNodeType() == Node.CDATA_SECTION_NODE) {
                buffer.append(node.getNodeValue());
            } else if (node.getNodeType() == Node.TEXT_NODE) {
                buffer.append(node.getNodeValue());
            }
        }
        rule.setExample(buffer.toString());
    }
    
    /**
     * Parse a priority node
     * @param rule the rule being constructed
     * @param priorityNode must be a priority element
     */
    private void parsePriorityNode(Rule rule, Node priorityNode) {
        StringBuffer buffer = new StringBuffer();
        NodeList nodeList = priorityNode.getChildNodes();
        for (int i = 0; i < nodeList.getLength(); i++) {
            Node node = nodeList.item(i);
            if (node.getNodeType() == Node.TEXT_NODE) {
                buffer.append(node.getNodeValue());
            }
        }
        rule.setPriority(new Integer(buffer.toString().trim()).intValue());
    }
    
    /**
     * Parse a properties node
     * @param rule the rule being constructed
     * @param propertiesNode must be a properties element node
     */
    private void parsePropertiesNode(Rule rule, Node propertiesNode) {
        NodeList nodeList = propertiesNode.getChildNodes();
        for (int i = 0; i < nodeList.getLength(); i++) {
            Node node = nodeList.item(i);
            if ((node.getNodeType() == Node.ELEMENT_NODE) && (node.getNodeName().equals("property"))) {
                parsePropertyNode(rule, node);
            }
        }
    }
    
    /**
     * Parse a property node
     * @param rule the rule being constructed
     * @param propertyNode must be a property element node
     */
    private void parsePropertyNode(Rule rule, Node propertyNode) {
        Element propertyElement = (Element) propertyNode;
        String name = propertyElement.getAttribute("name");
        String value = propertyElement.getAttribute("value");
        if (value.trim().length() == 0) {
            NodeList nodeList = propertyNode.getChildNodes();
            for (int i = 0; i < nodeList.getLength(); i++) {
                Node node = nodeList.item(i);
                if ((node.getNodeType() == Node.ELEMENT_NODE) && (node.getNodeName().equals("value"))) {
                    value = parseValueNode(node);
                }
            }
        }
        if (propertyElement.hasAttribute("pluginname")) {
            rule.addProperty("pluginname", propertyElement.getAttributeNode("pluginname").getNodeValue());
        }
        rule.addProperty(name, value);
    }
    
    /**
     * Parse a value node
     * @param valueNode must be a value element node
     * @return the value
     */
    private String parseValueNode(Node valueNode) {
        StringBuffer buffer = new StringBuffer();
        NodeList nodeList = valueNode.getChildNodes();
        for (int i = 0; i < nodeList.getLength(); i++) {
            Node node = nodeList.item(i);
            if (node.getNodeType() == Node.CDATA_SECTION_NODE) {
                buffer.append(node.getNodeValue());
            } else if (node.getNodeType() == Node.TEXT_NODE) {
                buffer.append(node.getNodeValue());
            }
        }
        return buffer.toString();
    }
}