/*
 * Copyright (c) 2002-2016, the original author or authors.
 *
 * This software is distributable under the BSD license. See the terms of the
 * BSD license in the documentation provided with this software.
 *
 * http://www.opensource.org/licenses/bsd-license.php
 */
package jline.internal;

import java.io.BufferedInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.FileNotFoundException;
import java.net.URL;
import java.nio.charset.Charset;
import java.nio.charset.IllegalCharsetNameException;
import java.util.Map;
import java.util.Properties;

import static jline.internal.Preconditions.checkNotNull;

/**
 * Provides access to configuration values.
 *
 * @author <a href="mailto:jason@planet57.com">Jason Dillon</a>
 * @author <a href="mailto:gnodet@gmail.com">Guillaume Nodet</a>
 * @since 2.4
 */
public class Configuration
{
    /**
     * System property which can point to a file or URL containing configuration properties to load.
     *
     * @since 2.7
     */
    public static final String JLINE_CONFIGURATION = "jline.configuration";

    /**
     * Default configuration file name loaded from user's home directory.
     */
    public static final String JLINE_RC = ".jline.rc";

    private static volatile Properties properties;

    private static Properties initProperties() {
        URL url = determineUrl();
        Properties props = new Properties();
        try {
            loadProperties(url, props);
        }
        catch (FileNotFoundException e) {
            // debug here and no stack trace, as this can happen normally if default jline.rc file is missing
            Log.debug("Unable to read configuration: ", e.toString());
        }
        catch (IOException e) {
            Log.warn("Unable to read configuration from: ", url, e);
        }
        return props;
    }

    private static void loadProperties(final URL url, final Properties props) throws IOException {
        Log.debug("Loading properties from: ", url);
        InputStream input = url.openStream();
        try {
            props.load(new BufferedInputStream(input));
        }
        finally {
            try {
                input.close();
            }
            catch (IOException e) {
                // ignore
            }
        }

        if (Log.DEBUG) {
            Log.debug("Loaded properties:");
            for (Map.Entry<Object,Object> entry : props.entrySet()) {
                Log.debug("  ", entry.getKey(), "=", entry.getValue());
            }
        }
    }

    private static URL determineUrl() {
        // See if user has customized the configuration location via sysprop
        String tmp = System.getProperty(JLINE_CONFIGURATION);
        if (tmp != null) {
            return Urls.create(tmp);
        }
        else {
            // Otherwise try the default
            File file = new File(getUserHome(), JLINE_RC);
            return Urls.create(file);
        }
    }

    /**
     * @since 2.7
     */
    public static void reset() {
        Log.debug("Resetting");
        properties = null;

        // force new properties to load
        getProperties();
    }

    /**
     * @since 2.7
     */
    public static Properties getProperties() {
        // Not sure its worth to guard this with any synchronization, volatile field probably sufficient
        if (properties == null) {
            properties = initProperties();
        }
        return properties;
    }

    public static String getString(final String name, final String defaultValue) {
        checkNotNull(name);

        String value;

        // Check sysprops first, it always wins
        value = System.getProperty(name);

        if (value == null) {
            // Next try userprops
            value = getProperties().getProperty(name);

            if (value == null) {
                // else use the default
                value = defaultValue;
            }
        }

        return value;
    }

    public static String getString(final String name) {
        return getString(name, null);
    }

    public static boolean getBoolean(final String name) {
        return getBoolean(name, false);
    }

    public static boolean getBoolean(final String name, final boolean defaultValue) {
        String value = getString(name);
        if (value == null) {
            return defaultValue;
        }
        return value.length() == 0
            || value.equalsIgnoreCase("1")
            || value.equalsIgnoreCase("on")
            || value.equalsIgnoreCase("true");
    }

    /**
     * @since 2.6
     */
    public static int getInteger(final String name, final int defaultValue) {
        String str = getString(name);
        if (str == null) {
            return defaultValue;
        }
        return Integer.parseInt(str);
    }

    /**
     * @since 2.6
     */
    public static long getLong(final String name, final long defaultValue) {
        String str = getString(name);
        if (str == null) {
            return defaultValue;
        }
        return Long.parseLong(str);
    }

    //
    // System property helpers
    //

    /**
     * @since 2.7
     */
    public static String getLineSeparator() {
        return System.getProperty("line.separator");
    }

    public static File getUserHome() {
        return new File(System.getProperty("user.home"));
    }

    public static String getOsName() {
        return System.getProperty("os.name").toLowerCase();
    }

    /**
     * @since 2.7
     */
    public static boolean isWindows() {
        return getOsName().startsWith("windows");
    }

    public static boolean isHpux() {
        return getOsName().startsWith("hp");
    }

    // FIXME: Sort out use of property access of file.encoding in InputStreamReader, should consolidate configuration access here

    public static String getFileEncoding() {
        return System.getProperty("file.encoding");
    }

    /**
     * Get the default encoding.  Will first look at the LC_ALL, LC_CTYPE, and LANG environment variables, then the input.encoding
     * system property, then the default charset according to the JVM.
     *
     * @return The default encoding to use when none is specified.
     */
    public static String getEncoding() {
        // Check for standard locale environment variables, in order of precedence, first.
        // See http://www.gnu.org/s/libc/manual/html_node/Locale-Categories.html
        for (String envOption : new String[]{"LC_ALL", "LC_CTYPE", "LANG"}) {
            String envEncoding = extractEncodingFromCtype(System.getenv(envOption));
            if (envEncoding != null) {
                try {
                    if (Charset.isSupported(envEncoding)) {
                        return envEncoding;
                    }
                } catch (IllegalCharsetNameException e) {
                    continue;
                }
            }
        }
        return getString("input.encoding", Charset.defaultCharset().name());
    }

    /**
     * Parses the LC_CTYPE value to extract the encoding according to the POSIX standard, which says that the LC_CTYPE
     * environment variable may be of the format <code>[language[_territory][.codeset][@modifier]]</code>
     *
     * @param ctype The ctype to parse, may be null
     * @return The encoding, if one was present, otherwise null
     */
    static String extractEncodingFromCtype(String ctype) {
        if (ctype != null && ctype.indexOf('.') > 0) {
            String encodingAndModifier = ctype.substring(ctype.indexOf('.') + 1);
            if (encodingAndModifier.indexOf('@') > 0) {
                return encodingAndModifier.substring(0, encodingAndModifier.indexOf('@'));
            } else {
                return encodingAndModifier;
            }
        }
        return null;
    }
}