// License: GPL. For details, see LICENSE file.
package org.openstreetmap.josm.spi.preferences;

import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.TreeMap;
import java.util.stream.Collectors;

import org.openstreetmap.josm.io.DefaultProxySelector;
import org.openstreetmap.josm.io.auth.CredentialsAgent;
import org.openstreetmap.josm.io.auth.CredentialsManager;
import org.openstreetmap.josm.tools.Logging;
import org.openstreetmap.josm.tools.Utils;

/**
 * Abstract implementation of the {@link IPreferences} interface.
 * @since 12847
 */
public abstract class AbstractPreferences implements IPreferences {
    /** The preference key for sensitive keys */
    private static final String KEY_SENSITIVE_KEYS = "sensitive.keys";

    /** A set of sensitive keys that should not be seen/distributed outside of specific callers (like a {@link CredentialsAgent}) */
    private static final Set<String> SENSITIVE_KEYS = new HashSet<>();

    @Override
    public synchronized String get(final String key, final String def) {
        return getSetting(key, new StringSetting(def), StringSetting.class).getValue();
    }

    @Override
    public boolean put(final String key, String value) {
        return putSetting(key, Utils.isEmpty(value) ? null : new StringSetting(value));
    }

    @Override
    public boolean getBoolean(final String key, final boolean def) {
        return Boolean.parseBoolean(get(key, Boolean.toString(def)));
    }

    @Override
    public boolean putBoolean(final String key, final boolean value) {
        return put(key, Boolean.toString(value));
    }

    @Override
    public synchronized int getInt(String key, int def) {
        String v = get(key, Integer.toString(def));
        if (v.isEmpty())
            return def;

        try {
            return Integer.parseInt(v);
        } catch (NumberFormatException e) {
            // fall out
            Logging.trace(e);
        }
        return def;
    }

    @Override
    public boolean putInt(String key, int value) {
        return put(key, Integer.toString(value));
    }

    @Override
    public long getLong(String key, long def) {
        String v = get(key, Long.toString(def));
        if (null == v)
            return def;

        try {
            return Long.parseLong(v);
        } catch (NumberFormatException e) {
            // fall out
            Logging.trace(e);
        }
        return def;
    }

    @Override
    public boolean putLong(final String key, final long value) {
        return put(key, Long.toString(value));
    }

    @Override
    public synchronized double getDouble(String key, double def) {
        String v = get(key, Double.toString(def));
        if (null == v)
            return def;

        try {
            return Double.parseDouble(v);
        } catch (NumberFormatException e) {
            // fall out
            Logging.trace(e);
        }
        return def;
    }

    @Override
    public boolean putDouble(final String key, final double value) {
        return put(key, Double.toString(value));
    }

    @Override
    public List<String> getList(String key, List<String> def) {
        return getSetting(key, new ListSetting(def), ListSetting.class).getValue();
    }

    @Override
    public boolean putList(String key, List<String> value) {
        return putSetting(key, value == null ? null : new ListSetting(value));
    }

    @Override
    public List<List<String>> getListOfLists(String key, List<List<String>> def) {
        return getSetting(key, new ListListSetting(def), ListListSetting.class).getValue();
    }

    @Override
    public boolean putListOfLists(String key, List<List<String>> value) {
        return putSetting(key, value == null ? null : new ListListSetting(value));
    }

    @Override
    public List<Map<String, String>> getListOfMaps(String key, List<Map<String, String>> def) {
        return getSetting(key, new MapListSetting(def), MapListSetting.class).getValue();
    }

    @Override
    public boolean putListOfMaps(String key, List<Map<String, String>> value) {
        return putSetting(key, value == null ? null : new MapListSetting(value));
    }

    /**
     * Gets a map of all settings that are currently stored
     * @return The settings
     */
    public abstract Map<String, Setting<?>> getAllSettings();

    /**
     * Set a value for a certain setting. The changed setting is saved to the preference file immediately.
     * Due to caching mechanisms on modern operating systems and hardware, this shouldn't be a performance problem.
     * @param key the unique identifier for the setting
     * @param setting the value of the setting. In case it is null, the key-value entry will be removed.
     * @return {@code true}, if something has changed (i.e. value is different than before)
     */
    public abstract boolean putSetting(String key, Setting<?> setting);

    /**
     * Get settings value for a certain key and provide default a value.
     * @param <T> the setting type
     * @param key the identifier for the setting
     * @param def the default value. For each call of getSetting() with a given key, the default value must be the same.
     * <code>def</code> must not be null, but the value of <code>def</code> can be null.
     * @param klass the setting type (same as T)
     * @return the corresponding value if the property has been set before, {@code def} otherwise
     */
    public abstract <T extends Setting<?>> T getSetting(String key, T def, Class<T> klass);

    /**
     * Gets all normal (string) settings that have a key starting with the prefix
     * @param prefix The start of the key
     * @return The key names of the settings
     */
    public Map<String, String> getAllPrefix(String prefix) {
        return getAllSettings().entrySet().stream()
                .filter(e -> e.getKey().startsWith(prefix) && (e.getValue() instanceof StringSetting))
                .collect(Collectors.toMap(Entry::getKey, e -> ((StringSetting) e.getValue()).getValue(), (a, b) -> b, TreeMap::new));
    }

    /**
     * Gets all list settings that have a key starting with the prefix
     * @param prefix The start of the key
     * @return The key names of the list settings
     */
    public List<String> getAllPrefixCollectionKeys(String prefix) {
        return getAllSettings().entrySet().stream()
                .filter(entry -> entry.getKey().startsWith(prefix) && entry.getValue() instanceof ListSetting)
                .map(Entry::getKey)
                .collect(Collectors.toCollection(LinkedList::new));
    }

    @Override
    public void addSensitive(CredentialsAgent caller, String key) {
        if (SENSITIVE_KEYS.isEmpty()) {
            populateSensitiveKeys();
        }
        if (CredentialsManager.getInstance().getCredentialsAgentClass().equals(caller.getClass())) {
            SENSITIVE_KEYS.add(key);
            putList("sensitive.keys", SENSITIVE_KEYS.stream().sorted().collect(Collectors.toList()));
        }
    }

    @Override
    public Collection<String> getSensitive() {
        if (SENSITIVE_KEYS.isEmpty()) {
            populateSensitiveKeys();
        }
        return Collections.unmodifiableSet(SENSITIVE_KEYS);
    }

    @Override
    public void removeSensitive(String key) {
        if (KEY_SENSITIVE_KEYS.equals(key)) {
            throw new IllegalArgumentException(KEY_SENSITIVE_KEYS + " cannot be removed from the sensitive key list.");
        }
        // Reset the key first -- avoid race conditions where a sensitive value might be visible if we start restricting access in the future.
        put(key, null);
        SENSITIVE_KEYS.remove(key);
        putList(KEY_SENSITIVE_KEYS, SENSITIVE_KEYS.stream().sorted().collect(Collectors.toList()));
    }

    /**
     * Populate the sensitive key set from preferences
     */
    private void populateSensitiveKeys() {
        SENSITIVE_KEYS.addAll(getList(KEY_SENSITIVE_KEYS, Arrays.asList("sensitive.keys", "osm-server.username", "osm-server.password",
                DefaultProxySelector.PROXY_USER, DefaultProxySelector.PROXY_PASS,
                "oauth.access-token.key", "oauth.access-token.secret")));
    }
}
