/*******************************************************************************
 * Copyright (c) 2015 Oracle and/or its affiliates. All rights reserved.
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License v1.0 and Eclipse Distribution License v. 1.0
 * which accompanies this distribution.
 * The Eclipse Public License is available at http://www.eclipse.org/legal/epl-v10.html
 * and the Eclipse Distribution License is available at
 * http://www.eclipse.org/org/documents/edl-v10.php.
 *
 * Contributors:
 *     06/08/2015-2.6 Tomas Kraus
 *       - initial API and implementation.
 ******************************************************************************/
package org.eclipse.persistence.internal.logging;

import java.security.AccessController;
import java.util.HashMap;
import java.util.Map;

import org.eclipse.persistence.internal.localization.LoggingLocalization;
import org.eclipse.persistence.internal.localization.i18n.LoggingLocalizationResource;
import org.eclipse.persistence.internal.security.PrivilegedAccessHelper;
import org.eclipse.persistence.internal.security.PrivilegedGetSystemProperty;
import org.eclipse.persistence.logging.AbstractSessionLog;
import org.eclipse.persistence.logging.SessionLog;

/**
 * INTERNAL:
 * Log to EclipseLink logger and optionally to standard error output.
 * Standard error output logging is enabled by:
 * <ul><li>{@code org.eclipse.persistence.jpa.log.stderr} for all logger categories</li>
 * <li>{@code org.eclipse.persistence.jpa.<category>.log.stderr} for individual logger category</li></ul>
 * Logger API is based on {@link SessionLog}.
 * @since 2.7
 */
public class StdErrLogger {

    /** Logger. */
    private static final SessionLog LOGGER = AbstractSessionLog.getLog();

    /** Standard error output logger property name prefix. */
    private static final String PROPERTY_NAME_PREFIX = "eclipselink.log";

    /** Standard error output logger property name suffix. */
    private static final String PROPERTY_NAME_SUFFIX = "stderr";

    /** Standard error output logger property name components separator. */
    private static final char PROPERTY_NAME_SEPARATOR = '.';

    /** {@link String} to separate logging category from message text in standard error output. */
    private static final String CATEGORY_SEPARATOR = ": ";

    /** {@link String} to prefix stack trace message in standard error output. */
    private static final String STACK_TRACE_PREFIX = " - ";

    /** Standard error output triggers for specific EclipseLink logging categories. */
    private static final Map<String, Boolean> logEnabledCategory = initLogEnabledCategory();

    // PERF: Global standard error output trigger to check before getting from Map.
    /** Global standard error output trigger. EclipseLink logging categories are allowed to be sent to standard error
     *  output when value is set to {@code true}. No standard error output will be done when value is set
     *  to {@code false}. */
    private static final boolean logEnabled = logEnabledCategory != null;

    /**
     * INTERNAL:
     * Initialize {@logEnabledCategory} standard error output triggers for specific EclipseLink logging categories.
     * @return Standard error output triggers {@link Map} for specific EclipseLink logging categories or {@code null}
     *         if no standard error output was enabled.
     */
    private static Map<String, Boolean> initLogEnabledCategory() {
        final int PropertyExtensionsLength = PROPERTY_NAME_PREFIX.length() + PROPERTY_NAME_SUFFIX.length() + 2;
        final Map<String, Boolean> logEnabledMap = new HashMap<>(SessionLog.loggerCatagories.length);
        boolean globalTrigger;
        if (PrivilegedAccessHelper.shouldUsePrivilegedAccess()) {
            globalTrigger = Boolean.valueOf(AccessController.doPrivileged(new PrivilegedGetSystemProperty(
                    PROPERTY_NAME_PREFIX + PROPERTY_NAME_SEPARATOR + PROPERTY_NAME_SUFFIX)));
        } else {
            globalTrigger = Boolean.valueOf(System.getProperty(
                    PROPERTY_NAME_PREFIX + PROPERTY_NAME_SEPARATOR + PROPERTY_NAME_SUFFIX));
        }
        boolean enable = globalTrigger;
        for (final String category : SessionLog.loggerCatagories) {
            if (globalTrigger) {
                logEnabledMap.put(category, Boolean.TRUE);
            } else {
                StringBuilder propertyKey = new StringBuilder(PropertyExtensionsLength + category.length());
                propertyKey.append(PROPERTY_NAME_PREFIX);
                propertyKey.append(PROPERTY_NAME_SEPARATOR);
                propertyKey.append(category);
                propertyKey.append(PROPERTY_NAME_SEPARATOR);
                propertyKey.append(PROPERTY_NAME_SUFFIX);
                boolean trigger;
                if (PrivilegedAccessHelper.shouldUsePrivilegedAccess()) {
                    trigger = Boolean.valueOf(AccessController.doPrivileged(
                            new PrivilegedGetSystemProperty(propertyKey.toString())));
                } else {
                    trigger = Boolean.valueOf(System.getProperty(propertyKey.toString()));
                }
                logEnabledMap.put(category, Boolean.valueOf(trigger));
                enable = enable || trigger;
            }
        }
        if (!enable) {
            logEnabledMap.clear();
        }
        return enable ? logEnabledMap : null;
    }

    /**
     * INTERNAL:
     * Check if a message of the given category would actually be logged to standard error output.
     * @return Value of {@code true} when a message of the given {@code category} would actually be logged to standard
     *         error output or {@code false} otherwise.
     */
    private static final boolean shouldLogToStdErr(final String category) {
        final Boolean enabled = logEnabledCategory.get(category);
        return enabled != null ? enabled.booleanValue() : false;
    }

    /**
     * INTERNAL:
     * Log message to standard error output.
     * @param category   The EclipseLink logging category.
     * @param messageKey The {@link TraceLocalizationResource} log message key.
     * @param arguments  Arguments of the log message.
     */
    private static void logStdErr(
            final String category, final String messageKey, final Object... arguments) {
        final String message = arguments == null || arguments.length == 0 ?
                LoggingLocalization.buildMessage(messageKey) : LoggingLocalization.buildMessage(messageKey, arguments);
        final int messageLength = message != null ? message.length() : 0;
        final int categoryLength = category != null ? category.length() + CATEGORY_SEPARATOR.length() : 0;
        if (categoryLength > 0 || messageLength > 0) {
            final StringBuilder sb = new StringBuilder(categoryLength + messageLength);
            if (categoryLength > 0) {
                sb.append(category);
                sb.append(CATEGORY_SEPARATOR);
            }
            if (messageLength > 0) {
                sb.append(message);
            }
            System.err.println(sb.toString());
        }
    }

    /**
     * INTERNAL:
     * Log message to standard error output.
     * @param category  The EclipseLink logging category.
     * @param throwable {@link Throwable} to be logged.
     */
    private static void logThrowableStdErr(final String category, final Throwable throwable) {
        final int categoryLength = category != null ? category.length() + CATEGORY_SEPARATOR.length() : 0;
        for (StackTraceElement ste : throwable.getStackTrace()) {
            final String message = ste.toString();
            StringBuilder sb = new StringBuilder(categoryLength + STACK_TRACE_PREFIX.length() + message.length());
            if (categoryLength > 0) {
                sb.append(category);
                sb.append(CATEGORY_SEPARATOR);
            }
            sb.append(STACK_TRACE_PREFIX);
            sb.append(message);
            System.err.println(sb.toString());
        }
    }

    /**
     * INTERNAL:
     * Check if a message of the given {@code category} and {@code level} would actually be logged by EclipseLink logger
     * or to standard error output.
     * @param level    The log request level value.
     * @param category The EclipseLink logging category.
     * @return Value of {@code true} if message  will be logged or {@code false} otherwise.
     */
    public static final boolean shouldLog(final int level, final String category) {
        return (logEnabled && shouldLogToStdErr(category)) || LOGGER.shouldLog(level, category);
    }

    /**
     * INTERNAL:
     * Log message with no arguments to EclipseLink logger and standard error output.
     * @param level      The log request level value.
     * @param category   The EclipseLink logging category.
     * @param messageKey The {@link TraceLocalizationResource} log message key.
     */
    public static final void log(final int level, final String category, final String messageKey) {
        LOGGER.log(level, category, messageKey, null);
        if (logEnabled && shouldLogToStdErr(category)) {
            logStdErr(category, messageKey);
        }
    }

    /**
     * INTERNAL:
     * Log message with arguments array to EclipseLink logger and standard error output.
     * @param level      The log request level value.
     * @param category   The EclipseLink logging category.
     * @param messageKey {@link TraceLocalizationResource} message key.
     * @param arguments  Arguments of the log message.
     */
    public static final void log(
            final int level, final String category, final String messageKey, final Object... arguments) {
        LOGGER.log(level, category, messageKey, arguments);
        if (logEnabled && shouldLogToStdErr(category)) {
            logStdErr(category, messageKey, arguments);
        }
    }

    /**
     * INTERNAL:
     * Log {@link Throwable} to EclipseLink logger and standard error output.
     * @param level     The log request level value.
     * @param category  The EclipseLink logging category.
     * @param throwable {@link Throwable} to be logged.
     */
    public static final void logThrowable(final int level, final String category, final Throwable throwable) {
        LOGGER.logThrowable(level, category, throwable);
        if (logEnabled && shouldLogToStdErr(category)) {
            logThrowableStdErr(category, throwable);
        }
    }

}
