/*
 * The contents of this file are subject to the terms of the Common Development
 * and Distribution License (the License). You may not use this file except in
 * compliance with the License.
 *
 * You can obtain a copy of the License at http://www.netbeans.org/cddl.html
 * or http://www.netbeans.org/cddl.txt.

 * When distributing Covered Code, include this CDDL Header Notice in each file
 * and include the License file at http://www.netbeans.org/cddl.txt.
 * If applicable, add the following below the CDDL Header, with the fields
 * enclosed by brackets [] replaced by your own identifying information:
 * "Portions Copyrighted [year] [name of copyright owner]"
 *
 * The Original Software is NetBeans. The Initial Developer of the Original
 * Software is Sun Microsystems, Inc. Portions Copyright 1997-2006 Sun
 * Microsystems, Inc. All Rights Reserved.
 */

package org.netbeans.nbbuild;

import java.io.*;
import java.net.URL;
import java.util.*;
import java.util.Date;
import java.text.SimpleDateFormat;

import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.ParserConfigurationException;
import org.w3c.dom.*;

import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.FileScanner;
import org.apache.tools.ant.Project;
import org.apache.tools.ant.Task;
import org.apache.tools.ant.types.FileSet;

// CURRENTLY (20050411) USED BY:
// openide/arch/build.xml
// performance/test/reports/build.xml

/** Takes content of features.xml file and updates its missing attributes
 * with values read from issuezilla.
 *
 * @author Jaroslav Tulach
 */
public final class PlansPreprocess extends Task {
    /** Holds URL of issuezilla's interface */
    private String urlBase = "http://www.netbeans.org/issues/";
    
    /** Holds value of property sourceFile. */
    private java.io.File src;
    
    /** Holds value of property destination. */
    private java.io.File target;

    /** Holds value of property proxyHost. */
    private String proxyHost;    
    
    /** Holds value of property proxyPort. */
    private int proxyPort = -1;
    
    /** name of file with mapping between email addresses and nicknames */
    private java.io.File nicknames;

    /** true means follow the dependency tree and retrieve the issues which
     * block the requested issues */
    private boolean recurse = true;

    /** maximum IO failures during connection to IZ */
    private int maxIOFailures = 5;
    
    /** issues obtained from issuezilla */
    private HashMap issues = new HashMap (101);

    /** mapping of nick names */
    private Properties names = new Properties ();

    /** true if we should include all depending issues also as ref-links and not
     * just the subrequirements.
     */
    private boolean generateDependenciesAsLinks;
    
    /** http proxies to rotate */
    private String proxyPool = null;

    private static final String ATTR_MODE_NAME = "mode"; //NOI18N
    private static final int MODE_QUERIES_ONLY = 1;
    private static final int MODE_ISSUES_ONLY = 2;
    private static final int MODE_BOTH = 3;

    //
    // structures to be filled
    //
    int mode;
    private HashMap owners;
    private HashSet components;
    private HashSet subcomponents;
    
    /** Getter for property urlBase.
     * @return Value of property urlBase.
     */
    public String getUrlBase() {
        return this.urlBase;
    }
    
    /** Setter for property urlBase.
     * @param urlBase New value of property urlBase.
     */
    public void setUrlBase(String urlBase) {
        this.urlBase = urlBase;
    }

    /** Getter for property sourceFile.
     * @return Value of property sourceFile.
     */
    public java.io.File getSrc() {
        return this.src;
    }    

    /** Setter for property sourceFile.
     * @param sourceFile New value of property sourceFile.
     */
    public void setSrc(java.io.File sourceFile) {
        this.src = sourceFile;
    }
    
    /** Getter for property destination.
     * @return Value of property destination.
     */
    public java.io.File getTarget() {
        return this.target;
    }
    
    /** Setter for property destination.
     * @param destination New value of property destination.
     */
    public void setTarget(java.io.File destination) {
        this.target = destination;
    }

    /** Getter for property nicknames.
     * @return Value of property nicknames.
     */
    public java.io.File getNicknames () {
        return this.nicknames;
    }
    
    /** Setter for property nicknames.
     * @param nicknames names
     */
    public void setNicknames (java.io.File nicknames) {
        this.nicknames = nicknames;
    }
    
    /** 'recurse' means should this this ant task follow the dependency tree of
     * issues or not, the default is yes
     * @param recurse this ant task should follow the dependency and also
     * retrieve all the issues which block the issues in the requested list
     */
    public void setRecurse(boolean recurse) {
        this.recurse = recurse;
    }

    public boolean getRecurse() {
        return this.recurse;
    }
    
    /** Setter to generate subdependencies as links.
     */
    public void setGenerateDependenciesAsLinks (boolean g) {
        generateDependenciesAsLinks = g;
    }
    
    /** Setup a proxypool - automatic change of http proxy server when access failed
     * @param proxypool proxies to rotate separed by comma
     * @deprecated Use {@link #setProxyHost} and {@link #setProxyPort} instead
     */
    public void setProxypool(String proxypool) {
        this.proxyPool = proxypool;
        
        log( "The proxypool attribute is deprecated use the proxyhost and proxyport instead.", Project.MSG_WARN);
    }
    
    public void execute () throws BuildException {
        if (getTarget () == null) throw new BuildException ("Must set target");
        if (getSrc () == null) throw new BuildException ("Must set target");
        
        
        Object previousPort = null;
        Object previousHost = null;
        if (proxyPort != -1) {
            previousPort = System.getProperties ().put ("http.proxyPort", new Integer (proxyPort).toString ());
        }
        if (proxyHost != null) {
            previousHost = System.getProperties ().put ("http.proxyHost", proxyHost);
        }
        
        log ("Reading: " + getSrc ());
        
        try {
            Issuezilla iz = new Issuezilla (new java.net.URL (getUrlBase ()));
            
            // Should be removed after some testing
            if (proxyPool != null)
                iz.setProxyPool( proxyPool );
            
            log ("Searching for issue references");

            org.w3c.dom.Document dom = DocumentBuilderFactory.newInstance ().newDocumentBuilder().parse (getSrc ());
            NamedNodeMap attrsM = dom.getElementsByTagName("module-requirements").item(0).getAttributes();
            Node attrMode = attrsM.getNamedItem(ATTR_MODE_NAME);
            if (attrMode != null && "queries-only".equals(attrMode.getNodeValue())) {
                mode = MODE_QUERIES_ONLY;
            } else if (attrMode != null && "queries-and-issues".equals(attrMode.getNodeValue())) {
                mode = MODE_BOTH;
            } else { // "issues-only"
                mode = MODE_ISSUES_ONLY;
            }

            if (mode == MODE_ISSUES_ONLY) {
                // remove requirments with queries
                ArrayList toRemove = new ArrayList();
                NodeList list = dom.getElementsByTagName("requirement");
                int size = list.getLength();
                for (int i = 0; i < size; i++) {
                    Node node = list.item (i);
                    NamedNodeMap attrs = node.getAttributes();
                    Node attrQuery = attrs.getNamedItem("query");
                    if (attrQuery != null) {
                        toRemove.add(node);
                    }
                }
                
                for (Iterator i = toRemove.iterator(); i.hasNext(); ) {
                    Node node = (Node) i.next();
                    node.getParentNode().removeChild(node);
                }
            } else {
                Collection issuesFromQ = fillIssuesByQueries(dom, iz);
                if (mode == MODE_QUERIES_ONLY) {
                    // remove requirements with issue which is not generated by queries
                    ArrayList toRemove = new ArrayList();
                    NodeList list = dom.getElementsByTagName("requirement");
                    int size = list.getLength();
                    for (int i = 0; i < size; i++) {
                        Node node = list.item (i);
                        int issue = getIssueNumber(node);
                        if (!issuesFromQ.contains(new Integer(issue))) {
                            toRemove.add(node);
                        }
                    }
 
                     for (Iterator i = toRemove.iterator(); i.hasNext(); ) {
                        Node node = (Node) i.next();
                        node.getParentNode().removeChild(node);
                    }
               }
            }
            
            // find requirements related to issues
            NodeList list = dom.getElementsByTagName("requirement");
            ArrayList nodes = new ArrayList ();
            ArrayList numbers = new ArrayList ();
            int size = list.getLength();

            for (int i = 0; i < size; i++) {
                Node node = list.item (i);
                NamedNodeMap attrs = node.getAttributes();
                Node issueNode;
                if (null != (issueNode = attrs.getNamedItem("issue"))) {
                    nodes.add (node);
                    numbers.add (issueNode.getNodeValue());
                }
            }

            components = new HashSet ();
            subcomponents = new HashSet ();
            owners = new HashMap();
            
            if (numbers.size () > 0) {
                String previousModule = "";

                log ("Retrieving issues " + numbers + " from " + getUrlBase ());

                Issue[] issues = findIssues (iz, (String[])numbers.toArray (new String[0]));

                if (issues.length != nodes.size ()) {
                    throw new BuildException ("Error: issues = " + issues.length + " requested: " + nodes.size ());
                }

                log ("Updating the issues");

                if (getNicknames () != null) {
                    InputStream is = new FileInputStream (getNicknames ());
                    names.load (is);
                    is.close ();
                }

                for (int i = 0; i < issues.length; i++) {
                    Node node = (Node)nodes.get (i);
                    Issue issue = issues[i];


                    Node parent = node.getParentNode();
                    if (parent != null && parent.getNodeName().equals("module-requirements")) {
                        String mod = parent.getAttributes().getNamedItem("name").getNodeValue();
                        if (mod != null && !previousModule.equals(mod)) {
                            log("Processing module: " + mod);
                            previousModule = mod;
                        }
                    }
                    fillRequirement (iz, dom, issue, node, new HashSet ());
                }
            
                log ("Writing result to " + getTarget ());
            } else {
                log ("No issues to synchronize. Copying file to " + getTarget ());
            }

            Element componentsNode = dom.createElement ("components");
            Iterator it = components.iterator();
            while (it.hasNext())
                componentsNode.appendChild (createComponent(dom, false, (String) it.next()));
            
            it = subcomponents.iterator();
            while (it.hasNext())
                componentsNode.appendChild (createComponent(dom, true, (String) it.next()));
            
            dom.getDocumentElement().appendChild(componentsNode);
            
            Element ownersNode = dom.createElement ("owners");
            it = owners.keySet ().iterator();
            while (it.hasNext()) {
                String email = (String) it.next();
                String name = (String) owners.get(email);
                
                Element owner = dom.createElement ("owner");
                owner.setAttribute("nickname", name);
                owner.setAttribute("e-mail", email);
                ownersNode.appendChild (owner);
            }
            dom.getDocumentElement().appendChild(ownersNode);

            // today's date in format "YYYY-MM-DD" append as <other><today date="2002-03-05" /></other>
            dom.getDocumentElement().appendChild(ownersNode);
            Element otherNode = dom.createElement ("other");
            Element todayNode = dom.createElement ("today");
            todayNode.setAttribute("date", new SimpleDateFormat("yyyy-MM-dd").format(new Date()));
            Calendar cal = Calendar.getInstance();
	    cal.set(Calendar.DAY_OF_WEEK, Calendar.MONDAY);
	    todayNode.setAttribute("thisweek", new SimpleDateFormat("yyyy-MM-dd").format(cal.getTime()));
            cal.add(Calendar.DATE, 7);
            todayNode.setAttribute("nextweek", new SimpleDateFormat("yyyy-MM-dd").format(cal.getTime()));
            otherNode.appendChild(todayNode);
            cal.add(Calendar.DATE, -14);
            todayNode.setAttribute("lastweek", new SimpleDateFormat("yyyy-MM-dd").format(cal.getTime()));
            dom.getDocumentElement().appendChild(otherNode);
            
            
            //
            // identifies which requirements are duplicated
            //
            HashMap existing = new HashMap ();

	    list = dom.getElementsByTagName("module-requirements");
	    size = list.getLength();
	    for (int i = 0; i < size; i++) {
		Node module = list.item(i);
		NodeList topReqs = module.getChildNodes();
		int l2 = topReqs.getLength();
		for (int j = 0; j < l2; j++) {
		    Node req = topReqs.item(j);
		    if (req.getNodeName().equals("requirement")) {
			addAppearance(req, existing, dom);
		    }
		}
	    }
	    

            // process all requirements nodes
            list = dom.getElementsByTagName("requirement");

            
            size = list.getLength();
            for (int i = 0; i < size; i++) {
                Node node = list.item (i);
		addAppearance(node, existing, dom);
            }
            
            
            
            OutputStream os = new BufferedOutputStream (new FileOutputStream (getTarget ()));
            try {
                XMLUtil.write(dom, os);
            } finally {
                os.close();
            }
        } catch (IOException ex) {
            throw new BuildException (ex);
        } catch (org.xml.sax.SAXException ex) {
            throw new BuildException (ex);
        } catch (javax.xml.parsers.ParserConfigurationException ex) {
            throw new BuildException (ex);
        } finally {
            if (previousHost != null) {
                System.setProperty("http.proxyHost", (String)previousHost);
            }
            if (previousPort != null) {
                System.setProperty("http.proxy", (String)previousPort);
            }
        }
    }

    private Node findRequirement(Document dom, int number) {
        NodeList list = dom.getElementsByTagName("requirement");
        int size = list.getLength();
        for (int i = 0; i < size; i++) {
            Node req = list.item(i);
            if (getIssueNumber(req) == number) {
                return req;
            }
        }
        return null;
    }

    private int getIssueNumber(Node n) {
        NamedNodeMap attrs = n.getAttributes();
        Node attrIssue = attrs.getNamedItem("issue");
        if (attrIssue != null) {
            try {
                return Integer.parseInt(attrIssue.getNodeValue());
            } catch (NumberFormatException e) {
                // ignore
            }
        }
        return -1;
    }
    
    private Set fillIssuesByQueries(Document dom, Issuezilla iz) throws java.io.IOException, org.xml.sax.SAXException {
        ArrayList toRemove = new ArrayList();
        NodeList list = dom.getElementsByTagName("requirement");
        int size = list.getLength();
        HashSet issuesFromQueries = new HashSet();

        for (int i = 0; i < size; i++) {
            Node req = list.item(i);
            NamedNodeMap attrs = req.getAttributes();
            Node attrQuery = attrs.getNamedItem("query");
            
            if (attrQuery != null) {
                log("Processing query " + attrQuery.getNodeValue() + " ...");
                
                int n [] = iz.query(attrQuery.getNodeValue());
                for (int j = 0; j < n.length; j++) {
                    Node r = findRequirement(dom, n[j]);
                    if (r == null) {
                        log("Adding issue " + n[j] + " from query ...");
                        r = dom.createElement("requirement");
                        req.getParentNode().appendChild(r);
                        fillMissingAttribute(dom, String.valueOf(n[j]), r.getAttributes(), "issue");
                    }
                    
                    issuesFromQueries.add(new Integer(n[j]));
                }
                
                toRemove.add(req);
            }
        }

        for (Iterator i = toRemove.iterator(); i.hasNext(); ) {
            Node node = (Node) i.next();
            node.getParentNode().removeChild(node);
        }
        
        return issuesFromQueries;
    }
    
    void addAppearance(Node node, HashMap existing, org.w3c.dom.Document dom) {
                NamedNodeMap attrs = node.getAttributes();

                Node issueNode = attrs.getNamedItem("issue");
                if (issueNode == null) {
                    // ok, skip this requirement it does not connect to issuezilla
                    // at all
                    return;
                }
		Node appearanceNode = attrs.getNamedItem("appearance");
		if (appearanceNode != null)
		    return;

                Integer n = Integer.valueOf(issueNode.getNodeValue ());
                
                int[] cnt = (int[])existing.get (n);
                int assign;
                if (cnt == null) {
                    assign = 1;
                    existing.put (n, new int[] { 1 });
                } else {
                    assign = ++cnt[0];
                }
                
                Attr a = dom.createAttribute("appearance");
                a.setNodeValue(String.valueOf (assign));
                attrs.setNamedItem (a);
    }
    
    /** Fills in all missing attributes for a given requirement.
     * @param iz issuezilla
     * @param issue the issue to fill into the node
     * @param node the node representing requirement to fill in
     * @param alreadyProcessed already processed issues that should not be work with them anymore
     */
    private void fillRequirement (Issuezilla iz, Document dom, Issue issue, Node node, HashSet alreadyProcessed) 
    throws org.xml.sax.SAXException, java.io.IOException {
        
        NamedNodeMap attrs = node.getAttributes();

        fillMissingAttribute (dom, issue.getTargetMilestone (), attrs, "target");
        fillMissingAttribute (dom, String.valueOf (issue.getPriority ()), attrs, "issue-priority");
        fillMissingAttribute (dom, issue.getType (), attrs, "issue-type");
        fillMissingAttribute (dom, mapPriority (issue.getPriority ()), attrs, "priority");
	fillPercentageComplete(iz, dom, node, issue);
        fillMissingAttribute (dom, issue.getSummary (), attrs, "name");
        fillMissingAttribute (dom, String.valueOf (issue.getVotes ()), attrs, "votes");
        fillMissingAttribute (dom, String.valueOf (issue.getKeywords ()), attrs, "keywords");
        fillMissingAttribute (dom, mapUserImpact (issue.getType ()), attrs, "user-impact");
        fillMissingAttribute (dom, issue.getDuration(), attrs, "duration");
        
        String comp = String.valueOf (issue.getComponent());
        components.add (comp);

        String sub = String.valueOf (issue.getSubcomponent());
        subcomponents.add (sub);
        
        fillMissingAttribute (dom, comp, attrs, "component");
        fillMissingAttribute (dom, sub, attrs, "subcomponent");

        checkPersons (dom, issue, node, names, owners);
        checkRequirements (iz, dom, issue, node, alreadyProcessed);
        checkLongDescription (dom, issue, node);
    }
    
    private void fillPercentageComplete(Issuezilla iz, Document dom, Node node, Issue issue) 
    throws org.xml.sax.SAXException, java.io.IOException {
	NamedNodeMap attrs = node.getAttributes();
	Attr percentAttr = (Attr)attrs.getNamedItem("percentage-complete");
	String percent = mapPercentage(iz, issue);
	if (percentAttr != null && "100".equals(percent)) {
	    percentAttr.setValue(percent);
	} else {
    	    fillMissingAttribute (dom, mapPercentage (iz, issue), attrs, "percentage-complete");
	}
    }
    
    /** Adds a <requirements> <requirement ... /> </requirement> tags for a given issue
     */
    private void checkRequirements (Issuezilla iz, Document dom, Issue issue, Node node, HashSet alreadyProcessed) 
    throws org.xml.sax.SAXException, java.io.IOException {
        if (!alreadyProcessed.add (issue)) {
            // issue already there
            return;
        }

        if (!recurse)
            return;
        
        int[] deps = issue.getDependsOn();
        if (deps == null || deps.length == 0) {
            return;
        }

        if (generateDependenciesAsLinks) {
            // add all dependant issues as req-links
            TreeSet set = new TreeSet ();
            addDependencies (iz, issue, set);
            // remove this issue - we do not depend on it
            set.remove (issue);

            Iterator it = set.iterator ();
            while (it.hasNext ()) {
                Issue d = (Issue)it.next ();
                Element link = dom.createElement ("req-link");
                link.setAttribute ("href", urlBase + "show_bug.cgi?id=" + d.getId ());
                link.appendChild (dom.createTextNode (d.getSummary () + " (" + mapFinished (d) + ")"));
                node.appendChild (link);
            }
        }
        

        // looking for requirements subnode and all filled subrequirements
        Element subreq = null;
        HashSet issues = new HashSet ();
        {
            NodeList list = node.getChildNodes();
            int len = list.getLength ();
            for (int i = 0; i < len; i++) {
                Node n = list.item (i);
                if (n.getNodeName ().equals ("requirements") && n instanceof Element) {
                    subreq = (Element)n;
                    break;
                }
            }
        }
        
        if (subreq == null) {
            // if does not exists, create new one
            subreq = dom.createElement ("requirements");
            node.appendChild (subreq);
        } else {
            // the requirements exists, let's enumerate all requirements there
            NodeList list = subreq.getChildNodes();
            int len = list.getLength ();
            for (int i = 0; i < len; i++) {
                Node n = list.item (i);
                if (n.getNodeName ().equals ("requirement")) {
                    Node numbnode = n.getAttributes().getNamedItem ("issue");
                    if (numbnode != null) {
                        String number = numbnode.getNodeValue ();
                        if (number != null) {
                            // do not generate this issue number
                            issues.add (Integer.valueOf(number));
                        }
                    }
                }
            }
        }

        //
        // generating the missing requirements
        //
        
        for (int i = 0; i < deps.length; i++) {
            Integer number = new Integer (deps[i]);
            if (issues.contains (number)) {
                // already there
                continue;
            }
            
            Issue one = findIssue(iz, deps[i]);
            //Issue one = iz.getBug (deps[i]);
            
            Element req = dom.createElement ("requirement");
            req.setAttribute("issue", number.toString());
            if (mode == MODE_QUERIES_ONLY) {
                req.setAttribute("appearance", "2");
            }
            
            fillRequirement (iz, dom, one, req, alreadyProcessed);
            
            subreq.appendChild (req);
        }
    }
    
    /** Checks long-description and if it is missing adds it.
     * @param dom document
     * @param issue iz issue
     * @param node node to check
     */
    private static void checkLongDescription (Document dom, Issue issue, Node node) 
    throws IOException {
        if (issue.getDescriptions().length == 0) {
            return;
        }
        Issue.Description text = issue.getDescriptions ()[0];
        if ("".equals (text.getBody ())) {
            return;
        }
            
        
        NodeList list = node.getChildNodes();
        int len = list.getLength ();
        for (int i = 0; i < len; i++) {
            Node n = list.item (i);
            if (n.getNodeName ().equals ("long-description")) {
                // roles exists
                return;
            }
        }
        
        //
        // converts body into text where empty lines are replaced with <P>
        //
        
        StringBuffer buf = new StringBuffer (1024);
        BufferedReader r = new BufferedReader (new StringReader (text.getBody ()));
        
        Node body = dom.createElement ("body");
        for (;;) {
            String line = r.readLine ();
            
            if (line != null) {
                // break long words without spaces
                for (int i = 0; i < line.length (); ) {
                    int j = line.indexOf(' ', i);
                    if (j == -1) {
                        j = line.length ();
                    }

                    if (j - i > 100) {
                        line = line.substring (0, i + 100) + "\n" + line.substring (i + 100);
                        i = i + 100;
                    } else {
                        i = j + 1;
                    }
                }
            }
            
            if (line == null || line.length () == 0) {
                // add a paragraph into the body
                Node p = dom.createElement ("P");
                p.appendChild (dom.createTextNode (buf.toString ()));
                body.appendChild (p);
                
                buf.setLength (0);
            }
            
            if (line == null) break;

            if (line.length () > 0) {
                buf.append (line);
                buf.append ("\n");
            }
        }
        
        Element desc = dom.createElement ("long-description");
        desc.appendChild (body);
        node.appendChild (desc);
    }
        
    
    /** Checks whether roles are filled and if not, adds them from the issue.
     * @param dom document
     * @param issue iz issue
     * @param node node to check
     * @param names mapping of email addresses to nicknames
     * @param owners hashmap used for collection all people responsible for any issue
     */
    private static void checkPersons (Document dom, Issue issue, Node node, Properties names, HashMap owners) {
        Element el = (Element)node;
        NodeList list = el.getElementsByTagName("roles");
        if (list.getLength() > 0) {
            Element roles = (Element)list.item(0);
            if (roles.getParentNode() == el) {
                NodeList rlist = roles.getElementsByTagName("e-mail");
                for (int i = 0; i < rlist.getLength(); i++) {
                    Element email = (Element)rlist.item(i);
                    try {
                        NodeList tnodes = email.getChildNodes();
                        if (tnodes.getLength() > 0 && tnodes.item(0).toString().length() > 0) {
                            String assigned = tnodes.item(0).toString();
                            if (!owners.containsKey(assigned)) {
                                owners.put(assigned, findNickname(assigned, names));
                            }
                        }
                    } catch (DOMException ex) {
                    }
                }
            }
            return;
        }
         
        // else go on and create roles
        Element roles = dom.createElement ("roles");
        roles.appendChild (createPerson (dom, issue.getAssignedTo (), "responsible", names));
        roles.appendChild (createPerson (dom, issue.getReportedBy(), "initiator", names));

        owners.put(issue.getAssignedTo(), findNickname (issue.getAssignedTo(), names));
        
        String[] supervisors = issue.getObservedBy();
        for (int i = 0; i < supervisors.length; i++) {
            roles.appendChild (createPerson (dom, supervisors[i], "consultant", names));
        }
        
        node.appendChild (roles);
    }
        
    /** Creates person node for given address and role name.
     * 
     * @param dom dom document
     * @param email email address
     * @param roleName description of the role
     * @param names mapping of email addresses to nicknames
     */
    private static Element createPerson (
        Document dom, String email, String roleName, Properties names
    ) {
        Element person = dom.createElement ("person");
        String nick = findNickname (email, names);
        person.setAttribute("nickname", nick);
        
        Element mail = dom.createElement ("e-mail");
        mail.appendChild (dom.createTextNode (email));
        
        Element role = dom.createElement ("role");
        role.setAttribute ("name", roleName);
        
        person.appendChild (mail);
        person.appendChild (role);
        
        return person;
    }
        
        
    /** Creates (sub)component node for given name.
     * 
     * @param subcomponent
     * @param name the name of (sub)component
     */
    private static Element createComponent (
        Document dom, boolean subcomponent, String name
    ) {
        Element component = dom.createElement (subcomponent ? "subcomponent" : "component");
        component.setAttribute("name", name);
        
        return component;
    }
    

    /** Scans attributes of an DOM node and fill in missing value from an issue
     * @param value suggested value
     * @param attrs attributes
     * @param name the name of attribute that should be filled if missing
     */
    private void fillMissingAttribute (Document dom, String value, NamedNodeMap attrs, String name) {
        if (attrs.getNamedItem (name) == null) {
            Attr a = dom.createAttribute (name);
            a.setValue (value);
            attrs.setNamedItem (a);
        }
    }

    /** Finds out user impact from issue type
     */
    private String mapUserImpact (String type) {
        if ("FEATURE".equals (type)) {
            return "high";
        }
        
        if ("ENHANCEMENT".equals (type)) {
            return "low";
        }
	
        return "medium";
    }
    
    /** Map percentage.
     * @param issue the issue
     * @return percentage of done work
     */
    private String mapPercentage (Issuezilla iz, Issue issue) 
    throws org.xml.sax.SAXException, IOException {
        HashSet allIssues = new HashSet ();
        int cnt = countFinished (iz, issue, allIssues);
        int done = allIssues.size ();
        
        //if (cnt == done) {
        //
        // Task is 100% done when the issue isFinished.
        // Statuses of subtasks are ignored.
        //
        if (isFinished(issue)) {
            return "100"; // 100% done
        } else {
            return cnt + " of " + allIssues.size ();
        }
    }
    
    /** Counts list of issues this issue depends on and
     * adds them to the hashset. 
     *
     * @param set set of all issues that we depend on
     * @return number of finished issues
     */
    private int countFinished (Issuezilla iz, Issue issue, HashSet set) 
    throws org.xml.sax.SAXException, IOException {
        addDependencies (iz, issue, set);

        int sum = 0;
        Iterator it = set.iterator ();
        while (it.hasNext ()) {
            Issue d = (Issue)it.next ();
            if (isFinished (d)) {
                sum++;
            }
        }
        
        return sum;
    }

    /** Collects all issues that this on depends on
     * @param iz connection to issuezilla
     * @param issue the issue to collect the dependant onces for
     * @param set set of already contained issues
     */
    private void addDependencies (Issuezilla iz, Issue issue, Set set)
    throws org.xml.sax.SAXException, IOException {
        if (set.contains (issue)) {
            // simply stop the recursion
            return;
        }
        
        set.add (issue);

        if (!recurse)
            return;

        int[] deps = issue.getDependsOn();
        
        for (int i = 0; i < deps.length; i++) {
            Issue d = findIssue (iz, deps[i]);
            addDependencies (iz, d, set);
        }
    }
    
    /** Map finished state to a text.
     */
    private static String mapFinished (Issue issue) {
        if (isFinished (issue)) {
            return "Done";
        } else {
            return issue.getStatus ().toLowerCase ();
        }
    }
    
    /** @return true if the issue is finished, false otherwise
     */
    private static boolean isFinished (Issue issue) {    
        if ("RESOLVED".equals (issue.getStatus ())) {
            return true;
        }
        
        if ("VERIFIED".equals (issue.getStatus ())) {
            return true;
        }
     
        if ("CLOSED".equals (issue.getStatus ())) {
            return true;
        }
     
        return false;
    }
            
    
    /** Maps priority to a value.
     */
    private String mapPriority (int priority) {
        String propName = "issue.priority." + priority;
        String value = getProject ().getProperty(propName);
        if (value != null) {
            return value;
        }
        
        if (priority == 1)
            return "high";
        
        if (priority == 2)
            return "medium";

        // priority >= 3
        return "small";
    }
    
    /** Finds an issue in the issuezilla.
     * @param iz issuezilla connection
     * @param name string name of the issue
     * @return the issue
     * @exception if something is wrong
     */
    private Issue[] findIssues (Issuezilla iz, String[] name) 
    throws org.xml.sax.SAXException, IOException {
        int[] numbers = new int[name.length];
        for (int i = 0; i < numbers.length; i++) {
            numbers[i] = Integer.parseInt(name[i]);
        }
        
        Issue[] arr = iz.getBugs(numbers);
        
        for (int i = 0; i < numbers.length; i++) {
            issues.put(new Integer(numbers[i]), arr[i]);
        }
        
        return arr;
   }


    /** Finds an issue in the issuezilla.
     * @param iz issuezilla connection
     * @param number issue to read
     * @return the issue
     * @exception if something is wrong
     */
    private Issue findIssue (Issuezilla iz, int number) 
    throws org.xml.sax.SAXException, IOException {
        
        Integer integer = new Integer (number);
        Issue i;
        
        i = (Issue)issues.get (integer);
        if (i != null) {
            return i;
        }
        
        log ("Retrieving issue " + number);
        
        i = iz.getBug(number);
        issues.put(integer, i);
        return i;
    }
    
    /** Find nickname for given email.
     * @param email email
     * @param nicks mapping from email to nick names
     */
    private static String findNickname (String email, Properties nicks) {
        String value = nicks.getProperty (email);
        if (value != null) {
            return value;
        }
        
        int zav = email.indexOf('@');
        if (zav != -1) {
            String plain = email.substring (0, zav);
            
            value = nicks.getProperty (plain);
            if (value != null) {
                return value;
            }
            
            
            return plain;
        }
        
        return email;
    }
    
    /** Getter for property proxyHost.
     * @return Value of property proxyHost.
     */
    public String getProxyHost() {
        return this.proxyHost;
    }    
    
    /** Setter for property proxyHost.
     * @param proxyHost New value of property proxyHost.
     */
    public void setProxyHost(String proxyHost) {
        if ("".equals (proxyHost)) {
            proxyHost = null;
        }
        this.proxyHost = proxyHost;
    }    
        
    /** Getter for property proxyPort.
     * @return Value of property proxyPort.
     */
    public int getProxyPort() {
        return this.proxyPort;
    }
    
    /** Setter for property proxyPort.
     * @param proxyPort New value of property proxyPort.
     */
    public void setProxyPort(int proxyPort) {
        this.proxyPort = proxyPort;
    }
    
}
