/*
 * Copyright 2008 Ayman Al-Sairafi ayman.alsairafi@gmail.com
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License
 *       at http://www.apache.org/licenses/LICENSE-2.0
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package jsyntaxpane.util;

import java.awt.Color;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Wrapper around the Properties class with supprt for Heirarchical confogurations
 * and more functionality.
 *
 * Except for the getXXXX methods, all other Map Interface methods operate on the
 * current (non-parent) collection and do NOT touch the parent.
 * 
 * @author Ayman Al-Sairafi
 */
public class Configuration implements Map<String, String> {

    /**
     * Our parent
     */
    Configuration parent;
    /**
     * Our Class for the configuration
     */
    Class clazz;
    /**
     * The properties we have, excluding the parents defaults
     */
    Map<String, String> props;

    /**
     * Creates a new COnfiguration that uses parent as its parent
     * Configuration.
     *
     * @param theClass
     * @param parent
     */
    public Configuration(Class theClass, Configuration parent) {
        this(theClass);
        this.parent = parent;
    }

    /**
     * Creates an empty Configuration
     * @param theClass
     */
    public Configuration(Class theClass) {
        super();
        this.clazz = theClass;
    }

    /**
     * Get a string from this object or one of its parents.  If nothing
     * is found, null is returned.
     * If the Regex ${key} is found, then it is replaced by the value of that
     * key within this (or parent's) map.
     * Special COnstructs in ${}:
     * <li><code>class_path</code> will be replaced by the name of the
     * Configuration (usually ClassName) with "." replaced by "/", and then
     * converted to all lowercase</li>
     * <li><code>class_simpleName</code></li> is replaced by class.SimpleName
     * @param key
     * @return
     */
    public String getString(String key) {
        String value = null;
        if (props != null) {
            value = props.get(key);
        }
        for (Configuration p = parent; value == null && p != null; p = p.parent) {
            value = p.get(key);
        }
        // if we have a parent, then perform ${} replacements
        if (value != null) {
            Matcher m = PARENT_KEY.matcher(value);
            StringBuffer sb = new StringBuffer();
            while (m.find()) {
                String p_key = m.group(1);
                String p_value = getString(p_key);
                if (p_key.equals("class_path")) {
                    p_value = clazz.getName().replace(".", "/").toLowerCase();
                } else if (p_key.equals("class_simpleName")) {
                    p_value = clazz.getSimpleName();
                } else {
                    p_value = getString(p_key);
                    if (p_value == null) {
                        Logger.getLogger(this.getClass().getName()).warning(
                                "no value for ${" + p_key +
                                "} is defined");
                    }
                }
                m.appendReplacement(sb, p_value);
            }
            m.appendTail(sb);
            value = sb.toString();
        }
        return value;
    }

    /**
     * Returns a non-null value either by traversing the current
     * and parent(s) map, or returning the defaultValue
     * @param key
     * @param defaultValue
     * @throws NullPointerException if defaultValue is null
     * @return
     */
    public String getString(String key, String defaultValue) {
        if (defaultValue == null) {
            throw new NullPointerException("defaultValue cannot be null");
        }
        String value = getString(key);
        return (value == null) ? defaultValue : value;
    }

    /**
     * Gets an integer from the properties.  If number cannot be found
     * or if it cannot be decoded, the default is returned
     * The integer is decoded using {@link Integer.decode(String)}
     * @param key
     * @param Default
     * @return
     */
    public int getInteger(String key, int Default) {
        String v = getString(key);
        if (v == null) {
            return Default;
        }
        try {
            int i = Integer.decode(v);
            return i;
        } catch (NumberFormatException e) {
            LOG.log(Level.WARNING, null, e);
            return Default;
        }
    }

    /**
     * Returns a String[] of the comma separated items in the value.
     * 
     * Does NOT return null.  If the key is not found,
     * then an empty string array is returned.  So the return of this method
     * can always be used directly in a foreach loop
     * @param key
     * @return non-null String[] 
     */
    public String[] getPropertyList(String key) {
        String v = getString(key);
        if (v == null) {
            return EMPTY_LIST;
        } else {
            return COMMA_SEPARATOR.split(v);
        }
    }

    /**
     * Returns a boolean from the configuration
     * @param key
     * @param Default
     * @return
     */
    public boolean getBoolean(String key, boolean Default) {
        String b = getString(key);
        if (b == null) {
            return Default;
        }
        return Boolean.parseBoolean(b.trim());
    }

    /**
     * return the Color that has the given key or the Default
     * @param key
     * @param Default
     * @return
     */
    public Color getColor(String key, Color Default) {
        String c = getString(key);
        if (c == null) {
            return Default;
        } else {
            try {
                return Color.decode(c);
            } catch (NumberFormatException e) {
                return Default;
            }
        }
    }

    @SuppressWarnings("unchecked")
    @Override
    public void putAll(Map config) {
        if (props == null) {
            props = new HashMap<String, String>();
        }
        props.putAll(config);
    }

    /**
     * Returns ALL property names from this Configuration's parents and
     * this Configuration.  As usual, parents are added first so they
     * are overriden by children.
     *
     * @return Set of all String keys in this and parents
     */
    public Set<String> stringPropertyNames() {
        Set<String> propNames = new HashSet<String>();
        if (parent != null) {
            propNames.addAll(parent.stringPropertyNames());
        }
        if (props != null) {
            for (Object k : props.keySet()) {
                propNames.add(k.toString());
            }
        }
        return propNames;
    }

    @Override
    public String put(String key, String value) {
        if (props == null) {
            props = new HashMap<String, String>();
        }
        Object old = props.put(key, value);
        return (old == null) ? null : old.toString();
    }

    @Override
    public int size() {
        return (props == null) ? 0 : props.size();
    }

    @Override
    public boolean isEmpty() {
        return (props == null) ? true : props.isEmpty();
    }

    @Override
    public boolean containsKey(Object key) {
        return (props == null) ? false : props.containsKey(key);
    }

    @Override
    public boolean containsValue(Object value) {
        return (props == null) ? false : props.containsValue(value);
    }

    @Override
    public String get(Object key) {
        return (props == null) ? null : props.get(key);
    }

    @Override
    public String remove(Object key) {
        if (props == null) {
            return null;
        }
        Object old = props.remove(key);
        return (old == null) ? null : old.toString();
    }

    @Override
    public void clear() {
        if (props != null) {
            props.clear();
        }
    }

    @Override
    @SuppressWarnings("unchecked")
    public Set<String> keySet() {
        if (props == null) {
            return Collections.EMPTY_SET;
        } else {
            return props.keySet();
        }
    }

    @Override
    @SuppressWarnings("unchecked")
    public Collection<String> values() {
        if (props == null) {
            return Collections.EMPTY_SET;
        } else {
            return props.values();
        }
    }

    @Override
    @SuppressWarnings("unchecked")
    public Set<Entry<String, String>> entrySet() {
        if (props == null) {
            return Collections.EMPTY_SET;
        } else {
            return props.entrySet();
        }
    }

    @Override
    public String toString() {
        return "Configuration " + clazz + " for " + parent;
    }

    /**
     * Utility class to hold data for {@link getKeys} method.
     */
    public static class StringKeyMatcher {

        private StringKeyMatcher(String key, Matcher matcher, String group1, String value) {
            this.key = key;
            this.matcher = matcher;
            this.group1 = group1;
            this.value = value;
        }
        /**
         * The full key matched
         */
        public final String key;
        /**
         * matcher instance for the key
         */
        public final Matcher matcher;
        /**
         * Matched group 1. Could be null if no Group 1 is found
         */
        public final String group1;
        /**
         * Value for key matched
         */
        public final String value;
    }

    /**
     * Obtain a set of all keys (and parent's keys) that match the given pattern.
     * If no keys match, then an empty set is returned.
     * Use this instead of the {@link stringPropertyNames}
     * @param pattern
     * @return
     */
    public Set<StringKeyMatcher> getKeys(Pattern pattern) {
        Set<StringKeyMatcher> matched = new HashSet<StringKeyMatcher>();
        Set<String> all = stringPropertyNames();
        for (String k : all) {
            Matcher m = pattern.matcher(k);
            if (m.matches()) {
                StringKeyMatcher skm = new StringKeyMatcher(k, m,
                        (m.groupCount() >= 1) ? m.group(1) : null,
                        getString(k));
                matched.add(skm);
            }
        }
        return matched;
    }
    public static final String[] EMPTY_LIST = new String[0];
    public static final Pattern COMMA_SEPARATOR = Pattern.compile("\\s*,\\s*");
    private static Pattern PARENT_KEY = Pattern.compile("\\$\\{(\\w+)\\}");
    private static final Logger LOG = Logger.getLogger(Configuration.class.getName());
}
