// Thread.cpp - Arduino wrapper around the vendored OpenThread core.

#include "Thread.h"

#include <string.h>

#include "openthread-core-config.h"

extern "C" {
#include <openthread/error.h>
#include <openthread/instance.h>
#include <openthread/tasklet.h>
#include <openthread/thread.h>
#include <openthread/dataset.h>
#include <openthread/ip6.h>
#include <openthread/link.h>
#include <openthread/message.h>
#include <openthread/platform/radio.h>
#if OPENTHREAD_CONFIG_COAP_API_ENABLE
#include <openthread/coap.h>
#endif
}

#include "ot_platform_arduino.h"

ThreadClass Thread;
ThreadClass &NiusThread = Thread; // alias, see ArduinoNRF_Thread.h

namespace {

otInstance *sInstance;

} // namespace

ThreadClass::Status ThreadClass::begin()
{
    if (sInstance != nullptr)
    {
        return THREAD_OK;
    }

    nrfOtPlatformInit();

    sInstance = otInstanceInitSingle();

    return (sInstance != nullptr) ? THREAD_OK : THREAD_INTERNAL;
}

void ThreadClass::end()
{
    if (sInstance != nullptr)
    {
        otThreadSetEnabled(sInstance, false);
        otIp6SetEnabled(sInstance, false);
        otInstanceFinalize(sInstance);
        sInstance = nullptr;
    }
}

bool ThreadClass::isAvailable() { return sInstance != nullptr; }

ThreadClass::Status ThreadClass::setNetwork(const char   *networkName,
                                            uint8_t       channel,
                                            uint16_t      panId,
                                            const uint8_t networkKey[16],
                                            const uint8_t extendedPanId[8])
{
    static const uint8_t kDefaultExtPanId[8] = {0xAD, 0x0E, 0x12, 0x34, 0x56, 0x78, 0x9A, 0xBC};

    if (sInstance == nullptr)
    {
        return THREAD_NOT_STARTED;
    }
    if (networkName == nullptr || networkKey == nullptr || channel < 11 || channel > 26 ||
        panId == 0xFFFF || strlen(networkName) > 16)
    {
        return THREAD_BAD_PARAM;
    }

    otOperationalDataset dataset;

    memset(&dataset, 0, sizeof(dataset));

    dataset.mActiveTimestamp.mSeconds       = 1;
    dataset.mComponents.mIsActiveTimestampPresent = true;

    strncpy(dataset.mNetworkName.m8, networkName, sizeof(dataset.mNetworkName.m8) - 1);
    dataset.mComponents.mIsNetworkNamePresent = true;

    memcpy(dataset.mNetworkKey.m8, networkKey, 16);
    dataset.mComponents.mIsNetworkKeyPresent = true;

    memcpy(dataset.mExtendedPanId.m8, (extendedPanId != nullptr) ? extendedPanId : kDefaultExtPanId, 8);
    dataset.mComponents.mIsExtendedPanIdPresent = true;

    // Fixed mesh-local ULA prefix; both nodes must agree (it travels in the
    // dataset anyway once a real commissioner is involved).
    static const uint8_t kMlPrefix[8] = {0xFD, 0x00, 0x0D, 0xB8, 0x00, 0x00, 0x00, 0x00};
    memcpy(dataset.mMeshLocalPrefix.m8, kMlPrefix, 8);
    dataset.mComponents.mIsMeshLocalPrefixPresent = true;

    dataset.mPanId = panId;
    dataset.mComponents.mIsPanIdPresent = true;

    dataset.mChannel = channel;
    dataset.mComponents.mIsChannelPresent = true;

    return (otDatasetSetActive(sInstance, &dataset) == OT_ERROR_NONE) ? THREAD_OK : THREAD_INTERNAL;
}

ThreadClass::Status ThreadClass::start()
{
    if (sInstance == nullptr)
    {
        return THREAD_NOT_STARTED;
    }
    if (otIp6SetEnabled(sInstance, true) != OT_ERROR_NONE)
    {
        return THREAD_INTERNAL;
    }
    return (otThreadSetEnabled(sInstance, true) == OT_ERROR_NONE) ? THREAD_OK : THREAD_INTERNAL;
}

ThreadClass::Status ThreadClass::stop()
{
    if (sInstance == nullptr)
    {
        return THREAD_NOT_STARTED;
    }
    otThreadSetEnabled(sInstance, false);
    otIp6SetEnabled(sInstance, false);
    return THREAD_OK;
}

ThreadClass::Status ThreadClass::setLinkMode(LinkMode mode, uint32_t pollPeriodMs)
{
    if (sInstance == nullptr)
    {
        return THREAD_NOT_STARTED;
    }

    otLinkModeConfig cfg;

    memset(&cfg, 0, sizeof(cfg));

    switch (mode)
    {
    case LINK_ROUTER:
        cfg.mRxOnWhenIdle = true;
        cfg.mDeviceType   = true; // FTD: router eligible
        cfg.mNetworkData  = true; // full network data
        break;
    case LINK_MED:
        cfg.mRxOnWhenIdle = true;
        break;
    case LINK_SED:
        // Radio sleeps between polls; the sub-MAC wakes it for each data
        // request to the parent.
        otLinkSetPollPeriod(sInstance, pollPeriodMs);
        break;
    default:
        return THREAD_BAD_PARAM;
    }

    return (otThreadSetLinkMode(sInstance, cfg) == OT_ERROR_NONE) ? THREAD_OK : THREAD_INTERNAL;
}

void ThreadClass::process()
{
    if (sInstance == nullptr)
    {
        return;
    }

    nrfOtPlatformProcess(sInstance);
    nrfOtRadioProcess(sInstance);

    while (otTaskletsArePending(sInstance))
    {
        otTaskletsProcess(sInstance);
    }
}

ThreadClass::Role ThreadClass::role()
{
    if (sInstance == nullptr)
    {
        return ROLE_DISABLED;
    }
    return (Role)otThreadGetDeviceRole(sInstance);
}

const char *ThreadClass::roleString()
{
    switch (role())
    {
    case ROLE_DISABLED: return "disabled";
    case ROLE_DETACHED: return "detached";
    case ROLE_CHILD:    return "child";
    case ROLE_ROUTER:   return "router";
    case ROLE_LEADER:   return "leader";
    }
    return "?";
}

bool ThreadClass::isAttached() { return role() >= ROLE_CHILD; }

uint16_t ThreadClass::rloc16()
{
    return (sInstance != nullptr) ? otThreadGetRloc16(sInstance) : 0xFFFE;
}

void ThreadClass::getEui64(uint8_t eui[8])
{
    if (eui == nullptr)
    {
        return;
    }
    if (sInstance == nullptr)
    {
        memset(eui, 0xFF, 8);
        return;
    }
    otPlatRadioGetIeeeEui64(sInstance, eui);
}

otInstance *ThreadClass::instance() { return sInstance; }

// ============================================================================
// CoAP - text-payload convenience layer over the official otCoap API
// ============================================================================

#if OPENTHREAD_CONFIG_COAP_API_ENABLE

namespace {

constexpr int    kCoapSlotCount  = 8;
constexpr size_t kCoapTextMax    = 128;

struct CoapSlot
{
    otCoapResource             resource;
    ThreadClass::CoapGetHandler  getFn;
    ThreadClass::CoapPostHandler postFn;
    bool                       used;
};

CoapSlot sCoapSlots[kCoapSlotCount];

ThreadClass::CoapReplyHandler sCoapReplyFn;

void coapRequestHandler(void *aContext, otMessage *aMessage, const otMessageInfo *aInfo)
{
    CoapSlot  *slot = static_cast<CoapSlot *>(aContext);
    otCoapCode code = otCoapMessageGetCode(aMessage);
    otCoapType type = otCoapMessageGetType(aMessage);
    // Unicast confirmable requests get a piggybacked ACK; multicast /
    // non-confirmable ones get a non-confirmable response (GET) or none.
    otCoapType rspType =
        (type == OT_COAP_TYPE_CONFIRMABLE) ? OT_COAP_TYPE_ACKNOWLEDGMENT : OT_COAP_TYPE_NON_CONFIRMABLE;

    if (code == OT_COAP_CODE_GET && slot->getFn != nullptr)
    {
        const char *text = slot->getFn();
        otMessage  *rsp  = otCoapNewMessage(sInstance, nullptr);

        if (rsp == nullptr)
        {
            return;
        }
        otCoapMessageInitResponse(rsp, aMessage, rspType, OT_COAP_CODE_CONTENT);
        otCoapMessageSetPayloadMarker(rsp);
        otMessageAppend(rsp, text, (uint16_t)strlen(text != nullptr ? text : ""));
        if (otCoapSendResponse(sInstance, rsp, aInfo) != OT_ERROR_NONE)
        {
            otMessageFree(rsp);
        }
    }
    else if ((code == OT_COAP_CODE_POST || code == OT_COAP_CODE_PUT) && slot->postFn != nullptr)
    {
        char     payload[kCoapTextMax];
        uint16_t len = otMessageRead(aMessage, otMessageGetOffset(aMessage), payload, sizeof(payload) - 1);

        payload[len] = '\0';
        slot->postFn(payload);

        if (type == OT_COAP_TYPE_CONFIRMABLE)
        {
            otMessage *rsp = otCoapNewMessage(sInstance, nullptr);

            if (rsp != nullptr &&
                (otCoapMessageInitResponse(rsp, aMessage, OT_COAP_TYPE_ACKNOWLEDGMENT, OT_COAP_CODE_CHANGED) !=
                     OT_ERROR_NONE ||
                 otCoapSendResponse(sInstance, rsp, aInfo) != OT_ERROR_NONE))
            {
                otMessageFree(rsp);
            }
        }
    }
}

void coapResponseHandler(void *aContext, otMessage *aMessage, const otMessageInfo *aInfo, otError aResult)
{
    (void)aContext;
    (void)aInfo;

    ThreadClass::CoapReplyHandler fn = sCoapReplyFn;

    sCoapReplyFn = nullptr;

    if (fn == nullptr)
    {
        return;
    }
    if (aResult != OT_ERROR_NONE || aMessage == nullptr)
    {
        fn(nullptr); // timeout / send failure
        return;
    }

    char     payload[kCoapTextMax];
    uint16_t len = otMessageRead(aMessage, otMessageGetOffset(aMessage), payload, sizeof(payload) - 1);

    payload[len] = '\0';
    fn(payload);
}

CoapSlot *coapFindOrAddSlot(const char *path)
{
    for (int i = 0; i < kCoapSlotCount; i++)
    {
        if (sCoapSlots[i].used && strcmp(sCoapSlots[i].resource.mUriPath, path) == 0)
        {
            return &sCoapSlots[i];
        }
    }
    for (int i = 0; i < kCoapSlotCount; i++)
    {
        CoapSlot &slot = sCoapSlots[i];

        if (!slot.used)
        {
            slot.used              = true;
            slot.resource.mUriPath = path; // must outlive the slot (string literal)
            slot.resource.mHandler = coapRequestHandler;
            slot.resource.mContext = &slot;
            slot.resource.mNext    = nullptr;
            otCoapAddResource(sInstance, &slot.resource);
            return &slot;
        }
    }
    return nullptr;
}

otError coapSendText(const char *destAddr, const char *path, const char *text, otCoapCode code,
                     otCoapResponseHandler handler)
{
    otMessageInfo info;
    otMessage    *msg;
    otError       err;

    memset(&info, 0, sizeof(info));
    if (otIp6AddressFromString(destAddr, &info.mPeerAddr) != OT_ERROR_NONE)
    {
        return OT_ERROR_INVALID_ARGS;
    }
    info.mPeerPort = OT_DEFAULT_COAP_PORT;

    msg = otCoapNewMessage(sInstance, nullptr);
    if (msg == nullptr)
    {
        return OT_ERROR_NO_BUFS;
    }

    // GETs need a reply, so they are confirmable; POSTs are fire-and-forget
    // (and multicast destinations must not be confirmable anyway).
    otCoapMessageInit(msg, (handler != nullptr) ? OT_COAP_TYPE_CONFIRMABLE : OT_COAP_TYPE_NON_CONFIRMABLE, code);
    otCoapMessageGenerateToken(msg, 4);
    err = otCoapMessageAppendUriPathOptions(msg, path);

    if (err == OT_ERROR_NONE && text != nullptr && text[0] != '\0')
    {
        err = otCoapMessageSetPayloadMarker(msg);
        if (err == OT_ERROR_NONE)
        {
            err = otMessageAppend(msg, text, (uint16_t)strlen(text));
        }
    }

    if (err == OT_ERROR_NONE)
    {
        err = otCoapSendRequest(sInstance, msg, &info, handler, nullptr);
    }
    if (err != OT_ERROR_NONE)
    {
        otMessageFree(msg);
    }
    return err;
}

} // namespace

ThreadClass::Status ThreadClass::coapStart(uint16_t port)
{
    if (sInstance == nullptr)
    {
        return THREAD_NOT_STARTED;
    }
    return (otCoapStart(sInstance, port) == OT_ERROR_NONE) ? THREAD_OK : THREAD_INTERNAL;
}

ThreadClass::Status ThreadClass::onCoapGet(const char *path, CoapGetHandler handler)
{
    if (sInstance == nullptr)
    {
        return THREAD_NOT_STARTED;
    }
    if (path == nullptr || handler == nullptr)
    {
        return THREAD_BAD_PARAM;
    }

    CoapSlot *slot = coapFindOrAddSlot(path);

    if (slot == nullptr)
    {
        return THREAD_INTERNAL; // all slots in use
    }
    slot->getFn = handler;
    return THREAD_OK;
}

ThreadClass::Status ThreadClass::onCoapPost(const char *path, CoapPostHandler handler)
{
    if (sInstance == nullptr)
    {
        return THREAD_NOT_STARTED;
    }
    if (path == nullptr || handler == nullptr)
    {
        return THREAD_BAD_PARAM;
    }

    CoapSlot *slot = coapFindOrAddSlot(path);

    if (slot == nullptr)
    {
        return THREAD_INTERNAL;
    }
    slot->postFn = handler;
    return THREAD_OK;
}

ThreadClass::Status ThreadClass::coapPost(const char *destAddr, const char *path, const char *text)
{
    if (sInstance == nullptr)
    {
        return THREAD_NOT_STARTED;
    }
    if (destAddr == nullptr || path == nullptr)
    {
        return THREAD_BAD_PARAM;
    }
    return (coapSendText(destAddr, path, text, OT_COAP_CODE_POST, nullptr) == OT_ERROR_NONE) ? THREAD_OK
                                                                                             : THREAD_INTERNAL;
}

ThreadClass::Status ThreadClass::coapGet(const char *destAddr, const char *path, CoapReplyHandler onReply)
{
    if (sInstance == nullptr)
    {
        return THREAD_NOT_STARTED;
    }
    if (destAddr == nullptr || path == nullptr || onReply == nullptr)
    {
        return THREAD_BAD_PARAM;
    }
    if (sCoapReplyFn != nullptr)
    {
        return THREAD_INTERNAL; // one outstanding GET at a time
    }

    sCoapReplyFn = onReply;

    if (coapSendText(destAddr, path, nullptr, OT_COAP_CODE_GET, coapResponseHandler) != OT_ERROR_NONE)
    {
        sCoapReplyFn = nullptr;
        return THREAD_INTERNAL;
    }
    return THREAD_OK;
}

#else // !OPENTHREAD_CONFIG_COAP_API_ENABLE

ThreadClass::Status ThreadClass::coapStart(uint16_t) { return THREAD_INTERNAL; }
ThreadClass::Status ThreadClass::onCoapGet(const char *, CoapGetHandler) { return THREAD_INTERNAL; }
ThreadClass::Status ThreadClass::onCoapPost(const char *, CoapPostHandler) { return THREAD_INTERNAL; }
ThreadClass::Status ThreadClass::coapPost(const char *, const char *, const char *) { return THREAD_INTERNAL; }
ThreadClass::Status ThreadClass::coapGet(const char *, const char *, CoapReplyHandler) { return THREAD_INTERNAL; }

#endif // OPENTHREAD_CONFIG_COAP_API_ENABLE
