package de.renew.plugin;

import java.awt.Toolkit;
import java.awt.event.InputEvent;
import java.awt.event.KeyEvent;
import java.io.File;
import java.util.Collection;
import java.util.List;
import java.util.Properties;
import java.util.StringTokenizer;
import java.util.Vector;
import javax.swing.KeyStroke;


/**
 * This is a collection of static methods to be able to get typed values out
 *  java.util.Properties objects.
 *
 * @author J&ouml;rn Schumacher
 */
public class PropertyHelper {
    /**
     * Logger for logging purposes as specified by Apache Log4j
     */
    public static final org.apache.log4j.Logger LOGGER =
        org.apache.log4j.Logger.getLogger(PropertyHelper.class);

    /**
     * Returns the property as an int. Returns -1 if the property is not set or
     * an error occurs
     * @param p Set of properties containing property to be converted
     * @param property the property to convert to int
     * @return the int value of the given property
     */
    public static int getIntProperty(Properties p, String property) {
        return getIntProperty(p, property, -1);
    }

    /**
     * Returns the property as an int. Returns defaultValue if the property is not set or
     * an error occurs
     * @param p Set of properties containing property to be converted
     * @param property the property to convert to int
     * @param defaultValue Default value to return if the property is not set or an error occurs
     * @return the int value of the given property
     */
    public static int getIntProperty(Properties p, String property, int defaultValue) {
        String str = p.getProperty(property, Integer.toString(defaultValue));
        int result = defaultValue;
        try {
            result = Integer.parseInt(str);
        } catch (NumberFormatException e) {
            LOGGER.warn(
                "Invalid value for int property: " + property + "=" + str + "(using default: "
                    + defaultValue + ").");
            // use the default value
        }
        return result;
    }

    /**
     * Returns the property as a bool with a given default value.
     * If the property is set to an empty String, "true" is
     * returned. This is to reflect the fact that it is true that
     * the property is set. If the property is set to anything
     * else than the empty string or "true", returns false.
     *
     * @param props      the <code>Properties</code> object where to
     *                   look up the given <code>property</code>
     *
     * @param property   the name of the property to convert to
     *                   boolean
     *
     * @param def        the default value
     *
     * @return   the boolean value of the property, false if not
     *           set.
     **/
    public static boolean getBoolProperty(Properties props, String property, boolean def) {
        String value = props.getProperty(property);
        if (value == null) {
            return def;
        } else if (value.trim().equals("")) {
            return true;
        } else {
            return Boolean.valueOf(value).booleanValue();
        }
    }

    /**
     * Returns the property as a bool with a default of "false".
     * If the property is set to an empty String, "true" is
     * returned. This is to reflect the fact that it is true that
     * the property is set. If the property is set to anything
     * else than the empty string or "true", returns false.
     *
     * @param props      the <code>Properties</code> object where to
     *                   look up the given <code>property</code>
     *
     * @param property   the name of the property to convert to
     *                   boolean
     *
     * @return   the boolean value of the property, false if not
     *           set.
     **/
    public static boolean getBoolProperty(Properties props, String property) {
        return getBoolProperty(props, property, false);
    }

    /**
     * Returns a property as a KeyStroke. A KeyStroke is a list of modifiers and a key pressed
     * simultaneously. The property has to have the format "modifiers+key" where modifiers are separated by +.
     * If the format is incorrect the fallback will be used. If no fallback is specified, an undefined key and no modifiers
     * will be the keystroke.
     * @param props The properties object to read the hotkey from
     * @param propertyName The name of the property to read the hotkey from
     * @param fallback The KeyStroke fallback
     * @return The hotkey that was set in the properties or a fallback
     */
    public static KeyStroke getHotkeyProperty(
        Properties props, String propertyName, KeyStroke fallback)
    {
        String value = props.getProperty(propertyName);

        if (fallback == null) {
            fallback = KeyStroke.getKeyStroke(KeyEvent.CHAR_UNDEFINED, 0);
        }

        if (value == null) {
            return fallback;
        }

        String[] comps = value.split("[+]");
        String key = comps[comps.length - 1].trim();

        int keyCode = KeyEvent.getExtendedKeyCodeForChar(key.charAt(0));

        if (key.length() > 1) {
            if (key.toLowerCase().equals("delete")) {
                keyCode = KeyEvent.VK_DELETE;
            } else {
                LOGGER.error("The keybinding property " + propertyName + " contains no valid key.");
                return fallback;
            }
        }

        int modifiers = 0;
        for (int i = 0; i < comps.length - 1; i++) {
            String s = comps[i];
            switch (s) {
                case "shift":
                    modifiers = modifiers | InputEvent.SHIFT_DOWN_MASK;
                    break;
                case "ctrl":
                    modifiers = modifiers | InputEvent.CTRL_DOWN_MASK;
                    break;
                case "meta":
                    modifiers = modifiers | InputEvent.META_DOWN_MASK;
                    break;
                case "alt":
                    modifiers = modifiers | InputEvent.ALT_DOWN_MASK;
                    break;
                case "alt graph":
                    modifiers = modifiers | InputEvent.ALT_GRAPH_DOWN_MASK;
                    break;
                case "primary key":
                    modifiers = modifiers | Toolkit.getDefaultToolkit().getMenuShortcutKeyMaskEx();
                    break;
                default:
                    LOGGER.error(
                        "The keybinding property " + propertyName + " contains an unknown modifier "
                            + s + ".");
                    return fallback;
            }
        }

        return KeyStroke.getKeyStroke(keyCode, modifiers);
    }


    /**
     * Converts a String into a list, items separated by the given StringTokenizer
     *
     * @param list a String
     * @param tok a StringTokenizer
     * @param trim true if each token should be trimmed
     * @return Collection containing the tokens
     */
    public static Collection<String> parseListString(
        String list, StringTokenizer tok, boolean trim)
    {
        Collection<String> result = new Vector<String>(tok.countTokens());
        try {
            while (tok.hasMoreTokens()) {
                String currentToken = tok.nextToken();
                if (trim) {
                    currentToken = currentToken.trim();
                }
                result.add(currentToken);
            }
        } catch (ArrayIndexOutOfBoundsException e) {
            LOGGER.error("PluginLoader: " + e + " when parsing " + list + " as list!");
        }
        return result;
    }

    /**
     * Converts a String into a list, items separated by commas, trims each token
     * @param list List object to be converted into a comma seperated string
     * @return String representation of given list
     */
    public static Collection<String> parseListString(String list) {
        return parseListString(list, new StringTokenizer(list, ","), true);
    }

    /**
     * Converts a String containing paths seperated with the system's path separator
     * (":" on Unixes, ";" on Windows) into a collection containing the separate
     * paths.
     * @param list the list containing the paths to be returned as Collection
     * @return Collection containing the paths
     */
    public static Collection<String> parsePathListString(String list) {
        return parseListString(list, new StringTokenizer(list, File.pathSeparator), false);
    }

    /**
     * Returns a List containing Strings from the given properties object.
     * These are retrieved by getting properties [propName]_[index]
     * (with index starting from 0).
     * @param props Property Object to be turned into a String
     * @param propName Name of the property to be retrieved as String
     * @return String representation of specified properties
     */
    public static List<String> getListProperty(Properties props, String propName) {
        boolean goon = true;
        List<String> result = new Vector<String>();
        int i = 0;
        while (goon) {
            String currentProp = props.getProperty(propName + "_" + i);
            if (currentProp == null) {
                goon = false;
                break;
            } else {
                result.add(currentProp);
                i++;
            }
        }
        return result;
    }

    /**
     * Returns a <code>Class</code> object from the given
     * properties object, looking up the given property name.
     * The class given by the property must be a subclass of
     * <code>typeRequest</code>.
     * Returns <code>def</code> if the property is not set.
     * Also returns <code>def</code> if the property is set to an
     * invalid value (e.g. unknown class, incorrect type).
     *
     * @param props Property Object to be returned as class object
     * @param propName Name of the desired  property
     * @param typeRequest Type of desired Class
     * @param def Default return value if the property is not set or an error occurs
     * @return Class representation of the given property of specified type
     */
    public static Class<?> getClassProperty(
        Properties props, String propName, Class<?> typeRequest, Class<?> def)
    {
        Class<?> result = null;
        String className = props.getProperty(propName);
        if (className != null) {
            try {
                result = Class
                    .forName(className, true, PluginManager.getInstance().getBottomClassLoader());
                if (!typeRequest.isAssignableFrom(result)) {
                    LOGGER.error(
                        "Property " + propName + " is invalid, ignoring: " + result.getName()
                            + " is not a subtype of " + typeRequest.getName() + ".");
                    result = null;
                }
            } catch (ClassNotFoundException e) {
                LOGGER.error(
                    "Property " + propName + " is invalid, ignoring: Class " + className
                        + " not found.");
                result = null;
            }
        }

        if (result == null) {
            result = def;
        }
        return result;
    }

    /**
     * Returns a <code>Class</code> object from the given
     * properties object, looking up the given property name.
     * Behaves in the same way as
     * {@link #getClassProperty(Properties,String,Class,Class) getClassProperty(props, propName, typeRequest, null)},
     * i.e. uses <code>null</code> as default return value.
     *
     * @param props Property Object to be returned as class object
     * @param propName Name of the desired  property
     * @param typeRequest Type of desired Class
     * @return Class representation of the given property of specified type
     */
    public static Class<?> getClassProperty(
        Properties props, String propName, Class<?> typeRequest)
    {
        return getClassProperty(props, propName, typeRequest, null);
    }
}
