#pragma once

#include "MemoryPool.h"
#include "MeshTypes.h"
#include "Observer.h"
#include "PointerQueue.h"
#include "airtime.h"
#include "error.h"
#include <memory>

#if HAS_LORA_FEM
#include "LoRaFEMInterface.h"
#endif

// Forward decl to avoid a direct include of generated config headers / full LoRaConfig definition in this widely-included file.
typedef struct _meshtastic_Config_LoRaConfig meshtastic_Config_LoRaConfig;

#define MAX_TX_QUEUE 16 // max number of packets which can be waiting for transmission

#define MAX_LORA_PAYLOAD_LEN 255 // max length of 255 per Semtech's datasheets on SX12xx
#define MESHTASTIC_HEADER_LENGTH 16
#define MESHTASTIC_PKC_OVERHEAD 12

#define PACKET_FLAGS_HOP_LIMIT_MASK 0x07
#define PACKET_FLAGS_WANT_ACK_MASK 0x08
#define PACKET_FLAGS_VIA_MQTT_MASK 0x10
#define PACKET_FLAGS_HOP_START_MASK 0xE0
#define PACKET_FLAGS_HOP_START_SHIFT 5

/**
 * This structure has to exactly match the wire layout when sent over the radio link.  Used to keep compatibility
 * with the old radiohead implementation.
 */
typedef struct {
    NodeNum to, from; // can be 1 byte or four bytes

    PacketId id; // can be 1 byte or 4 bytes

    /**
     * Usage of flags:
     *
     * The bottom three bits of flags are use to store hop_limit when sent over the wire.
     **/
    uint8_t flags;

    /** The channel hash - used as a hint for the decoder to limit which channels we consider */
    uint8_t channel;

    // Last byte of the NodeNum of the next-hop for this packet
    uint8_t next_hop;

    // Last byte of the NodeNum of the node that will relay/relayed this packet
    uint8_t relay_node;
} PacketHeader;

/**
 * This structure represent the structured buffer : a PacketHeader then the payload. The whole is
 * MAX_LORA_PAYLOAD_LEN + 1 length
 * It makes the use of its data easier, and avoids manipulating pointers (and potential non aligned accesses)
 */
typedef struct {
    /** The header, as defined just before */
    PacketHeader header;

    /** The payload, of maximum length minus the header, aligned just to be sure */
    uint8_t payload[MAX_LORA_PAYLOAD_LEN + 1 - sizeof(PacketHeader)] __attribute__((__aligned__));

} RadioBuffer;

/**
 * Basic operations all radio chipsets must implement.
 *
 * This defines the SOLE API for talking to radios (because soon we will have alternate radio implementations)
 */
class RadioInterface
{
    friend class MeshRadio; // for debugging we let that class touch pool

    CallbackObserver<RadioInterface, void *> configChangedObserver =
        CallbackObserver<RadioInterface, void *>(this, &RadioInterface::reloadConfig);

    CallbackObserver<RadioInterface, void *> preflightSleepObserver =
        CallbackObserver<RadioInterface, void *>(this, &RadioInterface::preflightSleepCb);

    CallbackObserver<RadioInterface, void *> notifyDeepSleepObserver =
        CallbackObserver<RadioInterface, void *>(this, &RadioInterface::notifyDeepSleepCb);

  protected:
    bool disabled = false;

    float bw = 125;
    uint8_t sf = 9;
    uint8_t cr = 5;

    static constexpr uint8_t NUM_SYM_CAD =
        2; // Number of symbols used for CAD, 2 is the default since RadioLib 6.3.0 as per AN1200.48
    static constexpr uint8_t NUM_SYM_CAD_24GHZ =
        4; // Number of symbols used for CAD in 2.4 GHz, 4 is recommended in AN1200.22 of SX1280
    uint32_t slotTimeMsec = computeSlotTimeMsec();
    uint16_t preambleLength = 16; // 8 is default, but we use longer to increase the amount of sleep time when receiving
    static constexpr uint16_t preambleLengthDefault =
        16; // 8 is default, but we use longer to increase the amount of sleep time when receiving
    static constexpr uint16_t wideLoraPreambleLengthDefault = 12; // 12 is default for wide Lora
    uint32_t preambleTimeMsec = 165;                              // calculated on startup, this is the default for LongFast
    static constexpr uint32_t PROCESSING_TIME_MSEC =
        4500;                           // time to construct, process and construct a packet again (empirically determined)
    static constexpr uint8_t CWmin = 3; // minimum CWsize
    static constexpr uint8_t CWmax = 8; // maximum CWsize

    meshtastic_MeshPacket *sendingPacket = NULL; // The packet we are currently sending
    uint32_t lastTxStart = 0L;

    uint32_t computeSlotTimeMsec();

    /**
     * A temporary buffer used for sending/receiving packets, sized to hold the biggest buffer we might need
     * */
    RadioBuffer radioBuffer __attribute__((__aligned__));
    /**
     * Enqueue a received packet for the registered receiver
     */
    void deliverToReceiver(meshtastic_MeshPacket *p);

  public:
    /** pool is the pool we will alloc our rx packets from
     */
    RadioInterface();

    virtual ~RadioInterface() {}

    /// Fires once per valid received LoRa packet (arg = sender NodeNum). Used e.g. to flash LED_LORA.
    static Observable<uint32_t> loraRxPacketObservable;

    /**
     * Coerce LoRa config fields (bandwidth/spread_factor) derived from presets.
     * This is used during early bootstrapping so UIs that display these fields directly remain consistent.
     */
    // static void bootstrapLoRaConfigFromPreset(meshtastic_Config_LoRaConfig &loraConfig); // maybe superseded?

    /**
     * Return true if we think the board can go to sleep (i.e. our tx queue is empty, we are not sending or receiving)
     *
     * This method must be used before putting the CPU into deep or light sleep.
     */
    virtual bool canSleep() { return true; }

    virtual bool wideLora() { return false; }

    /// Whether the radio can tune sub-GHz bands. False for 2.4 GHz-only chips (SX128x);
    /// multiband chips like the LR1121 keep the default.
    virtual bool supportsSubGhz() { return true; }

    /// Prepare hardware for sleep.  Call this _only_ for deep sleep, not needed for light sleep.
    virtual bool sleep() { return true; }

    /// Disable this interface (while disabled, no packets can be sent or received)
    void disable()
    {
        disabled = true;
        sleep();
    }

    /**
     * Send a packet (possibly by enquing in a private fifo).  This routine will
     * later free() the packet to pool.  This routine is not allowed to stall.
     * If the txmit queue is full it might return an error
     */
    virtual ErrorCode send(meshtastic_MeshPacket *p) = 0;

    /** Return TX queue status */
    [[nodiscard]] virtual meshtastic_QueueStatus getQueueStatus()
    {
        meshtastic_QueueStatus qs;
        qs.res = qs.mesh_packet_id = qs.free = qs.maxlen = 0;
        return qs;
    }

    /** Attempt to cancel a previously sent packet.  Returns true if a packet was found we could cancel */
    virtual bool cancelSending(NodeNum from, PacketId id) { return false; }

    /** Attempt to find a packet in the TxQueue. Returns true if the packet was found. */
    virtual bool findInTxQueue(NodeNum from, PacketId id) { return false; }

    // methods from radiohead

    /// Initialise the Driver transport hardware and software.
    /// Make sure the Driver is properly configured before calling init().
    /// \return true if initialisation succeeded.
    virtual bool init();

    /// Apply any radio provisioning changes
    /// Make sure the Driver is properly configured before calling init().
    /// \return true if initialisation succeeded.
    virtual bool reconfigure();

    /** The delay to use for retransmitting dropped packets */
    [[nodiscard]] uint32_t getRetransmissionMsec(const meshtastic_MeshPacket *p);

    /** The delay to use when we want to send something */
    [[nodiscard]] uint32_t getTxDelayMsec();

    /** The CW to use when calculating SNR_based delays */
    [[nodiscard]] uint8_t getCWsize(float snr);

    /** The worst-case SNR_based packet delay */
    [[nodiscard]] uint32_t getTxDelayMsecWeightedWorst(float snr);

    /** Returns true if we should rebroadcast early like a ROUTER */
    [[nodiscard]] bool shouldRebroadcastEarlyLikeRouter(meshtastic_MeshPacket *p);

    /** The delay to use when we want to flood a message. Use a weighted scale based on SNR */
    [[nodiscard]] uint32_t getTxDelayMsecWeighted(meshtastic_MeshPacket *p);

    /** If the packet is not already in the late rebroadcast window, move it there */
    virtual void clampToLateRebroadcastWindow(NodeNum from, PacketId id) { return; }

    /**
     * If there is a packet pending TX in the queue with a worse hop limit, remove it pending replacement with a better version
     * @return Whether a pending packet was removed
     */
    virtual bool removePendingTXPacket(NodeNum from, PacketId id, uint32_t hop_limit_lt) { return false; }

    /**
     * Calculate airtime per
     * https://www.rs-online.com/designspark/rel-assets/ds-assets/uploads/knowledge-items/application-notes-for-the-internet-of-things/LoRa%20Design%20Guide.pdf
     * section 4
     *
     * @return num msecs for the packet
     */
    [[nodiscard]] uint32_t getPacketTime(const meshtastic_MeshPacket *p, bool received = false);
    [[nodiscard]] virtual uint32_t getPacketTime(uint32_t totalPacketLen, bool received = false) = 0;

    /**
     * Get the channel we saved.
     */
    [[nodiscard]] uint32_t getChannelNum();

    /**
     * Get the frequency we saved.
     */
    [[nodiscard]] virtual float getFreq();

    /// Some boards (1st gen Pinetab Lora module) have broken IRQ wires, so we need to poll via i2c registers
    virtual bool isIRQPending() { return false; }

    // Whether we use the default frequency slot given our LoRa config (region and modem preset)
    static bool uses_default_frequency_slot;

    // Whether we have a custom channel name
    static bool uses_custom_channel_name;

    static bool checkOrClampConfigLora(meshtastic_Config_LoRaConfig &loraConfig, bool clamp);

    // Check if a candidate region is compatible and valid, with no side effects (safe for
    // speculative UI checks). errBuf, if given, receives the failure reason.
    static bool checkConfigRegion(const meshtastic_Config_LoRaConfig &loraConfig, char *errBuf = nullptr, size_t errLen = 0);

    // Check if a candidate region is compatible and valid. On failure, logs at ERROR,
    // records a critical error, and sends a client notification.
    static bool validateConfigRegion(const meshtastic_Config_LoRaConfig &loraConfig);

    // Check if a candidate radio configuration is valid.
    static bool validateConfigLora(const meshtastic_Config_LoRaConfig &loraConfig);

    // Make a candidate radio configuration valid, even if it isn't.
    static void clampConfigLora(meshtastic_Config_LoRaConfig &loraConfig);

    // If preset is locked to a sibling of currentRegion among the swappable EU regions
    // (EU_868/EU_866/EU_N_868), return the sibling region owning the preset, else nullptr.
    static const RegionInfo *regionSwapForPreset(meshtastic_Config_LoRaConfig_RegionCode currentRegion,
                                                 meshtastic_Config_LoRaConfig_ModemPreset preset);

  protected:
    int8_t power = 17; // Set by applyModemConfig()

    float savedFreq;
    uint32_t savedChannelNum;

    /***
     * given a packet set sendingPacket and decode the protobufs into radiobuf.  Returns # of bytes to send (including the
     * PacketHeader & payload).
     *
     * Used as the first step of
     */
    [[nodiscard]] size_t beginSending(meshtastic_MeshPacket *p);

    /**
     * Some regulatory regions limit xmit power.
     * This function should be called by subclasses after setting their desired power.  It might lower it
     */
    void limitPower(int8_t MAX_POWER);

    /**
     * Save the frequency we selected for later reuse.
     */
    virtual void saveFreq(float savedFreq);

    /**
     * Save the channel we selected for later reuse.
     */
    virtual void saveChannelNum(uint32_t savedChannelNum);

    /**
     * Get current RSSI reading from the radio.
     * Returns 0 if not available.
     */
    virtual int16_t getCurrentRSSI() { return 0; }

  private:
    /**
     * Convert our modemConfig enum into wf, sf, etc...
     *
     * These parameters will be pull from the channelSettings global
     */
    void applyModemConfig();

    /// Return 0 if sleep is okay
    int preflightSleepCb(void *unused = NULL) { return canSleep() ? 0 : 1; }

    int notifyDeepSleepCb(void *unused = NULL);

    int reloadConfig(void *unused)
    {
        reconfigure();
        return 0;
    }
};

std::unique_ptr<RadioInterface> initLoRa();

/// Debug printing for packets
void printPacket(const char *prefix, const meshtastic_MeshPacket *p);
