#define NX_SERVICE_ASSUME_NON_DOMAIN
#include <string.h>
#include <stdatomic.h>
#include "service_guard.h"
#include "runtime/hosversion.h"
#include "kernel/shmem.h"
#include "kernel/tmem.h"
#include "services/applet.h"
#include "services/irs.h"
#include "applets/hid_la.h"

typedef struct {
    IrsIrSensorMode mode;
    u32 internal_status;
    IrsIrCameraHandle handle;
    TransferMemory transfermem;
    bool version_check;

    u8 is_negative_image_used;
    u8 format;
    IrsRect window_of_interest;
} IrsCameraEntry;

typedef struct {
    s64 sampling_number;
    u32 prefix_data;
    u32 prefix_bitcount;
} IrsTeraFilterArg;

static Service g_irsSrv;
static SharedMemory g_irsSharedmem;
static IrsPackedFunctionLevel g_irsFunctionLevel; // In sdknso there's various funcs which get the FunctionLevel, but the only ones which actually use the loaded data is the Run*Processor funcs.

static IrsPackedMcuVersion g_irsRequiredMcuVersion;

static IrsCameraEntry g_irsCameras[IRS_MAX_CAMERAS];

static const size_t g_irsImageFormatSizes[] = {
    [IrsImageTransferProcessorFormat_320x240]    = 320*240,
    [IrsImageTransferProcessorFormat_160x120]    = 160*120,
    [IrsImageTransferProcessorFormat_80x60]      = 80*60,
    [IrsImageTransferProcessorFormat_40x30]      = 40*30,
    [IrsImageTransferProcessorFormat_20x15]      = 20*15,
};

static const Result g_irsCameraStatusResults[] = {
    [IrsIrCameraStatus_Available]   = MAKERESULT(205, 160),
    [IrsIrCameraStatus_Unsupported] = MAKERESULT(205, 111),
    [IrsIrCameraStatus_Unconnected] = MAKERESULT(205, 110),
};

static Result _irsActivateIrsensor(bool activate);

static Result _irsGetIrsensorSharedMemoryHandle(Handle* handle_out);

static Result _irsCheckFirmwareVersion(IrsIrCameraHandle handle, IrsPackedMcuVersion version);

static Result _irsActivateIrsensorWithFunctionLevel(IrsPackedFunctionLevel level);

NX_GENERATE_SERVICE_GUARD(irs);

Result _irsInitialize(void) {
    Result rc=0;
    Handle sharedmem_handle;

    memset(g_irsCameras, 0, sizeof(g_irsCameras));
    for (u32 i=0; i<IRS_MAX_CAMERAS; i++) g_irsCameras[i].version_check = 1;

    g_irsRequiredMcuVersion = (IrsPackedMcuVersion){.major_version = 0x3, .minor_version = 0xB};

    if (hosversionAtLeast(4,0,0))
        g_irsRequiredMcuVersion = (IrsPackedMcuVersion){.major_version = 0x4, .minor_version = 0x12};
    if (hosversionAtLeast(5,0,0))
        g_irsRequiredMcuVersion = (IrsPackedMcuVersion){.major_version = 0x5, .minor_version = 0x18};
    if (hosversionAtLeast(6,0,0))
        g_irsRequiredMcuVersion = (IrsPackedMcuVersion){.major_version = 0x6, .minor_version = 0x1A};
    if (hosversionAtLeast(8,0,0))
        g_irsRequiredMcuVersion = (IrsPackedMcuVersion){.major_version = 0x8, .minor_version = 0x1B};

    g_irsFunctionLevel.ir_sensor_function_level = 0x0;

    if (hosversionAtLeast(4,0,0))
        g_irsFunctionLevel.ir_sensor_function_level = 0x1;
    if (hosversionAtLeast(5,0,0))
        g_irsFunctionLevel.ir_sensor_function_level = 0x2;
    if (hosversionAtLeast(6,0,0))
        g_irsFunctionLevel.ir_sensor_function_level = 0x3;

    rc = smGetService(&g_irsSrv, "irs");
    if (R_FAILED(rc))
        return rc;

    if (hosversionBefore(4,0,0))
        rc = _irsActivateIrsensor(1);
    else
        rc = _irsActivateIrsensorWithFunctionLevel(g_irsFunctionLevel);

    if (R_SUCCEEDED(rc)) rc = _irsGetIrsensorSharedMemoryHandle(&sharedmem_handle);

    if (R_SUCCEEDED(rc)) {
        shmemLoadRemote(&g_irsSharedmem, sharedmem_handle, 0x8000, Perm_R);

        rc = shmemMap(&g_irsSharedmem);
    }

    return rc;
}

void _irsCleanup(void) {
    IrsCameraEntry *entry;

    if (serviceIsActive(&g_irsSrv)) {
        for (u32 i=0; i<IRS_MAX_CAMERAS; i++) {
            entry = &g_irsCameras[i];
            if (entry->mode != IrsIrSensorMode_None) irsStopImageProcessor(entry->handle);
        }

        _irsActivateIrsensor(0);
    }

    serviceClose(&g_irsSrv);
    shmemClose(&g_irsSharedmem);

    g_irsFunctionLevel.ir_sensor_function_level = 0x0;
}

Service* irsGetServiceSession(void) {
    return &g_irsSrv;
}

void* irsGetSharedmemAddr(void) {
    return shmemGetAddr(&g_irsSharedmem);
}

static inline IrsStatusManager *_irsGetStatusManager(void) {
    return (IrsStatusManager*)irsGetSharedmemAddr();
}

static Result _irsCameraEntryGet(IrsIrCameraHandle handle, IrsCameraEntry **out_entry) {
    IrsCameraEntry *entry;
    *out_entry = NULL;

    if (handle.player_number >= IRS_MAX_CAMERAS) return MAKERESULT(Module_Libnx, LibnxError_BadInput);

    entry = &g_irsCameras[handle.player_number];
    *out_entry = entry;

    return 0;
}

static void _irsCameraEntryFree(IrsCameraEntry *entry) {
    tmemClose(&entry->transfermem);
}

static bool _irsGetIrSensorAruidStatus(u32 *out) {
    u64 aruid = appletGetAppletResourceUserId();
    for (u32 i=0; i<0x5; i++) {
        IrsAruidFormat *aruid_format = &_irsGetStatusManager()->aruid_format[i];
        if (atomic_load_explicit(&aruid_format->ir_sensor_aruid, memory_order_acquire) == aruid) {
            *out = atomic_load_explicit(&aruid_format->ir_sensor_aruid_status, memory_order_acquire);
            return 1;
        }
    }

    return 0;
}

static bool _irsIsAppletForeground(void) {
    u32 status=0;
    bool flag = _irsGetIrSensorAruidStatus(&status);
    return flag==0 || (status & BIT(0));
}

static bool _irsIsLibraryAppletCallEnabled(IrsIrCameraHandle handle, IrsIrCameraInternalStatus status) {
    if (handle.player_number >= IRS_MAX_CAMERAS) return 0;

    bool ret=0;
    if (status) {
        for (u32 i=0; i<IRS_MAX_CAMERAS; i++) {
            if (g_irsCameras[i].internal_status == status) break;
            if (i==IRS_MAX_CAMERAS-1) ret=1;
        }
    }

    g_irsCameras[handle.player_number].internal_status = status;

    return ret;
}

static void _irsSetInternalStatus(IrsIrCameraHandle handle, IrsIrCameraInternalStatus status) {
    if (handle.player_number >= IRS_MAX_CAMERAS) return;

    g_irsCameras[handle.player_number].internal_status = status;
}

static bool _irsGetVersionCheckFlag(IrsIrCameraHandle handle) {
    if (handle.player_number >= IRS_MAX_CAMERAS) return 0;

    return g_irsCameras[handle.player_number].version_check;
}

static void _irsSetVersionCheckFlag(IrsIrCameraHandle handle, bool flag) {
    if (handle.player_number >= IRS_MAX_CAMERAS) return;

    g_irsCameras[handle.player_number].version_check = flag;
}

Result irsGetIrCameraStatus(IrsIrCameraHandle handle, IrsIrCameraStatus *out) {
    if (handle.player_number >= IRS_MAX_CAMERAS) return MAKERESULT(Module_Libnx, LibnxError_BadInput);

    IrsIrCameraStatus tmp = atomic_load_explicit(&_irsGetStatusManager()->device_format[handle.player_number].ir_camera_status, memory_order_acquire);
    if (tmp > IrsIrCameraStatus_Unconnected) return MAKERESULT(Module_Libnx, LibnxError_ShouldNotHappen);
    *out = tmp;

    return 0;
}

static Result _irsGetIrCameraInternalStatus(IrsIrCameraHandle handle, IrsIrCameraInternalStatus *out) {
    if (handle.player_number >= IRS_MAX_CAMERAS) return MAKERESULT(Module_Libnx, LibnxError_BadInput);

    u32 tmp = atomic_load_explicit(&_irsGetStatusManager()->device_format[handle.player_number].ir_camera_internal_status, memory_order_acquire);
    if (tmp > IrsIrCameraInternalStatus_Setting) return MAKERESULT(Module_Libnx, LibnxError_ShouldNotHappen);
    *out = tmp;

    return 0;
}

static Result _irsCheckInternalStatus(IrsIrCameraHandle handle) {
    Result rc=0;
    IrsIrCameraInternalStatus status;

    rc = _irsGetIrCameraInternalStatus(handle, &status);
    if (R_FAILED(rc)) return rc;

    bool flag = _irsIsLibraryAppletCallEnabled(handle, status);

    switch (status) {
        case IrsIrCameraInternalStatus_Stopped:
        case IrsIrCameraInternalStatus_Ready:
        break; // Leave rc at value 0 for success.

        case IrsIrCameraInternalStatus_Unknown2: // These are seperate with sdknso.
        case IrsIrCameraInternalStatus_Unknown3:
            rc = status == IrsIrCameraInternalStatus_Unknown2 ? MAKERESULT(205, 123) : MAKERESULT(205, 124);
            // sdknso would use errorResultShow() here with rc when flag is set, however we won't do so.
        break;

        case IrsIrCameraInternalStatus_Unknown4:
            rc = MAKERESULT(205, 161);
        break;

        case IrsIrCameraInternalStatus_FirmwareUpdateNeeded:
            if (flag) {
                HidLaControllerFirmwareUpdateArg arg;
                hidLaCreateControllerFirmwareUpdateArg(&arg);
                arg.enable_force_update = 1;
                rc = hidLaShowControllerFirmwareUpdate(&arg);

                if (R_FAILED(rc)) {
                    rc = R_VALUE(rc) == MAKERESULT(Module_Libnx, LibnxError_LibAppletBadExit) ? MAKERESULT(205, 125) : rc;
                    break;
                }
            }
        // fallthrough

        case IrsIrCameraInternalStatus_FirmwareVersionRequested:
        case IrsIrCameraInternalStatus_FirmwareVersionIsInvalid:
        case IrsIrCameraInternalStatus_Setting:
            rc = MAKERESULT(205, 160);
        break;

        default:
            rc = MAKERESULT(Module_Libnx, LibnxError_ShouldNotHappen);
        break;
    }

    return rc;
}

Result irsGetIrCameraHandle(IrsIrCameraHandle *handle, HidNpadIdType id) {
    u32 tmp = id;
    return serviceDispatchInOut(&g_irsSrv, 311, tmp, *handle);
}

Result irsCheckFirmwareUpdateNecessity(IrsIrCameraHandle handle, bool *out) {
    if (hosversionBefore(4,0,0)) // sdknso didn't implement this until 4.x (the RequiredMcuVersion was also updated with that version).
        return MAKERESULT(Module_Libnx, LibnxError_IncompatSysVer);

    Result rc=0;
    IrsIrCameraStatus status;
    IrsIrCameraInternalStatus internal_status;

    if (!_irsIsAppletForeground()) return MAKERESULT(205, 150);

    rc = irsGetIrCameraStatus(handle, &status);
    if (R_FAILED(rc)) return rc;

    if (status == IrsIrCameraStatus_Available) {
        if (_irsGetVersionCheckFlag(handle)) {
            rc = _irsCheckFirmwareVersion(handle, g_irsRequiredMcuVersion);
            if (R_SUCCEEDED(rc)) _irsSetVersionCheckFlag(handle, false);
        }

        rc = _irsGetIrCameraInternalStatus(handle, &internal_status);
        if (R_SUCCEEDED(rc)) {
            bool flag;
            if (internal_status == IrsIrCameraInternalStatus_FirmwareVersionIsInvalid)
                flag = 1;
            else if (internal_status == IrsIrCameraInternalStatus_Ready || internal_status == IrsIrCameraInternalStatus_Setting)
                flag = 0;
            else if (internal_status == IrsIrCameraInternalStatus_Stopped)
                flag = 0;
            else
                rc = MAKERESULT(205, 150);

            if (R_SUCCEEDED(rc)) {
                _irsSetVersionCheckFlag(handle, true);
                *out = flag;
            }
        }
    }
    else if (status == IrsIrCameraStatus_Unsupported) {
        rc = MAKERESULT(205, 111);
    }
    else if (status == IrsIrCameraStatus_Unconnected) {
        rc = MAKERESULT(205, 110);
    }

    return rc;
}

Result irsGetImageProcessorStatus(IrsIrCameraHandle handle, IrsImageProcessorStatus *out) {
    if (hosversionBefore(4,0,0))
        return MAKERESULT(Module_Libnx, LibnxError_IncompatSysVer);

    Result rc=0;
    IrsIrCameraInternalStatus tmp;

    rc = _irsGetIrCameraInternalStatus(handle, &tmp);
    if (R_SUCCEEDED(rc)) {
        if (tmp == IrsIrCameraInternalStatus_FirmwareVersionRequested || tmp == IrsIrCameraInternalStatus_FirmwareVersionIsInvalid || tmp == IrsIrCameraInternalStatus_Ready)
            *out = IrsImageProcessorStatus_Stopped;
        else
            *out = IrsImageProcessorStatus_Running;
    }
    return rc;
}

static IrsProcessorState *_irsGetRingLifo(IrsIrCameraHandle handle, IrsIrSensorMode mode) {
    if (handle.player_number >= IRS_MAX_CAMERAS) return NULL;
    IrsDeviceFormat *device = &_irsGetStatusManager()->device_format[handle.player_number];
    u32 tmp = atomic_load_explicit(&device->ir_sensor_mode, memory_order_acquire);
    if (tmp != mode) return NULL;

    return &device->processor_state;
}

// sdknso has multiple funcs for this (for each *ProcessorState), but we'll just use one func instead.
static s32 _irsRingLifoRead(IrsProcessorState *lifo, void* out, s32 count, IrsValidationCb validation_cb, void* userdata, size_t entrysize, s64 max_entrycount) {
    IrsTeraPluginProcessorState tmpdata; // validation_cb is only used with TeraPluginProcessor.
    if (validation_cb && entrysize > sizeof(tmpdata)) return 0;

    s64 max_first_entryindex = 0-max_entrycount; // sdknso uses {inparam}-{max_entrycount}, but the inparam is always 0 anyway.
    u8 *out8 = (u8*)out;
    if (count <= 0) return 0;
    if (count > max_entrycount) count = max_entrycount; // sdknso does this in the callers, but we'll do it here instead.

    s32 total_entries=0;

    s64 sampling_number0=0, sampling_number1=0;
    s64 prev_samplenum=0;
    u32 num_samples=0;

    do {
        sampling_number0 = atomic_load_explicit(&lifo->start, memory_order_acquire);
        sampling_number1 = atomic_load_explicit(&lifo->start, memory_order_acquire);

        s64 start_samplenum = max_first_entryindex + sampling_number1;
        s64 max_prev_samplenum = sampling_number0 - (max_entrycount+1);
        s64 timediff = sampling_number1 - (s64)atomic_load_explicit(&lifo->count, memory_order_acquire);
        s64 tmp0 = timediff > start_samplenum ? timediff : start_samplenum;
        prev_samplenum = max_prev_samplenum > tmp0 ? max_prev_samplenum : tmp0;

        s64 tmp = sampling_number1 - prev_samplenum;
        if (tmp <= 0) break;
        num_samples = tmp;

        s64 entryindex = prev_samplenum + (s64)num_samples - 1;
        total_entries = 0;
        s64 entrycount = entryindex - prev_samplenum;

        if (entrycount >= 0) {
            entryindex = prev_samplenum + entrycount;
            s64 next_samplenum = prev_samplenum+(max_entrycount+1);

            for (s64 i=0; i<=entrycount; i++) {
                u8 *data_src = &lifo->data[((entryindex-i) % (max_entrycount+1)) * entrysize];

                if (validation_cb) memcpy(&tmpdata, data_src, entrysize);

                sampling_number0 = atomic_load_explicit(&lifo->start, memory_order_acquire);

                if (atomic_load_explicit(&lifo->count, memory_order_acquire) < num_samples || sampling_number0 <= prev_samplenum || next_samplenum <= sampling_number0) break;

                bool is_valid=true;
                if (validation_cb) is_valid = validation_cb(userdata, &tmpdata);
                if (is_valid) {
                    memcpy(&out8[total_entries*entrysize], data_src, entrysize);
                    total_entries++;
                }

                if (total_entries >= count) break;
            }
        }

        sampling_number0 = atomic_load_explicit(&lifo->start, memory_order_acquire);
    } while (sampling_number0 <= prev_samplenum || atomic_load_explicit(&lifo->count, memory_order_acquire) < num_samples || prev_samplenum+max_entrycount+1 <= sampling_number0);

    return total_entries;
}

static Result _irsActivateIrsensor(bool activate) {
    u64 AppletResourceUserId = appletGetAppletResourceUserId();

    return serviceDispatchIn(&g_irsSrv, activate ? 302 : 303, AppletResourceUserId,
        .in_send_pid = true,
    );
}

static Result _irsGetIrsensorSharedMemoryHandle(Handle* handle_out) {
    u64 AppletResourceUserId = appletGetAppletResourceUserId();

    return serviceDispatchIn(&g_irsSrv, 304, AppletResourceUserId,
        .in_send_pid = true,
        .out_handle_attrs = { SfOutHandleAttr_HipcCopy },
        .out_handles = handle_out,
    );
}

static Result _irsStopImageProcessor(IrsIrCameraHandle handle) {
    const struct {
        IrsIrCameraHandle handle;
        u64 AppletResourceUserId;
    } in = { handle, appletGetAppletResourceUserId() };

    return serviceDispatchIn(&g_irsSrv, 305, in,
        .in_send_pid = true,
    );
}

static Result _irsRunMomentProcessor(IrsIrCameraHandle handle, const IrsPackedMomentProcessorConfig *config) {
    const struct {
        IrsIrCameraHandle handle;
        u32 pad;
        u64 AppletResourceUserId;
        IrsPackedMomentProcessorConfig config;
    } in = { handle, 0, appletGetAppletResourceUserId(), *config };

    return serviceDispatchIn(&g_irsSrv, 306, in,
        .in_send_pid = true,
    );
}

static Result _irsRunClusteringProcessor(IrsIrCameraHandle handle, const IrsPackedClusteringProcessorConfig *config) {
    const struct {
        IrsIrCameraHandle handle;
        u32 pad;
        u64 AppletResourceUserId;
        IrsPackedClusteringProcessorConfig config;
    } in = { handle, 0, appletGetAppletResourceUserId(), *config };

    return serviceDispatchIn(&g_irsSrv, 307, in,
        .in_send_pid = true,
    );
}

static Result _irsRunImageTransferProcessor(IrsIrCameraHandle handle, const IrsPackedImageTransferProcessorConfig *config, TransferMemory *tmem) {
    const struct {
        IrsIrCameraHandle handle;
        u32 pad;
        u64 AppletResourceUserId;
        IrsPackedImageTransferProcessorConfig config;
        u64 TransferMemory_size;
    } in = { handle, 0, appletGetAppletResourceUserId(), *config, tmem->size };

    return serviceDispatchIn(&g_irsSrv, 308, in,
        .in_send_pid = true,
        .in_num_handles = 1,
        .in_handles = { tmem->handle },
    );
}

static Result _irsGetImageTransferProcessorState(IrsIrCameraHandle handle, void* buffer, size_t size, IrsImageTransferProcessorState *state) {
    const struct {
        IrsIrCameraHandle handle;
        u64 AppletResourceUserId;
    } in = { handle, appletGetAppletResourceUserId() };

    return serviceDispatchInOut(&g_irsSrv, 309, in, *state,
        .buffer_attrs = { SfBufferAttr_HipcMapAlias | SfBufferAttr_Out },
        .buffers = { { buffer, size } },
        .in_send_pid = true,
    );
}

static Result _irsRunTeraPluginProcessor(IrsIrCameraHandle handle, const IrsPackedTeraPluginProcessorConfig *config) {
    const struct {
        IrsIrCameraHandle handle;
        IrsPackedTeraPluginProcessorConfig config;
        u64 AppletResourceUserId;
    } in = { handle, *config, appletGetAppletResourceUserId() };

    return serviceDispatchIn(&g_irsSrv, 310, in,
        .in_send_pid = true,
    );
}

static Result _irsRunPointingProcessor(IrsIrCameraHandle handle, const IrsPackedPointingProcessorConfig *config) {
    const struct {
        IrsIrCameraHandle handle;
        IrsPackedPointingProcessorConfig config;
        u64 AppletResourceUserId;
    } in = { handle, *config, appletGetAppletResourceUserId() };

    return serviceDispatchIn(&g_irsSrv, 312, in,
        .in_send_pid = true,
    );
}

static Result _irsSuspendImageProcessor(IrsIrCameraHandle handle) {
    const struct {
        IrsIrCameraHandle handle;
        u64 AppletResourceUserId;
    } in = { handle, appletGetAppletResourceUserId() };

    return serviceDispatchIn(&g_irsSrv, 313, in,
        .in_send_pid = true,
    );
}

static Result _irsCheckFirmwareVersion(IrsIrCameraHandle handle, IrsPackedMcuVersion version) {
    if (hosversionBefore(3,0,0))
        return MAKERESULT(Module_Libnx, LibnxError_IncompatSysVer);

    const struct {
        IrsIrCameraHandle handle;
        IrsPackedMcuVersion version;
        u32 pad;
        u64 AppletResourceUserId;
    } in = { handle, version, 0, appletGetAppletResourceUserId() };

    return serviceDispatchIn(&g_irsSrv, 314, in,
        .in_send_pid = true,
    );
}

static Result _irsRunImageTransferExProcessor(IrsIrCameraHandle handle, const IrsPackedImageTransferProcessorExConfig *config, TransferMemory *tmem) {
    const struct {
        IrsIrCameraHandle handle;
        u32 pad;
        u64 AppletResourceUserId;
        IrsPackedImageTransferProcessorExConfig config;
        u64 TransferMemory_size;
    } in = { handle, 0, appletGetAppletResourceUserId(), *config, tmem->size };

    return serviceDispatchIn(&g_irsSrv, 316, in,
        .in_send_pid = true,
        .in_num_handles = 1,
        .in_handles = { tmem->handle },
    );
}

static Result _irsRunIrLedProcessor(IrsIrCameraHandle handle, const IrsPackedIrLedProcessorConfig *config) {
    if (hosversionBefore(4,0,0))
        return MAKERESULT(Module_Libnx, LibnxError_IncompatSysVer);

    const struct {
        IrsIrCameraHandle handle;
        IrsPackedIrLedProcessorConfig config;
        u64 AppletResourceUserId;
    } in = { handle, *config, appletGetAppletResourceUserId() };

    return serviceDispatchIn(&g_irsSrv, 317, in,
        .in_send_pid = true,
    );
}

static Result _irsStopImageProcessorAsync(IrsIrCameraHandle handle) {
    if (hosversionBefore(4,0,0))
        return MAKERESULT(Module_Libnx, LibnxError_IncompatSysVer);

    const struct {
        IrsIrCameraHandle handle;
        u64 AppletResourceUserId;
    } in = { handle, appletGetAppletResourceUserId() };

    return serviceDispatchIn(&g_irsSrv, 318, in,
        .in_send_pid = true,
    );
}

static Result _irsActivateIrsensorWithFunctionLevel(IrsPackedFunctionLevel level) {
    const struct {
        IrsPackedFunctionLevel level;
        u32 pad;
        u64 AppletResourceUserId;
    } in = { level, 0, appletGetAppletResourceUserId() };

    return serviceDispatchIn(&g_irsSrv, 319, in,
        .in_send_pid = true,
    );
}

Result irsStopImageProcessor(IrsIrCameraHandle handle) {
    Result rc=0;
    IrsCameraEntry *entry = NULL;

    if (!serviceIsActive(&g_irsSrv))
        return MAKERESULT(Module_Libnx, LibnxError_NotInitialized);

    rc = _irsCameraEntryGet(handle, &entry);
    if (R_FAILED(rc))
        return rc;

    _irsSetInternalStatus(handle, IrsIrCameraInternalStatus_Stopped);
    _irsSetVersionCheckFlag(handle, true);

    bool old_sysver = hosversionBefore(4,0,0);

    if (old_sysver)
        rc = _irsStopImageProcessor(handle);
    else
        rc = _irsStopImageProcessorAsync(handle);
    if (R_SUCCEEDED(rc)) entry->mode = IrsIrSensorMode_None;

    if (R_SUCCEEDED(rc) && !old_sysver) {
        for (u32 i=0; i<0x14d; i++) {
            IrsImageProcessorStatus status;
            irsGetImageProcessorStatus(handle, &status);
            if (status == IrsImageProcessorStatus_Stopped) break;
            svcSleepThread(15000000);
        }
    }

    _irsCameraEntryFree(entry);
    return rc;
}

Result irsStopImageProcessorAsync(IrsIrCameraHandle handle) {
    Result rc=0;
    IrsCameraEntry *entry = NULL;

    if (!serviceIsActive(&g_irsSrv))
        return MAKERESULT(Module_Libnx, LibnxError_NotInitialized);

    rc = _irsCameraEntryGet(handle, &entry);
    if (R_FAILED(rc))
        return rc;

    _irsSetInternalStatus(handle, IrsIrCameraInternalStatus_Stopped);
    _irsSetVersionCheckFlag(handle, true);

    rc = _irsStopImageProcessorAsync(handle);
    if (R_SUCCEEDED(rc)) entry->mode = IrsIrSensorMode_None;

    _irsCameraEntryFree(entry);
    return rc;
}

Result irsRunMomentProcessor(IrsIrCameraHandle handle, const IrsMomentProcessorConfig *config) {
    Result rc=0;
    IrsCameraEntry *entry = NULL;
    IrsPackedMomentProcessorConfig packed_config;

    memset(&packed_config, 0, sizeof(packed_config));

    packed_config.exposure_time = config->exposure_time;
    packed_config.light_target = config->light_target;
    packed_config.gain = config->gain;
    packed_config.is_negative_image_used = config->is_negative_image_used;
    packed_config.window_of_interest = config->window_of_interest;
    packed_config.required_mcu_version = g_irsRequiredMcuVersion;
    packed_config.preprocess = config->preprocess;
    packed_config.preprocess_intensity_threshold = config->preprocess_intensity_threshold;

    rc = _irsCameraEntryGet(handle, &entry);
    if (R_FAILED(rc))
        return rc;
    entry->handle = handle;

    if (g_irsFunctionLevel.ir_sensor_function_level >= 0x1 && (entry->mode != IrsIrSensorMode_None && entry->mode != IrsIrSensorMode_MomentProcessor)) {
        rc = _irsSuspendImageProcessor(handle);
    }

    if (R_SUCCEEDED(rc)) rc = _irsRunMomentProcessor(handle, &packed_config);

    if (R_SUCCEEDED(rc)) {
        entry->mode = IrsIrSensorMode_MomentProcessor;
        entry->is_negative_image_used = packed_config.is_negative_image_used;
        entry->window_of_interest = packed_config.window_of_interest;
    }

    return rc;
}

Result irsGetMomentProcessorStates(IrsIrCameraHandle handle, IrsMomentProcessorState *states, s32 count, s32 *total_out) {
    Result rc=0;
    IrsCameraEntry *entry = NULL;

    rc = _irsCameraEntryGet(handle, &entry);
    if (R_FAILED(rc))
        return rc;

    *total_out = 0;
    // sdknso would fill states with default values here, but we won't do that.

    rc = MAKERESULT(205, 160);
    if (!_irsIsAppletForeground()) return rc;

    Result rc2 = _irsCheckInternalStatus(handle);
    if (R_FAILED(rc2))
        return rc2;

    if (entry->mode != IrsIrSensorMode_MomentProcessor && entry->mode != IrsIrSensorMode_IrLedProcessor) return rc;
    IrsProcessorState *lifo = _irsGetRingLifo(handle, IrsIrSensorMode_MomentProcessor);
    if (lifo==NULL) return rc;
    rc = 0;

    *total_out = _irsRingLifoRead(lifo, states, count, NULL, NULL, sizeof(*states), 5);

    if (!*total_out) {
        IrsIrCameraStatus status;
        rc = irsGetIrCameraStatus(handle, &status);
        if (R_SUCCEEDED(rc)) rc = g_irsCameraStatusResults[status]; // sdknso would verify that status is within bounds first, but that's redundant since irsGetIrCameraStatus() already does so.
        return rc;
    }

    if (entry->window_of_interest.width != 320 || entry->window_of_interest.height != 240) { // sdknso doesn't check this but we will, since multiplying by 1.0f is pointless (when the width and height are the defaults checked here).
        float scale = 76800.0f / (float)(entry->window_of_interest.width * entry->window_of_interest.height); // 76800 == 320*240
        for (s32 statei=0; statei<*total_out; statei++) {
            for (s32 stati=0; stati<0x30; stati++) states[statei].statistic[stati].average_intensity *= scale;
        }
    }

    return rc;
}

IrsMomentStatistic irsCalculateMomentRegionStatistic(const IrsMomentProcessorState *state, IrsRect rect, s32 region_x, s32 region_y, s32 region_width, s32 region_height) {
    s16 width = rect.width / 8;
    s16 height = rect.height / 6;
    float widthf = (float)width;
    float heightf = (float)height;
    double sum0=0, sum1=0, sum2=0;

    // sdknso doesn't have this set of validation.
    if (region_x < 0) region_x = 0;
    if (region_y < 0) region_y = 0;
    if (region_x > 5) region_x = 5;
    if (region_y > 7) region_y = 7;
    if (region_x+region_width > 6) region_width = 6 - region_x;
    if (region_y+region_height > 8) region_height = 8 - region_y;

    if (region_width >= 1 && region_height >= 1) {
        for (s32 x=region_x; x<region_x+region_width; x++) {
            for (s32 y=region_y; y<region_y+region_height; y++) {
                const IrsMomentStatistic *stat = &state->statistic[y + x*8];
                float intensity = stat->average_intensity*widthf*heightf;
                sum0+= (double)(intensity*stat->centroid_x);
                sum1+= (double)intensity;
                sum2+= (double)(intensity*stat->centroid_y);
            }
        }
    }

    double tmp = sum1 / (double)(region_width*region_height*width*height);
    if (sum1 == 0.0f) {
        return (IrsMomentStatistic){.average_intensity = (float)tmp};
    }
    return (IrsMomentStatistic){.average_intensity = (float)tmp, .centroid_x = (float)(sum0 / sum1), .centroid_y = (float)(sum2 / sum1)};
}

Result irsRunClusteringProcessor(IrsIrCameraHandle handle, const IrsClusteringProcessorConfig *config) {
    Result rc=0;
    IrsCameraEntry *entry = NULL;
    IrsPackedClusteringProcessorConfig packed_config;

    memset(&packed_config, 0, sizeof(packed_config));

    packed_config.exposure_time = config->exposure_time;
    packed_config.light_target = config->light_target;
    packed_config.gain = config->gain;
    packed_config.is_negative_image_used = config->is_negative_image_used;
    packed_config.window_of_interest = config->window_of_interest;
    packed_config.required_mcu_version = g_irsRequiredMcuVersion;
    packed_config.object_pixel_count_min = config->object_pixel_count_min;
    packed_config.object_pixel_count_max = config->object_pixel_count_max;
    packed_config.object_intensity_min = config->object_intensity_min;
    packed_config.is_external_light_filter_enabled = config->is_external_light_filter_enabled;

    rc = _irsCameraEntryGet(handle, &entry);
    if (R_FAILED(rc))
        return rc;
    entry->handle = handle;

    if (g_irsFunctionLevel.ir_sensor_function_level >= 0x1 && (entry->mode != IrsIrSensorMode_None && entry->mode != IrsIrSensorMode_ClusteringProcessor)) {
        rc = _irsSuspendImageProcessor(handle);
    }

    if (R_SUCCEEDED(rc)) rc = _irsRunClusteringProcessor(handle, &packed_config);

    if (R_SUCCEEDED(rc)) {
        entry->mode = IrsIrSensorMode_ClusteringProcessor;
        entry->is_negative_image_used = packed_config.is_negative_image_used;
        entry->window_of_interest = packed_config.window_of_interest;
    }

    return rc;
}

Result irsGetClusteringProcessorStates(IrsIrCameraHandle handle, IrsClusteringProcessorState *states, s32 count, s32 *total_out) {
    Result rc=0;
    IrsCameraEntry *entry = NULL;

    rc = _irsCameraEntryGet(handle, &entry);
    if (R_FAILED(rc))
        return rc;

    *total_out = 0;
    // sdknso would fill states with default values here, but we won't do that.

    rc = MAKERESULT(205, 160);
    if (!_irsIsAppletForeground()) return rc;

    Result rc2 = _irsCheckInternalStatus(handle);
    if (R_FAILED(rc2))
        return rc2;

    if (entry->mode != IrsIrSensorMode_ClusteringProcessor) return rc;
    IrsProcessorState *lifo = _irsGetRingLifo(handle, IrsIrSensorMode_ClusteringProcessor);
    if (lifo==NULL) return rc;
    rc = 0;

    *total_out = _irsRingLifoRead(lifo, states, count, NULL, NULL, sizeof(*states), 5);

    if (!*total_out) {
        IrsIrCameraStatus status;
        rc = irsGetIrCameraStatus(handle, &status);
        if (R_SUCCEEDED(rc)) rc = g_irsCameraStatusResults[status]; // sdknso would verify that status is within bounds first, but that's redundant since irsGetIrCameraStatus() already does so.
    }

    return rc;
}

Result irsRunImageTransferProcessor(IrsIrCameraHandle handle, const IrsImageTransferProcessorConfig *config, size_t size) {
    Result rc=0;
    IrsCameraEntry *entry = NULL;
    IrsPackedImageTransferProcessorConfig packed_config;

    memset(&packed_config, 0, sizeof(packed_config));

    packed_config.exposure_time = config->exposure_time;
    packed_config.light_target = config->light_target;
    packed_config.gain = config->gain;
    packed_config.is_negative_image_used = config->is_negative_image_used;
    packed_config.required_mcu_version = g_irsRequiredMcuVersion;
    packed_config.format = config->format;

    rc = _irsCameraEntryGet(handle, &entry);
    if (R_FAILED(rc))
        return rc;
    entry->handle = handle;

    if (g_irsFunctionLevel.ir_sensor_function_level >= 0x1 && entry->mode != IrsIrSensorMode_None) {
        rc = _irsSuspendImageProcessor(handle);
        if (R_SUCCEEDED(rc)) _irsCameraEntryFree(entry);
    }

    if (R_SUCCEEDED(rc)) {
        rc = tmemCreate(&entry->transfermem, size, Perm_None);
        if (R_FAILED(rc)) return rc;

        rc = _irsRunImageTransferProcessor(handle, &packed_config, &entry->transfermem);
    }

    if (R_SUCCEEDED(rc)) {
        entry->mode = IrsIrSensorMode_ImageTransferProcessor;
        entry->is_negative_image_used = packed_config.is_negative_image_used;
        entry->format = packed_config.format;
    }

    if (R_FAILED(rc)) _irsCameraEntryFree(entry);

    return rc;
}

Result irsRunImageTransferExProcessor(IrsIrCameraHandle handle, const IrsImageTransferProcessorExConfig *config, size_t size) {
    if (hosversionBefore(4,0,0))
        return MAKERESULT(Module_Libnx, LibnxError_IncompatSysVer);

    Result rc=0;
    IrsCameraEntry *entry = NULL;
    IrsPackedImageTransferProcessorExConfig packed_config;

    memset(&packed_config, 0, sizeof(packed_config));

    packed_config.exposure_time = config->exposure_time;
    packed_config.light_target = config->light_target;
    packed_config.gain = config->gain;
    packed_config.is_negative_image_used = config->is_negative_image_used;
    packed_config.required_mcu_version = g_irsRequiredMcuVersion;
    packed_config.orig_format = config->orig_format;
    packed_config.trimming_format = config->trimming_format;
    packed_config.trimming_start_x = config->trimming_start_x;
    packed_config.trimming_start_y = config->trimming_start_y;
    packed_config.is_external_light_filter_enabled = config->is_external_light_filter_enabled;

    rc = _irsCameraEntryGet(handle, &entry);
    if (R_FAILED(rc))
        return rc;
    entry->handle = handle;

    if (g_irsFunctionLevel.ir_sensor_function_level >= 0x1 && entry->mode != IrsIrSensorMode_None) {
        rc = _irsSuspendImageProcessor(handle);
        if (R_SUCCEEDED(rc)) _irsCameraEntryFree(entry);
    }

    if (R_SUCCEEDED(rc)) {
        rc = tmemCreate(&entry->transfermem, size, Perm_None);
        if (R_FAILED(rc)) return rc;

        rc = _irsRunImageTransferExProcessor(handle, &packed_config, &entry->transfermem);
    }

    if (R_SUCCEEDED(rc)) {
        entry->mode = IrsIrSensorMode_ImageTransferProcessor;
        entry->is_negative_image_used = packed_config.is_negative_image_used;
        entry->format = packed_config.trimming_format;
    }

    if (R_FAILED(rc)) _irsCameraEntryFree(entry);

    return rc;
}

Result irsGetImageTransferProcessorState(IrsIrCameraHandle handle, void* buffer, size_t size, IrsImageTransferProcessorState *state) {
    Result rc=0;
    IrsCameraEntry *entry = NULL;

    if (!_irsIsAppletForeground()) return MAKERESULT(205, 160);

    rc = _irsCheckInternalStatus(handle);
    if (R_FAILED(rc))
        return rc;

    rc = _irsCameraEntryGet(handle, &entry);
    if (R_FAILED(rc))
        return rc;

    if (entry->mode != IrsIrSensorMode_ImageTransferProcessor) return MAKERESULT(205, 160);

    rc = _irsGetImageTransferProcessorState(handle, buffer, size, state);

    if (R_SUCCEEDED(rc) && entry->is_negative_image_used) {
        if (entry->format >= sizeof(g_irsImageFormatSizes)/sizeof(g_irsImageFormatSizes[0]))
            rc = MAKERESULT(Module_Libnx, LibnxError_ShouldNotHappen);

        if (R_SUCCEEDED(rc)) {
            u8 *bufptr = (u8*)buffer;
            size_t tmpsize = g_irsImageFormatSizes[entry->format];
            if (tmpsize > size) tmpsize = size;

            for (size_t i=0; i<tmpsize; i++) bufptr[i] = ~bufptr[i];
        }
    }

    if (R_FAILED(rc)) {
        IrsIrCameraStatus status;
        rc = irsGetIrCameraStatus(handle, &status);
        if (R_SUCCEEDED(rc)) rc = g_irsCameraStatusResults[status]; // sdknso would verify that status is within bounds first, but that's redundant since irsGetIrCameraStatus() already does so.
    }

    return rc;
}

Result irsRunPointingProcessor(IrsIrCameraHandle handle) {
    Result rc=0;
    IrsCameraEntry *entry = NULL;
    IrsPackedPointingProcessorConfig packed_config;

    memset(&packed_config, 0, sizeof(packed_config));

    packed_config.window_of_interest.width = 320;
    packed_config.window_of_interest.height = 240;
    packed_config.required_mcu_version = g_irsRequiredMcuVersion;

    rc = _irsCameraEntryGet(handle, &entry);
    if (R_FAILED(rc))
        return rc;
    entry->handle = handle;

    if (g_irsFunctionLevel.ir_sensor_function_level >= 0x1 && (entry->mode != IrsIrSensorMode_None && entry->mode != IrsIrSensorMode_PointingProcessor)) {
        rc = _irsSuspendImageProcessor(handle);
    }

    if (R_SUCCEEDED(rc)) rc = _irsRunPointingProcessor(handle, &packed_config);

    if (R_SUCCEEDED(rc)) entry->mode = IrsIrSensorMode_PointingProcessor;

    return rc;
}

Result irsGetPointingProcessorMarkerStates(IrsIrCameraHandle handle, IrsPointingProcessorMarkerState *states, s32 count, s32 *total_out) {
    Result rc=0;
    IrsCameraEntry *entry = NULL;

    rc = _irsCameraEntryGet(handle, &entry);
    if (R_FAILED(rc))
        return rc;

    *total_out = 0;
    // sdknso would fill states with default values here, but we won't do that.

    rc = MAKERESULT(205, 160);
    if (!_irsIsAppletForeground()) return rc;

    Result rc2 = _irsCheckInternalStatus(handle);
    if (R_FAILED(rc2))
        return rc2;

    if (entry->mode != IrsIrSensorMode_PointingProcessor) return rc;
    IrsProcessorState *lifo = _irsGetRingLifo(handle, IrsIrSensorMode_PointingProcessor);
    if (lifo==NULL) return rc;
    rc = 0;

    *total_out = _irsRingLifoRead(lifo, states, count, NULL, NULL, sizeof(*states), 6);

    if (!*total_out) {
        IrsIrCameraStatus status;
        rc = irsGetIrCameraStatus(handle, &status);
        if (R_SUCCEEDED(rc)) rc = g_irsCameraStatusResults[status]; // sdknso would verify that status is within bounds first, but that's redundant since irsGetIrCameraStatus() already does so.
    }

    return rc;
}

Result irsGetPointingProcessorStates(IrsIrCameraHandle handle, IrsPointingProcessorState *states, s32 count, s32 *total_out) {
    Result rc=0;
    IrsPointingProcessorMarkerState tmp_states[6];

    *total_out = 0;
    rc = irsGetPointingProcessorMarkerStates(handle, tmp_states, count, total_out);

    if (R_SUCCEEDED(rc)) { // sdknso doesn't check this, but we will.
        for (s32 i=0; i<*total_out; i++) {
            float pos_x = 0.0f, pos_y = 0.0f;
            u32 poscount=0;

            states[i].sampling_number = tmp_states[i].sampling_number;
            states[i].timestamp = tmp_states[i].timestamp;

            for (u32 pointi=0; pointi<3; pointi++) {
                if (tmp_states[i].data[pointi].pointing_status) {
                    pos_x+= tmp_states[i].data[pointi].position_x;
                    pos_y+= tmp_states[i].data[pointi].position_y;
                    poscount++;
                }
            }

            states[i].pointing_status = poscount < 3;

            if (!poscount) {
                states[i].position_x = 0.0f;
                states[i].position_y = 0.0f;
            }
            else {
                states[i].position_x = (pos_x / (float)poscount / -160.0f) + 1.0f;
                states[i].position_y = (pos_y / (float)poscount / 120.0f) + 1.0f;
            }
        }
    }

    return rc;
}

Result irsRunTeraPluginProcessor(IrsIrCameraHandle handle, const IrsTeraPluginProcessorConfig *config) {
    Result rc=0;
    IrsCameraEntry *entry = NULL;
    IrsPackedTeraPluginProcessorConfig packed_config;

    memset(&packed_config, 0, sizeof(packed_config));

    packed_config.required_mcu_version = g_irsRequiredMcuVersion;
    packed_config.mode = config->mode;

    if (hosversionAtLeast(6,0,0)) {
        packed_config.unk_x5 = 0x2 | (config->unk_x1 << 7);
        packed_config.unk_x6 = config->unk_x2;
        packed_config.unk_x7 = config->unk_x3;
    }

    rc = _irsCameraEntryGet(handle, &entry);
    if (R_FAILED(rc))
        return rc;
    entry->handle = handle;

    if (g_irsFunctionLevel.ir_sensor_function_level >= 0x1 && entry->mode != IrsIrSensorMode_None) {
        rc = _irsSuspendImageProcessor(handle);
    }

    // sdknso would assert here when g_irsFunctionLevel.ir_sensor_function_level is >= {certain value} - but that can't happen since it's above the value set during init, so we won't impl that.

    if (R_SUCCEEDED(rc)) rc = _irsRunTeraPluginProcessor(handle, &packed_config);

    if (R_SUCCEEDED(rc)) entry->mode = IrsIrSensorMode_TeraPluginProcessor;

    return rc;
}

static bool _irsValidateTeraPluginProcessorState(void* userdata, void* arg) {
    IrsTeraFilterArg *filter = (IrsTeraFilterArg*)userdata;
    IrsTeraPluginProcessorState *state = arg;

    // sdknso would call a parsing func here, but the output from it is unused so we won't impl that.

    for (u32 i=0; i<filter->prefix_bitcount; i++) {
        u8 data = state->plugin_data[i>>3] >> (i & 0x7);
        u8 prefix = filter->prefix_data >> i;
        if ((data & 1) != (prefix & 1)) return false;
    }

    return state->sampling_number >= filter->sampling_number;
}

Result irsGetTeraPluginProcessorStates(IrsIrCameraHandle handle, IrsTeraPluginProcessorState *states, s32 count, s64 sampling_number, u32 prefix_data, u32 prefix_bitcount, s32 *total_out) {
    Result rc=0;
    IrsCameraEntry *entry = NULL;
    IrsTeraFilterArg userdata={.sampling_number = sampling_number, .prefix_data = prefix_data, .prefix_bitcount = prefix_bitcount};

    rc = _irsCameraEntryGet(handle, &entry);
    if (R_FAILED(rc))
        return rc;

    *total_out = 0;
    // sdknso would fill states with default values here, but we won't do that.

    rc = MAKERESULT(205, 160);
    if (!_irsIsAppletForeground()) return rc;

    Result rc2 = _irsCheckInternalStatus(handle);
    if (R_FAILED(rc2))
        return rc2;

    if (entry->mode != IrsIrSensorMode_TeraPluginProcessor) return rc;
    IrsProcessorState *lifo = _irsGetRingLifo(handle, IrsIrSensorMode_TeraPluginProcessor);
    if (lifo==NULL) return rc;
    rc = 0;

    *total_out = _irsRingLifoRead(lifo, states, count, _irsValidateTeraPluginProcessorState, &userdata, sizeof(*states), 5);

    if (!*total_out) {
        IrsIrCameraStatus status;
        rc = irsGetIrCameraStatus(handle, &status);
        if (R_SUCCEEDED(rc)) rc = g_irsCameraStatusResults[status]; // sdknso would verify that status is within bounds first, but that's redundant since irsGetIrCameraStatus() already does so.
    }

    return rc;
}

Result irsRunIrLedProcessor(IrsIrCameraHandle handle, const IrsIrLedProcessorConfig *config) {
    Result rc=0;
    IrsCameraEntry *entry = NULL;
    IrsPackedIrLedProcessorConfig packed_config;

    memset(&packed_config, 0, sizeof(packed_config));

    packed_config.required_mcu_version = g_irsRequiredMcuVersion;
    packed_config.light_target = config->light_target;

    rc = _irsCameraEntryGet(handle, &entry);
    if (R_FAILED(rc))
        return rc;
    entry->handle = handle;

    if (g_irsFunctionLevel.ir_sensor_function_level >= 0x1 && (entry->mode != IrsIrSensorMode_None && entry->mode != IrsIrSensorMode_IrLedProcessor)) {
        rc = _irsSuspendImageProcessor(handle);
    }

    if (R_SUCCEEDED(rc)) rc = _irsRunIrLedProcessor(handle, &packed_config);

    if (R_SUCCEEDED(rc)) entry->mode = IrsIrSensorMode_IrLedProcessor;

    return rc;
}

Result irsRunAdaptiveClusteringProcessor(IrsIrCameraHandle handle, const IrsAdaptiveClusteringProcessorConfig *config) {
    if (hosversionBefore(5,0,0))
        return MAKERESULT(Module_Libnx, LibnxError_IncompatSysVer);

    IrsTeraPluginProcessorConfig tmp_config={0};

    tmp_config.mode = config->mode == 1 ? 0x10 : 0xf;

    if (hosversionAtLeast(6,0,0)) {
        IrsAdaptiveClusteringTargetDistance tmp = config->target_distance;
        // sdknso would set some tmp_config fields to 0 again in some cases, but we won't do that.
        if (tmp == IrsAdaptiveClusteringTargetDistance_Middle) {
            tmp_config.unk_x1 = 0x1;
            tmp_config.unk_x2 = 0x3;
        }
        else if (tmp == IrsAdaptiveClusteringTargetDistance_Far) {
            tmp_config.unk_x1 = 0x1;
            tmp_config.unk_x2 = 0x8;
        }
        else if (tmp != IrsAdaptiveClusteringTargetDistance_Near)
            return MAKERESULT(Module_Libnx, LibnxError_BadInput);
    }

    return irsRunTeraPluginProcessor(handle, &tmp_config);
}

// sdknso DecodeMarkerDetectionState (which is called by GetAdaptiveClusteringProcessorStates) uses nerd_gillette_internal* functionality.

Result irsRunHandAnalysis(IrsIrCameraHandle handle, const IrsHandAnalysisConfig *config) {
    IrsTeraPluginProcessorConfig tmp_config={0};

    u32 mode = config->mode;

    if (mode < IrsHandAnalysisMode_Silhouette || mode > IrsHandAnalysisMode_SilhouetteOnly)
        return MAKERESULT(Module_Libnx, LibnxError_BadInput);

    if (hosversionBefore(4,0,0)) {
        if (mode == IrsHandAnalysisMode_SilhouetteOnly)
            return MAKERESULT(Module_Libnx, LibnxError_IncompatSysVer);
        tmp_config.mode = mode-0x1;
    }
    else {
        tmp_config.mode = mode != IrsHandAnalysisMode_SilhouetteOnly ? mode+0x4 : 0xa;
    }

    return irsRunTeraPluginProcessor(handle, &tmp_config);
}

// The remaining HandAnalysis funcs in sdknso uses nerd_gillette_internal* functionality.

void irsGetMomentProcessorDefaultConfig(IrsMomentProcessorConfig *config) {
    memset(config, 0, sizeof(*config));

    config->exposure_time = 300000;
    config->gain = 8;
    config->window_of_interest.width = 320;
    config->window_of_interest.height = 240;
    config->preprocess = 1;
    config->preprocess_intensity_threshold = 0x50;
}

void irsGetClusteringProcessorDefaultConfig(IrsClusteringProcessorConfig *config) {
    memset(config, 0, sizeof(*config));

    config->exposure_time = 200000;
    config->gain = 2;
    config->window_of_interest.width = 320;
    config->window_of_interest.height = 240;
    config->object_pixel_count_min = 0x3;
    config->object_pixel_count_max = 0x12C00;
    config->object_intensity_min = 150;
    config->is_external_light_filter_enabled = 1;
}

void irsGetDefaultImageTransferProcessorConfig(IrsImageTransferProcessorConfig *config) {
    memset(config, 0, sizeof(*config));

    config->exposure_time = 300000;
    config->gain = 8;
}

void irsGetDefaultImageTransferProcessorExConfig(IrsImageTransferProcessorExConfig *config) {
    memset(config, 0, sizeof(*config));

    config->exposure_time = 300000;
    config->gain = 8;
}