/*
 * Copyright (c) 2015, 2023, Oracle and/or its affiliates. All rights reserved.
 * ORACLE PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 */

package jdk.internal.logger;

import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.function.BiFunction;
import java.lang.System.LoggerFinder;
import java.lang.System.Logger;
import java.lang.ref.WeakReference;
import java.util.Objects;
import java.util.function.BooleanSupplier;

import jdk.internal.logger.LoggerFinderLoader.TemporaryLoggerFinder;
import jdk.internal.misc.VM;
import sun.util.logging.PlatformLogger;

/**
 * This class is a factory for Lazy Loggers; only system loggers can be
 * Lazy Loggers.
 */
public final class LazyLoggers {

    static final RuntimePermission LOGGERFINDER_PERMISSION =
                new RuntimePermission("loggerFinder");

    private LazyLoggers() {
        throw new InternalError();
    }

    /**
     * This class is used to hold the factories that a Lazy Logger will use
     * to create (or map) its wrapped logger.
     * @param <L> {@link Logger} or a subclass of {@link Logger}.
     */
    private static final class LazyLoggerFactories<L extends Logger> {

        /**
         * A factory method to create an SPI logger.
         * Usually, this will be something like LazyLoggers::getSystemLogger.
         */
        final BiFunction<String, Module, L> loggerSupplier;


        public LazyLoggerFactories(BiFunction<String, Module, L> loggerSupplier) {
            this(Objects.requireNonNull(loggerSupplier),
                 (Void)null);
        }

        private LazyLoggerFactories(BiFunction<String, Module, L> loggerSupplier,
                          Void unused) {
            this.loggerSupplier = loggerSupplier;
        }

    }

    static interface LoggerAccessor {
        /**
         * The logger name.
         * @return The name of the logger that is / will be lazily created.
         */
        public String getLoggerName();

        /**
         * Returns the wrapped logger object.
         * @return the wrapped logger object.
         */
        public Logger wrapped();

        /**
         * A PlatformLogger.Bridge view of the wrapped logger object.
         * @return A PlatformLogger.Bridge view of the wrapped logger object.
         */
        public PlatformLogger.Bridge platform();
    }

    /**
     * The LazyLoggerAccessor class holds all the logic that delays the creation
     * of the SPI logger until such a time that the VM is booted and the logger
     * is actually used for logging.
     *
     * This class uses the services of the BootstrapLogger class to instantiate
     * temporary loggers if appropriate.
     */
    static final class LazyLoggerAccessor implements LoggerAccessor {

        // The factories that will be used to create the logger lazyly
        final LazyLoggerFactories<? extends Logger> factories;

        // We need to pass the actual caller module when creating the logger.
        private final WeakReference<Module> moduleRef;

        // whether this is the loading thread, can be null
        private final BooleanSupplier isLoadingThread;

        // The name of the logger that will be created lazyly
        final String name;
        // The plain logger SPI object - null until it is accessed for the
        // first time.
        private volatile Logger w;
        // A PlatformLogger.Bridge view of w.
        private volatile PlatformLogger.Bridge p;


        private LazyLoggerAccessor(String name,
                                   LazyLoggerFactories<? extends Logger> factories,
                                   Module module) {
            this(name, factories, module, null);
        }

        private LazyLoggerAccessor(String name,
                                   LazyLoggerFactories<? extends Logger> factories,
                                   Module module, BooleanSupplier isLoading) {

            this(Objects.requireNonNull(name), Objects.requireNonNull(factories),
                    Objects.requireNonNull(module), isLoading, null);
        }

        private LazyLoggerAccessor(String name,
                                   LazyLoggerFactories<? extends Logger> factories,
                                   Module module, BooleanSupplier isLoading, Void unused) {
            this.name = name;
            this.factories = factories;
            this.moduleRef = new WeakReference<>(module);
            this.isLoadingThread = isLoading;
        }

        /**
         * The logger name.
         * @return The name of the logger that is / will be lazily created.
         */
        @Override
        public String getLoggerName() {
            return name;
        }

        // must be called in synchronized block
        // set wrapped logger if not set
        private void setWrappedIfNotSet(Logger wrapped) {
            if (w == null) {
                w = wrapped;
            }
        }

        /**
         * Returns the logger SPI object, creating it if 'w' is still null.
         * @return the logger SPI object.
         */
        public Logger wrapped() {
            Logger wrapped = w;
            if (wrapped != null) return wrapped;
            // Wrapped logger not created yet: create it.
            // BootstrapLogger has the logic to decide whether to invoke the
            // SPI or use a temporary (BootstrapLogger or SimpleConsoleLogger)
            // logger.
            wrapped = BootstrapLogger.getLogger(this, isLoadingThread);
            synchronized(this) {
                // if w has already been in between, simply drop 'wrapped'.
                setWrappedIfNotSet(wrapped);
                return w;
            }
        }

        /**
         * A PlatformLogger.Bridge view of the wrapped logger.
         * @return A PlatformLogger.Bridge view of the wrapped logger.
         */
        public PlatformLogger.Bridge platform() {
            // We can afford to return the platform view of the previous
            // logger - if that view is not null.
            // Because that view will either be the BootstrapLogger, which
            // will redirect to the new wrapper properly, or the temporary
            // logger - which in effect is equivalent to logging something
            // just before the application initialized LogManager.
            PlatformLogger.Bridge platform = p;
            if (platform != null) return platform;
            synchronized (this) {
                if (w != null) {
                    if (p == null) p = PlatformLogger.Bridge.convert(w);
                    return p;
                }
            }
            // If we reach here it means that the wrapped logger may not
            // have been created yet: attempt to create it.
            // BootstrapLogger has the logic to decide whether to invoke the
            // SPI or use a temporary (BootstrapLogger or SimpleConsoleLogger)
            // logger.
            final Logger wrapped = BootstrapLogger.getLogger(this, isLoadingThread);
            synchronized(this) {
                // if w has already been set, simply drop 'wrapped'.
                setWrappedIfNotSet(wrapped);
                if (p == null) p = PlatformLogger.Bridge.convert(w);
                return p;
            }
        }

        /**
         * Makes this accessor release a temporary logger.
         * This method is called
         * by BootstrapLogger when JUL is the default backend and LogManager
         * is initialized, in order to replace temporary SimpleConsoleLoggers by
         * real JUL loggers. See BootstrapLogger for more details.
         * If {@code replace} is {@code true}, then this method will force
         * the accessor to eagerly recreate its wrapped logger.
         * Note: passing {@code replace=false} is no guarantee that the
         * method will not actually replace the released logger.
         * @param temporary The temporary logger too be released.
         * @param replace   Whether the released logger should be eagerly
         *                  replaced.
         */
        void release(SimpleConsoleLogger temporary, boolean replace) {
            PlatformLogger.ConfigurableBridge.LoggerConfiguration conf =
                PlatformLogger.ConfigurableBridge.getLoggerConfiguration(temporary);
            PlatformLogger.Level level = conf != null
                    ? conf.getPlatformLevel()
                    : null;
            synchronized (this) {
                if (this.w == temporary) {
                    this.w = null; this.p = null;
                }
            }
            PlatformLogger.Bridge platform =  replace || level != null
                    ? this.platform() : null;

            if (level != null) {
                conf = (platform != null && platform != temporary)
                        ? PlatformLogger.ConfigurableBridge.getLoggerConfiguration(platform)
                        : null;
                if (conf != null) conf.setPlatformLevel(level);
            }
        }

        /**
         * Replace 'w' by the real SPI logger and flush the log messages pending
         * in the temporary 'bootstrap' Logger. Called by BootstrapLogger when
         * this accessor's bootstrap logger is accessed and BootstrapLogger
         * notices that the VM is no longer booting.
         * @param bootstrap This accessor's bootstrap logger (usually this is 'w').
         */
        Logger getConcreteLogger(BootstrapLogger bootstrap) {
            assert VM.isBooted();
            synchronized(this) {
                // another thread may have already invoked flush()
                if (this.w == bootstrap) {
                    this.w = null; this.p = null;
                }
            }
            return this.wrapped();
        }

        PlatformLogger.Bridge getConcretePlatformLogger(BootstrapLogger bootstrap) {
            assert VM.isBooted();
            synchronized(this) {
                // another thread may have already invoked flush()
                if (this.w == bootstrap) {
                    this.w = null; this.p = null;
                }
            }
            return this.platform();
        }

        // Creates the wrapped logger by invoking the SPI.
        Logger createLogger() {
            final Module module = moduleRef.get();
            if (module == null) {
                throw new IllegalStateException("The module for which this logger"
                        + " was created has been garbage collected");
            }
            return this.factories.loggerSupplier.apply(name, module);
        }

        /**
         * Creates a new lazy logger accessor for the named logger. The given
         * factories will be use when it becomes necessary to actually create
         * the logger.
         * @param name The logger name.
         * @param factories The factories that should be used to create the
         *                  wrapped logger.
         * @param module The module for which the logger is being created
         * @return A new LazyLoggerAccessor.
         */
        public static LazyLoggerAccessor makeAccessor(String name,
                LazyLoggerFactories<? extends Logger> factories, Module module) {
                return new LazyLoggerAccessor(name, factories, module);
        }

    }

    /**
     * An implementation of {@link Logger} that redirects all calls to a wrapped
     * instance of {@code Logger}.
     */
    private static class LazyLoggerWrapper
        extends AbstractLoggerWrapper<Logger> {

        final LoggerAccessor loggerAccessor;

        public LazyLoggerWrapper(LazyLoggerAccessor loggerSinkSupplier) {
            this(Objects.requireNonNull(loggerSinkSupplier), (Void)null);
        }

        private LazyLoggerWrapper(LazyLoggerAccessor loggerSinkSupplier,
                Void unused) {
            this.loggerAccessor = loggerSinkSupplier;
        }

        @Override
        final Logger wrapped() {
            return loggerAccessor.wrapped();
        }

        @Override
        PlatformLogger.Bridge platformProxy() {
            return loggerAccessor.platform();
        }

    }

    // Do not expose this outside of this package.
    private static volatile LoggerFinder provider;
    @SuppressWarnings("removal")
    private static LoggerFinder accessLoggerFinder() {
        LoggerFinder prov = provider;
        if (prov == null) {
            // no need to lock: it doesn't matter if we call
            // getLoggerFinder() twice - since LoggerFinder already caches
            // the result.
            // This is just an optimization to avoid the cost of calling
            // doPrivileged every time.
            final SecurityManager sm = System.getSecurityManager();
            prov = sm == null ? LoggerFinder.getLoggerFinder() :
                AccessController.doPrivileged(
                        (PrivilegedAction<LoggerFinder>)LoggerFinder::getLoggerFinder);
            if (prov instanceof TemporaryLoggerFinder) return prov;
            provider = prov;
        }
        return prov;
    }

    // Avoid using lambda here as lazy loggers could be created early
    // in the bootstrap sequence...
    private static final BiFunction<String, Module, Logger> loggerSupplier =
           new BiFunction<>() {
        @Override
        public Logger apply(String name, Module module) {
            return LazyLoggers.getLoggerFromFinder(name, module);
        }
    };

    private static final LazyLoggerFactories<Logger> factories =
           new LazyLoggerFactories<>(loggerSupplier);


    // A concrete implementation of Logger that delegates to a  System.Logger,
    // but only creates the System.Logger instance lazily when it's used for
    // the first time.
    // The JdkLazyLogger uses a LazyLoggerAccessor objects, which relies
    // on the logic embedded in BootstrapLogger to avoid loading the concrete
    // logger provider until the VM has finished booting.
    //
    private static final class JdkLazyLogger extends LazyLoggerWrapper {
        JdkLazyLogger(String name, Module module) {
            this(LazyLoggerAccessor.makeAccessor(name, factories, module),
                 (Void)null);
        }
        private JdkLazyLogger(LazyLoggerAccessor holder, Void unused) {
            super(holder);
        }
    }

    static Logger makeLazyLogger(String name, Module module, BooleanSupplier isLoading) {
        final LazyLoggerAccessor holder = new LazyLoggerAccessor(name, factories, module, isLoading);
        return new JdkLazyLogger(holder, null);
    }

    /**
     * Gets a logger from the LoggerFinder. Creates the actual concrete
     * logger.
     * @param name    name of the logger
     * @param module  module on behalf of which the logger is created
     * @return  The logger returned by the LoggerFinder.
     */
    @SuppressWarnings("removal")
    static Logger getLoggerFromFinder(String name, Module module) {
        final SecurityManager sm = System.getSecurityManager();
        if (sm == null) {
            return accessLoggerFinder().getLogger(name, module);
        } else {
            return AccessController.doPrivileged((PrivilegedAction<Logger>)
                    () -> {return accessLoggerFinder().getLogger(name, module);},
                    null, LOGGERFINDER_PERMISSION);
        }
    }

    /**
     * Returns a (possibly lazy) Logger for the caller.
     *
     * @param name the logger name
     * @param module The module on behalf of which the logger is created.
     *               If the module is not loaded from the Boot ClassLoader,
     *               the LoggerFinder is accessed and the logger returned
     *               by {@link LoggerFinder#getLogger(java.lang.String, java.lang.Module)}
     *               is returned to the caller directly.
     *               Otherwise, the logger returned by
     *               {@link #getLazyLogger(java.lang.String, java.lang.Module)}
     *               is returned to the caller.
     *
     * @return  a (possibly lazy) Logger instance.
     */
    public static final Logger getLogger(String name, Module module) {
        if (DefaultLoggerFinder.isSystem(module)) {
            return getLazyLogger(name, module);
        } else {
            return getLoggerFromFinder(name, module);
        }
    }

    /**
     * Returns a (possibly lazy) Logger suitable for system classes.
     * Whether the returned logger is lazy or not depend on the result
     * returned by {@link BootstrapLogger#useLazyLoggers()}.
     *
     * @param name the logger name
     * @param module the module on behalf of which the logger is created.
     * @return  a (possibly lazy) Logger instance.
     */
    public static final Logger getLazyLogger(String name, Module module) {

        // BootstrapLogger has the logic to determine whether a LazyLogger
        // should be used. Usually, it is worth it only if:
        //   - the VM is not yet booted
        //   - or, the backend is JUL and there is no configuration
        //   - or, the backend is a custom backend, as we don't know what
        //     that is going to load...
        // So if for instance the VM is booted and we use JUL with a custom
        // configuration, we're not going to delay the creation of loggers...
        final boolean useLazyLogger = BootstrapLogger.useLazyLoggers();
        if (useLazyLogger) {
            return new JdkLazyLogger(name, module);
        } else {
            // Directly invoke the LoggerFinder.
            return getLoggerFromFinder(name, module);
        }
    }

}
