/*
     This file is part of GNUnet.
     (C) 2009 Christian Grothoff (and other contributing authors)

     GNUnet is free software; you can redistribute it and/or modify
     it under the terms of the GNU General Public License as published
     by the Free Software Foundation; either version 2, or (at your
     option) any later version.

     GNUnet 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
     General Public License for more details.

     You should have received a copy of the GNU General Public License
     along with GNUnet; see the file COPYING.  If not, write to the
     Free Software Foundation, Inc., 59 Temple Place - Suite 330,
     Boston, MA 02111-1307, USA.
 */

package org.gnunet.util;

import com.google.common.base.Charsets;
import com.google.common.base.Function;
import com.google.common.base.Optional;
import com.google.common.collect.HashBasedTable;
import com.google.common.collect.Table;
import com.google.common.io.Files;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.BufferedWriter;
import java.io.File;
import java.io.IOError;
import java.io.IOException;
import java.nio.charset.Charset;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Configuration management.
 *
 * @author Florian Dold
 */
public class Configuration {
    public static class ParsingError extends RuntimeException {
        ParsingError(String msg) {
            super(msg);
        }

        ParsingError(String msg, final Throwable t) {
            super(msg, t);
        }
    }

    private static final Logger logger = LoggerFactory
            .getLogger(Configuration.class);

    private static Pattern section = Pattern.compile("\\[(.*?)\\]");
    private static Pattern tag = Pattern.compile("\\s*(\\S+?)\\s*=(.*?)");
    private static Pattern whitspace = Pattern.compile("\\s*");

    // rows are sections, colums are options
    private final Table<String, String, String> sections = HashBasedTable.create();

    private final Map<String, Set<String>> sectionSources = new HashMap<String, Set<String>>(20);

    /**
     * Start with an empty configuration.
     */
    public Configuration() {
    }


    /**
     * Expand an expression of the form "$FOO/BAR" to "DIRECTORY/BAR"
     * where either in the "PATHS" section or the environtment "FOO" is
     * set to "DIRECTORY".  We also support default expansion,
     * i.e. ${VARIABLE:-default} will expand to $VARIABLE if VARIABLE is
     * set in PATHS or the environment, and otherwise to "default".  Note
     * that "default" itself can also be a $-expression, thus
     * "${VAR1:-{$VAR2}}" will expand to VAR1 and if that is not defined
     * to VAR2.
     *
     * @param orig string to $-expand
     * @return $-expanded string
     */
    public String expandDollar(String orig) {

        if (orig.length() < 2 || !orig.startsWith("$"))
            return orig;
        String defaultVal;
        String substVal;
        String outside;

        if (orig.charAt(1) == '{')
        {
            int open = 1;
            int p = 2;
            while (open != 0) {
                if (orig.length() == p) {
                    logger.debug("unclosed '{'");
                    break;
                }
                if (orig.charAt(p) == '{') {
                    open++;
                } else if (orig.charAt(p) == '}') {
                    open--;
                }
                p++;
            }
            if (p != 2) {
                String inside = orig.substring(2, p-1);
                String[] parts = inside.split(Pattern.quote(":-"), 2);
                if (parts.length == 1) {
                    substVal = orig.substring(2, p-1);
                    defaultVal = "";
                } else {
                    substVal = parts[0];
                    defaultVal = parts[1];
                }
                if (orig.length() > p) {
                    outside = orig.substring(p, orig.length());
                } else {
                    outside = "";
                }
            } else {
                outside = "";
                substVal = "";
                defaultVal = "";
            }
        } else {
            defaultVal = "";
            int p = 1;
            for (; p < orig.length(); p++) {
                if (orig.charAt(p) == '\\' || orig.charAt(p) == '/') {
                    break;
                }
            }
            if (p != orig.length()) {
                substVal = orig.substring(1, p);
                outside = orig.substring(p, orig.length());
            } else {
                substVal = orig.substring(1, orig.length());
                outside = "";
            }
        }

        String result;

        Map<String, String> env = System.getenv();
        if (env.containsKey(substVal)) {
            result = env.get(substVal);
        } else {
            Optional<String> path = getValueString("PATHS", substVal);
            if (path.isPresent()) {
                result = path.get();
            } else {
                result = expandDollar(defaultVal);
            }
        }

        return result + outside;

    }

    /**
     * Returns all configuration options in a section.
     *
     * @param s the section of interest
     * @return an unmodifiable view of the section.
     */
    public Map<String, String> getSection(String s) {
        Map<String, String> m = sections.row(s);
        return Collections.unmodifiableMap(m);
    }

    /**
     * Returns the names of all non-empty sections
     *
     * @return set of non-empty section names
     */
    public Set<String> getSections() {
        return sections.rowKeySet();
    }

    /**
     * Get a configuration value that should be in a set of predefined strings
     *
     * @param section section of interest
     * @param option  option of interest
     * @param choices list of legal values
     * @return matching value from choices
     */
    public Optional<String> getValueChoice(String section, String option,
                                 Iterable<String> choices) {
        Optional<String> value = getValueString(section, option);
        if (!value.isPresent()) {
            return value;
        }
        for (String c : choices) {
            if (c.equals(value.get())) {
                return value;
            }
        }
        logger.error("Failure in configuration section {}: invalid value", section);
        return Optional.absent();
    }


    /**
     * Get a configuration value that should be a number
     *
     * @param section section of interest
     * @param option  option of interest
     * @return null if value not in configuration, the option's value otherwise
     */
    public Optional<Long> getValueNumber(String section, String option) {
        Optional<String> num_str = getValueString(section, option);
        if (!num_str.isPresent()) {
            logSectionSources(section);
            return Optional.absent();
        }
        try {
            return Optional.of(Long.parseLong(num_str.get()));
        } catch (NumberFormatException e) {
            logger.error("Failure in configuration section "
                    + section + " option " + option + ": " + e.getMessage(), e);
            return Optional.absent();
        }
    }

    private void logSectionSources(String section) {
        Set<String> sources = sectionSources.get(section);
        if (sources == null) {
            logger.info("No sources for section '{}'", section);
        } else {
            logger.info("Sources for section '{}': {}", section, sources);
        }
    }

    /**
     * Set an option to a string value in a section.
     *
     * @param section section of interest
     * @param option  option of interest
     * @return value
     */
    public Optional<String> getValueString(String section, String option) {
        if (haveValue(section, option)) {
            return Optional.of(sections.get(section, option));
        }
        return Optional.absent();
    }

    /**
     * Gets a configuration value that should be in a set of {"YES","NO"}.
     *
     * @param section section of interest
     * @param option  option of interest
     * @return true, false, null
     */
    public Optional<Boolean> getValueYesNo(String section, String option) {
        final Optional<String> v = getValueChoice(section, option,
                Arrays.asList("YES", "NO"));
        if (!v.isPresent()) {
            Set<String> sources = sectionSources.get(section);
            if (sources == null) {
                logger.info("No sources for section '{}'", section);
            } else {
                logger.info("Sources for section '{}': {}", section, sources);
            }
            logger.error(String.format(
                    "Failure in configuration section '%s': option '%s' not found",
                    section, option));
            return Optional.absent();
        }
        if (v.get().equalsIgnoreCase("YES")) {
            return Optional.of(true);
        }
        if (v.get().equalsIgnoreCase("NO")) {
            return Optional.of(false);
        }

        logger.error(String.format("Configuration error: section '%s', option '%s' not recognized as YES or NO", section, option));

        return Optional.absent();
    }

    /**
     * Tests if we have a value for a particular option.
     *
     * @param section section of interest
     * @param option  option of interest
     * @return true if so, false of not
     */
    public boolean haveValue(String section, String option) {
        return sections.contains(section, option);
    }


    private void parseFromLines(Iterator<String> it, String filename) {
        String current_section = "";
        int lineNumber = 1;
        while (it.hasNext()) {
            String line = it.next();
            String[] split_line = line.split("#");
            if (split_line.length == 0)
                continue;

            // strip comment
            line = split_line[0];
            Matcher m;

            if ((m = tag.matcher(line)).matches()) {
                String option = m.group(1).trim();
                String value = m.group(2).trim();

                if (value.length() != 0 && value.charAt(0) == '"') {
                    int pos = value.indexOf('"', 1);
                    if (pos == -1) {
                        logger.warn("incorrecly quoted config value");
                        continue;
                    }
                    value = value.substring(1, pos);
                }
                setValueString(current_section, option, value);
            } else if ((m = section.matcher(line)).matches()) {
                current_section = m.group(1).trim();
                if (sectionSources.containsKey(current_section)) {
                    sectionSources.get(current_section).add(filename);
                } else {
                    sectionSources.put(current_section, new HashSet<String>(Collections.singleton(filename)));
                }
            } else if (whitspace.matcher(line).matches()) {
                // whitespace is ok
            } else {
                logger.warn(String.format("skipped unreadable line %s in configuration file '%s': '%s'", lineNumber,
                        filename, line));
            }

            lineNumber++;
        }
    }

    /**
     * Parse a configuration file, add all of the options in the file to the
     * configuration environment.
     *
     * @param filename name of the configuration file
     */
    public void parse(String filename) {
        filename = replaceHome(filename);

        Iterator<String> it;
        try {
            List<String> lines = Files.readLines(new File(filename), Charset.defaultCharset());
            it = lines.iterator();
        } catch (IOException e) {
            throw new ParsingError("Cannot read configuration file '" + filename + "'");
        }
        parseFromLines(it, filename);

    }

    private String replaceHome(String filename) {
        String home = System.getenv("HOME");
        return home != null ? filename.replace("~", home) : filename;
    }

    /**
     * Remove the given section and all options in it.
     */
    public void removeSection(String section) {
        sections.row(section).clear();
    }

    /**
     * Set an option to a string value in a section.
     *
     * @param section section of interest
     * @param option  option of interest
     * @param value   value to set
     */
    public void setValueNumber(String section, String option,
                               long value) {
        setValueString(section, option, "" + value);
    }

    /**
     * Set an option to a string value in a section.
     *
     * @param section section of interest
     * @param option  option of interest
     * @param value   value to set
     */
    public void setValueString(String section, String option,
                               String value) {
        sections.put(section, option, value);
    }

    /**
     * Write configuration file.
     *
     * @param filename where to write the configuration
     */
    public void write(String filename) throws IOException {
        BufferedWriter w = Files.newWriter(new File(filename), Charsets.UTF_8);
        try {
            for (String section : sections.rowKeySet()) {
                w.write("["+section+"]");
                w.newLine();
                for (Map.Entry<String,String> e : sections.row(section).entrySet()) {
                    w.write(e.getKey() + " = " + e.getValue());
                    w.newLine();
                }
            }
        } finally {
            w.close();
        }
    }

    /**
     * Serialize the configuration to a string.
     * @return the serialized configuration
     */
    public String serialize() {
        StringBuffer buf = new StringBuffer();
        for (Map.Entry<String, Map<String,String>> section : sections.rowMap().entrySet()) {
            buf.append("[" + section.getKey() + "]\n");
            for (Map.Entry<String, String> option : section.getValue().entrySet()) {
                buf.append(option.getKey() + " = " + option.getValue() + "\n");
            }
        }
        return buf.toString();
    }

    /**
     * Serialize the given configuration sections a string.
     *
     * @param sectionNames sections to serialize
     * @return the serialized sections
     */
    public String serialize(String... sectionNames) {
        StringBuffer buf = new StringBuffer();
        for (String sectionName : sectionNames) {
            buf.append("[" + sectionName + "]\n");
            for (Map.Entry<String, String> option : sections.row(sectionName).entrySet()) {
                buf.append(option.getKey() + " = " + option.getValue() + "\n");
            }
        }
        return buf.toString();
    }


    public void loadDefaults() {
        Collection<File> dirs = new ArrayList<File>();
        String pfx = System.getenv("GNUNET_PREFIX");
        if (pfx != null) {
            dirs.add(new File(pfx, "share/gnunet/config.d/"));
            dirs.add(new File(pfx, "config.d/"));
            dirs.add(new File(pfx, "gnunet/config.d/"));
        }
        for (File dir : dirs) {
            if (dir.exists() && dir.isDirectory()) {
                File[] files = dir.listFiles();
                if (files == null) {
                    continue;
                }
                for (File f : files) {
                    parse(f.getAbsolutePath());
                }
            }
        }
    }

    /**
     * Read a filename from an option in the given section.
     * In contrast to getValueString, getValueFilename $-expands the configuration value.
     *
     * @param section section of interest
     * @param option option of interest
     * @return an optional with the filname, which may be non-present
     *         if the section/option does not exist
     */
    public Optional<String> getValueFilename(String section, String option) {
        return getValueString(section, option).transform(new Function<String, String>() {
            @Override
            public String apply(java.lang.String input) {
                return expandDollar(input);
            }
        });
    }

    public void deserialize(String str) {
        String[] linesArray = str.split("\\r?\\n");
        parseFromLines(Arrays.asList(linesArray).iterator(), "<serialized-config>");
    }

    public static class ConfigurationException extends RuntimeException {
        public ConfigurationException(String string) {
            super(string);
        }
    }

    /**
     * Write the configuration to a temporary file that is
     * deleted once the JVM exits.
     *
     * @return temporary file with the configuration written to it
     */
    public File writeTemp() {
        File f;
        try {
            f = File.createTempFile("gnunet-config", ".conf");
            f.deleteOnExit();
            Files.write(serialize(), f, Charsets.UTF_8);
        } catch (IOException e) {
            throw new IOError(e);
        }
        return f;
    }

    public Configuration clone() {
        Configuration cfg = new Configuration();
        cfg.sections.putAll(this.sections);
        cfg.sectionSources.putAll(this.sectionSources);
        return cfg;
    }
}
