#include "SimRadio.h"
#include "MeshService.h"
#include "Router.h"

SimRadio::SimRadio() : NotifiedWorkerThread("SimRadio")
{
    instance = this;
}

SimRadio *SimRadio::instance;

ErrorCode SimRadio::send(meshtastic_MeshPacket *p)
{
    printPacket("enqueuing for send", p);

    bool dropped = false;
    ErrorCode res = txQueue.enqueue(p, &dropped) ? ERRNO_OK : ERRNO_UNKNOWN;

    if (dropped) {
        txDrop++;
    }

    if (res != ERRNO_OK) { // we weren't able to queue it, so we must drop it to prevent leaks
        packetPool.release(p);
        return res;
    }

    // set (random) transmit delay to let others reconfigure their radio,
    // to avoid collisions and implement timing-based flooding
    LOG_DEBUG("Set random delay before tx");
    setTransmitDelay();
    return res;
}

void SimRadio::setTransmitDelay()
{
    meshtastic_MeshPacket *p = txQueue.getFront();
    // We want all sending/receiving to be done by our daemon thread.
    // We use a delay here because this packet might have been sent in response to a packet we just received.
    // So we want to make sure the other side has had a chance to reconfigure its radio.

    /* We assume if rx_snr = 0 and rx_rssi = 0, the packet was generated locally.
     *   This assumption is valid because of the offset generated by the radio to account for the noise
     *   floor.
     */
    if (p->rx_snr == 0 && p->rx_rssi == 0) {
        startTransmitTimer(true);
    } else {
        // If there is a SNR, start a timer scaled based on that SNR.
        LOG_DEBUG("rx_snr found. hop_limit:%d rx_snr:%f", p->hop_limit, p->rx_snr);
        startTransmitTimerRebroadcast(p);
    }
}

void SimRadio::startTransmitTimer(bool withDelay)
{
    // If we have work to do and the timer wasn't already scheduled, schedule it now
    if (!txQueue.empty()) {
        uint32_t delayMsec = !withDelay ? 1 : getTxDelayMsec();
        // LOG_DEBUG("xmit timer %d", delay);
        notifyLater(delayMsec, TRANSMIT_DELAY_COMPLETED, false);
    }
}

void SimRadio::startTransmitTimerRebroadcast(meshtastic_MeshPacket *p)
{
    // If we have work to do and the timer wasn't already scheduled, schedule it now
    if (!txQueue.empty()) {
        uint32_t delayMsec = getTxDelayMsecWeighted(p);
        // LOG_DEBUG("xmit timer %d", delay);
        notifyLater(delayMsec, TRANSMIT_DELAY_COMPLETED, false);
    }
}

void SimRadio::handleTransmitInterrupt()
{
    // This can be null if we forced the device to enter standby mode.  In that case
    // ignore the transmit interrupt
    if (sendingPacket)
        completeSending();

    isReceiving = true;
    if (receivingPacket) // This happens when we don't consider something a collision if we weren't sending long enough
        handleReceiveInterrupt();
}

void SimRadio::completeSending()
{
    // We are careful to clear sending packet before calling printPacket because
    // that can take a long time
    auto p = sendingPacket;
    sendingPacket = NULL;

    if (p) {
        txGood++;
        if (!isFromUs(p))
            txRelay++;
        printPacket("Completed sending", p);

        // We are done sending that packet, release it
        packetPool.release(p);
        // LOG_DEBUG("Done with send");
    }
}

/** Could we send right now (i.e. either not actively receiving or transmitting)? */
bool SimRadio::canSendImmediately()
{
    // We wait _if_ we are partially though receiving a packet (rather than just merely waiting for one).
    // To do otherwise would be doubly bad because not only would we drop the packet that was on the way in,
    // we almost certainly guarantee no one outside will like the packet we are sending.
    bool busyTx = sendingPacket != NULL;
    bool busyRx = isReceiving && isActivelyReceiving();

    if (busyTx || busyRx) {
        if (busyTx)
            LOG_WARN("Can not send yet, busyTx");
        if (busyRx)
            LOG_WARN("Can not send yet, busyRx");
        return false;
    } else
        return true;
}

bool SimRadio::isActivelyReceiving()
{
    return receivingPacket != nullptr;
}

bool SimRadio::isChannelActive()
{
    return receivingPacket != nullptr;
}

/** Attempt to cancel a previously sent packet.  Returns true if a packet was found we could cancel */
bool SimRadio::cancelSending(NodeNum from, PacketId id)
{
    auto p = txQueue.remove(from, id);
    if (p)
        packetPool.release(p); // free the packet we just removed

    bool result = (p != NULL);
    LOG_DEBUG("cancelSending id=0x%x, removed=%d", id, result);
    return result;
}

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

void SimRadio::onNotify(uint32_t notification)
{
    switch (notification) {
    case ISR_TX:
        handleTransmitInterrupt();
        //  LOG_DEBUG("tx complete - starting timer");
        startTransmitTimer();
        break;
    case ISR_RX:
        handleReceiveInterrupt();
        //  LOG_DEBUG("rx complete - starting timer");
        startTransmitTimer();
        break;
    case TRANSMIT_DELAY_COMPLETED:
        if (receivingPacket) { // This happens when we had a timer pending and we started receiving
            handleReceiveInterrupt();
            startTransmitTimer();
            break;
        }
        LOG_DEBUG("delay done");

        // If we are not currently in receive mode, then restart the random delay (this can happen if the main thread
        // has placed the unit into standby)  FIXME, how will this work if the chipset is in sleep mode?
        if (!txQueue.empty()) {
            if (!canSendImmediately()) {
                // LOG_DEBUG("Currently Rx/Tx-ing: set random delay");
                setTransmitDelay(); // currently Rx/Tx-ing: reset random delay
            } else {
                if (isChannelActive()) { // check if there is currently a LoRa packet on the channel
                    // LOG_DEBUG("Channel is active: set random delay");
                    setTransmitDelay(); // reset random delay
                } else {
                    // Send any outgoing packets we have ready
                    meshtastic_MeshPacket *txp = txQueue.dequeue();
                    assert(txp);
                    startSend(txp);
                    // Packet has been sent, count it toward our TX airtime utilization.
                    uint32_t xmitMsec = RadioInterface::getPacketTime(txp);
                    airTime->logAirtime(TX_LOG, xmitMsec);

                    notifyLater(xmitMsec, ISR_TX, false); // Model the time it is busy sending
                }
            }
        } else {
            // LOG_DEBUG("done with txqueue");
        }
        break;
    default:
        assert(0); // We expected to receive a valid notification from the ISR
    }
}

/** start an immediate transmit */
void SimRadio::startSend(meshtastic_MeshPacket *txp)
{
    printPacket("Start low level send", txp);
    isReceiving = false;
    size_t numbytes = beginSending(txp);
    meshtastic_MeshPacket *p = packetPool.allocCopy(*txp);

    // A packet we originate that's encrypted for someone else (a PKI DM, channel == 0) can't be
    // decrypted here. Attempting it only logs a spurious "no suitable channel" miss, and the
    // ciphertext (up to MAX_LORA_PAYLOAD_LEN + MESHTASTIC_PKC_OVERHEAD) overflows the decoded
    // loopback payload. Carry such packets as ciphertext instead so the receiving sim node can
    // decrypt them as if they had arrived over the air (see unpackAndReceive()).
    bool carryEncrypted = p->pki_encrypted;
    if (!carryEncrypted) {
        perhapsDecode(p);
        // Channel packets we couldn't decrypt (e.g. relaying an unknown channel) are carried too.
        carryEncrypted = (p->which_payload_variant == meshtastic_MeshPacket_encrypted_tag);
    }

    meshtastic_Compressed c = meshtastic_Compressed_init_default;
    // The Compressed wrapper is re-encoded back into decoded.payload.bytes (the same 233-byte field
    // its data is copied from), so the carried bytes must leave room for the protobuf framing or
    // pb_encode_to_bytes() below overflows and silently drops the loopback payload. meshtastic_Compressed_size
    // is the max encoded size for a full data field, so (meshtastic_Compressed_size - sizeof(c.data.bytes))
    // is the worst-case framing overhead to reserve.
    constexpr size_t loopbackCapacity = sizeof(p->decoded.payload.bytes) - (meshtastic_Compressed_size - sizeof(c.data.bytes));
    if (carryEncrypted) {
        // Sentinel portnum UNKNOWN_APP marks the payload as ciphertext for unpackAndReceive().
        c.portnum = meshtastic_PortNum_UNKNOWN_APP;
        if (p->encrypted.size <= loopbackCapacity) {
            memcpy(&c.data.bytes, p->encrypted.bytes, p->encrypted.size);
            c.data.size = p->encrypted.size;
        } else {
            LOG_WARN("Encrypted payload (%u) exceeds sim loopback capacity (%u)! Send empty payload", (unsigned)p->encrypted.size,
                     (unsigned)loopbackCapacity);
        }
    } else {
        c.portnum = p->decoded.portnum;
        // LOG_DEBUG("Send back to simulator with portNum %d", p->decoded.portnum);
        if (p->decoded.payload.size <= loopbackCapacity) {
            memcpy(&c.data.bytes, p->decoded.payload.bytes, p->decoded.payload.size);
            c.data.size = p->decoded.payload.size;
        } else {
            LOG_WARN("Payload size larger than compressed message allows! Send empty payload");
        }
    }

    // pb_encode_to_bytes writes into decoded.payload, which aliases `encrypted` in the union, so all
    // reads of p->encrypted above must be complete before this point.
    p->decoded = meshtastic_Data_init_zero;
    p->which_payload_variant = meshtastic_MeshPacket_decoded_tag;
    p->decoded.payload.size =
        pb_encode_to_bytes(p->decoded.payload.bytes, sizeof(p->decoded.payload.bytes), &meshtastic_Compressed_msg, &c);
    p->decoded.portnum = meshtastic_PortNum_SIMULATOR_APP;

    service->sendQueueStatusToPhone(router->getQueueStatus(), 0, p->id);
    service->sendToPhone(p); // Sending back to simulator
    service->loop();         // Process the send immediately
}

// Simulates device received a packet via the LoRa chip
void SimRadio::unpackAndReceive(meshtastic_MeshPacket &p)
{
    // Simulator packet (=Compressed packet) is encapsulated in a MeshPacket, so need to unwrap first
    meshtastic_Compressed scratch;
    if (p.which_payload_variant == meshtastic_MeshPacket_decoded_tag) {
        memset(&scratch, 0, sizeof(scratch));
        if (pb_decode_from_bytes(p.decoded.payload.bytes, p.decoded.payload.size, &meshtastic_Compressed_msg, &scratch)) {
            if (scratch.portnum == meshtastic_PortNum_UNKNOWN_APP) {
                // The sender carried ciphertext verbatim (a packet it couldn't decrypt, e.g. a PKI DM,
                // see startSend()). Restore it as an encrypted packet so the router decrypts it as if
                // received over the air, instead of treating the ciphertext as a plaintext payload. The
                // outer MeshPacket still carries from/to/id/channel/pki_encrypted, which decrypt needs.
                p.which_payload_variant = meshtastic_MeshPacket_encrypted_tag;
                memcpy(&p.encrypted.bytes, scratch.data.bytes, scratch.data.size);
                p.encrypted.size = scratch.data.size;
            } else {
                // Extract the original payload and replace
                memcpy(&p.decoded.payload, &scratch.data, sizeof(scratch.data));
                // Switch the port from PortNum_SIMULATOR_APP back to the original PortNum
                p.decoded.portnum = scratch.portnum;
            }
        } else
            LOG_ERROR("Error decoding proto for simulator message!");
    }
    // Let SimRadio receive as if it did via its LoRa chip
    startReceive(&p);
}

void SimRadio::startReceive(meshtastic_MeshPacket *p)
{
#ifdef USERPREFS_SIMRADIO_EMULATE_COLLISIONS
    if (isActivelyReceiving()) {
        LOG_WARN("Collision detected, dropping current and previous packet!");
        rxBad++;
        airTime->logAirtime(RX_ALL_LOG, getPacketTime(receivingPacket, true));
        packetPool.release(receivingPacket);
        receivingPacket = nullptr;
        return;
    } else if (sendingPacket) {
        uint32_t airtimeLeft = tillRun(millis());
        if (airtimeLeft <= 0) {
            LOG_WARN("Transmitting packet was already done");
            handleTransmitInterrupt(); // Finish sending first
        } else if ((interval - airtimeLeft) > preambleTimeMsec) {
            // Only if transmitting for longer than preamble there is a collision
            // (channel should actually be detected as active otherwise)
            LOG_WARN("Collision detected during transmission!");
            return;
        }
    }
    isReceiving = true;
    receivingPacket = packetPool.allocCopy(*p);
    uint32_t airtimeMsec = getPacketTime(p, true);
    notifyLater(airtimeMsec, ISR_RX, false); // Model the time it is busy receiving
#else
    isReceiving = true;
    receivingPacket = packetPool.allocCopy(*p);
    handleReceiveInterrupt(); // Simulate receiving the packet immediately
    startTransmitTimer();
#endif
}

meshtastic_QueueStatus SimRadio::getQueueStatus()
{
    meshtastic_QueueStatus qs;

    qs.res = qs.mesh_packet_id = 0;
    qs.free = txQueue.getFree();
    qs.maxlen = txQueue.getMaxLen();

    return qs;
}

void SimRadio::handleReceiveInterrupt()
{
    if (receivingPacket == nullptr) {
        return;
    }

    if (!isReceiving) {
        LOG_DEBUG("*** WAS_ASSERT *** handleReceiveInterrupt called when not in receive mode");
        return;
    }

    LOG_DEBUG("HANDLE RECEIVE INTERRUPT");
    rxGood++;

    meshtastic_MeshPacket *mp = packetPool.allocCopy(*receivingPacket); // keep a copy in packetPool
    packetPool.release(receivingPacket);                                // release the original
    receivingPacket = nullptr;

    printPacket("Lora RX", mp);

    airTime->logAirtime(RX_LOG, RadioInterface::getPacketTime(mp, true));

    deliverToReceiver(mp);
}

size_t SimRadio::getPacketLength(meshtastic_MeshPacket *mp)
{
    auto &p = mp->decoded;
    return (size_t)p.payload.size + sizeof(PacketHeader);
}

int16_t SimRadio::readData(uint8_t *data, size_t len)
{
    int16_t state = RADIOLIB_ERR_NONE;

    if (state == RADIOLIB_ERR_NONE) {
        // add null terminator
        data[len] = 0;
    }

    return state;
}

/**
 * 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
 */
uint32_t SimRadio::getPacketTime(uint32_t pl, bool received)
{
    float bandwidthHz = bw * 1000.0f;
    bool headDisable = false; // we currently always use the header
    float tSym = (1 << sf) / bandwidthHz;

    bool lowDataOptEn = tSym > 16e-3 ? true : false; // Needed if symbol time is >16ms

    float tPreamble = (preambleLength + 4.25f) * tSym;
    float numPayloadSym =
        8 + max(ceilf(((8.0f * pl - 4 * sf + 28 + 16 - 20 * headDisable) / (4 * (sf - 2 * lowDataOptEn))) * cr), 0.0f);
    float tPayload = numPayloadSym * tSym;
    float tPacket = tPreamble + tPayload;

    uint32_t msecs = tPacket * 1000;
    return msecs;
}

int16_t SimRadio::getCurrentRSSI()
{
    // Simulated radio - return a reasonable default noise floor
    return -120;
}