/*
 *  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 openbook.util;

import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;

/**
 * A set of static utility functions to read properties from file, manage properties with multi-part keys,
 * array style properties etc.
 *
 * @author Pinaki Poddar
 *
 */
public class PropertyHelper {
    /**
     * Filter the properties by the given name.
     *
     * @param name a part of the key
     * @param prefix if true, property key must begin with the given name. Otherwise, the key merely contains the
     * name to qualify.
     *
     * @return key-value pairs that match.
     */
    public static Map<String,Object> filter(Map<String,Object> props, String name, boolean prefix,
            boolean includeArrays) {
        Map<String, Object> result = new HashMap<>();
        for (String key : props.keySet()) {
            if (key == null)
                continue;
            boolean match = prefix ? key.startsWith(name) : (key.indexOf(name) != -1);
            if (match && !isArray(key)) {
                result.put(key, props.get(key));
            }
        }
        if (includeArrays) {
            Map<String,List<Object>> arrayProperties = filterArrayKeys(props, name, prefix);
            result.putAll(arrayProperties);
        }
        return result;
    }

    /**
     * Select only those property keys which ends with an array marker such as <code>openjpa.DataCache[1]</code>.
     * The multiple values of the property is inserted into the resultant map as a List of object against the
     * original key.
     * <br>
     * For example, if the original map had three key-value pairs as
     * <LI><code>openjpa.DataCache[1]=true</code>
     * <LI><code>openjpa.DataCache[2]=false</code>
     * <LI><code>openjpa.DataCache[3]=default</code>
     * <br>
     * Then that will result into a single entry in the resultant Map under the key <code>openjpa.DataCache</code>
     * with a value as a List of three elements namely <code>{true, false, default}</code>. The array index values
     * are not significant other than they must all be different for the same base key.
     *
     * @param name part of the property key
     * @param prefix does the name must appear as a prefix?
     *
     * @return key-value pairs that match.
     */
    public static Map<String,List<Object>> filterArrayKeys(Map<String,Object> props, String name, boolean prefix) {
        Map<String, List<Object>> result = new HashMap<>();
        for (String key : props.keySet()) {
            boolean match = prefix ? key.startsWith(name) : (key.indexOf(name) != -1);
            if (match && isArray(key)) {
                String realKey = removeArray(key);
                List<Object> values = result.get(realKey);
                if (values == null) {
                    values = new ArrayList<>();
                    result.put(realKey, values);
                }
                values.add(props.get(key));
            }
        }
        return result;
    }


    /**
     * Load properties from the given name resource.
     * The given named resource is first looked up as resource on the current thread's context
     * and if not found as a file input.
     *
     * @param resource name a of resource.
     *
     * @return empty properties if no resource found.
     */
    public static Map<String,Object> load(String resource) {
        Properties p = new Properties();
        try {
            InputStream stream = Thread.currentThread().getContextClassLoader().getResourceAsStream(resource);
            if (stream == null) {
                stream = new FileInputStream(resource);
            }
            p.load(stream);
        } catch (Exception e) {
            System.err.println("Error reading " + resource + " due to " + e);
        }
        return toMap(p);
    }

    /**
     * Affirm if the given resource is available either as a resource in the current thread's context classpath
     * or as a file.
     *
     */
    public static boolean canFind(String resource) {
        if (resource == null)
            return false;
        InputStream stream = Thread.currentThread().getContextClassLoader().getResourceAsStream(resource);
        if (stream != null)
            return true;
        return new File(resource).exists();
    }

    public static Map<String, Object> toMap(Properties p) {
        Map<String, Object> result = new HashMap<>();
        for (Object k : p.keySet()) {
            result.put(k.toString(), p.get(k));
        }
        return result;
    }

    /**
     * Overwrites any key-value pair in the given map for which a System property is available
     * @param original properties to be overwritten
     * @return the original property overwritten with System properties
     */
    public static Map<String, Object> overwriteWithSystemProperties(Map<String, Object> original) {
        Properties properties = System.getProperties();
        for (Object syskey : properties.keySet()) {
            if (original.containsKey(syskey)) {
                original.put(syskey.toString(), properties.get(syskey));
            }
        }
        return original;
    }


    public static int getInteger(Map<String,Object> props, String key, int def) {
        int result = def;
        try {
            Object value = props.get(key);
            if (value != null)
                result = Integer.parseInt(value.toString());
        } catch (NumberFormatException nfe) {

        }
        return result;
    }

    public static double getDouble(Map<String,Object> props, String key, double def) {
        double result = def;
        try {
            Object value = props.get(key);
            if (value != null)
                result = Double.parseDouble(value.toString());
        } catch (NumberFormatException nfe) {

        }
        return result;
    }

    public static String getString(Map<String,Object> props, String key, String def) {
        Object value = props.get(key);
        if (value != null)
            return value.toString();
        return def;
    }

    public static List<String> getStringList(Map<String,Object> props, String key) {
        return getStringList(props, key, Collections.EMPTY_LIST);
    }

    public static List<String> getStringList(Map<String,Object> props, String key, List<String> def) {
        Object value = props.get(key);
        if (value != null)
            return Arrays.asList(value.toString().split("\\,"));
        return def;
    }

    public static Map<String,String> getMap(Map<String,Object> props, String key) {
        return getMap(props, key, Collections.EMPTY_MAP);
    }

    public static Map<String,String> getMap(Map<String,Object> props, String key, Map<String,String> def) {
        List<String> pairs = getStringList(props, key);
        if (pairs == null || pairs.isEmpty())
            return def;
        Map<String,String> result = new LinkedHashMap<>();
        for (String pair : pairs) {
            int index = pair.indexOf("->");
            if (index != -1) {
                String name  = pair.substring(0, index).trim();
                String value = pair.substring(index + 2).trim();
                result.put(name, value);
            }
        }
        return result;
    }

    /**
     * Affirms if the given string using array [] symbol at the end.
     *
     * @param key a string to check for array symbol.
     */
    private static boolean isArray(String key) {
        if (key == null || key.length() < 3 || !key.endsWith("]"))
            return false;
        int i = key.indexOf("[");
        if (i == -1 || i != key.lastIndexOf("["))
            return false;
        String index = key.substring(i+1,key.length()-1);
        try {
            Integer.parseInt(index);
        } catch (NumberFormatException e) {
            System.err.println("Bad index " + index + " in " + key);
            return false;
        }
        return true;
    }

    private static String removeArray(String key) {
        int i = key.indexOf("[");
        return key.substring(0,i);
    }

    public static Set<String> getSubsectionKeys(Set<String> keys, String section) {
        String prefix = asPrefix(section);
        Set<String> subsections = new HashSet<>();
        for (String key : keys) {
            if (key.startsWith(prefix)) {
                subsections.add(prefix + getPrefix(key.substring(prefix.length())));
            }
        }
        return subsections;
    }

    private static final String DOT = ".";
    private static String asPrefix(String s) {
        if (s.endsWith(DOT)) return s;
        return s + DOT;
    }
    public static String getPrefix(String s) {
        int i = s.indexOf(DOT);
        return (i == -1) ? s : s.substring(0, i);
    }

    /**
     * Get the portion of the given map whose key has the given section at prefix.
     *
     * @param props a set of name-value pair
     * @param section a string representing section of a key
     *
     * @return a new map with only the keys that starts with the given section.
     */
    public static Map<String,Object> getSection(Map<String,Object> props, String section) {
        return getSection(props, section, false);
    }

    /**
     * Get the portion of the given map whose key has the given section at prefix.
     *
     * @param props a set of name-value pair
     * @param section a string representing section of a key
     * @param retain if true the key of resultant map is same as the original map. Otherwise
     * the resultant map keys are without the section prefix.
     *
     * @return the map with only the keys that starts with the given section.
     */
    public static Map<String,Object> getSection(Map<String,Object> props, String section, boolean retain) {
        Map<String,Object> result = new HashMap<>(props);
        Set<String> keys = props.keySet();
        String prefix = asPrefix(section);
        for (String key : keys) {
            if (key.startsWith(prefix)) {
                String newKey = retain ? key : key.substring(prefix.length());
                result.put(newKey, props.get(key));
            }
        }
        return result;
    }
}
