/**
 * @file libwrtd-internal.c
 *
 * Copyright (c) 2018-2019 CERN (home.cern)
 *
 * SPDX-License-Identifier: LGPL-3.0-or-later
 */

#include <stdarg.h>
#include <stddef.h>
#include <stdlib.h>
#include <assert.h>
#include <errno.h>
#include "libwrtd.h"
#include "libwrtd-private.h"
#include "mockturtle/libmockturtle.h"

inline bool wrtd_id_eq(const char *l, const char *r)
{
        return strncmp(l, r, WRTD_ID_LEN) == 0;
}

inline bool wrtd_id_null(const char *id)
{
        return id[0] == 0;
}

inline void wrtd_id_copy(char *dst, const char *src)
{
        strncpy(dst, src, WRTD_ID_LEN);
}

enum wrtd_status wrtd_id_copy_buf(struct wrtd_dev *wrtd,
                                  char *dst, int32_t dst_buffer_size,
                                  const char *id, const char *caller_func)
{
        if(wrtd == NULL){
                return WRTD_ERROR_NOT_INITIALIZED;
        }

        /* If buffer_size is zero, just report on necessary
           buffer size. According to IVI 3.2, section 3.1.2.1. */
        if (dst_buffer_size == 0)
                return WRTD_ID_LEN;

        if(dst == NULL){
                return wrtd_return_error
                        (wrtd, WRTD_ERROR_NULL_POINTER,
                         "Null pointer passed for function %s/%s, "
                         "parameter dst",
                         caller_func, __func__);
        }

        /* This violates IVI 3.2, section 3.1.2.1 which dictates to
           ignore overflow if buffer_size is negative. Still, it is
           the safer thing to do.*/
        if (dst_buffer_size < 0)
                return wrtd_return_error(wrtd, WRTD_ERROR_INVALID_VALUE,
                                         "Invalid value (%d) for function %s/%s, "
                                         "parameter dst_buffer_size",
                                         dst_buffer_size, caller_func, __func__);

        memset(dst, 0, dst_buffer_size);

        int ret = snprintf(dst, dst_buffer_size, "%s", id);
        if (ret < dst_buffer_size)
                return WRTD_SUCCESS;
        else
                return ret + 1;
}

enum wrtd_status wrtd_validate_id(struct wrtd_dev *wrtd, const char *rep_cap_id,
                                  const char *caller_func)
{
        if(wrtd == NULL){
                return WRTD_ERROR_NOT_INITIALIZED;
        }

        if ((wrtd_id_null(rep_cap_id)) || (strlen(rep_cap_id) > WRTD_ID_LEN))
                return wrtd_return_error
                        (wrtd, WRTD_ERROR_BADLY_FORMED_SELECTOR,
                         "%s/%s: Badly-formed selector",
                         caller_func, __func__);

        return WRTD_SUCCESS;
}

enum wrtd_status wrtd_return_error(struct wrtd_dev *dev,
                                   enum wrtd_status error_code,
                                   const char *format, ...)
{
        va_list args;

        if(dev == NULL){
                return WRTD_ERROR_NOT_INITIALIZED;
        }

        va_start(args, format);
        dev->err = error_code;
        vsnprintf(dev->error_msg, sizeof(dev->error_msg), format, args);
        va_end(args);

        return error_code;
}

enum wrtd_status wrtd_log_read(struct wrtd_dev *wrtd,
                               struct wrtd_log_entry *log,
                               const char *caller_func)
{
        struct trtl_msg msg;
        struct polltrtl p[WRTD_MAX_CPUS];
        int ret, i;

        if(wrtd == NULL){
                return WRTD_ERROR_NOT_INITIALIZED;
        }

        for (i = 0; i < wrtd->nbr_cpus; ++i) {
                p[i].trtl = wrtd->trtl;
                p[i].idx_hmq = WRTD_HMQ;
                p[i].idx_cpu = i;
                p[i].events = POLLIN;
        }

        /* Clean up errno to be able to distinguish between error cases and
           normal behaviour when the function returns less messages
           than expected */
        errno = 0;

        ret = trtl_msg_poll(p, wrtd->nbr_cpus, 0);
        if (ret == 0) {
                // Nothing to read
                log->type = WRTD_LOG_MSG_EV_NONE;
                return WRTD_SUCCESS;
        }
        if (ret < 0)
                return wrtd_return_error
                        (wrtd, WRTD_ERROR_INTERNAL, "%s/%s(poll): %s",
                         caller_func, __func__, trtl_strerror(errno));

        for (i = 0; i < wrtd->nbr_cpus; ++i) {
                if (!(p[i].revents & POLLIN))
                        continue;
                ret = trtl_msg_async_recv(wrtd->trtl, i, WRTD_HMQ, &msg, 1);
                if (ret < 0)
                        return wrtd_return_error
                                (wrtd, WRTD_ERROR_INTERNAL, "%s/%s(recv): %s",
                                 caller_func, __func__, trtl_strerror(errno));

                memcpy(log, msg.data, sizeof(struct wrtd_log_entry));
                return WRTD_SUCCESS;
        }
        /* This should never happen. However, due to a -so far unconfirmed-
         issue with MockTurtle, sometimes trtl_async_recv() might return 0,
        even though trtl_msg_poll() (which apparently only checks for async
        messages) reports that there is an async message to read.
        By returning success here we allow the software to continue and read
        the message with another call. */
        log->type = WRTD_LOG_MSG_EV_NONE;
        return WRTD_SUCCESS;
}

enum wrtd_status wrtd_msg_get_config(struct wrtd_dev *wrtd, unsigned cpu,
                                     struct wrtd_config_msg *res,
                                     const char* caller_func)
{
        struct trtl_msg msg;
        int err;

        if(wrtd == NULL){
                return WRTD_ERROR_NOT_INITIALIZED;
        }

        memset(&msg.hdr, 0, sizeof(struct trtl_hmq_header));
        msg.hdr.flags = TRTL_HMQ_HEADER_FLAG_RPC;
        msg.hdr.msg_id = WRTD_ACTION_GET_CONFIG;
        msg.hdr.len = 0;

        err = trtl_msg_sync(wrtd->trtl, cpu, WRTD_HMQ, &msg, &msg,
                            WRTD_DEFAULT_TIMEOUT);

        if (err)
                return wrtd_return_error
                        (wrtd, WRTD_ERROR_INTERNAL, "%s/%s: %s",
                         caller_func, __func__, trtl_strerror(errno));

        if (msg.hdr.msg_id != WRTD_ACTION_GET_CONFIG
            || msg.hdr.len != 5)
                return wrtd_return_error(wrtd, WRTD_ERROR_UNEXPECTED_RESPONSE,
                                         "%s/%s: Unexpected response from device",
                                         caller_func, __func__);


        res->root_addr = msg.data[0];
        res->sync_flag = msg.data[1];
        res->now.seconds = msg.data[2];
        res->now.ns = msg.data[3];
        res->now.frac = msg.data[4];

        return WRTD_SUCCESS;
}

enum wrtd_status wrtd_msg_readw(struct wrtd_dev *wrtd, unsigned cpu,
                                uint32_t addr, uint32_t count,
                                uint32_t *dest, const char *caller_func)
{
        if(wrtd == NULL){
                return WRTD_ERROR_NOT_INITIALIZED;
        }

        while (count > 0) {
                uint32_t tlen;
                struct trtl_msg msg;
                int err;
                struct wrtd_io_msg *io_msg;

                /* Partial transaction length.  */
                tlen = wrtd->hmq_words[cpu] - sizeof(struct wrtd_io_msg) / 4;
                if (tlen > count)
                        tlen = count;

                memset(&msg.hdr, 0, sizeof(struct trtl_hmq_header));
                msg.hdr.flags = TRTL_HMQ_HEADER_FLAG_RPC;
                msg.hdr.msg_id = WRTD_ACTION_READW;
                msg.hdr.len = sizeof(struct wrtd_io_msg) / 4;

                io_msg = (struct wrtd_io_msg *)msg.data;
                /* For input: addr + len.  */
                io_msg->addr = addr;
                io_msg->nwords = tlen;

                err = trtl_msg_sync(wrtd->trtl, cpu, WRTD_HMQ, &msg, &msg,
                                    WRTD_DEFAULT_TIMEOUT);

                if (err)
                        return wrtd_return_error
                                (wrtd, WRTD_ERROR_INTERNAL, "%s/%s: %s",
                                 caller_func, __func__, trtl_strerror(errno));

                if (msg.hdr.msg_id != WRTD_ACTION_READW
                    || msg.hdr.len != tlen)
                        return wrtd_return_error(
                                wrtd, WRTD_ERROR_UNEXPECTED_RESPONSE,
                                "%s/%s: Unexpected response from device",
                                caller_func, __func__);

                memcpy(dest, msg.data, tlen * sizeof(uint32_t));

                dest += tlen;
                addr += tlen;
                count -= tlen;
        }
        return WRTD_SUCCESS;
}

enum wrtd_status wrtd_msg_writew(struct wrtd_dev *wrtd, unsigned cpu,
                                 uint32_t addr, uint32_t count,
                                 const uint32_t *src, const char *caller_func)
{
        if(wrtd == NULL){
                return WRTD_ERROR_NOT_INITIALIZED;
        }

        while (count > 0) {
                uint32_t tlen;
                struct trtl_msg msg;
                int err;
                struct wrtd_io_msg *io_msg;

                /* Partial transaction length.  */
                tlen = wrtd->hmq_words[cpu] - sizeof(struct wrtd_io_msg) / 4;
                if (tlen > count)
                        tlen = count;

                memset(&msg.hdr, 0, sizeof(struct trtl_hmq_header));
                msg.hdr.flags = TRTL_HMQ_HEADER_FLAG_RPC;
                msg.hdr.msg_id = WRTD_ACTION_WRITEW;
                msg.hdr.len = (sizeof(struct wrtd_io_msg) / 4) + tlen;

                io_msg = (struct wrtd_io_msg *)msg.data;
                io_msg->addr = addr;
                io_msg->nwords = tlen;
                memcpy(io_msg->data, src, tlen * sizeof(uint32_t));

                err = trtl_msg_sync(wrtd->trtl, cpu, WRTD_HMQ, &msg, &msg,
                                    WRTD_DEFAULT_TIMEOUT);

                if (err)
                        return wrtd_return_error
                                (wrtd, WRTD_ERROR_INTERNAL, "%s/%s: %s",
                                 caller_func, __func__, trtl_strerror(errno));

                if (msg.hdr.msg_id != WRTD_ACTION_WRITEW
                    || msg.hdr.len != 0)
                        return wrtd_return_error(
                                wrtd, WRTD_ERROR_UNEXPECTED_RESPONSE,
                                "%s/%s: Unexpected response from device",
                                caller_func, __func__);

                src += tlen;
                addr += tlen;
                count -= tlen;
        }
        return WRTD_SUCCESS;
}

enum wrtd_status wrtd_fill_roots(struct wrtd_dev *wrtd, const char *caller_func)
{
        enum wrtd_status status;
        unsigned cpu;

        if(wrtd == NULL){
                return WRTD_ERROR_NOT_INITIALIZED;
        }

        if (wrtd->roots[0].ver_major != 0)
                return WRTD_SUCCESS;

        for (cpu = 0; cpu < wrtd->nbr_cpus; cpu++) {
                status = wrtd_msg_readw(wrtd, cpu,
                                        wrtd->root_addr[cpu],
                                        sizeof(struct wrtd_root) / 4,
                                        (uint32_t*)&wrtd->roots[cpu], caller_func);
                if (status != WRTD_SUCCESS)
                        return status;
                if (wrtd->roots[cpu].ver_major != WRTD_VERSION_MAJOR) {
                        /* Invalidate root.  */
                        wrtd->roots[cpu].ver_major = 0;
                        return wrtd_return_error
                                (wrtd, WRTD_ERROR_VERSION_MISMATCH,
                                 "%s/%s: incorrect major version (%d instead of %d)",
                                 caller_func, __func__,
                                 wrtd->roots[cpu].ver_major,
                                 WRTD_VERSION_MAJOR);
                }
                if (wrtd->roots[cpu].ver_minor > WRTD_VERSION_MINOR) {
                        /* Invalidate root.  */
                        wrtd->roots[cpu].ver_major = 0;
                        return wrtd_return_error
                                (wrtd, WRTD_ERROR_VERSION_MISMATCH,
                                 "%s/%s: incorrect minor version (%d instead of %d)",
                                 caller_func, __func__,
                                 wrtd->roots[cpu].ver_minor,
                                 WRTD_VERSION_MINOR);
                }
        }
        return WRTD_SUCCESS;
}

enum wrtd_status wrtd_fill_alarms(struct wrtd_dev *wrtd, const char *caller_func)
{
        enum wrtd_status status;
        unsigned cpu;
        unsigned n;

        if(wrtd == NULL){
                return WRTD_ERROR_NOT_INITIALIZED;
        }

        if (wrtd->alarms != NULL)
                return WRTD_SUCCESS;

        status = wrtd_fill_roots(wrtd, caller_func);
        WRTD_RETURN_IF_ERROR(status);

        /* Count number of alarms.  */
        for (cpu = 0; cpu < wrtd->nbr_cpus; cpu++)
                wrtd->nbr_alarms += wrtd->roots[cpu].nbr_alarms;

        /* Allocates.  */
        wrtd->alarms = (struct wrtd_lib_alarm *)
                malloc(wrtd->nbr_alarms * sizeof(struct wrtd_lib_alarm));
        if (wrtd->alarms == NULL)
                return wrtd_return_error(wrtd, WRTD_ERROR_OUT_OF_MEMORY,
                                         "%s/%s: Could not allocate necessary memory.",
                                         caller_func, __func__);
        n = 0;
        for (cpu = 0; cpu < wrtd->nbr_cpus; cpu++) {
                unsigned idx;
                for (idx = 0; idx < wrtd->roots[cpu].nbr_alarms; idx++) {
                        status = wrtd_msg_readw
                                (wrtd, cpu,
                                 (wrtd->roots[cpu].alarms_addr
                                  + idx * sizeof(struct wrtd_alarm)),
                                 sizeof(struct wrtd_alarm) / 4,
                                 (uint32_t *)&wrtd->alarms[n].alarm, caller_func);
                        if (status != WRTD_SUCCESS) {
                                free(wrtd->alarms);
                                wrtd->alarms = NULL;
                                return status;
                        }
                        wrtd->alarms[n].cpu = cpu;
                        wrtd->alarms[n].idx = idx;
                        n++;
                }
        }

        return WRTD_SUCCESS;
}

enum wrtd_status wrtd_alloc_rules(struct wrtd_dev *wrtd,
                                  struct wrtd_lib_rule **rulesp,
                                  const char *caller_func)
{
        struct wrtd_lib_rule *rules;
        unsigned n;
        unsigned cpu;

        if(wrtd == NULL){
                return WRTD_ERROR_NOT_INITIALIZED;
        }

        rules = (struct wrtd_lib_rule *)
                calloc(wrtd->nbr_rules, sizeof(struct wrtd_lib_rule));
        if (rules == NULL)
                return wrtd_return_error(wrtd, WRTD_ERROR_OUT_OF_MEMORY,
                                         "%s/%s: Could not allocate necessary memory.",
                                         caller_func, __func__);

        n = 0;
        for (cpu = 0; cpu < wrtd->nbr_cpus; cpu++) {
                unsigned nbr_rules = wrtd->roots[cpu].nbr_rules;
                unsigned idx;
                for (idx = 0; idx < nbr_rules; idx++) {
                        rules[n].cpu = cpu;
                        rules[n].local_idx = idx;
                        rules[n].modified = 0;
                        n++;
                }
        }
        assert(n == wrtd->nbr_rules);
        *rulesp = rules;

        return WRTD_SUCCESS;
}

enum wrtd_status wrtd_fill_rules(struct wrtd_dev *wrtd, const char *caller_func)
{
        enum wrtd_status status;
        unsigned i;
        unsigned cpu;

        if(wrtd == NULL){
                return WRTD_ERROR_NOT_INITIALIZED;
        }

        if (wrtd->rules != NULL)
                return WRTD_SUCCESS;

        status = wrtd_fill_roots(wrtd, caller_func);
        WRTD_RETURN_IF_ERROR(status);

        /* Count number of rules.  */
        assert(wrtd->nbr_rules == 0);
        for (cpu = 0; cpu < wrtd->nbr_cpus; cpu++)
                wrtd->nbr_rules += wrtd->roots[cpu].nbr_rules;

        /* Allocates.  */
        status = wrtd_alloc_rules(wrtd, &wrtd->rules, caller_func);
        WRTD_RETURN_IF_ERROR(status);

        /* Read rules from cpus.  */
        for (i = 0; i < wrtd->nbr_rules; i++) {
                unsigned cpu = wrtd->rules[i].cpu;
                unsigned int addr;

                addr = (wrtd->roots[cpu].rules_addr
                        + wrtd->rules[i].local_idx * sizeof(struct wrtd_rule));
                status = wrtd_msg_readw
                        (wrtd, cpu, addr, sizeof(struct wrtd_rule) / 4,
                         (uint32_t *)&wrtd->rules[i].rule, caller_func);
                if (status != WRTD_SUCCESS) {
                        free(wrtd->rules);
                        wrtd->rules = NULL;
                        return status;
                }
        }

        return WRTD_SUCCESS;
}

enum wrtd_status wrtd_write_alarm(struct wrtd_dev *wrtd, unsigned idx,
                                  const char *caller_func)
{
        enum wrtd_status status;
        struct wrtd_lib_alarm *al;

        if(wrtd == NULL){
                return WRTD_ERROR_NOT_INITIALIZED;
        }

        al = &wrtd->alarms[idx];

        status = wrtd_msg_writew
                (wrtd, al->cpu,
                 (wrtd->roots[al->cpu].alarms_addr
                  + al->idx * sizeof(struct wrtd_alarm)),
                 sizeof(struct wrtd_alarm) / 4,
                 (uint32_t *)&al->alarm, caller_func);

        return status;
}

enum wrtd_status wrtd_write_rule(struct wrtd_dev *wrtd, unsigned idx,
                                 const char *caller_func)
{
        enum wrtd_status status;
        struct wrtd_lib_rule *rule;

        if(wrtd == NULL){
                return WRTD_ERROR_NOT_INITIALIZED;
        }

        rule = &wrtd->rules[idx];

        status = wrtd_msg_writew
                (wrtd, rule->cpu,
                 (wrtd->roots[rule->cpu].rules_addr
                  + rule->local_idx * sizeof(struct wrtd_rule)),
                 sizeof(struct wrtd_rule) / 4,
                 (uint32_t *)&rule->rule, caller_func);

        return status;
}

enum wrtd_status wrtd_write_root_flags(struct wrtd_dev *wrtd, unsigned cpu,
                                       const char *caller_func)
{
        enum wrtd_status status;

        if(wrtd == NULL){
                return WRTD_ERROR_NOT_INITIALIZED;
        }

        status = wrtd_msg_writew
                (wrtd, cpu,
                 wrtd->root_addr[cpu] + offsetof(struct wrtd_root, log_flags),
                 1, (uint32_t *)&wrtd->roots[cpu].log_flags, caller_func);
        WRTD_RETURN_IF_ERROR(status);

        return WRTD_SUCCESS;
}

enum wrtd_status wrtd_find_alarm(struct wrtd_dev *wrtd,
                                 const char *rep_cap_id,
                                 unsigned *idx,
                                 const char *caller_func)
{
        enum wrtd_status status;
        unsigned i;

        if(wrtd == NULL){
                return WRTD_ERROR_NOT_INITIALIZED;
        }

        /* 1. Validate rep_cap_id.  */
        status = wrtd_validate_id(wrtd, rep_cap_id, caller_func);
        WRTD_RETURN_IF_ERROR(status);

        /* 2. Load all alarms.  */
        status = wrtd_fill_alarms(wrtd, caller_func);
        WRTD_RETURN_IF_ERROR(status);

        /* 3. Search it.  */
        for (i = 0; i < wrtd->nbr_alarms; i++)
                if (wrtd_id_eq(wrtd->alarms[i].alarm.event.id, rep_cap_id)) {
                        *idx = i;
                        return WRTD_SUCCESS;
                }

        return wrtd_return_error(wrtd, WRTD_ERROR_ALARM_DOES_NOT_EXIST,
                                 "%s/%s: The specified alarm has not been defined",
                                 caller_func, __func__);
}

enum wrtd_status wrtd_find_rule(struct wrtd_dev *wrtd,
                                const char *rep_cap_id,
                                unsigned *idx,
                                const char *caller_func)
{
        enum wrtd_status status;
        unsigned i;

        if(wrtd == NULL){
                return WRTD_ERROR_NOT_INITIALIZED;
        }

        /* 1. Validate rep_cap_id.  */
        status = wrtd_validate_id(wrtd, rep_cap_id, caller_func);
        WRTD_RETURN_IF_ERROR(status);

        /* 2. Load all rules.  */
        status = wrtd_fill_rules(wrtd, caller_func);
        WRTD_RETURN_IF_ERROR(status);

        /* 3. Search it.  */
        for (i = 0; i < wrtd->nbr_rules; i++)
                if (wrtd_id_eq(wrtd->rules[i].rule.conf.id, rep_cap_id)) {
                        *idx = i;
                        return WRTD_SUCCESS;
                }

        return wrtd_return_error(wrtd, WRTD_ERROR_RULE_DOES_NOT_EXIST,
                                 "%s/%s: The specified rule has not been defined",
                                 caller_func, __func__);
}

inline enum wrtd_status wrtd_alarm_check_disabled(struct wrtd_dev *wrtd, unsigned idx,
                                                  const char *caller_func)
{
        if (wrtd->alarms[idx].alarm.enabled)
                return wrtd_return_error(wrtd, WRTD_ERROR_RESOURCE_ACTIVE,
                                         "%s/%s: Resource is enabled.",
                                         caller_func, __func__);
        return WRTD_SUCCESS;
}

inline enum wrtd_status wrtd_rule_check_disabled(struct wrtd_dev *wrtd, unsigned idx,
                                                 const char *caller_func)
{
        if (wrtd->rules[idx].rule.conf.enabled)
                return wrtd_return_error(wrtd, WRTD_ERROR_RESOURCE_ACTIVE,
                                         "%s/%s: Resource is enabled.",
                                         caller_func, __func__);
        return WRTD_SUCCESS;
}

enum wrtd_status wrtd_find_fw(struct wrtd_dev *wrtd,
                              const char *rep_cap_id,
                              unsigned *idx,
                              const char *caller_func)
{
        enum wrtd_status status;
        unsigned i;

        if(wrtd == NULL){
                return WRTD_ERROR_NOT_INITIALIZED;
        }

        /* 1. Validate rep_cap_id.  */
        status = wrtd_validate_id(wrtd, rep_cap_id, caller_func);
        WRTD_RETURN_IF_ERROR(status);

        /* 2. Load all roots.  */
        status = wrtd_fill_roots(wrtd, caller_func);
        WRTD_RETURN_IF_ERROR(status);

        /* 3. Search it.  */
        for (i = 0; i < wrtd->nbr_cpus; i++)
                if (wrtd_id_eq(wrtd->roots[i].fw_name, rep_cap_id)) {
                        *idx = i;
                        return WRTD_SUCCESS;
                }

        return wrtd_return_error(wrtd, WRTD_ERROR_RESOURCE_UNKNOWN,
                                 "%s/%s: Unknown firmware name",
                                 caller_func, __func__);
}
