/*
 * Copyright (c) 2018-2019, Arm Limited and affiliates.
 * SPDX-License-Identifier: Apache-2.0
 *
 * 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 "nsconfig.h"
#include "common_functions.h"

#define rot(x,k) (((x)<<(k)) | ((x)>>(32-(k))))

#define final(a,b,c) \
{ \
    c ^= b; c -= rot(b, 14); \
    a ^= c; a -= rot(c, 11); \
    b ^= a; b -= rot(a, 25); \
    c ^= b; c -= rot(b, 16); \
    a ^= c; a -= rot(c, 4); \
    b ^= a; b -= rot(a, 14); \
    c ^= b; c -= rot(b, 24); \
}

#define mix(a,b,c) \
{ \
    a -= c; a ^= rot(c, 4); c += b; \
    b -= a; b ^= rot(a, 6); a += c; \
    c -= b; c ^= rot(b, 8); b += a; \
    a -= c; a ^= rot(c, 16); c += b; \
    b -= a; b ^= rot(a, 19); a += c; \
    c -= b; c ^= rot(b, 4); b += a; \
}

static uint32_t global_seed = 1;


uint16_t tr51_calc_nearest_prime_number(uint16_t start_value)
{
    if (start_value < 2) {
        return 0;
    }
    uint16_t divider = start_value - 1;
    while (start_value) {
        if (start_value % divider--) {
            if (divider == 1) {
                break;
            }
        } else {
            divider = ++start_value - 1;
        }
    }
    return start_value;
}

static void tr51_seed_rand(uint32_t seed)
{
    if (!seed) {
        seed = 1;
    }
    global_seed = seed;
}

static int32_t tr51_get_rand(void)
{
    uint32_t random_val = ((global_seed * 1103515245) + 12345) & 0x7fffffff;
    global_seed = random_val;
    return random_val;
}

/**
 * @brief Calculate channel table based on TR51 channel function.
 * @param number_of_channels Number of channels in table.
 * @param nearest_prime Nearest prime number. Must be equal to or larger than number_of_channels.
 * @param channel_table Output channel table. Has to be at least nearest_prime in length.
 */
static void tr51_calculate_channel_table(uint16_t number_of_channels, uint16_t nearest_prime, int16_t *channel_table)
{
    int32_t i, j, k;
    tr51_seed_rand(1);
    for (i = 0; i < nearest_prime; i++) {
        channel_table[i] = -1;
    }
    for (i = 0; i < number_of_channels; i++) {
        j = tr51_get_rand() % number_of_channels;
        k = 0;
        while (k <= i) {
            if (j == channel_table[k]) {
                j = tr51_get_rand() % number_of_channels;
                k = 0;
            } else {
                k = k + 1;
            }
        }
        channel_table[i] = j;
    }
}

static void tr51_compute_cfd(uint8_t *mac, uint8_t *first_element, uint8_t *step_size, uint16_t channel_table_length)
{
    *first_element = (mac[5] ^ mac[6] ^ mac[7]) % channel_table_length;
    *step_size = (mac[7] % (channel_table_length - 1)) + 1;
}

static uint8_t tr51_find_excluded(int32_t channel, uint32_t *excluded_channels)
{
    if (excluded_channels != NULL) {
        uint8_t index = channel / 32;
        channel %= 32;
        if (excluded_channels[index] & ((uint32_t)1 << channel)) {
            return true;
        }
    }
    return false;
}

/**
 * @brief Calculate hopping sequence for a specific peer using tr51 channel function.
 * @param channel_table Used channel table.
 * @param channel_table_length Length of the used channel table.
 * @param first_element Start generated by CFD function.
 * @param step_size Step size generated by CFD function.
 * @param output_table Output hopping sequence table.
 * @param excluded_channels Bit mask where excluded channels are set to 1.
 * @return Number of channels in sequence.
 */
static uint16_t tr51_calculate_hopping_sequence(int16_t *channel_table, uint16_t channel_table_length, uint8_t first_element, uint8_t step_size, uint8_t *output_table, uint32_t *excluded_channels)
{
    uint16_t cntr = channel_table_length;
    uint8_t index = first_element;
    uint8_t slot = 0;
    while (cntr--) {
        if (channel_table[index] != -1) {
            if (tr51_find_excluded(channel_table[index], excluded_channels) == false) {
                output_table[slot] = channel_table[index];
                slot++;
            }
        }
        index += step_size;
        index %= channel_table_length;
    }
    return slot;
}

static uint32_t dh1cf_hashword(const uint32_t *key, size_t key_length, uint32_t init_value)
{
    uint32_t a, b, c;
    a = b = c = 0xdeadbeef + (((uint32_t)key_length) << 2) + init_value;
    while (key_length > 3) {
        a += key[0];
        b += key[1];
        c += key[2];
        mix(a, b, c);
        key_length -= 3;
        key += 3;
    }
    switch (key_length) {
        case 3:
            c += key[2];
        /* fall through */
        case 2:
            b += key[1];
        /* fall through */
        case 1:
            a += key[0];
            final(a, b, c);
        /* fall through */
        case 0:
            break;
    }
    return c;
}

int32_t dh1cf_get_uc_channel_index(uint16_t slot_number, uint8_t *mac, int16_t number_of_channels)
{
    int32_t channel_number;
    uint32_t key[3];
    key[0] = (uint32_t) slot_number;
    key[1] = common_read_32_bit(&mac[4]);
    key[2] = common_read_32_bit(&mac[0]);
    channel_number = dh1cf_hashword(key, 3, 0) % number_of_channels;
    return channel_number;
}

int32_t dh1cf_get_bc_channel_index(uint16_t slot_number, uint16_t bsi, int16_t number_of_channels)
{
    int32_t channel_number;
    uint32_t key[3];
    key[0] = (uint32_t) slot_number;
    key[1] = (uint32_t) bsi << 16;
    key[2] = 0;
    channel_number = dh1cf_hashword(key, 3, 0) % number_of_channels;
    return channel_number;
}

int tr51_init_channel_table(int16_t *channel_table, int16_t number_of_channels)
{
    uint16_t nearest_prime = tr51_calc_nearest_prime_number(number_of_channels);
    tr51_calculate_channel_table(number_of_channels, nearest_prime, channel_table);
    return 0;
}

int32_t tr51_get_uc_channel_index(int16_t *channel_table, uint8_t *output_table, uint16_t slot_number, uint8_t *mac, int16_t number_of_channels, uint32_t *excluded_channels)
{
    uint16_t nearest_prime = tr51_calc_nearest_prime_number(number_of_channels);
    uint8_t first_element;
    uint8_t step_size;
    tr51_compute_cfd(mac, &first_element, &step_size, nearest_prime);
    tr51_calculate_hopping_sequence(channel_table, nearest_prime, first_element, step_size, output_table, excluded_channels);
    return output_table[slot_number];
}

int32_t tr51_get_bc_channel_index(int16_t *channel_table, uint8_t *output_table, uint16_t slot_number, uint16_t bsi, int16_t number_of_channels, uint32_t *excluded_channels)
{
    uint16_t nearest_prime = tr51_calc_nearest_prime_number(number_of_channels);
    uint8_t mac[8] = {0, 0, 0, 0, 0, 0, (uint8_t)(bsi >> 8), (uint8_t)bsi};
    uint8_t first_element;
    uint8_t step_size;
    tr51_compute_cfd(mac, &first_element, &step_size, nearest_prime);
    tr51_calculate_hopping_sequence(channel_table, nearest_prime, first_element, step_size, output_table, excluded_channels);
    return output_table[slot_number];
}
