/*
 * PropertiesHelper.java  $Revision: 1.8 $  $Date: 2007/10/03 04:39:39 $
 *
 * Copyright (C) 2007 Meldcraft Corporation
 *
 * This file is part of Scaffold.
 * 
 * Scaffold is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * Scaffold 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 Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public License
 * along with Scaffold.  If not, see <http://www.gnu.org/licenses/>.
 */

package com.meldcraft.util;

import java.io.*;
import java.lang.reflect.InvocationTargetException;
import java.net.URL;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * @version $Revision: 1.8 $ $Date: 2007/10/03 04:39:39 $
 * @author Rob Richards
 */
public class PropertiesHelper
{
// Static variables
   public static final String   ORDERED_KEYS = "PROPERTIES_HELPER_ORDERED_KEYS";

// Member variables
// Constructors and initialization
    // prevent instantiation
    PropertiesHelper()
    {
    }

// Interface implemenations
// Public methods
    /**
     * Returns the value of key as an Object of type; returns null if the
     * value for key is null or is not of type or cannot be converted to
     * to via a single arg constructor.
     */
    public static Object getObject(Map properties, String key, Class type)
    {
        return getObject(properties, key, type, null);
    }

    /**
     * Returns the value of key as an Object of type; returns defaultValue if
     * the value for key is null or is not of type or cannot be converted to
     * to via a single arg constructor.
     */
    public static Object getObject(Map properties, String key,
                                   Class type, Object defaultValue)
    {
        Object val = properties.get(key);

        if (val == null)
        {
            val = defaultValue;
        }
        else if (!(type.isAssignableFrom(val.getClass())))
        {
            try
            {
                val = ReflectUtil.createInstance(type, val.getClass(), val);
            }
            catch (Exception e)
            {
                boolean success = false;
                try
                {
                    val = ReflectUtil.convert(val, type);
                    success = true;
                }
                catch (SecurityException e1)
                {
                }
                catch (IllegalArgumentException e1)
                {
                }
                catch (NoSuchMethodException e1)
                {
                }
                catch (InstantiationException e1)
                {
                }
                catch (IllegalAccessException e1)
                {
                }
                catch (InvocationTargetException e1)
                {
                }

                if (!success)
                {
                    val = defaultValue;
                }
            }
        }

        return val;
    }

    /**
     * Returns the value of key as a String; returns empty string ("") if
     * the value is not defined.
     */
    public static String getString(Map properties, String key)
    {
        return getString(properties, key, "");
    }

    /**
     * Returns the value of key as a String; returns defaultValue if the
     * value is not defined.
     */
    public static String getString(Map properties,
                                   String key, String defaultValue)
    {
        return getString(properties.get(key), defaultValue);
    }

    /**
     * Returns the value of key as a boolean; returns false if the value
     * is not defined or is not parsable as a boolean. The following
     * strings are accepted as boolean true (case insensitive):
     * "true", "t", "yes", "y".  Numbers or strings that are parsable as
     * numbers are accepted as boolean true if they are not equal to zero.
     */
    public static boolean getBoolean(Map properties, String key)
    {
        return getBoolean(properties, key, false);
    }

    /**
     * Returns the value of key as a boolean; returns defaultValue if the
     * value is not defined or is not parsable as a boolean. The following
     * strings are accepted as boolean true (case insensitive):
     * "true", "t", "yes", "y".  Numbers or strings that are parsable as
     * numbers are accepted as boolean true if they are not equal to zero.
     */
    public static boolean getBoolean(Map properties,
                                     String key, boolean defaultValue)
    {
        return getBoolean(properties.get(key), defaultValue);
    }

    /**
     * Returns the value of key as a int; returns 0.0 if the value
     * is not defined or is not parsable as a int.
     */
    public static int getInt(Map properties, String key)
    {
        return getInt(properties, key, 0);
    }

    /**
     * Returns the value of key as a int; returns defaultValue if the
     * value is not defined or is not parsable as a int.
     */
    public static int getInt(Map properties,
                             String key, int defaultValue)
    {
        return getInt(properties.get(key), defaultValue);
    }

    /**
     * Returns the value of key as a long; returns 0.0 if the value
     * is not defined or is not parsable as a long.
     */
    public static long getLong(Map properties, String key)
    {
        return getLong(properties, key, 0);
    }

    /**
     * Returns the value of key as a long; returns defaultValue if the
     * value is not defined or is not parsable as a long.
     */
    public static long getLong(Map properties,
                               String key, long defaultValue)
    {
        return getLong(properties.get(key), defaultValue);
    }

    /**
     * Returns the value of key as a float; returns 0.0 if the value
     * is not defined or is not parsable as a float.
     */
    public static float getFloat(Map properties, String key)
    {
        return getFloat(properties, key, 0.0f);
    }

    /**
     * Returns the value of key as a float; returns defaultValue if the
     * value is not defined or is not parsable as a float.
     */
    public static float getFloat(Map properties,
                                 String key, float defaultValue)
    {
        return getFloat(properties.get(key), defaultValue);
    }

    /**
     * Returns the value of key as a double; returns 0.0 if the value
     * is not defined or is not parsable as a double.
     */
    public static double getDouble(Map properties, String key)
    {
        return getDouble(properties, key, 0.0);
    }

    /**
     * Returns the value of key as a double; returns defaultValue if the
     * value is not defined or is not parsable as a double.
     */
    public static double getDouble(Map properties,
                                   String key, double defaultValue)
    {
        return getDouble(properties.get(key), defaultValue);
    }

    public static String[] getStringArray(Map properties, String value)
    {
        return getStringArray(getString(properties, value));
    }

    public static String[] getStringArray(Map properties, String value, String[] defaultValues)
    {
        return getStringArray(getString(properties, value), defaultValues);
    }

    public static String[] getStringArray(Map properties, String value, String delimiter)
    {
        return getStringArray(getString(properties, value), delimiter);
    }

    public static String[] getStringArray(Map properties, String value, String delimiter, String[] defaultValues, boolean trimValues)
    {
        return getStringArray(getString(properties, value), delimiter, defaultValues, trimValues);
    }

    public static Map asImmutableMap(ResourceBundle bundle)
    {
        return Collections.unmodifiableMap(asMap(bundle));
    }

    public static Map asMap(ResourceBundle bundle)
    {
        HashMap map = new HashMap();
        Enumeration keys = bundle.getKeys();
        while (keys.hasMoreElements())
        {
            String key = (String)keys.nextElement();
            Object value = bundle.getObject(key);
            map.put(key, value);
        }
        return map;
    }

    /**
     * Returns a Map containing the properties in file.
     */
    public static Map load(File file) throws IOException
    {
        Map ret = null;

        try
        {
            ret = load(file.toURI().toURL());
        }
        catch (java.net.MalformedURLException e)
        {
            // should never happen
            throw new RuntimeException(e);
        }

        return ret;
    }

    /**
     * Returns a Map containing the properties in the resource defined by url.
     */
    public static Map load(URL url) throws IOException
    {
        return load(url.openStream());
    }

    /**
     * Returns a Map containing the properties in the input.
     */
    public static Map load(InputStream input) throws IOException
    {
        Map ret = null;

        ret =  asMap(new PropertyResourceBundle(new BufferedInputStream(input)));

        return ret;
    }

    /**
     * Returns properties in String form suitable for storing as
     * java.util.Properties format.
     */
    public static String asPropertiesFileFormat(Map properties)
    {
        return asPropertiesFileFormat(properties, null);
    }

    /**
     * Returns properties in String form suitable for storing as
     * java.util.Properties format.  Each key is prefixed with prefix.
     */
    public static String asPropertiesFileFormat(Map properties,
                                                String prefix)
    {
        String ret = new String();

        ByteArrayOutputStream out = new ByteArrayOutputStream();
        try     
        {
            storeProps(out, properties, prefix);
            ret = out.toString(); 
            out.close();

            // remove time stamp
            ret = ret.replaceFirst("^#.*[\r\n]+", "");

            // force unix style end of line
            ret = ret.replaceAll("\r\n", "\n");
        }
        catch (IOException e)
        {
            throw new RuntimeException(e);
        }

        return ret;
    }

    /**
     * Writes properties to out in Properties file format.
     */
    public static void store(OutputStream out, Map properties)
        throws IOException
    {
        store(out, properties, null);
    }

    /**
     * Writes properties to out in Properties file format.  Each key in
     * properties is prefixed with prefix.
     */
    public static void store(OutputStream out, Map properties, String prefix)
        throws IOException
    {
        String str = asPropertiesFileFormat(properties, prefix);
        out.write(str.getBytes());
    }

    /**
     * Returns a Map containing all of the entries in properties with a key
     * that begins with prefix (all keys in properties are assumed be be
     * of type String).  The keys in the returned Map will have the prefix
     * removed from them.
     */
    public static Map getSubset(Map properties, String prefix)
    {
        return getSubset(properties, prefix, false);
    }

    /**
     * Returns a Map containing all of the entries in properties with a key
     * that begins with prefix (all keys in properties are assumed be be
     * of type String).  The keys in the returned Map will have the prefix
     * removed from them.  If remove is true, keys will be removed from
     * properties.
     */
    public static Map getSubset(Map properties, String prefix, boolean remove)
    {
        Map ret = new HashMap();

        String[] keys = (String[])properties.keySet().toArray(new String[0]);
        for (int i=0 ; i < keys.length ; i++)
        {
            String key = keys[i];
            if (key.startsWith(prefix) && (key.length() > prefix.length()))
            {
                String name = key.substring(prefix.length());
                ret.put(name, properties.get(key));

                if (remove)
                {
                    properties.remove(key);
                }
            }
        }

        return ret;
    }

    /**
     * Adds all entries in subset to properties, prefixing all of the subset keys
     * with prefix.
     * 
     * @param properties
     * @param subset
     * @param prefix
     */
    public static void addSubset(Map properties, Map subset, String prefix)
    {
        Map sub;
        if (prefix == null)
        {
            sub = subset;
        }
        else
        {
            sub = new HashMap();
            String[] keys = (String[])subset.keySet().toArray(new String[0]);
            for (int i=0 ; i < keys.length ; i++)
            {
                String key = keys[i];
                properties.put(prefix + key, subset.get(key));
            }
        }
        properties.putAll(sub);
    }

    public static String[] getEnumeratedStrings(Map properties, String prefix)
    {
        return getEnumeratedStrings(properties, prefix, 0);
    }

    public static String[] getEnumeratedStrings(Map properties, String prefix,
                                                int start)
    {
        return getEnumeratedStrings(properties, prefix, start, Integer.MAX_VALUE);
    }

    public static String[] getEnumeratedStrings(Map properties, String prefix,
                                                int start, int end)
    {
        return getEnumeratedStrings(properties, prefix, start, end, false);
    }

    public static String[] getEnumeratedStrings(Map properties, String prefix,
                                                int start, int end,
                                                boolean sparse)
    {
        return getEnumeratedStrings(properties, prefix, start, end, false, null);
    }

    public static String[] getEnumeratedStrings(Map properties, String prefix,
                                                int start, int end,
                                                boolean sparse,
                                                String sparseValue)
    {
        String[] ret = null;

        ArrayList list = new ArrayList();
        for (int i=start ; i < end ; i++)
        {
            String val = getString(properties.get(prefix+i), null);
            if (val == null)
            {
                if (sparse)
                {
                    list.add(sparseValue);
                }
                else
                {
                    break;
                }
            }
            else
            {
                list.add(val);
            }
        }

        if (list.size() > 0)
        {
            ret = (String[])list.toArray(new String[list.size()]);
        }

        return ret;
    }

    public static Map getMap(Map properties, String key)
    {
        Map ret = null;

        String value = getString(properties, key, null);
        if (value != null)
        {
            Pattern pat = Pattern.compile("[^,=]*=");
            Matcher match = pat.matcher(value);

            HashMap map = new HashMap();
            ArrayList list = new ArrayList();
            if(match.find())
            {
                int keyStart = match.start();
                int keyEnd = match.end();
                while (match.find())
                {
                    int nextStart = match.start();
                    int nextEnd = match.end();

                    String mkey = value.substring(keyStart, keyEnd-1).trim();
                    String mval = value.substring(keyEnd, nextStart-1).trim();
                    mkey = add(map, mkey, mval);
                    if (! list.contains(mkey))
                    {
                        list.add(mkey);
                    }

                    keyStart = nextStart;
                    keyEnd = nextEnd;
                }

                String mkey = value.substring(keyStart, keyEnd-1).trim();
                String mval = value.substring(keyEnd, value.length());
                mkey = add(map, mkey, mval);
                if (! list.contains(mkey))
                {
                    list.add(mkey);
                }
            }
            if (map.size() > 0)
            {
                map.put(ORDERED_KEYS, list.toArray(new String[0]));
                ret = map;
            }
        }

        return ret;
    }

    public static String getString(Object obj, String defaultValue)
    {
        String ret = defaultValue;

        if (obj instanceof String)
        {
            ret = (String)obj;
        }
        else if (obj != null)
        {
            ret = obj.toString();
        }

        return ret;
    }

    public static boolean getBoolean(Object obj, boolean defaultValue)
    {
        boolean ret = defaultValue;

        if (obj instanceof String)
        {
            String str = (String)obj;

            ret = str.equalsIgnoreCase("true") ||
                  str.equalsIgnoreCase("t") ||
                  str.equalsIgnoreCase("yes") ||
                  str.equalsIgnoreCase("y");

            if (!ret)
            {
                try
                {
                    ret = Double.valueOf(str).doubleValue() != 0;
                }
                catch (NumberFormatException e)
                {
                    // ignored
                }
            }
        }
        else if (obj instanceof Boolean)
        {
            ret = ((Boolean)obj).booleanValue();
        }
        else if (obj instanceof Number)
        {
            ret = (((Number)obj).doubleValue() != 0);
        }

        return ret;
    }

    public static int getInt(Object obj, int defaultValue)
    {
        int ret = defaultValue;

        if (obj instanceof String)
        {
            try
            {
                ret = Integer.valueOf((String)obj).intValue();
            }
            catch (NumberFormatException e)
            {
                // ignored
            }
        }
        else if (obj instanceof Number)
        {
            ret = ((Number)obj).intValue();
        }

        return ret;
    }

    public static long getLong(Object obj, long defaultValue)
    {
        long ret = defaultValue;

        if (obj instanceof String)
        {
            try
            {
                ret = Long.valueOf((String)obj).longValue();
            }
            catch (NumberFormatException e)
            {
                // ignored
            }
        }
        else if (obj instanceof Number)
        {
            ret = ((Number)obj).longValue();
        }

        return ret;
    }

    public static float getFloat(Object obj, float defaultValue)
    {
        float ret = defaultValue;

        if (obj instanceof String)
        {
            try
            {
                ret = Float.valueOf((String)obj).floatValue();
            }
            catch (NumberFormatException e)
            {
                // ignored
            }
        }
        else if (obj instanceof Number)
        {
            ret = ((Number)obj).floatValue();
        }

        return ret;
    }

    public static double getDouble(Object obj, double defaultValue)
    {
        double ret = defaultValue;

        if (obj instanceof String)
        {
            try
            {
                ret = Double.valueOf((String)obj).doubleValue();
            }
            catch (NumberFormatException e)
            {
                // ignored
            }
        }
        else if (obj instanceof Number)
        {
            ret = ((Number)obj).doubleValue();
        }

        return ret;
    }

    public static String[] getStringArray(String value)
    {
        return getStringArray(value, (String[])null);
    }

    public static String[] getStringArray(String value, String[] defaultValue)
    {
        return getStringArray(value, null, defaultValue, true);
    }

    public static String[] getStringArray(String value, String delimiter)
    {
        return getStringArray(value, delimiter, null, true);
    }

    public static String[] getStringArray(String value, String delimiter, String[] defaultValue, boolean trim)
    {
        String[] ret = defaultValue;

        if ((value != null) && (value.length() > 0))
        {
            if (delimiter == null)
            {
                delimiter = ",";
            }

            ArrayList list = new ArrayList();
            int start = 0;
            for (int i=0 ; i < value.length() ; i++)
            {
                int ch = value.codePointAt(i);
                if (delimiter.indexOf(ch) != -1)
                {
                    String str = value.substring(start,i);
                    start = i + 1;
                    if (trim)
                    {
                        str = str.trim();
                    }
                    list.add(str);
                }
            }
            String str = value.substring(start, value.length());
            if (trim)
            {
                str = str.trim();
            }
            list.add(str);
            ret = (String[])list.toArray(new String[list.size()]);
            /* this coallesces consequetive delimiters
            StringTokenizer tok = new StringTokenizer(value, delimiter);
            ret = new String[tok.countTokens()];
            for (int i=0 ; tok.hasMoreTokens() ; i++)
            {
                String str = tok.nextToken();
                if (trim)
                {
                    str = str.trim();
                }
                ret[i] = str;
            }
            */
        }

        return ret;
    }

    public static void insertStringArray(Map config, int index, String key, String[] values)
    {
        insertStringArray(config, index, key, values, null);
    }

    public static void insertStringArray(Map config, int index, String key, String[] values, String delimiter)
    {
        insertStringArray(config, index, key, values, null, true);
    }

    public static void insertStringArray(Map config, int index, String key, String[] values, String delimiter, boolean trim)
    {
        if (values != null)
        {
            String[] current = getStringArray(config, key, delimiter, null, trim);
            String[] newValues = values;
            if ((current != null) && (current.length > 0))
            {
                newValues = new String[values.length + current.length];
                if (index > current.length)
                {
                    index = current.length;
                }
                else if (index < 0)
                {
                    index = 0;
                }

                System.arraycopy(current,0, newValues,0,index);
                System.arraycopy(values,0, newValues,index,values.length);
                if (index < current.length)
                {
                    int idx = values.length + index;
                    int len = current.length - index;
                    System.arraycopy(current,index, newValues,idx,len);
                }
            }
            setStringArray(config, key, newValues, delimiter, trim);
        }
    }

    public static void setStringArray(Map config, String key, String[] values)
    {
        setStringArray(config, key, values, null);
    }

    public static void setStringArray(Map config, String key, String[] values, String delimiter)
    {
        setStringArray(config, key, values, null, true);
    }

    public static void setStringArray(Map config, String key, String[] values, String delimiter, boolean trim)
    {
        String ret = null;
        if ((values != null) && (values.length > 0))
        {
            StringBuffer buf = new StringBuffer(256);
            if (delimiter == null)
            {
                delimiter = ",";
            }

            for (int i=0 ; i < values.length ; i++)
            {
                String str = values[i];
                if (str == null)
                {
                    str = "";
                }

                if (trim)
                {
                    str = str.trim();
                }

                if (i > 0)
                {
                    buf.append(delimiter);
                }
                buf.append(str);
            }
            ret = buf.toString();
        }

        if (ret == null)
        {
            config.remove(key);
        }
        else
        {
            config.put(key, ret);
        }
    }

// Protected methods
    /**
     * Writes properties to out in Properties file format.  Each key in
     * properties is prefixed with prefix.
     */
    protected static void storeProps(OutputStream out, Map properties, String prefix)
        throws IOException
    {
        if (properties.size() == 0)
        {
            return;
        }

        Map pmap = properties;

        if ((prefix != null) && !prefix.equals(""))
        {
            pmap = new HashMap();
            String[] keys = (String[])properties.keySet().toArray(new String[0]);
            for (int i=0 ; i < keys.length ; i++)
            {
                String key = prefix + keys[i];
                pmap.put(key, properties.get(keys[i]));
            }
        }

        SortedProperties props = new SortedProperties();
        String[] keys = (String[])pmap.keySet().toArray(new String[0]);
        for (int i=0 ; i < keys.length ; i++)
        {
            props.put(keys[i], pmap.get(keys[i]));
        }
        props.store(out, null);
    }

    static String add(Map map, String key, String value)
    {
        int len = key.length();
        if ((len > 1) && (key.charAt(len-1) == '+'))
        {
            int endidx = len - 2;
            for (int i=endidx ; i >= 0 ; i--)
            {
                if (Character.isWhitespace(key.charAt(i)))
                {
                    endidx--;
                }
                else
                {
                    break;
                }
            }

            if (endidx >= 0)
            {
                key = key.substring(0, endidx+1);
                String str = (String)map.get(key);
                if (str != null)
                {
                    if (value == null)
                    {
                        value = str;
                    }
                    else
                    {
                        value = str + value;
                    }
                }
            }
        }

        map.put(key, value);
        return key;
    }

// Private methods
// Inner classes
    public static class SortedProperties extends Properties
    {
        public Enumeration keys()
        {
            Enumeration keys = super.keys();
            Vector vec = new Vector();
            while (keys.hasMoreElements())
            {
                Object key = keys.nextElement();
                vec.addElement(key);
            }
            Collections.sort(vec);
            return vec.elements();
        }
    }
}
