/*
 * $Id$
 *
 * Copyright 1996-2008 Sun Microsystems, Inc.  All Rights Reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Sun designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Sun in the LICENSE file that accompanied this code.
 *
 * This code 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
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa Clara,
 * CA 95054 USA or visit www.sun.com if you need additional information or
 * have any questions.
 */
package com.sun.javatest.util;

import java.io.IOException;
import java.io.Writer;
import java.util.Comparator;
import java.util.Iterator;
import java.util.Map;
import java.util.TreeMap;

/**
 * A class that provides a tree of information nodes that can be
 * selectively prointed, suitable for simple command line help.
 */
public class HelpTree
{
    /**
     * A node within a HelpTree.  A node has a name, a description,
     * and zero or more child nodes.
     */
    public static class Node {

        /**
         * Create a node, with no children.
         * @param name the name for the node
         * @param description the descrption for the node
         */
        public Node(String name, String description) {
            this.name = name;
            this.description = description;
        }

        /**
         * Create a node, with given children.
         * @param name the name for the node
         * @param description the description for the node
         * @param children the child nodes for the node
         */
        public Node(String name, String description, Node[] children) {
            this.name = name;
            this.description = description;
            this.children = children;
        }

        /**
         * Create a node, with no children. The name and description are
         * obtained from a resource bundle, using keys based on a common
         * prefix. The key for the name will be <i>prefix</i>.name and
         * the key for the description will be <i>prefix</i>.desc.
         * @param i18n the resource bundle from which to obtain the
         * name and description for the node.
         * @param prefix the prefix for the names of the name and description
         * entries in the resource bundle.
         */
        public Node(I18NResourceBundle i18n, String prefix) {
            name = i18n.getString(prefix + ".name");
            description = i18n.getString(prefix + ".desc");
        }

        /**
         * Create a node, with given children. The name and description are
         * obtained from a resource bundle, using keys based on a common
         * prefix. The key for the name will be <i>prefix</i>.name and
         * the key for the description will be <i>prefix</i>.desc.
         * @param i18n the resource bundle from which to obtain the
         * name and description for the node.
         * @param prefix the prefix for the names of the name and description
         * entries in the resource bundle.
         * @param children the child nodes for this node
         */
        public Node(I18NResourceBundle i18n, String prefix, Node[] children) {
            this(i18n, prefix);
            this.children = children;
        }

        /**
         * Create a node and its children. The name and description are
         * obtained from a resource bundle, using keys based on a common
         * prefix. The key for the name will be <i>prefix</i>.name and
         * the key for the description will be <i>prefix</i>.desc.
         * The children will each be created with no children of their
         * own, using a prefix of <i>prefix</i>.<i>entry</i>.
         * @param i18n the resource bundle from which to obtain the
         * name and description for the node.
         * @param prefix the prefix for the names of the name and description
         * entries in the resource bundle.
         * @param entries the array of <i>entry</i> names used to create
         * the child nodes.
         */
        public Node(I18NResourceBundle i18n, String prefix, String[] entries) {
            this(i18n, prefix);
            children = new Node[entries.length];
            for (int i = 0; i < children.length; i++)
                children[i] = new Node(i18n, prefix + '.' + entries[i]);
        }

        /**
         * Get the name of this node.
         * @return the name of this node
         */
        public final String getName() {
            return name;
        }

        /**
         * Get the description of this node.
         * @return the description of this node
         */
        public final String getDescription() {
            return description;
        }

        /**
         * Get the number of children of this node.
         * @return the number of children of this node
         */
        public int getChildCount() {
            return (children == null ? 0 : children.length);
        }

        /**
         * Get a specified child of this node.
         * @param i the index of the desired child
         * @return the specified child of this node
         */
        public Node getChild(int i) {
            if (i >= getChildCount())
                throw new IllegalArgumentException();
            return children[i];
        }

        private String name;
        private String description;
        private Node[] children;
    }

    /**
     * A selection of nodes within a HelpTree.
     * @see HelpTree#find
     */
    public class Selection {
        private Selection(Node node) {
            this(node, null);
        }

        private Selection(Map map) {
            this(null, map);
        }

        private Selection(Node node, Map map) {
            this.node = node;
            this.map = map;
        }

        private Node node;
        private Map map;
    }

    /**
     * Create an empty HelpTree object.
     */
    public HelpTree() {
        nodes = new Node[0];
    }

    /**
     * Create a HelpTree object containing a given set of nodes.
     * @param nodes the contents of the HelpTree
     */
    public HelpTree(Node[] nodes) {
        this.nodes = nodes;
    }

    /**
     * Add a node to a help tree.
     * @param node the node to be added to the tree
     */
    public void addNode(Node node) {
        nodes = (Node[]) DynamicArray.append(nodes, node);
    }

    /**
     * Get the indentation used to adjust the left margin when writing
     * the child nodes for a node.
     * @return the indentation used to adjust the left margin when writing
     * the child nodes for a node
     * @see #setNodeIndent
     */
    public int getNodeIndent() {
        return nodeIndent;
    }

    /**
     * Set the indentation used to adjust the left margin when writing
     * the child nodes for a node.
     * @param n the indentation used to adjust the left margin when writing
     * the child nodes for a node
     * @see #getNodeIndent
     */
    public void setNodeIndent(int n) {
        nodeIndent = n;
    }

    /**
     * Get the indentation used to adjust the left margin when writing
     * the description of a node.
     * @return the indentation used to adjust the left margin when writing
     * the description of a node
     * @see #setDescriptionIndent
     */
    public int getDescriptionIndent() {
        return descriptionIndent;
    }

    /**
     * Set the indentation used to adjust the left margin when writing
     * the description of a node.
     * @param n the indentation used to adjust the left margin when writing
     * the description of a node
     * @see #getDescriptionIndent
     */
    public void setDescriptionIndent(int n) {
        descriptionIndent = n;
    }

    /**
     * Get a selection representing the nodes that match the given words.
     * If there are nodes whose name or description contain all of the
     * given words, then those nodes will be returned.
     * Otherwise, all nodes whose name or description contain at least one
     * of the given words will be returned.
     * @param words the words to be searched for
     * @return a Selection containing the matching nodes
     */
    public Selection find(String[] words) {
        Selection s = find(words, ALL);

        if (s == null && words.length > 1)
            s = find(words, ANY);

        return s;
    }

    /**
     * Get a selection representing the nodes that match all of the given words.
     * @param words the words to be searched for
     * @return a Selection containing the matching nodes
     */
    public Selection findAll(String[] words) {
        return find(words, ALL);
    }

    /**
     * Get a selection representing the nodes that each match
     * at least one of the given words.
     * @param words the words to be searched for
     * @return a Selection containing the matching nodes
     */
    public Selection findAny(String[] words) {
        return find(words, ANY);
    }

    private Selection find(String[] words, int mode) {
        Map map = null;

        for (int i = 0; i < nodes.length; i++) {
            Node node = nodes[i];
            Selection s = find(node, words, mode);
            if (s != null) {
                if (map == null)
                    map = new TreeMap(nodeComparator);
                map.put(node, s);
            }
        }

        return (map == null ? null : new Selection(map));
    }

    private Selection find(Node node, String[] words, int mode) {
        if (mode == ALL) {
            if (containsAllOf(node.name, words) || containsAllOf(node.description, words))
                return new Selection(node);
        }
        else if (mode == ANY) {
            if (containsAnyOf(node.name, words) || containsAnyOf(node.description, words))
                return new Selection(node);
        }
        else
            throw new IllegalArgumentException();

        if (node.children == null)
            return null;

        Map map = null;

        for (int i = 0; i < node.children.length; i++) {
            Node child = node.children[i];
            Selection s = find(child, words, mode);
            if (s != null) {
                if (map == null)
                    map = new TreeMap(nodeComparator);
                map.put(child, s);
            }
        }

        return (map == null ? null : new Selection(node, map));
    }

    /**
     * Write out all the nodes in this HelpTree.
     * @param out the writer to which to write the nodes.
     * If out is a com.sun.javatest.util.WrapWriter, it will be
     * used directly, otherwise a WrapWriter will be created
     * that will write to the given writer.
     * @throws IOException if the is a problem writing the
     * nodes.
     * @see WrapWriter
     */
    public void write(Writer out) throws IOException {
        WrapWriter ww = getWrapWriter(out);

        for (int i = 0; i < nodes.length; i++) {
            write(ww, nodes[i]);
            ww.write('\n');
        }

        if (ww != out)
            ww.flush();
    }

    /**
     * Write out selected nodes in this HelpTree.
     * @param out the writer to which to write the nodes.
     * If out is a com.sun.javatest.util.WrapWriter, it will be
     * used directly, otherwise a WrapWriter will be created
     * that will write to the given writer.
     * @param s a Selection object containing the nodes to be written
     * @throws IOException if the is a problem writing the
     * nodes.
     * @see WrapWriter
     */
    public void write(Writer out, Selection s) throws IOException {
        WrapWriter ww = getWrapWriter(out);

        write(ww, s.map);

        if (ww != out)
            ww.flush();
    }

    /**
     * Write out a summary of all the nodes in this HelpTree.
     * The summary will contain the name and description of the
     * top level nodes, but not any of their children.
     * @param out the writer to which to write the nodes.
     * If out is a com.sun.javatest.util.WrapWriter, it will be
     * used directly, otherwise a WrapWriter will be created
     * that will write to the given writer.
     * @throws IOException if the is a problem writing the
     * nodes.
     * @see WrapWriter
     */
    public void writeSummary(Writer out) throws IOException {
        WrapWriter ww = getWrapWriter(out);

        for (int i = 0; i < nodes.length; i++)
            writeHead(ww, nodes[i]);

        if (ww != out)
            ww.flush();
    }

    private void write(WrapWriter out, Map m) throws IOException {
        int margin = out.getLeftMargin();
        for (Iterator iter = m.entrySet().iterator(); iter.hasNext(); ) {
            Map.Entry e = (Map.Entry) (iter.next());
            Node node = (Node) (e.getKey());
            Selection s = (Selection) (e.getValue());
            if (s.map == null)
                write(out, node);
            else {
                writeHead(out, node);
                out.setLeftMargin(margin + nodeIndent);
                write(out, s.map);
                out.setLeftMargin(margin);
            }
            if (margin == 0)
                out.write('\n');
        }
    }

    private void write(WrapWriter out, Node node) throws IOException {
        int baseMargin = out.getLeftMargin();

        writeHead(out, node);

        Node[] children = node.children;
        if (children != null && children.length > 0) {
            out.setLeftMargin(baseMargin + nodeIndent);
            for (int i = 0; i < children.length; i++)
                write(out, children[i]);
        }

        out.setLeftMargin(baseMargin);
    }

    private void writeHead(WrapWriter out, Node node) throws IOException {
        int baseMargin = out.getLeftMargin();

        String name = node.name;
        String desc = node.description;
        if (name != null) {
            out.write(name);
            out.write(' ');
            if (desc != null) {
                out.setLeftMargin(baseMargin + descriptionIndent);
                if (out.getCharsOnLineSoFar() + 2 > out.getLeftMargin())
                    out.write('\n');
                out.write(desc);
            }
            out.write('\n');
        }

        out.setLeftMargin(baseMargin);
    }

    private boolean containsAllOf(String text, String[] words) {
        for (int i = 0; i < words.length; i++) {
            if (!contains(text, words[i]))
                return false;
        }
        return true;
    }

    private boolean containsAnyOf(String text, String[] words) {
        for (int i = 0; i < words.length; i++) {
            if (contains(text, words[i]))
                return true;
        }
        return false;
    }

    private boolean contains(String text, String word) {
        int startIndex = text.toLowerCase().indexOf(word.toLowerCase());
        if (startIndex == -1)
            return false;

        int endIndex = startIndex + word.length();

        return ((startIndex == 0 || !Character.isLetter(text.charAt(startIndex - 1)))
                && (endIndex == text.length() || !Character.isLetter(text.charAt(endIndex))));
    }

    private WrapWriter getWrapWriter(Writer out) {
        return (out instanceof WrapWriter ? (WrapWriter) out : new WrapWriter(out));
    }

    private Node[] nodes;

    private int nodeIndent = 4;
    private int descriptionIndent = 16;

    private static final int ALL = 1;
    private static final int ANY = 2;

    private static Comparator nodeComparator = new Comparator() {
            public int compare(Object o1, Object o2) {
                Node n1 = (Node) o1;
                Node n2 = (Node) o2;

                int v = compareStrings(n1.name, n2.name);
                return (v != 0 ? v : compareStrings(n1.description, n2.description));
            }

            private int compareStrings(String s1, String s2) {
                if (s1 == null && s2 == null)
                    return 0;

                if (s1 == null || s2 == null)
                    return (s1 == null ? -1 : +1);

                return s1.toLowerCase().compareTo(s2.toLowerCase());
            }
        };
}
