#pragma once

#include "MemoryPool.h"
#include "MeshTypes.h"
#include "PointerQueue.h"
#include "configuration.h"
#include "detect/LoRaRadioType.h"

// Sentinel marking the end of a modem preset array. Declared `const` rather
// than `constexpr` because the cast from 0xFF to the enum is out-of-range and
// therefore not a valid constant expression on Clang 16+ (Apple Clang on
// macOS). The value is only ever compared at runtime, so static-init is fine.
static const meshtastic_Config_LoRaConfig_ModemPreset MODEM_PRESET_END =
    static_cast<meshtastic_Config_LoRaConfig_ModemPreset>(0xFF);

#define PRESET(name) meshtastic_Config_LoRaConfig_ModemPreset_##name

// Override slot magic numbers for RegionInfo.overrideSlot
#define OVERRIDE_SLOT_DEFAULT_CHANNEL_HASH 0 // Use hash of primary channel name
#define OVERRIDE_SLOT_PRESET_HASH -1         // Use hash of preset name instead
// Positive values (1-32767) are explicit slot numbers

// Region profile: bundles the preset list with regulatory parameters shared across regions
struct RegionProfile {
    const meshtastic_Config_LoRaConfig_ModemPreset *presets; // sentinel-terminated
    float spacing;                                           // gaps between radio channels
    float padding;                                           // padding at each side of the "operating channel"
    bool audioPermitted;
    bool licensedOnly;        // a region profile for licensed operators only
    int8_t textThrottle;      // throttle for text - future expansion
    int8_t positionThrottle;  // throttle for location data - future expansion
    int8_t telemetryThrottle; // throttle for telemetry - future expansion
};

/**
 * Get the effective duty cycle for the current region based on device role.
 * For EU_866, returns 10% for fixed devices (ROUTER, ROUTER_LATE) and 2.5% for mobile devices.
 * For other regions, returns the standard duty cycle.
 */
extern float getEffectiveDutyCycle();

// True if `preset` appears in at least one region's preset list, i.e. it is a real preset
// some region offers rather than a fabricated or long-retired enum value. Defined in
// RadioInterface.cpp, where the region table lives.
extern bool isKnownModemPreset(meshtastic_Config_LoRaConfig_ModemPreset preset);

extern const RegionProfile PROFILE_STD;
extern const RegionProfile PROFILE_EU868;
extern const RegionProfile PROFILE_UNDEF;
extern const RegionProfile PROFILE_LITE;
extern const RegionProfile PROFILE_NARROW;
extern const RegionProfile PROFILE_HAM_20KHZ;
extern const RegionProfile PROFILE_HAM_100KHZ;

// Map from old region names to new region enums
struct RegionInfo {
    meshtastic_Config_LoRaConfig_RegionCode code;
    float freqStart;
    float freqEnd;
    float dutyCycle;    // modified by getEffectiveDutyCycle
    uint8_t powerLimit; // Or zero for not set
    bool freqSwitching;
    bool wideLora;
    const RegionProfile *profile;
    meshtastic_Config_LoRaConfig_ModemPreset defaultPreset;
    int16_t overrideSlot; // per-region override slot for frequency selection
                          // Magic values: 0 = use channel name hash, -1 = use preset name hash, >0 = explicit slot
    const char *name;     // EU433 etc

    // Preset accessors (delegate through profile)
    meshtastic_Config_LoRaConfig_ModemPreset getDefaultPreset() const { return defaultPreset; }
    const meshtastic_Config_LoRaConfig_ModemPreset *getAvailablePresets() const { return profile->presets; }
    bool supportsPreset(meshtastic_Config_LoRaConfig_ModemPreset preset) const
    {
        for (size_t i = 0; profile->presets[i] != MODEM_PRESET_END; i++) {
            if (profile->presets[i] == preset)
                return true;
        }
        // UNSET is "no region chosen yet", not a regulatory domain: the radio is held silent
        // either way (see the region==UNSET gates in RadioLibInterface::send/handleReceive),
        // so there is nothing here to enforce. Rejecting would instead destroy a preset the
        // user already picked - the clamp rewrites it to LONG_FAST, and that clamp runs on
        // every boot and on every set_config while the region is unset. Accept any preset a
        // real region offers; fabricated values still fail and are clamped as before.
        if (code == meshtastic_Config_LoRaConfig_RegionCode_UNSET)
            return isKnownModemPreset(preset);
        return false;
    }
    size_t getNumPresets() const
    {
        size_t n = 0;
        while (profile->presets[n] != MODEM_PRESET_END)
            n++;
        return n;
    }
};

extern const RegionInfo regions[];
extern const RegionInfo *myRegion;

extern void initRegion();
extern const RegionInfo *getRegion(meshtastic_Config_LoRaConfig_RegionCode code);

// Fill `map` with the region->valid-preset table, grouped so regions sharing a
// preset list reference the same group. Sent to clients during want_config so
// their UI can block illegal region+preset combinations.
extern void getRegionPresetMap(meshtastic_LoRaRegionPresetMap &map);

// Valid LoRa spread factor range and defaults
constexpr uint8_t LORA_SF_MIN = 5;
constexpr uint8_t LORA_SF_MAX = 12;
constexpr uint8_t LORA_SF_DEFAULT = 11; // LONG_FAST default

// Valid LoRa coding rate range and default
constexpr uint8_t LORA_CR_MIN = 4;
constexpr uint8_t LORA_CR_MAX = 8;
constexpr uint8_t LORA_CR_DEFAULT = 5; // LONG_FAST default

// Default bandwidth in kHz (LONG_FAST)
constexpr float LORA_BW_DEFAULT_KHZ = 250.0f;

/// Clamp spread factor to the valid LoRa range [5, 12].
/// Out-of-range values (including 0 from unset preset mode) return LORA_SF_DEFAULT.
static inline uint8_t clampSpreadFactor(uint8_t sf)
{
    // We check for RF95 radios that are incompatible with Spreading Factors 5 and 6.
    if (radioType == RF95_RADIO && (sf == 5 || sf == 6))
        return LORA_SF_DEFAULT;

    if (sf < LORA_SF_MIN || sf > LORA_SF_MAX)
        return LORA_SF_DEFAULT;
    return sf;
}

/// Clamp coding rate to the valid LoRa range [4, 8].
/// Out-of-range values return LORA_CR_DEFAULT.
static inline uint8_t clampCodingRate(uint8_t cr)
{
    if (cr < LORA_CR_MIN || cr > LORA_CR_MAX)
        return LORA_CR_DEFAULT;
    return cr;
}

/// Ensure bandwidth is positive. Non-positive values return LORA_BW_DEFAULT_KHZ.
static inline float clampBandwidthKHz(float bwKHz)
{
    if (bwKHz <= 0.0f)
        return LORA_BW_DEFAULT_KHZ;
    return bwKHz;
}

static inline float bwCodeToKHz(uint16_t bwCode)
{
    if (bwCode == 8)
        return 7.8f;
    if (bwCode == 10)
        return 10.4f;
    if (bwCode == 16)
        return 15.6f;
    if (bwCode == 21)
        return 20.8f;
    if (bwCode == 31)
        return 31.25f;
    if (bwCode == 42)
        return 41.7f;
    if (bwCode == 62)
        return 62.5f;
    if (bwCode == 200)
        return 203.125f;
    if (bwCode == 400)
        return 406.25f;
    if (bwCode == 800)
        return 812.5f;
    if (bwCode == 1600)
        return 1625.0f;
    return (float)bwCode;
}

static inline uint16_t bwKHzToCode(float bwKHz)
{
    if (bwKHz > 7.7f && bwKHz < 7.9f)
        return 8;
    if (bwKHz > 10.3f && bwKHz < 10.5f)
        return 10;
    if (bwKHz > 15.5f && bwKHz < 15.7f)
        return 16;
    if (bwKHz > 20.7f && bwKHz < 20.9f)
        return 21;
    if (bwKHz > 31.24f && bwKHz < 31.26f)
        return 31;
    if (bwKHz > 41.6f && bwKHz < 41.8f)
        return 42;
    if (bwKHz > 62.49f && bwKHz < 62.51f)
        return 62;
    if (bwKHz > 203.12f && bwKHz < 203.13f)
        return 200;
    if (bwKHz > 406.24f && bwKHz < 406.26f)
        return 400;
    if (bwKHz > 812.49f && bwKHz < 812.51f)
        return 800;
    if (bwKHz > 1624.99f && bwKHz < 1625.01f)
        return 1600;
    return (uint16_t)(bwKHz + 0.5f);
}

/// Clamp a bandwidth *code* (the on-wire config.lora.bandwidth value) to a usable value.
/// A code of 0 (proto default / unset) returns the default bandwidth's code; any other value
/// is returned unchanged (RadioLib validates the exact discrete bandwidth downstream).
static inline uint16_t clampBandwidthCode(uint16_t bwCode)
{
    if (bwCode == 0)
        return bwKHzToCode(LORA_BW_DEFAULT_KHZ);
    return bwCode;
}

static inline void modemPresetToParams(meshtastic_Config_LoRaConfig_ModemPreset preset, bool wideLora, float &bwKHz, uint8_t &sf,
                                       uint8_t &cr)
{
    switch (preset) {
    case PRESET(SHORT_TURBO):
        bwKHz = wideLora ? 1625.0f : 500.0f;
        cr = 5;
        sf = 7;
        break;
    case PRESET(SHORT_FAST):
        bwKHz = wideLora ? 812.5f : 250.0f;
        cr = 5;
        sf = 7;
        break;
    case PRESET(SHORT_SLOW):
        bwKHz = wideLora ? 812.5f : 250.0f;
        cr = 5;
        sf = 8;
        break;
    case PRESET(MEDIUM_FAST):
        bwKHz = wideLora ? 812.5f : 250.0f;
        cr = 5;
        sf = 9;
        break;
    case PRESET(MEDIUM_SLOW):
        bwKHz = wideLora ? 812.5f : 250.0f;
        cr = 5;
        sf = 10;
        break;
    case PRESET(MEDIUM_TURBO):
        bwKHz = wideLora ? 1625.0f : 500.0f;
        cr = 5;
        sf = 9;
        break;
    case PRESET(LONG_TURBO):
        bwKHz = wideLora ? 1625.0f : 500.0f;
        cr = 8;
        sf = 11;
        break;
    case PRESET(LONG_MODERATE):
        bwKHz = wideLora ? 406.25f : 125.0f;
        cr = 8;
        sf = 11;
        break;
    case PRESET(LONG_SLOW):
        bwKHz = wideLora ? 406.25f : 125.0f;
        cr = 8;
        sf = 12;
        break;
    case PRESET(LITE_FAST):
        bwKHz = 125;
        cr = 5;
        sf = 9;
        break;
    case PRESET(LITE_SLOW):
        bwKHz = 125;
        cr = 5;
        sf = 10;
        break;
    case PRESET(NARROW_FAST):
        bwKHz = 62.5f;
        cr = 6;
        sf = 7;
        break;
    case PRESET(NARROW_SLOW):
        bwKHz = 62.5f;
        cr = 6;
        sf = 8;
        break;
    case PRESET(TINY_FAST):
        bwKHz = 15.6f;
        cr = 5;
        sf = 7;
        break;
    case PRESET(TINY_SLOW):
        bwKHz = 15.6f;
        cr = 6;
        sf = 8;
        break;
    default: // LONG_FAST (or illegal)
        bwKHz = wideLora ? 812.5f : 250.0f;
        cr = 5;
        sf = 11;
        break;
    }
}

static inline float modemPresetToBwKHz(meshtastic_Config_LoRaConfig_ModemPreset preset, bool wideLora)
{
    float bwKHz = 0;
    uint8_t sf = 0;
    uint8_t cr = 0;
    modemPresetToParams(preset, wideLora, bwKHz, sf, cr);
    return bwKHz;
}