package gov.nasa.gsfc.seadas.ocsswrest.utilities;

import java.io.*;
import java.net.URL;
import java.security.CodeSource;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.logging.*;
import java.util.logging.Formatter;

/*
 * Copyright (C) 2010 Brockmann Consult GmbH (info@brockmann-consult.de)
 *
 * This program 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 3 of the License, or (at your option)
 * any later version.
 * This program 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 this program; if not, see http://www.gnu.org/licenses/
 */

public final class OCSSWRuntimeConfig {

    public static final String CONFIG_KEY_CERES_CONTEXT = "ocsswws.context";
    public static final String CONFIG_KEY_DEBUG = "debug";
    public static final String CONFIG_KEY_MAIN_CLASS = "mainClass";
    public static final String CONFIG_KEY_CLASSPATH = "classpath";
    public static final String CONFIG_KEY_HOME = "home";
    public static final String CONFIG_KEY_CONFIG_FILE_NAME = "config";
    public static final String CONFIG_KEY_MODULES = "modules";
    public static final String CONFIG_KEY_LIB_DIRS = "libDirs";
    public static final String CONFIG_KEY_APP = "app";
    public static final String CONFIG_KEY_CONSOLE_LOG = "consoleLog";
    public static final String CONFIG_KEY_LOG_LEVEL = "logLevel";

    public static final String DEFAULT_CERES_CONTEXT = "ocsswws";
    public static final String DEFAULT_MAIN_CLASS_NAME = "gov.nasa.gsfc.seadas.ocsswws.Server";
    public static final String DEFAULT_MODULES_DIR_NAME = "modules";
    public static final String DEFAULT_CONFIG_DIR_NAME = "config";
    public static final String DEFAULT_LIB_DIR_NAME = "lib";

    public static final SimpleDateFormat LOG_TIME_STAMP_FORMAT = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ");

    private final Properties properties;

    private String contextId;

    private String debugKey;
    private boolean debug;

    private String homeDirKey;
    private String homeDirPath;

    private String mainClassKey;
    private String mainClassName;

    private String classpathKey;
    private String mainClassPath;

    private String applicationIdKey;
    private String applicationId;

    private String configFileKey;
    private String configFilePath;
    private String defaultRelConfigFilePath;
    private String defaultHomeConfigFilePath;

    private String modulesDirKey;
    private String modulesDirPath;
    private String defaultHomeModulesDirPath;

    private String libDirsKey;
    private String[] libDirPaths;
    private String defaultHomeLibDirPath;

    private Logger logger;
    private boolean homeDirAssumed;

    private String consoleLogKey;
    private String logLevelKey;
    private Level logLevel;
    private boolean consoleLog;

    public OCSSWRuntimeConfig() throws RuntimeConfigException {
        properties = System.getProperties();
        initAll();
    }

    public String getContextId() {
        return contextId;
    }

    public String getContextProperty(String key) {
        return getContextProperty(key, null);
    }

    public String getContextProperty(String key, String defaultValue) {
        return getProperty(contextId + '.' + key, defaultValue);
    }

    public boolean isDebug() {
        return debug;
    }

    public String getMainClassName() {
        return mainClassName;
    }

    public String getMainClassPath() {
        return mainClassPath;
    }

    public String getHomeDirPath() {
        return homeDirPath;
    }

    public String getConfigFilePath() {
        return configFilePath;
    }

    public String[] getLibDirPaths() {
        return libDirPaths;
    }

    public String getModulesDirPath() {
        return modulesDirPath;
    }

    public boolean isUsingModuleRuntime() {
        return DEFAULT_MAIN_CLASS_NAME.equals(mainClassName);
    }

    public String getApplicationId() {
        return applicationId;
    }

    public Logger getLogger() {
        return logger;
    }

    /////////////////////////////////////////////////////////////////////////
    // Private
    /////////////////////////////////////////////////////////////////////////

    private void initAll() throws RuntimeConfigException {
        initContext();
        initDebug();
        initHomeDirAndConfiguration();
        initDebug(); // yes, again
        initMainClassName();
        initClasspathPaths();
        initModulesDir();
        initLibDirs();
        if (isUsingModuleRuntime()) {
            initAppId();
        }
        initLogLevel();
        initConsoleLog();
        initLogger();
        setAutoDetectProperties();
    }

    private void initContext() {

        // Initialize context identifier. Mandatory.
        contextId = System.getProperty(CONFIG_KEY_CERES_CONTEXT, DEFAULT_CERES_CONTEXT);

        // Initialize application specific configuration keys
        homeDirKey = String.format("%s.%s", contextId, CONFIG_KEY_HOME);
        debugKey = String.format("%s.%s", contextId, CONFIG_KEY_DEBUG);
        configFileKey = String.format("%s.%s", contextId, CONFIG_KEY_CONFIG_FILE_NAME);
        modulesDirKey = String.format("%s.%s", contextId, CONFIG_KEY_MODULES);
        libDirsKey = String.format("%s.%s", contextId, CONFIG_KEY_LIB_DIRS);
        mainClassKey = String.format("%s.%s", contextId, CONFIG_KEY_MAIN_CLASS);
        classpathKey = String.format("%s.%s", contextId, CONFIG_KEY_CLASSPATH);
        applicationIdKey = String.format("%s.%s", contextId, CONFIG_KEY_APP);
        logLevelKey = String.format("%s.%s", contextId, CONFIG_KEY_LOG_LEVEL);
        consoleLogKey = String.format("%s.%s", contextId, CONFIG_KEY_CONSOLE_LOG);

        // Initialize default file and directory paths
        char sep = File.separatorChar;
        defaultRelConfigFilePath = String.format("%s/%s", DEFAULT_CONFIG_DIR_NAME, configFileKey).replace('/', sep);
        defaultHomeConfigFilePath = String.format("${%s}/%s", homeDirKey, defaultRelConfigFilePath).replace('/', sep);
        defaultHomeModulesDirPath = String.format("${%s}/%s", homeDirKey, DEFAULT_MODULES_DIR_NAME).replace('/', sep);
        defaultHomeLibDirPath = String.format("${%s}/%s", homeDirKey, DEFAULT_LIB_DIR_NAME).replace('/', sep);
    }

    private void initDebug() {
        debug = Boolean.valueOf(System.getProperty(debugKey, Boolean.toString(debug)));
    }

    private void initHomeDirAndConfiguration() throws RuntimeConfigException {
        maybeInitHomeDirAndConfigFile();
        if (configFilePath != null) {
            loadConfiguration();
        } else {
            trace("A configuration file is not used.");
        }
        initHomeDirIfNotAlreadyDone();
    }

    private void maybeInitHomeDirAndConfigFile() throws RuntimeConfigException {
        maybeInitHomeDir();
        maybeInitConfigFile();
        if (homeDirPath == null && configFilePath == null) {
            // we have no home and no config file
            assumeHomeDir();
        }
        if (configFilePath == null) {
            maybeInitDefaultConfigFile();
        }
    }

    private void maybeInitHomeDir() throws RuntimeConfigException {
        String homeDirPath = getProperty(homeDirKey);
        if (!isNullOrEmptyString(homeDirPath)) {
            // ok set: must be an existing directory
            File homeDir = new File(homeDirPath);
            if (!homeDir.isDirectory()) {
                throw createInvalidPropertyValueException(homeDirKey, homeDirPath);
            }
            try {
                this.homeDirPath = homeDir.getCanonicalPath();
            } catch (IOException e) {
                throw new RuntimeConfigException(e.getMessage(), e);
            }
        }
    }

    private void maybeInitConfigFile() throws RuntimeConfigException {
        String configFilePath = getProperty(configFileKey);
        if (!isNullOrEmptyString(configFilePath)) {
            File configFile = new File(configFilePath);
            // ok set: must be an existing file
            if (!configFile.isFile()) {
                throw createInvalidPropertyValueException(configFileKey, configFile.getPath());
            }
            try {
                this.configFilePath = configFile.getCanonicalPath();
            } catch (IOException e) {
                throw new RuntimeConfigException(e.getMessage(), e);
            }
        }
    }

    private void maybeInitDefaultConfigFile() {
        File configFile = new File(substitute(defaultHomeConfigFilePath));
        if (configFile.isFile()) {
            configFilePath = configFile.getPath();
        }
    }

    private void assumeHomeDir() {
        List<File> possibleHomeDirList = createPossibleHomeDirList();
        List<String> homeContentPathList = createHomeContentPathList();
        // The existance of the files in homeContentPathList is optional.
        // We assume that the home directory is the one in which most files of homeContentPathList are present.
        trace("Auto-detecting home directory...");
        File mostLikelyHomeDir = findMostLikelyHomeDir(possibleHomeDirList, homeContentPathList);
        if (mostLikelyHomeDir != null) {
            homeDirPath = mostLikelyHomeDir.getPath();
        } else {
            homeDirPath = new File(".").getAbsolutePath();
        }
        homeDirAssumed = true;
        setProperty(homeDirKey, homeDirPath);
    }

    private File findMostLikelyHomeDir(List<File> possibleHomeDirList, List<String> homeContentPathList) {
        int numFoundMax = 0;
        File mostLikelyHomeDir = null;
        for (File possibleHomeDir : possibleHomeDirList) {
            trace(String.format("Is [%s] my home directory?", possibleHomeDir));

            int numFound = 0;
            for (String homeContentPath : homeContentPathList) {
                File homeContentFile = new File(possibleHomeDir, homeContentPath);
                if (homeContentFile.exists()) {
                    trace(String.format("  [%s] contained? Yes.", homeContentPath));
                    numFound++;
                } else {
                    trace(String.format("  [%s] contained? No.", homeContentPath));
                }
            }
            if (numFound == 0) {
                trace("No.");
            } else {
                trace(String.format("Maybe. %d related file(s) found.", numFound));
            }
            if (numFound > numFoundMax) {
                try {
                    mostLikelyHomeDir = possibleHomeDir.getCanonicalFile();
                    numFoundMax = numFound;
                } catch (IOException e) {
                    // ???
                }
            }
        }
        return mostLikelyHomeDir;
    }

    private static List<File> createPossibleHomeDirList() {
        List<File> homeDirCheckList = new ArrayList<File>(4);

        // include codeSource dir in check list
        CodeSource lib = OCSSWRuntimeConfig.class.getProtectionDomain().getCodeSource();
        if (lib != null) {
            URL libUrl = lib.getLocation();
            if (libUrl.getProtocol().equals("file")) {
                String libPath = libUrl.getPath();
                File libParentDir = new File(libPath).getParentFile();
                if (libParentDir != null) {
                    // include one above libParentDir
                    if (libParentDir.getParentFile() != null) {
                        homeDirCheckList.add(libParentDir.getParentFile());
                    }
                    // include libParentDir
                    homeDirCheckList.add(libParentDir);
                }
            }
        }
        // include CWD in check list
        homeDirCheckList.add(new File(".").getAbsoluteFile());
        // include one above CWD in check list
        homeDirCheckList.add(new File("src/test").getAbsoluteFile());
        return homeDirCheckList;
    }

    private List<String> createHomeContentPathList() {
        List<String> homeContentPathList = new ArrayList<String>(8);
        homeContentPathList.add(defaultRelConfigFilePath);
        homeContentPathList.add("bin");
        homeContentPathList.add(DEFAULT_LIB_DIR_NAME);
        homeContentPathList.add(DEFAULT_MODULES_DIR_NAME);
        return homeContentPathList;
    }

    private void loadConfiguration() throws RuntimeConfigException {
        trace(String.format("Loading configuration from [%s]", configFilePath));
        try {
            InputStream stream = new FileInputStream(configFilePath);
            //InputStream stream1 = new FileInputStream("config/ocsswws.config");
            try {
                Properties fileProperties = new Properties();
                fileProperties.load(stream);
                // @todo check tests - code was not backward compatible with Java 5
                // so i changed it - but this is not the only place of uncompatibilty
                // add default properties so that they override file properties
                //Set<String> propertyNames = fileProperties.stringPropertyNames();
//                for (String propertyName : propertyNames) {
//                    String propertyValue = fileProperties.getProperty(propertyName);
//                    if (!isPropertySet(propertyName)) {
//                        setProperty(propertyName, propertyValue);
//                        trace(String.format("Configuration property [%s] added", propertyName));
//                    } else {
//                        trace(String.format("Configuration property [%s] ignored", propertyName));
//                    }
//                }

                Enumeration<?> enumeration = fileProperties.propertyNames();
                while (enumeration.hasMoreElements()) {
                    final Object key = enumeration.nextElement();
                    if (key instanceof String) {
                        final Object value = fileProperties.get(key);
                        if (value instanceof String) {
                            final String keyString = (String) key;
                            String propertyValue = fileProperties.getProperty(keyString);
                            if (!isPropertySet(keyString)) {
                                setProperty(keyString, propertyValue);
                                trace(String.format("Configuration property [%s] added", keyString));
                            } else {
                                trace(String.format("Configuration property [%s] ignored", keyString));
                            }
                        }
                    }
                }
            } finally {
                stream.close();
            }
        } catch (IOException e) {
            throw new RuntimeConfigException(String.format("Failed to load configuration [%s]", configFilePath),
                    e);
        }
    }

    private void initHomeDirIfNotAlreadyDone() throws RuntimeConfigException {
        if (homeDirPath == null || homeDirAssumed) {
            maybeInitHomeDir();
        }
        if (homeDirPath == null) {
            homeDirPath = new File(".").getAbsolutePath();
            homeDirAssumed = true;
        }
        // remove redundant '.'s and '..'s.
        try {
            homeDirPath = new File(homeDirPath).getCanonicalPath();
        } catch (IOException e) {
            throw new RuntimeConfigException("Home directory is invalid.", e);
        }
        if (homeDirAssumed) {
            trace(String.format("Home directory not set. Using assumed default."));
        }
        trace(String.format("Home directory is [%s]", homeDirPath));
    }

    private void initMainClassName() throws RuntimeConfigException {
        mainClassName = getProperty(mainClassKey, DEFAULT_MAIN_CLASS_NAME);
        if (isNullOrEmptyString(mainClassName)) {
            throw createMissingPropertyKeyException(mainClassKey);
        }
    }

    private void initClasspathPaths() {
        mainClassPath = getProperty(classpathKey, null);
    }

    private void initModulesDir() throws RuntimeConfigException {
        this.modulesDirPath = null;
        String modulesDirPath = getProperty(modulesDirKey);
        if (modulesDirPath != null) {
            File modulesDir = new File(modulesDirPath);
            if (!modulesDir.isDirectory()) {
                throw createInvalidPropertyValueException(modulesDirKey, modulesDirPath);
            }
            this.modulesDirPath = modulesDir.getPath();
        } else {
            // try default
            File modulesDir = new File(substitute(defaultHomeModulesDirPath));
            if (modulesDir.isDirectory()) {
                this.modulesDirPath = modulesDir.getPath();
            }
        }
    }

    private void initLibDirs() throws RuntimeConfigException {
        this.libDirPaths = new String[0];
        String libDirPathsString = getProperty(libDirsKey);
        if (libDirPathsString != null) {
            String[] libDirPaths = splitLibDirPaths(libDirPathsString);
            for (String libDirPath : libDirPaths) {
                File libDir = new File(libDirPath);
                if (!libDir.isDirectory()) {
                    throw createInvalidPropertyValueException(libDirsKey, libDirPathsString);
                }
            }
            this.libDirPaths = libDirPaths;
        } else {
            // try default
            libDirPathsString = substitute(defaultHomeLibDirPath);
            File libDir = new File(libDirPathsString);
            if (libDir.isDirectory()) {
                this.libDirPaths = new String[]{libDirPathsString};
            }
        }
    }

    private void initAppId() throws RuntimeConfigException {
        applicationId = getProperty(applicationIdKey);
        if (applicationId != null && applicationId.length() == 0) {
            throw createMissingPropertyKeyException(applicationIdKey);
        }
    }

    private void initLogLevel() {
        String logLevelStr = getProperty(logLevelKey, Level.OFF.getName());
        Level[] validLevels = new Level[]{
                Level.SEVERE,
                Level.WARNING,
                Level.INFO,
                Level.CONFIG,
                Level.FINE,
                Level.FINER,
                Level.FINEST,
                Level.ALL,
                Level.OFF,
        };

        logLevel = Level.OFF;
        for (Level level : validLevels) {
            if (level.getName().equalsIgnoreCase(logLevelStr)) {
                logLevel = level;
                break;
            }
        }
    }

    private void initConsoleLog() {
        String consoleLogStr = getProperty(consoleLogKey, "false");
        consoleLog = Boolean.parseBoolean(consoleLogStr);
    }

    private void initLogger() {
        ConsoleHandler consoleHandler = null;

        Logger rootLogger = LogManager.getLogManager().getLogger("");
        Handler[] handlers = rootLogger.getHandlers();
        for (Handler handler : handlers) {
            if (handler instanceof ConsoleHandler) {
                consoleHandler = (ConsoleHandler) handler;
                rootLogger.removeHandler(handler);
            }
        }

        logger = Logger.getLogger(contextId);
        logger.setLevel(logLevel);
        if (!logLevel.equals(Level.OFF)) {
            LogFormatter formatter = new LogFormatter();
            if (consoleLog) {
                if (consoleHandler == null) {
                    consoleHandler = new ConsoleHandler();
                }
                consoleHandler.setFormatter(formatter);
                consoleHandler.setLevel(logLevel);
                logger.addHandler(consoleHandler);
            }
            String userHomePath = getProperty("user.home", ".");
            File logDir = new File(userHomePath, '.' + contextId + "/log");
            logDir.mkdirs();
            String logFilePattern = new File(logDir, contextId + "-%g.log").getPath();
            try {
                FileHandler fileHandler = new FileHandler(logFilePattern);
                fileHandler.setFormatter(formatter);
                fileHandler.setLevel(logLevel);
                logger.addHandler(fileHandler);
            } catch (IOException e) {
                System.err.println("Error: Failed to create log file: " + logFilePattern);
            }
        }
    }

    private void setAutoDetectProperties() {
        setPropertyIfNotSet(this.homeDirKey, getHomeDirPath());
        setPropertyIfNotSet(configFileKey, getConfigFilePath());
        setPropertyIfNotSet(modulesDirKey, getModulesDirPath());
        String libDirPaths = assembleLibDirPaths(getLibDirPaths());
        setPropertyIfNotSet(libDirsKey, libDirPaths.length() > 0 ? libDirPaths : null);
    }

    private void setPropertyIfNotSet(String key, String value) {
        if (!isPropertySet(key)) {
            setProperty(key, value);
        }
    }

    private static boolean isNullOrEmptyString(String value) {
        return value == null || value.length() == 0;
    }

    private static String[] splitLibDirPaths(String libDirPathsString) {
        List<String> libDirPathList = new ArrayList<String>(8);
        StringTokenizer stringTokenizer = new StringTokenizer(libDirPathsString, File.pathSeparator);
        while (stringTokenizer.hasMoreElements()) {
            String libDirPath = (String) stringTokenizer.nextElement();
            libDirPathList.add(libDirPath);
        }
        return libDirPathList.toArray(new String[libDirPathList.size()]);
    }

    private static String assembleLibDirPaths(String[] libDirPaths) {
        StringBuilder sb = new StringBuilder(64);
        for (String libDirPath : libDirPaths) {
            if (sb.length() > 0) {
                sb.append(File.pathSeparator);
            }
            sb.append(libDirPath);
        }
        return sb.toString();
    }

    private static RuntimeConfigException createMissingPropertyKeyException(String key) {
        return new RuntimeConfigException(
                String.format("Property '%s' has not been set.", key));
    }

    private static RuntimeConfigException createInvalidPropertyValueException(String key, String value) {
        return new RuntimeConfigException(String.format("Value of property '%s' is invalid: %s", key, value));
    }

    private void trace(String msg) {
        if (debug) {
            System.out.println(String.format("[DEBUG] ceres-config: %s", msg));
        }
    }

    private boolean isPropertySet(String key) {
        return properties.containsKey(key);
    }

    private String getProperty(String key) {
        return getProperty(key, null);
    }

    private String getProperty(String key, String defaultValue) {
        String property = properties.getProperty(key, defaultValue);
        if (property != null) {
            return substitute(property);
        }
        return property;
    }

    private void setProperty(String key, String value) {
        if (value != null) {
            properties.setProperty(key, value);
        } else {
            properties.remove(key);
        }
    }

    /**
     * Substitues all occurences of <code>${<i>configKey</i>}</code> in the given string
     * with the value of <code><i>configKey</i></code>.
     *
     * @param value the string in which to perform the substitution.
     * @return the resulting string.
     */
    private String substitute(String value) {
        if (value.indexOf('$') == -1) {
            return value;
        }
        StringReader r = new StringReader(value);
        try {
            return new TemplateReader(r, properties).readAll();
        } catch (IOException e) {
            return value;
        }
    }

    private static class LogFormatter extends Formatter {

        @Override
        public String format(LogRecord record) {
            StringWriter sw = new StringWriter();
            PrintWriter pw = new PrintWriter(sw);
            pw.append('[');
            pw.append(record.getLevel().toString());
            pw.append(']');
            pw.append(' ');
            pw.append(LOG_TIME_STAMP_FORMAT.format(new Date(record.getMillis())));
            pw.append(' ');
            pw.append('-');
            pw.append(' ');
            pw.append(record.getMessage());
            Throwable thrown = record.getThrown();
            if (thrown != null) {
                pw.println();
                thrown.printStackTrace(pw);
            }
            pw.println();
            return sw.toString();
        }
    }
}


