/*
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.addthis.basis.jvm;

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
import java.util.function.Supplier;

import com.google.common.annotations.Beta;
import com.google.common.util.concurrent.Uninterruptibles;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import sun.misc.SharedSecrets;

/**
 * Utility methods related to JVM shutdown. The primary features are an exit method that won't cause JVM hangs, and
 * a utility for enforcing semantics for "if this object is made, it _must_ be closed" (or similar). There are a couple
 * extras as well, and I have a few fairly good ideas for the future: a try-with-resources type object that ensures
 * the JVM won't effectively halt until released might be easier than the function-passing method, and generalization
 * such that it isn't necessarily tied to the actual Runtime (and reliant on all static methods). However, this more
 * than suffices for now.
 */
@Beta
public final class Shutdown {
    private static final Logger log = LoggerFactory.getLogger(Shutdown.class);

    private static final AtomicBoolean CALLED_EXIT_SELF = new AtomicBoolean(false);
    private static final AtomicInteger EXIT_HOLDER = new AtomicInteger(0);
    private static final AtomicReference<Thread> EXIT_THREAD = new AtomicReference<>(null);
    private static final boolean HOOK_ADDED;
    private static final boolean SENTINAL_ADDED;
    static {
        boolean hookAdded = false;
        try {
            SharedSecrets.getJavaLangAccess().registerShutdownHook(3, true, () -> {
                if (EXIT_HOLDER.get() != 0) {
                    Runtime.getRuntime().halt(EXIT_HOLDER.get());
                }
            });
            hookAdded = true;
        } catch (Throwable t) {
            log.error("Could not register super-shutdown hook deluxe", t);
        }
        HOOK_ADDED = hookAdded;
        if (!HOOK_ADDED) {
            Thread sentinelHook = new Thread(() -> {
                log.debug("Shutdown has already started, our secret-weapon hook did not work, and we don't know who " +
                          "started the shutdown or what exit code they will try to use! Therefore, we will make this " +
                          "shutdown hook thread block another 2 seconds and then halt the jvm ourselves.");
                Uninterruptibles.sleepUninterruptibly(2, TimeUnit.SECONDS);
                Runtime.getRuntime().halt(EXIT_HOLDER.get());
            }, "JVM Shutdown Sentinel") {
                @Override public synchronized void start() {
                    Thread invokingThread = Thread.currentThread();
                    // if the thread calling "exit" logic does not seem to be ours, then we cannot trust its exit value
                    if (!invokingThread.equals(EXIT_THREAD.get()) && (EXIT_HOLDER.get() != 0)) {
                        super.start();
                    }
                }
            };
            SENTINAL_ADDED = tryAddShutdownHook(sentinelHook);
        } else {
            SENTINAL_ADDED = false;
        }
    }

    public static void exit(int exitCode) {
        boolean exitBecameNonZero;
        if (exitCode != 0) {
            log.warn("Calling exit with non-zero exit code: {}", exitCode);
            exitBecameNonZero = EXIT_HOLDER.compareAndSet(0, exitCode);
        } else {
            exitBecameNonZero = false;
        }
        int prevExitCode;
        if (exitBecameNonZero) {
            prevExitCode = 0;
        } else {
            prevExitCode = EXIT_HOLDER.get();
        }
        if (!CALLED_EXIT_SELF.compareAndSet(false, true)) {
            if (exitBecameNonZero && HOOK_ADDED) {
                log.warn("Changing exit code to: {}", exitCode);
            } else if (exitBecameNonZero) {
                log.error("Previously, we already started shutdown with a normal exit code! Furthermore, our mega-" +
                          "hook did not get set up right! Halting the JVM immediately to try to ensure error is seen.");
                Runtime.getRuntime().halt(exitCode);
            } else {
                log.error("Tried to call exit with exit code {}, but someone else already called with {}.",
                          exitCode, prevExitCode);
            }
            return;
        }
        if (HOOK_ADDED || SENTINAL_ADDED) {
            Thread exitThread =
                    new Thread(() -> Runtime.getRuntime().exit(exitCode), "JVM Exit Thread (code: " + exitCode + ")");
            EXIT_THREAD.set(exitThread);
            exitThread.start();
        } else if (exitCode != 0) {
            log.error(
                    "Our secret weapon must have failed, _and_ either the JVM started shutting down before this class" +
                    " was ever initialized (try to load it sooner!) or some other very bad thing has occured. In any " +
                    "event, since this is an \"exit\" call anyway, we are just going to halt now because this is the " +
                    "best effort attempt we can make.");
            Runtime.getRuntime().halt(exitCode);
        }
    }

    public static <T> void createWithShutdownHook(Supplier<T> creator, Consumer<T> hook) {
        CompletableFuture<Runnable> creationFuture = new CompletableFuture<>();
        Thread shutdownHook = new Thread(creationFuture.join()::run, "Shutdown Hook for " + creator);
        boolean hookAdded = tryAddShutdownHook(shutdownHook);
        if (hookAdded) {
            try {
                T created = creator.get();
                creationFuture.complete(() -> hook.accept(created));
            } catch (Throwable t) {
                if (!tryRemoveShutdownHook(shutdownHook)) {
                    creationFuture.complete(
                            () -> log.debug("skipping hook: {} because creator: {} failed to run normally",
                                            hook, creator));
                }
                throw t;
            }
        }
    }

    public static boolean tryAddShutdownHook(Thread shutdownHook) {
        try {
            Runtime.getRuntime().addShutdownHook(shutdownHook);
            return true;
        } catch (IllegalStateException logged) {
            log.debug("adding shutdown hook failed", logged);
            return false;
        }
    }

    public static boolean tryRemoveShutdownHook(Thread shutdownHook) {
        try {
            return Runtime.getRuntime().removeShutdownHook(shutdownHook);
        } catch (IllegalStateException logged) {
            log.debug("removing shutdown hook failed", logged);
            return false;
        }
    }

    private Shutdown() {}
}

@@ -6066,16 +6066,22 @@
  Thread(
+() -%3E 
 creation
@@ -6097,13 +6097,14 @@
 in()
-::
+.
 run
+()
 , %22S

fix hang in Shutdown.createWithShutdownHook