//
//  SentryCrashMonitor.c
//
//  Created by Karl Stenerud on 2012-02-12.
//
//  Copyright (c) 2012 Karl Stenerud. All rights reserved.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall remain in place
// in this source code.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
//


#include "SentryCrashMonitor.h"
#include "SentryCrashMonitorContext.h"
#include "SentryCrashMonitorType.h"

#include "SentryCrashMonitor_Deadlock.h"
#include "SentryCrashMonitor_MachException.h"
#include "SentryCrashMonitor_CPPException.h"
#include "SentryCrashMonitor_NSException.h"
#include "SentryCrashMonitor_Signal.h"
#include "SentryCrashMonitor_System.h"
#include "SentryCrashMonitor_User.h"
#include "SentryCrashMonitor_AppState.h"
#include "SentryCrashMonitor_Zombie.h"
#include "SentryCrashDebug.h"
#include "SentryCrashThread.h"
#include "SentryCrashSystemCapabilities.h"

#include <memory.h>

//#define SentryCrashLogger_LocalLevel TRACE
#include "SentryCrashLogger.h"


// ============================================================================
#pragma mark - Globals -
// ============================================================================

typedef struct
{
    SentryCrashMonitorType monitorType;
    SentryCrashMonitorAPI* (*getAPI)(void);
} Monitor;

static Monitor g_monitors[] =
{
#if SentryCrashCRASH_HAS_MACH
    {
        .monitorType = SentryCrashMonitorTypeMachException,
        .getAPI = sentrycrashcm_machexception_getAPI,
    },
#endif
#if SentryCrashCRASH_HAS_SIGNAL
    {
        .monitorType = SentryCrashMonitorTypeSignal,
        .getAPI = sentrycrashcm_signal_getAPI,
    },
#endif
#if SentryCrashCRASH_HAS_OBJC
    {
        .monitorType = SentryCrashMonitorTypeNSException,
        .getAPI = sentrycrashcm_nsexception_getAPI,
    },
    {
        .monitorType = SentryCrashMonitorTypeMainThreadDeadlock,
        .getAPI = sentrycrashcm_deadlock_getAPI,
    },
    {
        .monitorType = SentryCrashMonitorTypeZombie,
        .getAPI = sentrycrashcm_zombie_getAPI,
    },
#endif
    {
        .monitorType = SentryCrashMonitorTypeCPPException,
        .getAPI = sentrycrashcm_cppexception_getAPI,
    },
    {
        .monitorType = SentryCrashMonitorTypeUserReported,
        .getAPI = sentrycrashcm_user_getAPI,
    },
    {
        .monitorType = SentryCrashMonitorTypeSystem,
        .getAPI = sentrycrashcm_system_getAPI,
    },
    {
        .monitorType = SentryCrashMonitorTypeApplicationState,
        .getAPI = sentrycrashcm_appstate_getAPI,
    },
};
static int g_monitorsCount = sizeof(g_monitors) / sizeof(*g_monitors);

static SentryCrashMonitorType g_activeMonitors = SentryCrashMonitorTypeNone;

static bool g_handlingFatalException = false;
static bool g_crashedDuringExceptionHandling = false;
static bool g_requiresAsyncSafety = false;

static void (*g_onExceptionEvent)(struct SentryCrash_MonitorContext* monitorContext);

// ============================================================================
#pragma mark - API -
// ============================================================================

static inline SentryCrashMonitorAPI* getAPI(Monitor* monitor)
{
    if(monitor != NULL && monitor->getAPI != NULL)
    {
        return monitor->getAPI();
    }
    return NULL;
}

static inline void setMonitorEnabled(Monitor* monitor, bool isEnabled)
{
    SentryCrashMonitorAPI* api = getAPI(monitor);
    if(api != NULL && api->setEnabled != NULL)
    {
        api->setEnabled(isEnabled);
    }
}

static inline bool isMonitorEnabled(Monitor* monitor)
{
    SentryCrashMonitorAPI* api = getAPI(monitor);
    if(api != NULL && api->isEnabled != NULL)
    {
        return api->isEnabled();
    }
    return false;
}

static inline void addContextualInfoToEvent(Monitor* monitor, struct SentryCrash_MonitorContext* eventContext)
{
    SentryCrashMonitorAPI* api = getAPI(monitor);
    if(api != NULL && api->addContextualInfoToEvent != NULL)
    {
        api->addContextualInfoToEvent(eventContext);
    }
}

void sentrycrashcm_setEventCallback(void (*onEvent)(struct SentryCrash_MonitorContext* monitorContext))
{
    g_onExceptionEvent = onEvent;
}

void sentrycrashcm_setActiveMonitors(SentryCrashMonitorType monitorTypes)
{
    if(sentrycrashdebug_isBeingTraced() && (monitorTypes & SentryCrashMonitorTypeDebuggerUnsafe))
    {
        static bool hasWarned = false;
        if(!hasWarned)
        {
            hasWarned = true;
            SentryCrashLOGBASIC_WARN("    ************************ Crash Handler Notice ************************");
            SentryCrashLOGBASIC_WARN("    *     App is running in a debugger. Masking out unsafe monitors.     *");
            SentryCrashLOGBASIC_WARN("    * This means that most crashes WILL NOT BE RECORDED while debugging! *");
            SentryCrashLOGBASIC_WARN("    **********************************************************************");
        }
        monitorTypes &= SentryCrashMonitorTypeDebuggerSafe;
    }
    if(g_requiresAsyncSafety && (monitorTypes & SentryCrashMonitorTypeAsyncUnsafe))
    {
        SentryCrashLOG_DEBUG("Async-safe environment detected. Masking out unsafe monitors.");
        monitorTypes &= SentryCrashMonitorTypeAsyncSafe;
    }

    SentryCrashLOG_DEBUG("Changing active monitors from 0x%x tp 0x%x.", g_activeMonitors, monitorTypes);

    SentryCrashMonitorType activeMonitors = SentryCrashMonitorTypeNone;
    for(int i = 0; i < g_monitorsCount; i++)
    {
        Monitor* monitor = &g_monitors[i];
        bool isEnabled = monitor->monitorType & monitorTypes;
        setMonitorEnabled(monitor, isEnabled);
        if(isMonitorEnabled(monitor))
        {
            activeMonitors |= monitor->monitorType;
        }
        else
        {
            activeMonitors &= ~monitor->monitorType;
        }
    }

    SentryCrashLOG_DEBUG("Active monitors are now 0x%x.", activeMonitors);
    g_activeMonitors = activeMonitors;
}

SentryCrashMonitorType sentrycrashcm_getActiveMonitors()
{
    return g_activeMonitors;
}


// ============================================================================
#pragma mark - Private API -
// ============================================================================

bool sentrycrashcm_notifyFatalExceptionCaptured(bool isAsyncSafeEnvironment)
{
    g_requiresAsyncSafety |= isAsyncSafeEnvironment; // Don't let it be unset.
    if(g_handlingFatalException)
    {
        g_crashedDuringExceptionHandling = true;
    }
    g_handlingFatalException = true;
    if(g_crashedDuringExceptionHandling)
    {
        SentryCrashLOG_INFO("Detected crash in the crash reporter. Uninstalling SentryCrash.");
        sentrycrashcm_setActiveMonitors(SentryCrashMonitorTypeNone);
    }
    return g_crashedDuringExceptionHandling;
}

void sentrycrashcm_handleException(struct SentryCrash_MonitorContext* context)
{
    context->requiresAsyncSafety = g_requiresAsyncSafety;
    if(g_crashedDuringExceptionHandling)
    {
        context->crashedDuringCrashHandling = true;
    }
    for(int i = 0; i < g_monitorsCount; i++)
    {
        Monitor* monitor = &g_monitors[i];
        if(isMonitorEnabled(monitor))
        {
            addContextualInfoToEvent(monitor, context);
        }
    }

    g_onExceptionEvent(context);

    if (context->currentSnapshotUserReported) {
        g_handlingFatalException = false;
    } else {
        if(g_handlingFatalException && !g_crashedDuringExceptionHandling) {
            SentryCrashLOG_DEBUG("Exception is fatal. Restoring original handlers.");
            sentrycrashcm_setActiveMonitors(SentryCrashMonitorTypeNone);
        }
    }
}
