/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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 <assert.h>
#include <stdint.h>
#include "mcu/mcu.h"
#include "nimble/ble.h"
#include "controller/ble_hw.h"
#include "CMAC.h"
#include <ipc_cmac/rand.h>
#include "tinycrypt/aes.h"

static struct tc_aes_key_sched_struct g_ctx;

int
ble_hw_rng_init(ble_rng_isr_cb_t cb, int bias)
{
    cmac_rand_set_isr_cb(cb);
    return 0;
}

int
ble_hw_rng_start(void)
{
    /* Chime the M33 in case we need random numbers generated */
    cmac_rand_start();
    CMAC->CM_EV_SET_REG = CMAC_CM_EV_SET_REG_EV1C_CMAC2SYS_IRQ_SET_Msk;
    return 0;
}

int
ble_hw_rng_stop(void)
{
    cmac_rand_stop();
    return 0;
}

#define BLE_HW_RESOLV_LIST_SIZE     (MYNEWT_VAL(BLE_LL_RESOLV_LIST_SIZE))

struct ble_hw_resolv_irk {
    uint32_t key[4];
};

struct ble_hw_resolv_list {
    uint8_t count;
    struct ble_hw_resolv_irk irk[BLE_HW_RESOLV_LIST_SIZE];
};

struct ble_hw_resolv_proc {
    uint32_t hash;
    uint8_t f_configured;
    uint8_t f_active;
    uint8_t f_match;
    uint8_t f_done;
    struct ble_hw_resolv_irk *irk;
    struct ble_hw_resolv_irk *irk_end;
    uint32_t crypto_prand_in[4];
    uint32_t crypto_e_out[4];
};

static struct ble_hw_resolv_list g_ble_hw_resolv_list;
static struct ble_hw_resolv_proc g_ble_hw_resolv_proc;

int
ble_hw_get_public_addr(ble_addr_t *addr)
{
    return -1;
}

int
ble_hw_get_static_addr(ble_addr_t *addr)
{
    return -1;
}

void
ble_hw_whitelist_clear(void)
{
}

int
ble_hw_whitelist_add(const uint8_t *addr, uint8_t addr_type)
{
    return 0;
}

void
ble_hw_whitelist_rmv(const uint8_t *addr, uint8_t addr_type)
{
}

uint8_t
ble_hw_whitelist_size(void)
{
    return 0;
}

void
ble_hw_whitelist_enable(void)
{
}


void
ble_hw_whitelist_disable(void)
{
}

int
ble_hw_whitelist_match(void)
{
    return 0;
}

int
ble_hw_encrypt_block(struct ble_encryption_block *ecb)
{
    uint32_t in_addr;
    uint32_t out_addr;

    /*
     * The following code bears some explanation. This function is called by
     * the LL task to encrypt blocks and calculate session keys. Address
     * resolution also calls this function. Furthermore, during connections,
     * the M0 crypto accelerator is used but this function is not called when
     * using it. During the entire connection event, the M0 crypto block cannot
     * be used as the crypto state (some of it) needs to remain un-changed.
     * Note that this is also true when address resolution is enabled: the
     * HW crypto block is set up and cannot be modified.
     *
     * Rather than attempt to share the M0 crypto block between the various
     * controller features which require it, we decided to use software to
     * perform the encryption task for anything being done at the link-layer
     * (outside of an ISR). If this function is called inside an ISR, and it
     * is when resolving addresses, the crypto accelerator is not being used
     * by a connection event. Thus, we check to see if we are inside of an ISR.
     * If so, we use the M0 crypto block. If outside of an ISR, we use the M33
     */
    if (!os_arch_in_isr()) {
        tc_aes128_set_encrypt_key(&g_ctx, ecb->key);
        tc_aes_encrypt(ecb->cipher_text, ecb->plain_text, &g_ctx);
        return 0;
    }

    /* Need to retain state of in/out pointers */
    in_addr = CMAC->CM_CRYPTO_IN_ADR2_REG;
    out_addr = CMAC->CM_CRYPTO_OUT_ADR_REG;

    while (CMAC->CM_CRYPTO_STAT_REG & CMAC_CM_CRYPTO_STAT_REG_CM_CRYPTO_BUSY_Msk);

    /* RECB, memory in/out, encryption */
    CMAC->CM_CRYPTO_CTRL_REG = CMAC_CM_CRYPTO_CTRL_REG_CM_CRYPTO_ECB_ENC_EN_Msk |
                               CMAC_CM_CRYPTO_CTRL_REG_CM_CRYPTO_IN_SEL_Msk |
                               CMAC_CM_CRYPTO_CTRL_REG_CM_CRYPTO_OUT_SEL_Msk |
                               CMAC_CM_CRYPTO_CTRL_REG_CM_CRYPTO_ENC_DECN_Msk;

    CMAC->CM_CRYPTO_KEY_31_0_REG = get_le32(&ecb->key[0]);
    CMAC->CM_CRYPTO_KEY_63_32_REG = get_le32(&ecb->key[4]);
    CMAC->CM_CRYPTO_KEY_95_64_REG = get_le32(&ecb->key[8]);
    CMAC->CM_CRYPTO_KEY_127_96_REG = get_le32(&ecb->key[12]);
    CMAC->CM_CRYPTO_IN_ADR2_REG = (uint32_t)ecb->plain_text;
    CMAC->CM_CRYPTO_OUT_ADR_REG = (uint32_t)ecb->cipher_text;

    CMAC->CM_EXC_STAT_REG = CMAC_CM_EXC_STAT_REG_EXC_CRYPTO_Msk;
    CMAC->CM_EV_SET_REG = CMAC_CM_EV_SET_REG_EV_CRYPTO_START_Msk;
    while (!(CMAC->CM_EXC_STAT_REG & CMAC_CM_EXC_STAT_REG_EXC_CRYPTO_Msk));
    CMAC->CM_EXC_STAT_REG = CMAC_CM_EXC_STAT_REG_EXC_CRYPTO_Msk;

    CMAC->CM_CRYPTO_IN_ADR2_REG = in_addr;
    CMAC->CM_CRYPTO_OUT_ADR_REG = out_addr;

    return 0;
}

void
ble_hw_resolv_list_clear(void)
{
    g_ble_hw_resolv_list.count = 0;
}

int
ble_hw_resolv_list_add(uint8_t *irk)
{
    struct ble_hw_resolv_irk *e;

    if (g_ble_hw_resolv_list.count == BLE_HW_RESOLV_LIST_SIZE) {
        return BLE_ERR_MEM_CAPACITY;
    }

    e = &g_ble_hw_resolv_list.irk[g_ble_hw_resolv_list.count];
    /* Prepare key here so we do not need to do it during resolving */
    e->key[0] = get_le32(&irk[0]);
    e->key[1] = get_le32(&irk[4]);
    e->key[2] = get_le32(&irk[8]);
    e->key[3] = get_le32(&irk[12]);

    g_ble_hw_resolv_list.count++;

    return BLE_ERR_SUCCESS;
}

void
ble_hw_resolv_list_rmv(int index)
{
    struct ble_hw_resolv_irk *e;

    if (index < g_ble_hw_resolv_list.count) {
        g_ble_hw_resolv_list.count--;

        e = &g_ble_hw_resolv_list.irk[index];
        memmove(e, e + 1, (g_ble_hw_resolv_list.count - index) * sizeof(e->key));
    }
}

uint8_t
ble_hw_resolv_list_size(void)
{
    return BLE_HW_RESOLV_LIST_SIZE;
}

int
ble_hw_resolv_list_match(void)
{
    return g_ble_hw_resolv_proc.f_match ?
           g_ble_hw_resolv_proc.irk - g_ble_hw_resolv_list.irk : -1;
}

static void
ble_hw_resolv_proc_next(void)
{
    void *src = &g_ble_hw_resolv_proc.irk->key;

    if (g_ble_hw_resolv_proc.irk == g_ble_hw_resolv_proc.irk_end) {
        g_ble_hw_resolv_proc.f_done = 1;
        g_ble_hw_resolv_proc.f_active = 0;
    } else {
        __asm__ volatile (".syntax unified                      \n"
                          "   ldm  %[ptr]!, {r1, r2, r3, r4}    \n"
                          "   ldr  %[ptr], =%[reg]              \n"
                          "   stm  %[ptr]!, {r1, r2, r3, r4}    \n"
                          : [ptr] "+l" (src)
                          : [reg] "i" (&CMAC->CM_CRYPTO_KEY_31_0_REG)
                          : "r1", "r2", "r3", "r4", "memory");

        CMAC->CM_EV_SET_REG = CMAC_CM_EV_SET_REG_EV_CRYPTO_START_Msk;
    }
}

void
ble_hw_resolv_proc_enable(void)
{
    assert(!g_ble_hw_resolv_proc.f_active);

    CMAC->CM_CRYPTO_CTRL_REG = CMAC_CM_CRYPTO_CTRL_REG_CM_CRYPTO_SW_REQ_ABORT_Msk;

    CMAC->CM_CRYPTO_CTRL_REG = CMAC_CM_CRYPTO_CTRL_REG_CM_CRYPTO_ECB_ENC_EN_Msk |
                               CMAC_CM_CRYPTO_CTRL_REG_CM_CRYPTO_IN_SEL_Msk |
                               CMAC_CM_CRYPTO_CTRL_REG_CM_CRYPTO_OUT_SEL_Msk |
                               CMAC_CM_CRYPTO_CTRL_REG_CM_CRYPTO_ENC_DECN_Msk;

    CMAC->CM_CRYPTO_IN_ADR2_REG = (uint32_t)g_ble_hw_resolv_proc.crypto_prand_in;
    CMAC->CM_CRYPTO_OUT_ADR_REG = (uint32_t)g_ble_hw_resolv_proc.crypto_e_out;

    g_ble_hw_resolv_proc.irk = g_ble_hw_resolv_list.irk;
    g_ble_hw_resolv_proc.irk_end = g_ble_hw_resolv_list.irk +
                                   g_ble_hw_resolv_list.count;
    g_ble_hw_resolv_proc.f_configured = 1;
    g_ble_hw_resolv_proc.f_active = 0;

    /*
     * It would be better to enable IRQ in ble_hw_resolv_proc_start, but this
     * would introduce a bit of latency when starting resolving procedure and
     * we need to save every us possible there in order to be able to resolve
     * RPA on time.
     */
    NVIC_ClearPendingIRQ(CRYPTO_IRQn);
    NVIC_EnableIRQ(CRYPTO_IRQn);
}

void
ble_hw_resolv_proc_disable(void)
{
    g_ble_hw_resolv_proc.f_configured = 0;
    g_ble_hw_resolv_proc.f_active = 0;
    g_ble_hw_resolv_proc.f_match = 0;
    g_ble_hw_resolv_proc.f_done = 1;

    NVIC_DisableIRQ(CRYPTO_IRQn);
}

void
ble_hw_resolv_proc_reset(const uint8_t *addr)
{
    g_ble_hw_resolv_proc.f_match = 0;
}

void
ble_hw_resolv_proc_start(const uint8_t *addr)
{
    assert(g_ble_hw_resolv_proc.f_configured);

    /* crypto_prand_in is already zeroed so prand is properly padded */
    g_ble_hw_resolv_proc.crypto_prand_in[3] = get_be24(&addr[3]) << 8;
    g_ble_hw_resolv_proc.hash = get_be24(&addr[0]);

    g_ble_hw_resolv_proc.f_match = 0;
    g_ble_hw_resolv_proc.f_done = 0;
    g_ble_hw_resolv_proc.f_active = 1;

    ble_hw_resolv_proc_next();
}

void
CRYPTO_IRQHandler(void)
{
    uint32_t hash;

    CMAC->CM_EXC_STAT_REG = CMAC_CM_EXC_STAT_REG_EXC_CRYPTO_Msk;

    hash = g_ble_hw_resolv_proc.crypto_e_out[3] >> 8;
    if (g_ble_hw_resolv_proc.hash == hash) {
        g_ble_hw_resolv_proc.f_active = 0;
        g_ble_hw_resolv_proc.f_match = 1;
        g_ble_hw_resolv_proc.f_done = 1;
    } else {
        g_ble_hw_resolv_proc.irk++;
        ble_hw_resolv_proc_next();
    }
}
