#pragma once

#include "../Device.h"
#include "Unit.h"

#include <Tactility/Mutex.h>
#include <Tactility/Thread.h>

#include <deque>
#include <utility>

namespace tt::hal::radio {

struct RxPacket {
    std::vector<uint8_t> data;
    float rssi;
    float snr;
};

struct TxPacket {
    std::vector<uint8_t> data;
    uint32_t address; // FSK only
};

class RadioDevice : public Device {

public:
    enum class Modulation {
        None,
        Fsk,
        LoRa,
        LrFhss
    };

    enum class Parameter {
        Power,
        Frequency,
        Bandwidth,
        SpreadFactor,
        CodingRate,
        SyncWord,
        PreambleLength,
        FrequencyDeviation,
        DataRate,
        AddressWidth,
        NarrowGrid
    };

    enum class ParameterStatus {
        NotDefined,
        ValueError,
        Success
    };

    typedef int RxSubscriptionId;
    typedef int TxId;


    enum class State {
        PendingOn,
        On,
        Error,
        PendingOff,
        Off
    };

    enum class TransmissionState {
        Queued,
        PendingTransmit,
        Transmitted,
        Timeout,
        Error
    };

    using TxStateCallback = std::function<void(TxId id, TransmissionState state)>;

protected:
    struct TxItem {
        TxId id;
        TxPacket packet;
        TxStateCallback callback;
    };

private:
    struct RxSubscription {
        RxSubscriptionId id;
        std::shared_ptr<std::function<void(Device::Id id, const RxPacket&)>> onData;
    };

    State state;
    Modulation modulation;
    Mutex mutex = Mutex(Mutex::Type::Recursive);
    std::vector<RxSubscription> rxSubscriptions;
    std::deque<TxItem> txQueue;
    TxId lastTxId = 0;
    RxSubscriptionId lastRxSubscriptionId = 0;

protected:
    const Mutex &getMutex() const { return mutex; }
    void setState(State newState);

    virtual void txQueuedSignal() = 0;

    size_t getTxQueueSize() const {
        auto lock = mutex.asScopedLock();
        lock.lock();
        const auto size = txQueue.size();
        return size;
    }

    TxItem popNextQueuedTx() {
        auto lock = mutex.asScopedLock();
        lock.lock();

        auto tx = std::move(txQueue.front());
        txQueue.pop_front();

        return tx;
    }

    void publishRx(const RxPacket& packet);

public:
    explicit RadioDevice()
        : state(State::Off), modulation(Modulation::None) {}

    ~RadioDevice() override = default;

    Type getType() const override { return Type::Radio; }

    bool setModulation(const Modulation newModulation);
    Modulation getModulation() const;
    virtual ParameterStatus setParameter(const Parameter parameter, const float value) = 0;
    virtual ParameterStatus getParameter(const Parameter parameter, float &value) const = 0;
    virtual Unit getParameterUnit(const Parameter parameter) const = 0;
    virtual bool canTransmit(const Modulation modulation) = 0;
    virtual bool canReceive(const Modulation modulation) = 0;

    virtual bool start() = 0;
    virtual bool stop() = 0;

    TxId transmit(const TxPacket& packet, TxStateCallback callback) {
        auto lock = mutex.asScopedLock();
        lock.lock();
        const auto txId = lastTxId;
        txQueue.push_back(TxItem{.id = txId, .packet = packet, .callback = callback});
        callback(txId, TransmissionState::Queued);
        lastTxId++;
        txQueuedSignal();
        return txId;
    }

    RxSubscriptionId subscribeRx(const std::function<void(Device::Id id, const RxPacket&)>& onData) {
        auto lock = mutex.asScopedLock();
        lock.lock();
        rxSubscriptions.push_back({
            .id = ++lastRxSubscriptionId,
            .onData = std::make_shared<std::function<void(Device::Id, const RxPacket&)>>(onData)
        });
        return lastRxSubscriptionId;
    }

    void unsubscribeRx(RxSubscriptionId subscriptionId) {
        auto lock = mutex.asScopedLock();
        lock.lock();
        std::erase_if(rxSubscriptions, [subscriptionId](auto& subscription) { return subscription.id == subscriptionId; });
    }

    State getState() const;
};

const char* toString(RadioDevice::Modulation modulation);
const char* toString(RadioDevice::Parameter parameter);
}
