#pragma once

#include "NodeDB.h"
#include <memory>

// Number of relayers we keep track of. Use 6 to be efficient with memory alignment of PacketRecord to 20 bytes
#define NUM_RELAYERS 6
#define HOP_LIMIT_HIGHEST_MASK 0x07 // Bits 0-2
#define HOP_LIMIT_OUR_TX_MASK 0x38  // Bits 3-5
#define HOP_LIMIT_OUR_TX_SHIFT 3    // Bits 3-5

/**
 * This is a mixin that adds a record of past packets we have seen
 */
class PacketHistory
{
  private:
    struct PacketRecord { // A record of a recent message broadcast, no need to be visible outside this class.
        NodeNum sender;
        PacketId id;
        uint32_t rxTimeMsec;              // Unix time in msecs - the time we received it,  0 means empty
        uint8_t next_hop;                 // The next hop asked for this packet
        uint8_t hop_limit;                // bit 0-2: Highest hop limit observed for this packet,
                                          // bit 3-5: our hop limit when we first transmitted it
        uint8_t relayed_by[NUM_RELAYERS]; // Array of nodes that relayed this packet
    };                                    // 4B + 4B + 4B + 1B + 1B + 6B = 20B

    uint32_t recentPacketsCapacity =
        0; // Can be set in constructor, no need to recompile. Used to allocate memory for mx_recentPackets.
    std::unique_ptr<PacketRecord[]> recentPackets; // Simple and fixed in size. Debloat.

#if !MESHTASTIC_EXCLUDE_PKT_HISTORY_HASH
    // Open-addressing hash table for O(1) lookup in find(), replacing the O(N) linear scan.
    // Maps (sender, id) -> index into recentPackets[]. Uses linear probing with a load factor <= 0.5.
    // The load factor invariant holds permanently: hashCapacity = 2 * nextPowerOf2(recentPacketsCapacity),
    // and at most recentPacketsCapacity entries can ever be live (one per recentPackets[] slot).
    static constexpr uint16_t HASH_EMPTY = 0xFFFF;
    std::unique_ptr<uint16_t[]> hashIndex;
    uint32_t hashCapacity = 0; // Always a power of 2
    uint32_t hashMask = 0;     // hashCapacity - 1, for fast modular indexing

    uint32_t hashSlot(NodeNum sender, PacketId id) const;
    void hashInsert(NodeNum sender, PacketId id, uint16_t slotIdx);
    void hashRemove(NodeNum sender, PacketId id);
    void hashRebuild();
#endif

    /** Find a packet record in history.
     * @param sender NodeNum
     * @param id PacketId
     * @return pointer to PacketRecord if found, NULL if not found */
    PacketRecord *find(NodeNum sender, PacketId id);

    /** Insert/Replace oldest PacketRecord in mx_recentPackets.
     * @param r PacketRecord to insert or replace */
    void insert(const PacketRecord &r); // Insert or replace a packet record in the history

    /* Check if a certain node was a relayer of a packet in the history given iterator
     * If wasSole is not nullptr, it will be set to true if the relayer was the only relayer of that packet
     * @return true if node was indeed a relayer, false if not */
    bool wasRelayer(const uint8_t relayer, const PacketRecord &r, bool *wasSole = nullptr);

    uint8_t getHighestHopLimit(const PacketRecord &r);
    void setHighestHopLimit(PacketRecord &r, uint8_t hopLimit);
    uint8_t getOurTxHopLimit(const PacketRecord &r);
    void setOurTxHopLimit(PacketRecord &r, uint8_t hopLimit);

  public:
    explicit PacketHistory(uint32_t size = -1); // Constructor with size parameter, default is PACKETHISTORY_MAX

    /**
     * Update recentBroadcasts and return true if we have already seen this packet
     *
     * @param withUpdate if true and not found we add an entry to recentPackets
     * @param wasFallback if not nullptr, packet will be checked for fallback to flooding and value will be set to true if so
     * @param weWereNextHop if not nullptr, packet will be checked for us being the next hop and value will be set to true if so
     * @param wasUpgraded if not nullptr, will be set to true if this packet has better hop_limit than previously seen
     */
    bool wasSeenRecently(const meshtastic_MeshPacket *p, bool withUpdate = true, bool *wasFallback = nullptr,
                         bool *weWereNextHop = nullptr, bool *wasUpgraded = nullptr);

    /* Check if a certain node was a relayer of a packet in the history given an ID and sender
     * If wasSole is not nullptr, it will be set to true if the relayer was the only relayer of that packet
     * @return true if node was indeed a relayer, false if not */
    bool wasRelayer(const uint8_t relayer, const uint32_t id, const NodeNum sender, bool *wasSole = nullptr);

    /**
     * Check two relayers against the same packet record with a single lookup.
     * Avoids redundant find() calls when checking multiple relayers for the same (id, sender) pair.
     * @param r1Result set to true if relayer1 was a relayer
     * @param r2Result set to true if relayer2 was a relayer
     * @param r2WasSole if not nullptr, set to true if relayer2 was the sole relayer
     */
    void checkRelayers(uint8_t relayer1, uint8_t relayer2, uint32_t id, NodeNum sender, bool *r1Result, bool *r2Result,
                       bool *r2WasSole = nullptr);

    // Remove a relayer from the list of relayers of a packet in the history given an ID and sender
    void removeRelayer(const uint8_t relayer, const uint32_t id, const NodeNum sender);

    // To check if the PacketHistory was initialized correctly by constructor
    bool initOk(void) { return recentPackets != nullptr && recentPacketsCapacity != 0; }
};
