// 
// Decompiled by Procyon v0.6.0
// 

package io.sentry;

import java.util.concurrent.atomic.AtomicReference;
import org.jetbrains.annotations.ApiStatus;
import io.sentry.hints.TransactionEnd;
import io.sentry.hints.SessionEnd;
import io.sentry.hints.BlockingFlushHint;
import java.util.Set;
import java.util.HashSet;
import org.jetbrains.annotations.TestOnly;
import io.sentry.exception.ExceptionMechanismException;
import io.sentry.protocol.Mechanism;
import io.sentry.hints.EventDropReason;
import io.sentry.protocol.SentryId;
import io.sentry.util.HintUtils;
import io.sentry.util.IntegrationUtils;
import io.sentry.util.Objects;
import org.jetbrains.annotations.NotNull;
import io.sentry.util.AutoClosableReentrantLock;
import org.jetbrains.annotations.Nullable;
import java.io.Closeable;

public final class UncaughtExceptionHandlerIntegration implements Integration, Thread.UncaughtExceptionHandler, Closeable
{
    @Nullable
    private Thread.UncaughtExceptionHandler defaultExceptionHandler;
    @NotNull
    private static final AutoClosableReentrantLock lock;
    @Nullable
    private IScopes scopes;
    @Nullable
    private SentryOptions options;
    private boolean registered;
    @NotNull
    private final io.sentry.UncaughtExceptionHandler threadAdapter;
    
    public UncaughtExceptionHandlerIntegration() {
        this(io.sentry.UncaughtExceptionHandler.Adapter.getInstance());
    }
    
    UncaughtExceptionHandlerIntegration(@NotNull final io.sentry.UncaughtExceptionHandler threadAdapter) {
        this.registered = false;
        this.threadAdapter = Objects.requireNonNull(threadAdapter, "threadAdapter is required.");
    }
    
    @Override
    public final void register(@NotNull final IScopes scopes, @NotNull final SentryOptions options) {
        if (this.registered) {
            options.getLogger().log(SentryLevel.ERROR, "Attempt to register a UncaughtExceptionHandlerIntegration twice.", new Object[0]);
            return;
        }
        this.registered = true;
        this.scopes = Objects.requireNonNull(scopes, "Scopes are required");
        this.options = Objects.requireNonNull(options, "SentryOptions is required");
        this.options.getLogger().log(SentryLevel.DEBUG, "UncaughtExceptionHandlerIntegration enabled: %s", this.options.isEnableUncaughtExceptionHandler());
        if (this.options.isEnableUncaughtExceptionHandler()) {
            try (final ISentryLifecycleToken ignored = UncaughtExceptionHandlerIntegration.lock.acquire()) {
                final Thread.UncaughtExceptionHandler currentHandler = this.threadAdapter.getDefaultUncaughtExceptionHandler();
                if (currentHandler != null) {
                    this.options.getLogger().log(SentryLevel.DEBUG, "default UncaughtExceptionHandler class='" + currentHandler.getClass().getName() + "'", new Object[0]);
                    if (currentHandler instanceof UncaughtExceptionHandlerIntegration) {
                        final UncaughtExceptionHandlerIntegration currentHandlerIntegration = (UncaughtExceptionHandlerIntegration)currentHandler;
                        if (currentHandlerIntegration.scopes != null && scopes.getGlobalScope() == currentHandlerIntegration.scopes.getGlobalScope()) {
                            this.defaultExceptionHandler = currentHandlerIntegration.defaultExceptionHandler;
                        }
                        else {
                            this.defaultExceptionHandler = currentHandler;
                        }
                    }
                    else {
                        this.defaultExceptionHandler = currentHandler;
                    }
                }
                this.threadAdapter.setDefaultUncaughtExceptionHandler(this);
            }
            this.options.getLogger().log(SentryLevel.DEBUG, "UncaughtExceptionHandlerIntegration installed.", new Object[0]);
            IntegrationUtils.addIntegrationToSdkVersion("UncaughtExceptionHandler");
        }
    }
    
    @Override
    public void uncaughtException(final Thread thread, final Throwable thrown) {
        if (this.options != null && this.scopes != null) {
            this.options.getLogger().log(SentryLevel.INFO, "Uncaught exception received.", new Object[0]);
            try {
                final UncaughtExceptionHint exceptionHint = new UncaughtExceptionHint(this.options.getFlushTimeoutMillis(), this.options.getLogger());
                final Throwable throwable = getUnhandledThrowable(thread, thrown);
                final SentryEvent event = new SentryEvent(throwable);
                event.setLevel(SentryLevel.FATAL);
                final ITransaction transaction = this.scopes.getTransaction();
                if (transaction == null && event.getEventId() != null) {
                    exceptionHint.setFlushable(event.getEventId());
                }
                final Hint hint = HintUtils.createWithTypeCheckHint(exceptionHint);
                final SentryId sentryId = this.scopes.captureEvent(event, hint);
                final boolean isEventDropped = sentryId.equals(SentryId.EMPTY_ID);
                final EventDropReason eventDropReason = HintUtils.getEventDropReason(hint);
                if ((!isEventDropped || EventDropReason.MULTITHREADED_DEDUPLICATION.equals(eventDropReason)) && !exceptionHint.waitFlush()) {
                    this.options.getLogger().log(SentryLevel.WARNING, "Timed out waiting to flush event to disk before crashing. Event: %s", event.getEventId());
                }
            }
            catch (final Throwable e) {
                this.options.getLogger().log(SentryLevel.ERROR, "Error sending uncaught exception to Sentry.", e);
            }
            if (this.defaultExceptionHandler != null) {
                this.options.getLogger().log(SentryLevel.INFO, "Invoking inner uncaught exception handler.", new Object[0]);
                this.defaultExceptionHandler.uncaughtException(thread, thrown);
            }
            else if (this.options.isPrintUncaughtStackTrace()) {
                thrown.printStackTrace();
            }
        }
    }
    
    @TestOnly
    @NotNull
    static Throwable getUnhandledThrowable(@NotNull final Thread thread, @NotNull final Throwable thrown) {
        final Mechanism mechanism = new Mechanism();
        mechanism.setHandled(false);
        mechanism.setType("UncaughtExceptionHandler");
        return new ExceptionMechanismException(mechanism, thrown, thread);
    }
    
    @Override
    public void close() {
        try (final ISentryLifecycleToken ignored = UncaughtExceptionHandlerIntegration.lock.acquire()) {
            if (this == this.threadAdapter.getDefaultUncaughtExceptionHandler()) {
                this.threadAdapter.setDefaultUncaughtExceptionHandler(this.defaultExceptionHandler);
                if (this.options != null) {
                    this.options.getLogger().log(SentryLevel.DEBUG, "UncaughtExceptionHandlerIntegration removed.", new Object[0]);
                }
            }
            else {
                this.removeFromHandlerTree(this.threadAdapter.getDefaultUncaughtExceptionHandler());
            }
        }
    }
    
    private void removeFromHandlerTree(@Nullable final Thread.UncaughtExceptionHandler currentHandler) {
        this.removeFromHandlerTree(currentHandler, new HashSet<Thread.UncaughtExceptionHandler>());
    }
    
    private void removeFromHandlerTree(@Nullable final Thread.UncaughtExceptionHandler currentHandler, @NotNull final Set<Thread.UncaughtExceptionHandler> visited) {
        if (currentHandler == null) {
            if (this.options != null) {
                this.options.getLogger().log(SentryLevel.DEBUG, "Found no UncaughtExceptionHandler to remove.", new Object[0]);
            }
            return;
        }
        if (!visited.add(currentHandler)) {
            if (this.options != null) {
                this.options.getLogger().log(SentryLevel.WARNING, "Cycle detected in UncaughtExceptionHandler chain while removing handler.", new Object[0]);
            }
            return;
        }
        if (!(currentHandler instanceof UncaughtExceptionHandlerIntegration)) {
            return;
        }
        final UncaughtExceptionHandlerIntegration currentHandlerIntegration = (UncaughtExceptionHandlerIntegration)currentHandler;
        if (this == currentHandlerIntegration.defaultExceptionHandler) {
            currentHandlerIntegration.defaultExceptionHandler = this.defaultExceptionHandler;
            if (this.options != null) {
                this.options.getLogger().log(SentryLevel.DEBUG, "UncaughtExceptionHandlerIntegration removed.", new Object[0]);
            }
        }
        else {
            this.removeFromHandlerTree(currentHandlerIntegration.defaultExceptionHandler, visited);
        }
    }
    
    static {
        lock = new AutoClosableReentrantLock();
    }
    
    @ApiStatus.Internal
    public static class UncaughtExceptionHint extends BlockingFlushHint implements SessionEnd, TransactionEnd
    {
        private final AtomicReference<SentryId> flushableEventId;
        
        public UncaughtExceptionHint(final long flushTimeoutMillis, @NotNull final ILogger logger) {
            super(flushTimeoutMillis, logger);
            this.flushableEventId = new AtomicReference<SentryId>();
        }
        
        @Override
        public boolean isFlushable(@Nullable final SentryId eventId) {
            final SentryId unwrapped = this.flushableEventId.get();
            return unwrapped != null && unwrapped.equals(eventId);
        }
        
        @Override
        public void setFlushable(@NotNull final SentryId eventId) {
            this.flushableEventId.set(eventId);
        }
    }
}
