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

#include <stdlib.h>
#include <assert.h>
#include <string.h>
#include "libwrtd.h"
#include "libwrtd-private.h"

struct rule_map {
        /* Rule. */
        struct wrtd_lib_rule *rule;
        struct wrtd_rule_stats stats;
        /* Rule index before reconfiguration.  */
        int old_idx;
        /* It's assigned cpu and index; -1 means unassigned. */
        int cpu;
        int global_idx;
};

struct device_map {
        /* Number of channels on this device. */
        unsigned int nbr_chs;
        /* Direction of channels on this device. */
        unsigned int chs_dir;
        /* Global channel index for first channel on this device. */
        unsigned int chs_idx;
        /* CPU that drives this device.  */
        unsigned int cpu;
        /* Device index on the cpu.  */
        unsigned int dev_idx;
};

/* The global configuration.  */
struct conf_map {
        /* The rules.
           NBR_RULES is the number of valid rules. */
        unsigned nbr_rules;
        struct rule_map *rules;

        /* The devices. */
        unsigned nbr_devs;
        struct device_map *devs;

        unsigned free_rule_slots[WRTD_MAX_CPUS];
};


/* Allocate and fill MAP.  */
static enum wrtd_status wrtd_reconfigure_alloc_map(struct wrtd_dev *wrtd,
                                                   struct conf_map *map,
                                                   const char *caller_func)
{
        enum wrtd_status status;
        unsigned cpu;
        unsigned n;
        unsigned i;
        unsigned ch_in_idx, ch_out_idx;
        int32_t nrules;

        /* Count number of rules.  */
        status = wrtd_attr_get_rule_count(wrtd, &nrules);
        WRTD_RETURN_IF_ERROR(status);
        map->nbr_rules = nrules;

        /* Count number of devices.  */
        n = 0;
        for (cpu = 0; cpu < wrtd->nbr_cpus; cpu++)
                n += wrtd->roots[cpu].nbr_devices;
        map->nbr_devs = n;

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

        map->devs = malloc(map->nbr_devs * sizeof(struct device_map));
        if (map->devs == NULL) {
                free(map->rules);
                map->rules = NULL;
                return wrtd_return_error(wrtd, WRTD_ERROR_OUT_OF_MEMORY,
                                         "%s/%s: Could not allocate necessary memory.",
                                         caller_func, __func__);
        }

        /* Fill rules.  */
        n = 0;
        for (i = 0; i < wrtd->nbr_rules; i++) {
                if (wrtd_id_null(&wrtd->rules[i].conf.id))
                        continue;
                memset(&map->rules[n].stats, 0, sizeof(struct wrtd_rule_stats));
                map->rules[n].rule = &wrtd->rules[i];
                map->rules[n].old_idx = i;
                map->rules[n].cpu = -1;
                map->rules[n].global_idx = -1;
                n++;
        }
        assert (n == map->nbr_rules);

        /* No rules placed.  */
        for (i = 0; i < wrtd->nbr_cpus; i++)
                map->free_rule_slots[i] = wrtd->roots[i].nbr_rules;

        /* Fill devs.  */
        n = 0;
        ch_in_idx  = 0;
        ch_out_idx = 0;
        for (cpu = 0; cpu < wrtd->nbr_cpus; cpu++)
                for (i = 0; i < wrtd->roots[cpu].nbr_devices; i++) {
                        map->devs[n].nbr_chs = wrtd->roots[cpu].devices_nbr_chs[i];
                        map->devs[n].chs_dir = wrtd->roots[cpu].devices_chs_dir[i];
                        if (map->devs[n].chs_dir == WRTD_CH_DIR_IN) {
                                map->devs[n].chs_idx = ch_in_idx;
                                ch_in_idx += map->devs[n].nbr_chs;
                        }
                        else {
                                map->devs[n].chs_idx = ch_out_idx;
                                ch_out_idx += map->devs[n].nbr_chs;
                        }
                        map->devs[n].cpu = cpu;
                        map->devs[n].dev_idx = i;
                        n++;
                }

        return WRTD_SUCCESS;
}

static enum wrtd_status wrtd_find_channel(struct wrtd_dev *wrtd,
                                          const struct conf_map *map,
                                          const union wrtd_id *id,
                                          unsigned int dir,
                                          unsigned *res_dev,
                                          unsigned *res_ch)
{
        unsigned i;
        unsigned j;

        union wrtd_id dev_id;

        for (i = 0; i < map->nbr_devs; i++) {
                struct device_map *d = &map->devs[i];
                for (j = 0; j < d->nbr_chs; j++) {
                        memset (&dev_id.c, 0, WRTD_ID_LEN);
                        snprintf(&dev_id.c[0], WRTD_ID_LEN, "LC-%c%d",
                                 (d->chs_dir == WRTD_CH_DIR_IN) ? 'I' : 'O',
                                 1 + d->chs_idx + j);
                        if (d->chs_dir == dir
                            && wrtd_id_eq(&dev_id, id)) {
                                *res_dev = i;
                                *res_ch = (d->dev_idx << 4) + j;
                                return WRTD_SUCCESS;
                        }
                }
        }

        return WRTD_ERROR_UNKNOWN_CHANNEL_NAME;
}

/* Assign RULE_NUM to CPU.  */
static enum wrtd_status wrtd_reconfigure_place_rule(struct wrtd_dev *wrtd,
                                                    struct conf_map *map,
                                                    unsigned rule_num,
                                                    unsigned cpu,
                                                    const char *caller_func)
{
        assert(map->rules[rule_num].cpu == -1);

        if (map->free_rule_slots[cpu] == 0)
                return wrtd_return_error
                        (wrtd, WRTD_ERROR_OUT_OF_RESOURCES,
                         "%s/%s: The device has no more resources to allocate.",
                         caller_func, __func__);

        map->rules[rule_num].cpu = cpu;
        map->free_rule_slots[cpu]--;

        if (map->rules[rule_num].rule->conf.dest_cpu == cpu)
                map->rules[rule_num].rule->conf.dest_cpu = WRTD_DEST_CPU_LOCAL;

        return WRTD_SUCCESS;
}

/* Place all rules: assign a cpu and the rule index. */
static enum wrtd_status wrtd_reconfigure_place(struct wrtd_dev *wrtd,
                                               struct conf_map *map,
                                               const char *caller_func)
{
        enum wrtd_status status;
        unsigned i;
        unsigned cpu;
        unsigned disable_idx;
        unsigned cpu_offset;

        /* Assign enabled rules to cpu.  */
        for (i = 0; i < map->nbr_rules; i++) {
                struct rule_map *m = &map->rules[i];
                struct wrtd_rule_config *rule = &m->rule->conf;
                unsigned dev;
                unsigned ch;
                signed cpu_affinity;

                /* Skip disabled rules.  */
                if (!rule->enabled)
                        continue;

                /* Place the rule on the same CPU as the input source.  */
                status = wrtd_find_channel(wrtd, map, &rule->source_id,
                                           WRTD_CH_DIR_IN, &dev, &ch);
                if (status == WRTD_SUCCESS) {
                        /* Input channel.  */
                        cpu_affinity = map->devs[dev].cpu;
                }
                else {
                        status = wrtd_find_alarm(wrtd, rule->source_id.c,
                                                 &ch, caller_func);
                        if (status == WRTD_SUCCESS) {
                                /* Alarm.  */
                                cpu_affinity = wrtd->alarms[ch].cpu;
                        }
                        else {
                                /* Network message, delay affinity decision  */
                                cpu_affinity = -1;
                        }
                }

                struct wrtd_root *root;
                /* Set destination cpu and channel.  */
                status = wrtd_find_channel(wrtd, map, &rule->dest_id,
                                           WRTD_CH_DIR_OUT, &dev, &ch);
                if (status == WRTD_SUCCESS) {
                        /* Output device.  */
                        rule->dest_cpu = map->devs[dev].cpu;
                        rule->dest_ch = ch;
                        /* If source is network message and this cpu can receive from
                           network, set cpu affinity to this cpu */
                        root = &wrtd->roots[rule->dest_cpu];
                        if ((cpu_affinity < 0) &&
                            (root->capabilities & WRTD_CAP_NET_RX))
                                cpu_affinity = rule->dest_cpu;
                        /* Otherwise find the first cpu that is capable of net RX */
                        else {
                                for (cpu = 0; cpu < wrtd->nbr_cpus; cpu++) {
                                        root = &wrtd->roots[cpu];
                                        if (root->capabilities & WRTD_CAP_NET_RX) {
                                                cpu_affinity = cpu;
                                                break;
                                        }
                                }
                        }
                }
                else {
                        /* Network.  */
                        rule->dest_ch = WRTD_DEST_CH_NET;
                        /* If source cpu can also send to
                           network, set dest cpu affinity to that cpu */
                        root = NULL;
                        if (cpu_affinity >= 0)
                                root = &wrtd->roots[cpu_affinity];
                        if ((root) && (root->capabilities & WRTD_CAP_NET_TX))
                                rule->dest_cpu = cpu_affinity;
                        /* Otherwise find the first cpu that is capable of net TX */
                        else {
                                for (cpu = 0; cpu < wrtd->nbr_cpus; cpu++) {
                                        root = &wrtd->roots[cpu];
                                        if (root->capabilities & WRTD_CAP_NET_TX) {
                                                rule->dest_cpu = cpu;
                                                break;
                                        }
                                }
                        }
                }

                /* If still cpu affinity is undecided, that means that this is a "network to
                   network" event. Check if destination CPU can also receive and use it, otherwise
                   find the first cpu that is capable of net RX*/
                if (cpu_affinity < 0) {
                        root = &wrtd->roots[rule->dest_cpu];
                        if (root->capabilities & WRTD_CAP_NET_RX)
                                cpu_affinity = rule->dest_cpu;
                        else {
                                for (cpu = 0; cpu < wrtd->nbr_cpus; cpu++) {
                                        root = &wrtd->roots[cpu];
                                        if (root->capabilities & WRTD_CAP_NET_RX) {
                                                cpu_affinity = cpu;
                                                break;
                                        }
                                }
                        }
                }

                /* Place rules according to input affinity.  */
                status = wrtd_reconfigure_place_rule
                        (wrtd, map, i, cpu_affinity, caller_func);
                WRTD_RETURN_IF_ERROR(status);
        }

        /* Assign indexes, handle collisions.  */
        disable_idx = 0;
        cpu_offset = 0;
        for(cpu = 0; cpu < wrtd->nbr_cpus; cpu++) {
                const unsigned nbr_cpu_rules = wrtd->roots[cpu].nbr_rules;
                /* SLOTS[N] contains the index of the rule in map, or -1
                   if the slot is free.  */
                int *slots;
                int free_slot_idx;

                /* Allocate slots array.  */
                slots = (int *)calloc(nbr_cpu_rules, sizeof(int));
                if (slots == NULL)
                        return wrtd_return_error
                                (wrtd, WRTD_ERROR_OUT_OF_MEMORY,
                                 "%s/%s: Could not allocate necessary memory.",
                                 caller_func, __func__);

                /* Mark all slots as unused.  */
                for (i = 0; i < nbr_cpu_rules; i++)
                        slots[i] = -1;

                /* Compute hash and place heads.  */
                for (i = 0; i < map->nbr_rules; i++) {
                        struct rule_map *m = &map->rules[i];
                        unsigned int hash;
                        if (m->cpu != cpu)
                                continue;

                        hash = wrtd_id_hash(&m->rule->conf.source_id)
                                % nbr_cpu_rules;

                        m->global_idx = cpu_offset + hash;
                        m->rule->conf.hash_chain = -1;
                        if (slots[hash] == -1)
                                slots[hash] = i;
                }

                /* Place collisions.  */
                free_slot_idx = 0;
                for (i = 0; i < map->nbr_rules; i++) {
                        struct rule_map *m = &map->rules[i];
                        unsigned head_idx;
                        unsigned head_rule;

                        if (m->cpu != cpu)
                                continue;
                        head_idx = m->global_idx - cpu_offset;
                        assert(head_idx < nbr_cpu_rules);
                        head_rule = slots[head_idx];

                        /* Skip it if already placed.  */
                        if (head_rule == i)
                                continue;

                        /* Find next free slot (there must be one).  */
                        while (slots[free_slot_idx] != -1) {
                                free_slot_idx++;
                                assert(free_slot_idx < nbr_cpu_rules);
                        }
                        m->global_idx = cpu_offset + free_slot_idx;
                        slots[free_slot_idx] = i;

                        /* Chain.  */
                        m->rule->conf.hash_chain =
                                map->rules[head_rule].rule->conf.hash_chain;
                        map->rules[head_rule].rule->conf.hash_chain =
                                free_slot_idx;

                        free_slot_idx++;
                }

                /* Place disabled rules.  */
                while(1) {
                        /* Find next disabled rule.  */
                        while (disable_idx < map->nbr_rules
                               && map->rules[disable_idx].rule->conf.enabled)
                                disable_idx++;
                        if (disable_idx >= map->nbr_rules)
                                break;

                        /* Find next free slot.  */
                        while (slots[free_slot_idx] != -1) {
                                free_slot_idx++;
                                assert(free_slot_idx < nbr_cpu_rules);
                        }

                        map->rules[disable_idx].global_idx =
                                cpu_offset + free_slot_idx;
                        free_slot_idx++;
                        disable_idx++;
                }

                free(slots);
                cpu_offset += nbr_cpu_rules;
        }

        return WRTD_SUCCESS;
}

static enum wrtd_status wrtd_reconfigure_write(struct wrtd_dev *wrtd,
                                               struct conf_map *map,
                                               struct wrtd_lib_rule *new_rules,
                                               const char *caller_func)
{
        enum wrtd_status status;
        struct wrtd_lib_rule *old_rules;
        unsigned i;

        /* FIXME: atomic update (using the freeze flag) ? */

        old_rules = wrtd->rules;

        /* Read stats. */
        for (i = 0; i < map->nbr_rules; i++) {
                status = wrtd_read_rule_stat
                        (wrtd, map->rules[i].old_idx,
                         0, sizeof(struct wrtd_rule_stats),
                         &map->rules[i].stats, __func__);
                WRTD_RETURN_IF_ERROR(status);
        }

        /* Copy rules. */
        for (i = 0; i < map->nbr_rules; i++)
                new_rules[map->rules[i].global_idx].conf =
                        map->rules[i].rule->conf;

        wrtd->rules = new_rules;

        for (i = 0; i < wrtd->nbr_rules; i++) {
                if (wrtd_id_null(&new_rules[i].conf.id)
                    && wrtd_id_null(&old_rules[i].conf.id)) {
                        /* Slot was unused, and it still unused.  */
                        continue;
                }
                if (!old_rules[i].modified &&
                    (wrtd_id_eq(&new_rules[i].conf.id,
                                &old_rules[i].conf.id))) {
                        /* Slot used by the same rule, and not modified.  */
                        continue;
                }
                /* Write the new rule.  */
                status = wrtd_write_rule_conf(wrtd, i, caller_func);

                WRTD_RETURN_IF_ERROR(status);
        }

        /* Write stats. */
        for (i = 0; i < map->nbr_rules; i++) {
                int nidx = map->rules[i].global_idx;
                status = wrtd_write_rule_stats
                        (wrtd, nidx, &map->rules[i].stats, __func__);
                WRTD_RETURN_IF_ERROR(status);
        }

        free(old_rules);

        return WRTD_SUCCESS;
}

enum wrtd_status wrtd_reconfigure(struct wrtd_dev *wrtd, const char *caller_func)
{
        enum wrtd_status status;
        struct conf_map map;
        struct wrtd_lib_rule *new_rules;

        /* Load rules and alarms.  */
        status = wrtd_fill_rules(wrtd, caller_func);
        WRTD_RETURN_IF_ERROR(status);

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

        /* Count number of rules, initialiaze the maps.  */
        status = wrtd_reconfigure_alloc_map(wrtd, &map, caller_func);
        WRTD_RETURN_IF_ERROR(status);

        /* Place rules.  */
        status = wrtd_reconfigure_place(wrtd, &map, caller_func);

        /* Allocate a new array of rules.  */
        new_rules = NULL;
        if (status == WRTD_SUCCESS)
                status = wrtd_alloc_rules(wrtd, &new_rules, caller_func);

        if (status == WRTD_SUCCESS)
                status = wrtd_reconfigure_write(wrtd, &map, new_rules, caller_func);

        free(map.rules);
        free(map.devs);

        return status;
}
