/*
 * $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.PrintWriter;
import java.util.Enumeration;
import java.util.Properties;

/**
 * Access to debugging settings which have been activated.
 * Indexing is by string matching; the key is usually but not always a fully qualified
 * classname.
 *
 * <p>
 * The system properties have three classes of "value" data.  The string may be
 * equal to the string "true" (case insensetive), it may be parsable into an
 * integer (by Integer.parseInt()), or it may be any other string.  In the
 * latter case where it is not TRUE and is not an int, it will be interpreted
 * as a zero if a integer is requested, and FALSE if a boolean is requested.
 *
 * <p>
 * The following definitions define how integer and boolean settings are
 * interpreted:
 * <dl>
 * <dt>A lookup that results in a valid integer:
 * <dd>Will return that integer for any of the getInt() methods
 * <dd>Will return false for any getBoolean() method if the integer is less than or equal to zero
 * <dd>Will return true for and getBoolean() method if the integer is greater than zero
 *
 * <dt>A lookup that results in "true" (any case combination):
 * <dd>Will return one for any of the getInt() methods
 * <dd>Will return true for any of the getBoolean() methods
 *
 * <dt>A lookup that results in neither of the above cases (not "true", nor an integer):
 * <dd>Will return zero for any of the getInt() methods
 * <dd>Will return false for any of the getBoolean() methods
 * </dl>
 *
 * @see java.lang.Integer#parseInt
 */

public class Debug {
    private Debug() {
        // no instances
    }

    /**
     * Print a debugging message.
     * @param s the message to be printed.
     */
    public static void print(String s) {
        out.print(s);
        out.flush();
    }

    /**
     * Print a debugging message and end the line.
     * @param s the message to be printed.
     */
    public static void println(String s) {
        out.println(s);
        out.flush();
    }

    /**
     * Check if overall debugging is enabled.
     * If it is not enabled, methods will return null, false, and zero,
     * as appropriate for the type.
     * @return true if debugging is enabled, and false otherwise
     */
    public static boolean isEnabled() {
        return masterSwitch;
    }

    /**
     * Do a raw lookup of a key, including matching of wildcard settings.
     * Is the given string matched by any of the debug system properties.
     * The matching is done according to import-style sematics, using the dot
     * separator.  In addtion wildcards from the system properties are allowed
     * <b>at the end</b> of the key specification.  Unlike imports, the wildcard can
     * match inner class names.
     *
     * @param key The name of the setting to be returned
     * @return The setting, or null if not found.  Null is returned if debugging is
     *  disabled entriely.
     */
    public static String getSetting(String key) {
        // important because there may be uninitialized objects
        if (masterSwitch == false)
            return null;

        String match = dProps.getProperty(key);

        if (match != null)
            return match;
        else {
            match = wildProps.search(key);
            // may be null
            return match;
        }
    }

    /**
     * Find out the debugging setting for class c.
     * Lookup is done by looking for fully qualified class name.
     *
     * @param c Class whose name should be used to lookup the setting, null results in
     *    a return value of zero.
     * @return the debugging setting for the specified class
     */
    public static boolean getBoolean(Class c) {
        init(false);

        if (!masterSwitch)
            return false;

        String key = getName(c);
        String setting = getSetting(key);
        boolean state = convertToBool(setting);

        return state;
    }

    /**
     * Find out the debugging setting for class c.
     * Lookup is done by looking for fully qualified class name with a dot and the
     * given suffix appended.
     *
     * @param c Class whose name should be used to lookup the setting, null results in
     *    a return value of zero.
     * @param suffix String to append to the classname, null will result in a lookup
     *    of just the classname.
     * @return the debugging setting for the specified class
     */
    public static boolean getBoolean(Class c, String suffix) {
        init(false);

        if (!masterSwitch)
            return false;

        StringBuffer buf = new StringBuffer(getName(c));
        if (suffix != null && suffix.length() != 0) {
            buf.append(Debug.SEPARATOR);
            buf.append(suffix);
        }

        String key = buf.toString();
        String setting = getSetting(key);
        boolean state = convertToBool(setting);

        return state;
    }

    /**
     * Get a named debugging setting.
     * @param s the name of the desired debugging setting
     * @return the value of the debugging setting
     */
    public static boolean getBoolean(String s) {
        init(false);

        if (!masterSwitch || s == null)
            return false;

        String setting = getSetting(s);
        boolean state = convertToBool(setting);

        return state;
    }

    /**
     * Find out the debugging setting for class c.
     * Lookup is done by looking for fully qualified class name.
     *
     * @param c the class whose name should be used to lookup the setting
     * @return the debugging setting for the given class, or 0 if no class
     *    was specified
     */
    public static int getInt(Class c) {
        init(false);

        if (!masterSwitch || c == null)
            return 0;

        String key = getName(c);
        String setting = getSetting(key);
        int state = convertToInt(setting);

        return state;
    }

    /**
     * Find out the debugging setting for class c.
     * Lookup is done by looking for fully qualified class name with a dot and the
     * given suffix appended.
     *
     * @param c a class whose name should be used to lookup the setting;
     *    null results in a return value of zero.
     * @param suffix a string to append to the classname;
     *    null will result in a lookup of just the classname.
     * @return the debugging setting for the class
     */
    public static int getInt(Class c, String suffix) {
        init(false);

        if (!masterSwitch || c == null)
            return 0;

        StringBuffer buf = new StringBuffer(getName(c));
        if (suffix != null && suffix.length() != 0) {
            buf.append(Debug.SEPARATOR);
            buf.append(suffix);
        }

        String key = buf.toString();
        String setting = getSetting(key);
        int state = convertToInt(setting);

        return state;
    }

    /**
     * Get a named debugging setting.
     * @param s the name of the desired debugging setting
     * @return the value of the debugging setting
     */
    public static int getInt(String s) {
        init(false);

        if (!masterSwitch)
            return 0;

        String setting = getSetting(s);
        return convertToInt(setting);

        /*
        String setting = dProps.getProperty(s);
        if (setting == null)
            return 0;
        else if (setting.equalsIgnoreCase("true"))
            return 1;
        else {
            try {
                return Integer.parseInt(setting);
            }
            catch (NumberFormatException e) {
                e.printStackTrace(out);
                return 0;
            }
        }
        */
    }

    /**
     * Get the debugging stream, used for writing debug messages.
     * @return the debugging stream, used to write debug messages
     */
    public static PrintWriter getWriter() {
        return out;
    }

    /**
     * Set properties containing debugging settings.
     * This is required if the security manager does not allow access to
     * the system properties.
     * @param props A table of properties conatining debugging settings
     */
    public static void setProperties(Properties props) {
        givenProps = props;
    }

    /**
     * Initialize (or re-initialize) debugging support.
     * @param force Force reprocessing of System properties.
     */
    public static void init(boolean force) {
        if (dProps != null && force != true)
            return;

        Properties props;

        try {
            props = System.getProperties();
        }
        catch (SecurityException e) {
            // this is the backup source of settings
            props = givenProps;
        }

        if (props == null) {
            // we're stuck, must disable debugging
            masterSwitch = false;
            return;
        }

        Enumeration keys = props.propertyNames();

        dProps = new Properties();
        wildProps = new WildcardProperties();

        while (keys.hasMoreElements()) {
            String key = (String)(keys.nextElement());
            if (key.startsWith(DEBUG_PREFIX)) {
                // this should be a setProperty() in JDK 1.2+
                if (key.equalsIgnoreCase(MASTER_KEY)) {
                    String val = props.getProperty(key);
                    // this will disable all debugging, all methods will return zero or false
                    if (val.equalsIgnoreCase(TRUE_STRING))
                        masterSwitch = false;
                }
                else if (key.endsWith(WILD_SUFFIX)) {
                    wildProps.put(key.substring(DEBUG_PREFIX.length()), props.getProperty(key));
                }
                else
                    dProps.put(key.substring(DEBUG_PREFIX.length()), props.getProperty(key));
            }
        }   // while
    }

    // -------- Private ---------

    /**
     * Convert a class object into a string appropriate for lookup.
     *
     * @param c Must not be null.
     */
    private static String getName(Class c) {
        // null checking skipped

        String name = c.getName();

        // compensate for inner classes for which getName() return the internal name
        name = name.replace('$', '.');

        return name;
    }

    /*
     * Take the user setting and interpret it.
     * If the setting corresponds to the true string, true is returned.  If it corresponds to
     * an integer greater than zero, true is returned.  Otherwise, false is returned.
     */
    private static boolean convertToBool(String setting) {
        if (setting == null)
            return false;

        if (setting.equalsIgnoreCase(TRUE_STRING))
            return true;
        else {
            try {
                int num = Integer.parseInt(setting);
                if (num > 0)
                    return true;
                else
                    return false;
            }
            catch (NumberFormatException e) {
                // not "true", not an integer
                return false;
            }
        }
    }

    /**
     * Take the user setting and interpret it.
     * If it is an integer, that is returned.  If it is true, 1 is returned.  If it is
     * anything else, zero is returned.
     */
    private static int convertToInt(String setting) {
        if (setting == null)
            return 0;

        if (setting.equalsIgnoreCase(TRUE_STRING))
            return 1;
        else {
            try {
                int num = Integer.parseInt(setting);
                return num;
            }
            catch (NumberFormatException e) {
                // not "true", not an integer
                return 0;
            }
        }
    }

    // ---- activate as needed ----
    private static void setEnabled(String debugKey, boolean state) {
    }

    private static void setEnabled(String debugKey, int level) {
    }

    private static void setOutput(PrintWriter w) {
        out = w;
    }

    private static final String SEPARATOR = ".";
    private static final String DEBUG_PREFIX = "debug" + SEPARATOR;
    private static final String WILD_SUFFIX = "*";
    private static final String TRUE_STRING = "true";
    private static final String MASTER_KEY = "debug.disable";

    private static Properties givenProps;           // settings used if System won't give
    private static Properties dProps;               // explicit props
    private static WildcardProperties wildProps;    // props which contain wildcards

    private static PrintWriter out;
    private static boolean defaultBool = false;
    private static int defaultInt = 0;

    /**
     * If false, normal debugging enabled.  If false,
     */
    private static boolean masterSwitch = true;

    static {
        out = new PrintWriter(System.err);
    }

    /**
     * Wildcards are compared against the given key minus one field.
     * So a search(<tt>"foo.bar.baz"</tt>) does a search for <tt>"foo.bar.*"</tt>.
     * <tt>"foo.*"</tt> will not be a match.
     */
    private static class WildcardProperties extends Properties {
        /**
         * This is the only method in this class that accounts for wildcards.
         */
        public String search(String key) {
            String lowerKey = key.toLowerCase();
            String target = trimTarget(lowerKey);

            Enumeration keys = propertyNames();
            while (keys.hasMoreElements()) {
                String k = (String)(keys.nextElement());
                String lowerK = k.toLowerCase();

                if (lowerK.startsWith(target)) {
                    // should strip target string and dot
                    String tail = lowerK.substring(target.length());
                    String head = lowerK.substring(0, target.length());
                    if (tail.equals(wildTail) || head.equals(lowerKey))
                        return getProperty(k);
                }
            }   // while

            return null;
        }

        /**
         * Remove the last element of the requested key to see if a wildcard fits into
         * that position.  Wildcards can only be valid up one level, so
         * foo.* cannot match foo.bar.baz, but will match foo.bar.  This method
         * turns foo.bar into foo so it the search mathod can use "foo".
         */
        String trimTarget(String t) {
            int index = t.lastIndexOf(Debug.SEPARATOR);
            if (index != -1)
                return t.substring(0, index);
            else
                return t;
        }

        static final String wildTail = Debug.SEPARATOR + Debug.WILD_SUFFIX;
    }
}
