package org.xbib.logging.configuration;

import java.util.ArrayList;
import java.util.EnumSet;
import java.util.List;
import java.util.Locale;
import java.util.Objects;
import java.util.Properties;
import java.util.function.Supplier;
import java.util.logging.ErrorManager;
import java.util.logging.Filter;
import java.util.logging.Formatter;
import java.util.logging.Handler;
import java.util.logging.Level;
import org.xbib.logging.LogContext;
import org.xbib.logging.Logger;
import org.xbib.logging.configuration.filters.FilterExpressions;
import org.xbib.logging.expression.Expression;
import org.xbib.logging.filters.AcceptAllFilter;
import org.xbib.logging.filters.DenyAllFilter;
import org.xbib.logging.util.ObjectBuilder;
import org.xbib.logging.util.StandardOutputStreams;

/**
 * A utility to parse a {@code logging.properties} file and configure a {@link LogContext}.
 */
@SuppressWarnings("WeakerAccess")
public class PropertyContextConfiguration extends ContextConfiguration {

    private static final String[] EMPTY_STRINGS = new String[0];
    private final Properties properties;

    private PropertyContextConfiguration(final LogContext logContext,
                                         final Properties properties) {
        super(logContext);
        properties.putAll(System.getProperties());
        this.properties = properties;
    }

    /**
     * Configures the {@link LogContext} based on the properties.
     *
     * @param logContext the log context to configure
     * @param properties the properties used to configure the log context
     * @return the context configuration for the properties
     */
    public static PropertyContextConfiguration configure(final LogContext logContext,
                                                         final Properties properties) {
        final PropertyContextConfiguration config = new PropertyContextConfiguration(
                Objects.requireNonNull(logContext),
                Objects.requireNonNull(properties));
        config.doConfigure();
        return config;
    }

    private void doConfigure() {
        // Start with the list of loggers to configure.  The root logger is always on the list.
        configureLogger("");
        // And, for each logger name, configure any filters, handlers, etc.
        for (String loggerName : getStringCsvArray("loggers")) {
            configureLogger(loggerName);
        }
        // Configure any declared handlers.
        for (String handlerName : getStringCsvArray("handlers")) {
            configureHandler(handlerName);
        }
        // Configure any declared filters.
        for (String filterName : getStringCsvArray("filters")) {
            configureFilter(filterName);
        }
        // Configure any declared formatters.
        for (String formatterName : getStringCsvArray("formatters")) {
            configureFormatter(formatterName);
        }
        // Configure any declared error managers.
        for (String errorManagerName : getStringCsvArray("errorManagers")) {
            configureErrorManager(errorManagerName);
        }
    }

    @SuppressWarnings({"ConstantConditions"})
    private void configureLogger(final String loggerName) {
        final Logger logger = getContext().getLogger(loggerName);
        // Get logger level
        final String levelName = getStringProperty(getKey("logger", loggerName, "level"));
        if (levelName != null) {
            logger.setLevel(Level.parse(levelName));
        }
        // Get logger filters
        final String filterName = getStringProperty(getKey("logger", loggerName, "filter"));
        if (filterName != null) {
            configureFilter(filterName);
            logger.setFilter(getFilter(filterName));
        }
        // Get logger handlers
        final String[] handlerNames = getStringCsvArray(getKey("logger", loggerName, "handlers"));
        for (String name : handlerNames) {
            if (configureHandler(name)) {
                logger.addHandler(getHandler(name));
            }
        }
        // Get logger properties
        final String useParentHandlersString = getStringProperty(getKey("logger", loggerName, "useParentHandlers"));
        if (useParentHandlersString != null) {
            logger.setUseParentHandlers(resolveBooleanExpression(useParentHandlersString));
        }
        final String useParentFiltersString = getStringProperty(getKey("logger", loggerName, "useParentFilters"));
        if (useParentFiltersString != null) {
            if (logger instanceof Logger) {
                logger.setUseParentFilters(resolveBooleanExpression(useParentHandlersString));
            }
        }
    }

    private boolean configureHandler(final String handlerName) {
        if (hasHandler(handlerName)) {
            // already configured!
            return true;
        }
        final String className = getStringProperty(getKey("handler", handlerName), true, false);
        if (className == null) {
            StandardOutputStreams.printError("Handler %s is not defined%n", handlerName);
            return false;
        }

        final ObjectBuilder<Handler> handlerBuilder = ObjectBuilder.of(this, Handler.class, className)
                .setModuleName(getStringProperty(getKey("handler", handlerName, "module")))
                .addPostConstructMethods(getStringCsvArray(getKey("handler", handlerName, "postConfiguration")));

        // Configure the constructor properties
        configureProperties(handlerBuilder, "handler", handlerName);
        final String encoding = getStringProperty(getKey("handler", handlerName, "encoding"));
        if (encoding != null) {
            handlerBuilder.addProperty("encoding", encoding);
        }

        final String filter = getStringProperty(getKey("handler", handlerName, "filter"));
        if (filter != null) {
            configureFilter(filter);
            handlerBuilder.addDefinedProperty("filter", Filter.class, getFilters().get(filter));
        }
        final String levelName = getStringProperty(getKey("handler", handlerName, "level"));
        if (levelName != null) {
            handlerBuilder.addProperty("level", levelName);
        }
        final String formatterName = getStringProperty(getKey("handler", handlerName, "formatter"));
        if (formatterName != null) {
            if (configureFormatter(formatterName)) {
                handlerBuilder.addDefinedProperty("formatter", Formatter.class, getFormatters()
                        .get(formatterName));
            }
        }
        final String errorManagerName = getStringProperty(getKey("handler", handlerName, "errorManager"));
        if (errorManagerName != null) {
            if (configureErrorManager(errorManagerName)) {
                handlerBuilder.addDefinedProperty("errorManager", ErrorManager.class, getErrorManagers()
                        .get(errorManagerName));
            }
        }

        final String[] handlerNames = getStringCsvArray(getKey("handler", handlerName, "handlers"));
        if (handlerNames.length > 0) {
            final List<Supplier<Handler>> subhandlers = new ArrayList<>();
            for (String name : handlerNames) {
                if (configureHandler(name)) {
                    subhandlers.add(getHandlers().get(name));
                }
            }
            handlerBuilder.addDefinedProperty("handlers", Handler[].class, (Supplier<Handler[]>) () -> {
                if (subhandlers.isEmpty()) {
                    return new Handler[0];
                }
                final Handler[] result = new Handler[subhandlers.size()];
                int i = 0;
                for (Supplier<Handler> supplier : subhandlers) {
                    result[i++] = supplier.get();
                }
                return result;
            });
        }
        addHandler(handlerName, handlerBuilder.build());
        return true;
    }

    private boolean configureFormatter(final String formatterName) {
        if (hasFilter(formatterName)) {
            // already configured!
            return true;
        }
        final String className = getStringProperty(getKey("formatter", formatterName), true, false);
        if (className == null) {
            StandardOutputStreams.printError("Formatter %s is not defined%n", formatterName);
            return false;
        }
        final ObjectBuilder<Formatter> formatterBuilder = ObjectBuilder.of(this, Formatter.class, className)
                .setModuleName(getStringProperty(getKey("formatter", formatterName, "module")))
                .addPostConstructMethods(getStringCsvArray(getKey("formatter", formatterName, "postConfiguration")));
        configureProperties(formatterBuilder, "formatter", formatterName);
        addFormatter(formatterName, formatterBuilder.build());
        return true;
    }

    private boolean configureErrorManager(final String errorManagerName) {
        if (hasErrorManager(errorManagerName)) {
            // already configured!
            return true;
        }
        final String className = getStringProperty(getKey("errorManager", errorManagerName), true, false);
        if (className == null) {
            StandardOutputStreams.printError("Error manager %s is not defined%n", errorManagerName);
            return false;
        }
        final ObjectBuilder<ErrorManager> errorManagerBuilder = ObjectBuilder.of(this, ErrorManager.class, className)
                .setModuleName(getStringProperty(getKey("errorManager", errorManagerName, "module")))
                .addPostConstructMethods(getStringCsvArray(getKey("errorManager", errorManagerName, "postConfiguration")));
        configureProperties(errorManagerBuilder, "errorManager", errorManagerName);
        addErrorManager(errorManagerName, errorManagerBuilder.build());
        return true;
    }

    private void configureFilter(final String filterName) {
        if (hasFilter(filterName)) {
            return;
        }
        // First determine if we're using a defined filters or filters expression. We assume a defined filters if there is
        // a filters.NAME property.
        String filterValue = getStringProperty(getKey("filter", filterName), true, false);
        if (filterValue == null) {
            // We are a filters expression, parse the expression and create a filters
            addFilter(filterName, () -> FilterExpressions.parse(getContext(), filterName));
        } else {
            // The AcceptAllFilter and DenyAllFilter are singletons.
            if (AcceptAllFilter.class.getName().equals(filterValue)) {
                addFilter(filterName, AcceptAllFilter::getInstance);
            } else if (DenyAllFilter.class.getName().equals(filterValue)) {
                addFilter(filterName, DenyAllFilter::getInstance);
            } else {
                // We assume we're a defined filter
                final ObjectBuilder<Filter> filterBuilder = ObjectBuilder.of(this, Filter.class, filterValue)
                        .setModuleName(getStringProperty(getKey("filter", filterName, "module")))
                        .addPostConstructMethods(getStringCsvArray(getKey("filter", filterName, "postConfiguration")));
                configureProperties(filterBuilder, "errorManager", filterName);
                addFilter(filterName, filterBuilder.build());
            }
        }
    }

    private String getStringProperty(final String key) {
        return getStringProperty(key, true, true);
    }

    private String getStringProperty(final String key, final boolean trim, final boolean resolveExpression) {
        String value = properties.getProperty(key);
        if (resolveExpression && value != null) {
            value = resolveExpression(value);
        } else if (value != null && trim) {
            value = value.trim();
        }
        return value;
    }

    private String[] getStringCsvArray(final String key) {
        final String property = properties.getProperty(key, "");
        if (property == null) {
            return EMPTY_STRINGS;
        }
        final String value = property.trim();
        if (value.isEmpty()) {
            return EMPTY_STRINGS;
        }
        return value.split("\\s*,\\s*");
    }

    private void configureProperties(final ObjectBuilder<?> builder, final String prefix, final String name) {
        // First configure constructor properties
        final String[] constructorPropertyNames = getStringCsvArray(getKey(prefix, name, "constructorProperties"));
        for (String propertyName : constructorPropertyNames) {
            final String valueString = getStringProperty(getKey(prefix, name, propertyName), false, true);
            if (valueString != null)
                builder.addConstructorProperty(propertyName, valueString);
        }

        // Next configure setter properties
        final String[] propertyNames = getStringCsvArray(getKey(prefix, name, "properties"));
        for (String propertyName : propertyNames) {
            final String valueString = getStringProperty(getKey(prefix, name, propertyName), false, true);
            if (valueString != null)
                builder.addProperty(propertyName, valueString);
        }
    }

    private static String getKey(final String prefix, final String objectName) {
        return !objectName.isEmpty() ? prefix + "." + objectName : prefix;
    }

    private static String getKey(final String prefix, final String objectName, final String key) {
        return !objectName.isEmpty() ? prefix + "." + objectName + "." + key : prefix + "." + key;
    }

    private static boolean resolveBooleanExpression(final String possibleExpression) {
        final String value = resolveExpression(possibleExpression);
        return !value.toLowerCase(Locale.ROOT).equals("false");
    }

    private static String resolveExpression(final String possibleExpression) {
        final EnumSet<Expression.Flag> flags = EnumSet.noneOf(Expression.Flag.class);
        final Expression expression = Expression.compile(possibleExpression, flags);
        return expression.evaluateWithPropertiesAndEnvironment(false);
    }
}
