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

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

struct rule_map {
        struct wrtd_lib_rule *rule;
        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;
};

struct conf_map {
        unsigned nbr_rules;
        struct rule_map *rules;
        unsigned nbr_devs;
        struct device_map *devs;
        unsigned free_rule_slots[WRTD_MAX_CPUS];
};


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].rule.conf.id))
                        continue;
                map->rules[n].rule = &wrtd->rules[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 char *id, unsigned int dir,
                                          unsigned *res_dev,
                                          unsigned *res_ch)
{
        unsigned i;
        unsigned j;

        char dev_id[WRTD_ID_LEN];

        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, 0, WRTD_ID_LEN);
                        dev_id[0] = 'L';
                        dev_id[1] = 'C';
                        dev_id[2] = '-';
                        if (d->chs_dir == WRTD_CH_DIR_IN) {
                                dev_id[3] = 'I';
                                dev_id[4] = '1' + d->chs_idx + j;
                        }
                        else {
                                dev_id[3] = 'O';
                                dev_id[4] = '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->rule.conf.dest_cpu == cpu)
                map->rules[rule_num].rule->rule.conf.dest_cpu = WRTD_DEST_CPU_LOCAL;

        return WRTD_SUCCESS;
}

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 *rule = &m->rule->rule;
                unsigned dev;
                unsigned ch;
                signed cpu_affinity;

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

                /* Place the rule on the same CPU as the input source.  */
                status = wrtd_find_channel(wrtd, map, rule->conf.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->conf.source_id, &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->conf.dest_id,
                                           WRTD_CH_DIR_OUT, &dev, &ch);
                if (status == WRTD_SUCCESS) {
                        /* Output device.  */
                        rule->conf.dest_cpu = map->devs[dev].cpu;
                        rule->conf.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->conf.dest_cpu];
                        if ((cpu_affinity < 0) &&
                            (root->capabilities & WRTD_CAP_NET_RX))
                                cpu_affinity = rule->conf.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->conf.dest_ch = WRTD_DEST_CH_NET;
                        /* If source cpu can also send to
                           network, set dest cpu affinity to that cpu */
                        if (cpu_affinity >=0)
                                root = &wrtd->roots[cpu_affinity];
                        if ((root) && (root->capabilities & WRTD_CAP_NET_TX))
                                rule->conf.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->conf.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->conf.dest_cpu];
                        if (root->capabilities & WRTD_CAP_NET_RX)
                                cpu_affinity = rule->conf.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->rule.conf.source_id)
                                % nbr_cpu_rules;

                        m->global_idx = cpu_offset + hash;
                        m->rule->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->rule.conf.hash_chain =
                                map->rules[head_rule].rule->rule.conf.hash_chain;
                        map->rules[head_rule].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->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;

        old_rules = wrtd->rules;

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

        /* Freeze all cpus.  */
        /* TODO.  */

        /* For each rule: load stats if different.  */
        /* TODO.  */

        wrtd->rules = new_rules;

        for(i = 0; i < wrtd->nbr_rules; i++) {
                if (wrtd_id_null(new_rules[i].rule.conf.id)
                    && wrtd_id_null(old_rules[i].rule.conf.id))
                        continue;
                if (!old_rules[i].modified &&
                    (wrtd_id_eq(new_rules[i].rule.conf.id,
                                old_rules[i].rule.conf.id)))
                        continue;
                status = wrtd_write_rule(wrtd, i, caller_func);
                /* FIXME: inconsistent state, free old_rules ?  */
                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.  */
        status = wrtd_reconfigure_alloc_map(wrtd, &map, caller_func);
        WRTD_RETURN_IF_ERROR(status);

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

        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);

        if (status != WRTD_SUCCESS)
                free(new_rules);
        free(map.rules);
        free(map.devs);

        return status;
}
