/*
 * CANopen Heartbeat consumer object.
 *
 * @file        CO_HBconsumer.c
 * @ingroup     CO_HBconsumer
 * @author      Janez Paternoster
 * @copyright   2021 Janez Paternoster
 *
 * This file is part of <https://github.com/CANopenNode/CANopenNode>, a CANopen Stack.
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this
 * file except in compliance with the License. You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software distributed under the License is
 * distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and limitations under the License.
 */

#include "301/CO_HBconsumer.h"

#if ((CO_CONFIG_HB_CONS)&CO_CONFIG_HB_CONS_ENABLE) != 0

/* Verify HB consumer configuration */
#if (((CO_CONFIG_HB_CONS)&CO_CONFIG_HB_CONS_CALLBACK_CHANGE) != 0)                                                     \
    && (((CO_CONFIG_HB_CONS)&CO_CONFIG_HB_CONS_CALLBACK_MULTI) != 0)
#error CO_CONFIG_HB_CONS_CALLBACK_CHANGE and CO_CONFIG_HB_CONS_CALLBACK_MULTI cannot be set simultaneously!
#endif

/*
 * Read received message from CAN module.
 *
 * Function will be called (by CAN receive interrupt) every time, when CAN message with correct identifier
 * will be received. For more information and description of parameters see file CO_driver.h.
 */
static void
CO_HBcons_receive(void* object, void* msg) {
    CO_HBconsNode_t* HBconsNode = object;
    uint8_t DLC = CO_CANrxMsg_readDLC(msg);
    const uint8_t* data = CO_CANrxMsg_readData(msg);

    if (DLC == 1U) {
        /* copy data and set 'new message' flag. */
        HBconsNode->NMTstate = (CO_NMT_internalState_t)data[0];
        CO_FLAG_SET(HBconsNode->CANrxNew);
#if ((CO_CONFIG_HB_CONS)&CO_CONFIG_FLAG_CALLBACK_PRE) != 0
        /* Optional signal to RTOS, which can resume task, which handles HBcons. */
        if (HBconsNode->pFunctSignalPre != NULL) {
            HBconsNode->pFunctSignalPre(HBconsNode->functSignalObjectPre);
        }
#endif
    }
}

/*
 * Initialize one Heartbeat consumer entry
 *
 * This function is called from the @ref CO_HBconsumer_init() or when writing to OD entry 1016.
 *
 * @param HBcons This object.
 * @param idx index of the node in HBcons object
 * @param nodeId see OD 0x1016 description
 * @param consumerTime_ms in milliseconds. see OD 0x1016 description
 * @return
 */
static CO_ReturnError_t CO_HBconsumer_initEntry(CO_HBconsumer_t* HBcons, uint8_t idx, uint8_t nodeId,
                                                uint16_t consumerTime_ms);

#if ((CO_CONFIG_HB_CONS)&CO_CONFIG_FLAG_OD_DYNAMIC) != 0
/*
 * Custom function for writing OD object "Consumer heartbeat time"
 *
 * For more information see file CO_ODinterface.h, OD_IO_t.
 */
static ODR_t
OD_write_1016(OD_stream_t* stream, const void* buf, OD_size_t count, OD_size_t* countWritten) {
    CO_HBconsumer_t* HBcons;

    if (stream == NULL) {
        return ODR_DEV_INCOMPAT;
    }
    HBcons = stream->object;

    if ((buf == NULL) || (stream->subIndex < 1U)
        || (stream->subIndex > HBcons->numberOfMonitoredNodes) || (count != sizeof(uint32_t))
        || (countWritten == NULL)) {
        return ODR_DEV_INCOMPAT;
    }

    uint32_t val = CO_getUint32(buf);
    uint8_t nodeId = (uint8_t)((val >> 16) & 0xFFU);
    uint16_t consumer_time = (uint16_t)(val & 0xFFFFU);
    CO_ReturnError_t ret = CO_HBconsumer_initEntry(HBcons, stream->subIndex - 1U, nodeId, consumer_time);
    if (ret != CO_ERROR_NO) {
        return ODR_PAR_INCOMPAT;
    }

    /* write value to the original location in the Object Dictionary */
    return OD_writeOriginal(stream, buf, count, countWritten);
}
#endif

CO_ReturnError_t
CO_HBconsumer_init(CO_HBconsumer_t* HBcons, CO_EM_t* em, CO_HBconsNode_t* monitoredNodes, uint8_t monitoredNodesCount,
                   OD_entry_t* OD_1016_HBcons, CO_CANmodule_t* CANdevRx, uint16_t CANdevRxIdxStart, uint32_t* errInfo) {
    ODR_t odRet;

    /* verify arguments */
    if ((HBcons == NULL) || (em == NULL) || (monitoredNodes == NULL) || (OD_1016_HBcons == NULL)
        || (CANdevRx == NULL)) {
        return CO_ERROR_ILLEGAL_ARGUMENT;
    }

    /* Configure object variables */
    (void)memset(HBcons, 0, sizeof(CO_HBconsumer_t));
    HBcons->em = em;
    HBcons->monitoredNodes = monitoredNodes;
    HBcons->CANdevRx = CANdevRx;
    HBcons->CANdevRxIdxStart = CANdevRxIdxStart;

    /* get actual number of monitored nodes */
    HBcons->numberOfMonitoredNodes = ((OD_1016_HBcons->subEntriesCount - 1U) < monitoredNodesCount)
                                         ? (OD_1016_HBcons->subEntriesCount - 1U)
                                         : monitoredNodesCount;

    for (uint8_t i = 0; i < HBcons->numberOfMonitoredNodes; i++) {
        uint32_t val;
        odRet = OD_get_u32(OD_1016_HBcons, i + 1U, &val, true);
        if (odRet != ODR_OK) {
            if (errInfo != NULL) {
                *errInfo = OD_getIndex(OD_1016_HBcons);
            }
            return CO_ERROR_OD_PARAMETERS;
        }

        uint8_t nodeId = (uint8_t)((val >> 16) & 0xFFU);
        uint16_t consumer_time = (uint16_t)(val & 0xFFFFU);
        CO_ReturnError_t ret = CO_HBconsumer_initEntry(HBcons, i, nodeId, consumer_time);
        if (ret != CO_ERROR_NO) {
            if (errInfo != NULL) {
                *errInfo = OD_getIndex(OD_1016_HBcons);
            }
            /* don't break a program, if only value of a parameter is wrong */
            if (ret != CO_ERROR_OD_PARAMETERS) {
                return ret;
            }
        }
    }

    /* configure extension for OD */
#if ((CO_CONFIG_HB_CONS)&CO_CONFIG_FLAG_OD_DYNAMIC) != 0
    HBcons->OD_1016_extension.object = HBcons;
    HBcons->OD_1016_extension.read = OD_readOriginal;
    HBcons->OD_1016_extension.write = OD_write_1016;
    odRet = OD_extension_init(OD_1016_HBcons, &HBcons->OD_1016_extension);
    if (odRet != ODR_OK) {
        if (errInfo != NULL) {
            *errInfo = OD_getIndex(OD_1016_HBcons);
        }
        return CO_ERROR_OD_PARAMETERS;
    }
#endif

    return CO_ERROR_NO;
}

static CO_ReturnError_t
CO_HBconsumer_initEntry(CO_HBconsumer_t* HBcons, uint8_t idx, uint8_t nodeId, uint16_t consumerTime_ms) {
    CO_ReturnError_t ret = CO_ERROR_NO;

    /* verify arguments */
    if ((HBcons == NULL) || (idx >= HBcons->numberOfMonitoredNodes)) {
        return CO_ERROR_ILLEGAL_ARGUMENT;
    }

    /* verify for duplicate entries */
    if ((consumerTime_ms != 0U) && (nodeId != 0U)) {
        for (uint8_t i = 0; i < HBcons->numberOfMonitoredNodes; i++) {
            CO_HBconsNode_t node = HBcons->monitoredNodes[i];
            if ((idx != i) && (node.time_us != 0U) && (node.nodeId == nodeId)) {
                ret = CO_ERROR_OD_PARAMETERS;
            }
        }
    }

    /* Configure one monitored node */
    if (ret == CO_ERROR_NO) {
        uint16_t COB_ID;

        CO_HBconsNode_t* monitoredNode = &HBcons->monitoredNodes[idx];
        monitoredNode->nodeId = nodeId;
        monitoredNode->time_us = (uint32_t)consumerTime_ms * 1000U;
        monitoredNode->NMTstate = CO_NMT_UNKNOWN;
#if (((CO_CONFIG_HB_CONS)&CO_CONFIG_HB_CONS_CALLBACK_CHANGE) != 0)                                                     \
    || (((CO_CONFIG_HB_CONS)&CO_CONFIG_HB_CONS_CALLBACK_MULTI) != 0)
        monitoredNode->NMTstatePrev = CO_NMT_UNKNOWN;
#endif
        CO_FLAG_CLEAR(monitoredNode->CANrxNew);

        /* is channel used */
        if ((monitoredNode->nodeId != 0U) && (monitoredNode->time_us != 0U)) {
            COB_ID = monitoredNode->nodeId + (uint16_t)CO_CAN_ID_HEARTBEAT;
            monitoredNode->HBstate = CO_HBconsumer_UNKNOWN;
        } else {
            COB_ID = 0;
            monitoredNode->time_us = 0;
            monitoredNode->HBstate = CO_HBconsumer_UNCONFIGURED;
        }

        /* configure Heartbeat consumer (or disable) CAN reception */
        ret = CO_CANrxBufferInit(HBcons->CANdevRx, HBcons->CANdevRxIdxStart + idx, COB_ID, 0x7FF, false,
                                 (void*)&HBcons->monitoredNodes[idx], CO_HBcons_receive);
    }
    return ret;
}

#if ((CO_CONFIG_HB_CONS)&CO_CONFIG_FLAG_CALLBACK_PRE) != 0
void
CO_HBconsumer_initCallbackPre(CO_HBconsumer_t* HBcons, void* object, void (*pFunctSignal)(void* object)) {
    if (HBcons != NULL) {
        uint8_t i;
        for (i = 0; i < HBcons->numberOfMonitoredNodes; i++) {
            HBcons->monitoredNodes[i].pFunctSignalPre = pFunctSignal;
            HBcons->monitoredNodes[i].functSignalObjectPre = object;
        }
    }
}
#endif

#if ((CO_CONFIG_HB_CONS)&CO_CONFIG_HB_CONS_CALLBACK_CHANGE) != 0
void
CO_HBconsumer_initCallbackNmtChanged(CO_HBconsumer_t* HBcons, uint8_t idx, void* object,
                                     void (*pFunctSignal)(uint8_t nodeId, uint8_t idx, CO_NMT_internalState_t NMTstate,
                                                          void* object)) {
    (void)idx;
    if (HBcons == NULL) {
        return;
    }

    HBcons->pFunctSignalNmtChanged = pFunctSignal;
    HBcons->pFunctSignalObjectNmtChanged = object;
}
#endif

#if ((CO_CONFIG_HB_CONS)&CO_CONFIG_HB_CONS_CALLBACK_MULTI) != 0
void
CO_HBconsumer_initCallbackNmtChanged(CO_HBconsumer_t* HBcons, uint8_t idx, void* object,
                                     void (*pFunctSignal)(uint8_t nodeId, uint8_t idx, CO_NMT_internalState_t NMTstate,
                                                          void* object)) {
    if (HBcons == NULL || idx >= HBcons->numberOfMonitoredNodes) {
        return;
    }

    CO_HBconsNode_t* const monitoredNode = &HBcons->monitoredNodes[idx];
    monitoredNode->pFunctSignalNmtChanged = pFunctSignal;
    monitoredNode->pFunctSignalObjectNmtChanged = object;
}

void
CO_HBconsumer_initCallbackHeartbeatStarted(CO_HBconsumer_t* HBcons, uint8_t idx, void* object,
                                           void (*pFunctSignal)(uint8_t nodeId, uint8_t idx, void* object)) {
    CO_HBconsNode_t* monitoredNode;

    if (HBcons == NULL || idx >= HBcons->numberOfMonitoredNodes) {
        return;
    }

    monitoredNode = &HBcons->monitoredNodes[idx];
    monitoredNode->pFunctSignalHbStarted = pFunctSignal;
    monitoredNode->functSignalObjectHbStarted = object;
}

void
CO_HBconsumer_initCallbackTimeout(CO_HBconsumer_t* HBcons, uint8_t idx, void* object,
                                  void (*pFunctSignal)(uint8_t nodeId, uint8_t idx, void* object)) {
    CO_HBconsNode_t* monitoredNode;

    if (HBcons == NULL || idx >= HBcons->numberOfMonitoredNodes) {
        return;
    }

    monitoredNode = &HBcons->monitoredNodes[idx];
    monitoredNode->pFunctSignalTimeout = pFunctSignal;
    monitoredNode->functSignalObjectTimeout = object;
}

void
CO_HBconsumer_initCallbackRemoteReset(CO_HBconsumer_t* HBcons, uint8_t idx, void* object,
                                      void (*pFunctSignal)(uint8_t nodeId, uint8_t idx, void* object)) {
    CO_HBconsNode_t* monitoredNode;

    if (HBcons == NULL || idx >= HBcons->numberOfMonitoredNodes) {
        return;
    }

    monitoredNode = &HBcons->monitoredNodes[idx];
    monitoredNode->pFunctSignalRemoteReset = pFunctSignal;
    monitoredNode->functSignalObjectRemoteReset = object;
}
#endif /* (CO_CONFIG_HB_CONS) & CO_CONFIG_HB_CONS_CALLBACK_MULTI */

void
CO_HBconsumer_process(CO_HBconsumer_t* HBcons, bool_t NMTisPreOrOperational, uint32_t timeDifference_us,
                      uint32_t* timerNext_us) {
    (void)timerNext_us; /* may be unused */

    bool_t allMonitoredActiveCurrent = true;
    bool_t allMonitoredOperationalCurrent = true;

    if (NMTisPreOrOperational && HBcons->NMTisPreOrOperationalPrev) {
        for (uint8_t i = 0; i < HBcons->numberOfMonitoredNodes; i++) {
            uint32_t timeDifference_us_copy = timeDifference_us;
            CO_HBconsNode_t* const monitoredNode = &HBcons->monitoredNodes[i];

            if (monitoredNode->HBstate == CO_HBconsumer_UNCONFIGURED) {
                /* continue, if node is not monitored */
                continue;
            }
            /* Verify if received message is heartbeat or bootup */
            if (CO_FLAG_READ(monitoredNode->CANrxNew)) {
                if (monitoredNode->NMTstate == CO_NMT_INITIALIZING) {
                    /* bootup message */
#if ((CO_CONFIG_HB_CONS)&CO_CONFIG_HB_CONS_CALLBACK_MULTI) != 0
                    if (monitoredNode->pFunctSignalRemoteReset != NULL) {
                        monitoredNode->pFunctSignalRemoteReset(monitoredNode->nodeId, i,
                                                               monitoredNode->functSignalObjectRemoteReset);
                    }
#endif
                    if (monitoredNode->HBstate == CO_HBconsumer_ACTIVE) {
                        CO_errorReport(HBcons->em, CO_EM_HB_CONSUMER_REMOTE_RESET, CO_EMC_HEARTBEAT, i);
                    }
                    monitoredNode->HBstate = CO_HBconsumer_UNKNOWN;

                } else {
                    /* heartbeat message */
#if ((CO_CONFIG_HB_CONS)&CO_CONFIG_HB_CONS_CALLBACK_MULTI) != 0
                    if (monitoredNode->HBstate != CO_HBconsumer_ACTIVE
                        && monitoredNode->pFunctSignalHbStarted != NULL) {
                        monitoredNode->pFunctSignalHbStarted(monitoredNode->nodeId, i,
                                                             monitoredNode->functSignalObjectHbStarted);
                    }
#endif
                    monitoredNode->HBstate = CO_HBconsumer_ACTIVE;
                    /* reset timer */
                    monitoredNode->timeoutTimer = 0;
                    timeDifference_us_copy = 0;
                }
                CO_FLAG_CLEAR(monitoredNode->CANrxNew);
            }

            /* Verify timeout */
            if (monitoredNode->HBstate == CO_HBconsumer_ACTIVE) {
                monitoredNode->timeoutTimer += timeDifference_us_copy;

                if (monitoredNode->timeoutTimer >= monitoredNode->time_us) {
                    /* timeout expired */
#if ((CO_CONFIG_HB_CONS)&CO_CONFIG_HB_CONS_CALLBACK_MULTI) != 0
                    if (monitoredNode->pFunctSignalTimeout != NULL) {
                        monitoredNode->pFunctSignalTimeout(monitoredNode->nodeId, i,
                                                           monitoredNode->functSignalObjectTimeout);
                    }
#endif
                    CO_errorReport(HBcons->em, CO_EM_HEARTBEAT_CONSUMER, CO_EMC_HEARTBEAT, i);
                    monitoredNode->NMTstate = CO_NMT_UNKNOWN;
                    monitoredNode->HBstate = CO_HBconsumer_TIMEOUT;
                }

#if ((CO_CONFIG_HB_CONS)&CO_CONFIG_FLAG_TIMERNEXT) != 0
                else if (timerNext_us != NULL) {
                    /* Calculate timerNext_us for next timeout checking. */
                    uint32_t diff = monitoredNode->time_us - monitoredNode->timeoutTimer;
                    if (*timerNext_us > diff) {
                        *timerNext_us = diff;
                    }
                }
#endif
            }

            if (monitoredNode->HBstate != CO_HBconsumer_ACTIVE) {
                allMonitoredActiveCurrent = false;
            }
            if (monitoredNode->NMTstate != CO_NMT_OPERATIONAL) {
                allMonitoredOperationalCurrent = false;
            }
#if (((CO_CONFIG_HB_CONS)&CO_CONFIG_HB_CONS_CALLBACK_CHANGE) != 0)                                                     \
    || (((CO_CONFIG_HB_CONS)&CO_CONFIG_HB_CONS_CALLBACK_MULTI) != 0)
            /* Verify, if NMT state of monitored node changed */
            if (monitoredNode->NMTstate != monitoredNode->NMTstatePrev) {
#if ((CO_CONFIG_HB_CONS)&CO_CONFIG_HB_CONS_CALLBACK_CHANGE) != 0
                if (HBcons->pFunctSignalNmtChanged != NULL) {
                    HBcons->pFunctSignalNmtChanged(monitoredNode->nodeId, i, monitoredNode->NMTstate,
                                                   HBcons->pFunctSignalObjectNmtChanged);
#else
                if (monitoredNode->pFunctSignalNmtChanged != NULL) {
                    monitoredNode->pFunctSignalNmtChanged(monitoredNode->nodeId, i, monitoredNode->NMTstate,
                                                          monitoredNode->pFunctSignalObjectNmtChanged);
#endif
                }
                monitoredNode->NMTstatePrev = monitoredNode->NMTstate;
            }
#endif
        }
    } else if (NMTisPreOrOperational || HBcons->NMTisPreOrOperationalPrev) {
        /* (pre)operational state changed, clear variables */
        for (uint8_t i = 0; i < HBcons->numberOfMonitoredNodes; i++) {
            CO_HBconsNode_t* const monitoredNode = &HBcons->monitoredNodes[i];
            monitoredNode->NMTstate = CO_NMT_UNKNOWN;
#if (((CO_CONFIG_HB_CONS)&CO_CONFIG_HB_CONS_CALLBACK_CHANGE) != 0)                                                     \
    || (((CO_CONFIG_HB_CONS)&CO_CONFIG_HB_CONS_CALLBACK_MULTI) != 0)
            monitoredNode->NMTstatePrev = CO_NMT_UNKNOWN;
#endif
            CO_FLAG_CLEAR(monitoredNode->CANrxNew);
            if (monitoredNode->HBstate != CO_HBconsumer_UNCONFIGURED) {
                monitoredNode->HBstate = CO_HBconsumer_UNKNOWN;
            }
        }
        allMonitoredActiveCurrent = false;
        allMonitoredOperationalCurrent = false;
    } else { /* MISRA C 2004 14.10 */
    }

    /* Clear emergencies when all monitored nodes becomes active.
     * We only have one emergency index for all monitored nodes! */
    if (!HBcons->allMonitoredActive && allMonitoredActiveCurrent) {
        CO_errorReset(HBcons->em, CO_EM_HEARTBEAT_CONSUMER, 0);
        CO_errorReset(HBcons->em, CO_EM_HB_CONSUMER_REMOTE_RESET, 0);
    }

    HBcons->allMonitoredActive = allMonitoredActiveCurrent;
    HBcons->allMonitoredOperational = allMonitoredOperationalCurrent;
    HBcons->NMTisPreOrOperationalPrev = NMTisPreOrOperational;
}

#if ((CO_CONFIG_HB_CONS)&CO_CONFIG_HB_CONS_QUERY_FUNCT) != 0
int8_t
CO_HBconsumer_getIdxByNodeId(CO_HBconsumer_t* HBcons, uint8_t nodeId) {
    uint8_t i;
    CO_HBconsNode_t* monitoredNode;

    if (HBcons == NULL) {
        return -1;
    }

    /* linear search for the node */
    monitoredNode = &HBcons->monitoredNodes[0];
    for (i = 0; i < HBcons->numberOfMonitoredNodes; i++) {
        if (monitoredNode->nodeId == nodeId) {
            return i;
        }
        monitoredNode++;
    }
    /* not found */
    return -1;
}

CO_HBconsumer_state_t
CO_HBconsumer_getState(CO_HBconsumer_t* HBcons, uint8_t idx) {
    CO_HBconsNode_t* monitoredNode;

    if (HBcons == NULL || idx >= HBcons->numberOfMonitoredNodes) {
        return CO_HBconsumer_UNCONFIGURED;
    }

    monitoredNode = &HBcons->monitoredNodes[idx];
    return monitoredNode->HBstate;
}

int8_t
CO_HBconsumer_getNmtState(CO_HBconsumer_t* HBcons, uint8_t idx, CO_NMT_internalState_t* nmtState) {
    CO_HBconsNode_t* monitoredNode;

    if (HBcons == NULL || nmtState == NULL || idx >= HBcons->numberOfMonitoredNodes) {
        return -1;
    }
    *nmtState = CO_NMT_INITIALIZING;

    monitoredNode = &HBcons->monitoredNodes[idx];

    if (monitoredNode->HBstate == CO_HBconsumer_ACTIVE) {
        *nmtState = monitoredNode->NMTstate;
        return 0;
    }
    return -1;
}
#endif /* (CO_CONFIG_HB_CONS) & CO_CONFIG_HB_CONS_QUERY_FUNCT */

#endif /* (CO_CONFIG_HB_CONS) & CO_CONFIG_HB_CONS_ENABLE */
