/* Copyright 2018 SiFive, Inc */
/* SPDX-License-Identifier: Apache-2.0 */

#include <metal/machine.h>
#include <metal/pmp.h>
#include <metal/cpu.h>

#define CONFIG_TO_INT(_config) (*((char *) &(_config)))
#define INT_TO_CONFIG(_int) (*((struct metal_pmp_config *)(char *) &(_int)))

struct metal_pmp *metal_pmp_get_device(void)
{
#ifdef __METAL_DT_PMP_HANDLE
    return __METAL_DT_PMP_HANDLE;
#else
    return NULL;
#endif
}

/* This function calculates the minimum granularity from the address
 * that pmpaddr takes on after writing all ones to pmpaddr when pmpcfg = 0.
 *
 * Detect the address granularity based on the position of the
 * least-significant 1 set in the address.
 *
 * For example, if the value read from pmpaddr is 0x3ffffc00, the
 * least-significant set bit is in bit 10 (counting from 0), resulting
 * in a detected granularity of 2^(10 + 2) = 4096.
 */
static uintptr_t _get_detected_granularity(uintptr_t address) {
    if(address == 0) {
        return (uintptr_t) -1;
    }

    /* Get the index of the least significant set bit */
    int index = 0;
    while(((address >> index) & 0x1) == 0) {
        index += 1;
    }

    /* The granularity is equal to 2^(index + 2) bytes */
    return (1 << (index + 2));
}

/* This function calculates the granularity requested by the user's provided
 * value for pmpaddr.
 *
 * Calculate the requested granularity based on the position of the
 * least-significant unset bit.
 *
 * For example, if the requested address is 0x20009ff, the least-significant
 * unset bit is at index 9 (counting from 0), resulting in a requested
 * granularity of 2^(9 + 3) = 4096.
 */
static uintptr_t _get_pmpaddr_granularity(uintptr_t address) {
    /* Get the index of the least significant unset bit */
    int index = 0;
    while(((address >> index) & 0x1) == 1) {
        index += 1;
    }

    /* The granularity is equal to 2^(index + 3) bytes */
    return (1 << (index + 3));
}

/* Get the number of pmp regions for the given hart */
int metal_pmp_num_regions(int hartid)
{
    struct metal_cpu *cpu = metal_cpu_get(hartid);

    return __metal_driver_cpu_num_pmp_regions(cpu);
}

/* Get the number of pmp regions for the current hart */
static unsigned int _pmp_regions() {
    return metal_pmp_num_regions(metal_cpu_get_current_hartid());
}

void metal_pmp_init(struct metal_pmp *pmp) {
    if(!pmp) {
        return;
    }

    struct metal_pmp_config init_config = {
        .L = METAL_PMP_UNLOCKED,
        .A = METAL_PMP_OFF,
        .X = 0,
        .W = 0,
        .R = 0,
    };

    for(unsigned int i = 0; i < _pmp_regions(); i++) {
        metal_pmp_set_region(pmp, i, init_config, 0);
    }

    /* Detect the region granularity by writing all 1s to pmpaddr0 while
     * pmpcfg0 = 0. */
    if(metal_pmp_set_address(pmp, 0, -1) != 0) {
        /* Failed to detect granularity */
        return;
    }

    /* Calculate the granularity based on the value that pmpaddr0 takes on */
    pmp->_granularity[metal_cpu_get_current_hartid()] = _get_detected_granularity(metal_pmp_get_address(pmp, 0));

    /* Clear pmpaddr0 */
    metal_pmp_set_address(pmp, 0, 0);
}

int metal_pmp_set_region(struct metal_pmp *pmp,
                       unsigned int region,
                       struct metal_pmp_config config,
                       size_t address)
{
    struct metal_pmp_config old_config;
    size_t old_address;
    size_t cfgmask;
    size_t pmpcfg;
    int rc = 0;

    if(!pmp) {
        /* Device handle cannot be NULL */
        return 1;
    }

    if(region > _pmp_regions()) {
        /* Region outside of supported range */
        return 2;
    }

    if(config.A == METAL_PMP_NA4 && pmp->_granularity[metal_cpu_get_current_hartid()] > 4) {
        /* The requested granularity is too small */
        return 3;
    }

    if(config.A == METAL_PMP_NAPOT &&
       pmp->_granularity[metal_cpu_get_current_hartid()] > _get_pmpaddr_granularity(address))
    {
        /* The requested granularity is too small */
        return 3;
    }

    rc = metal_pmp_get_region(pmp, region, &old_config, &old_address);
    if(rc) {
        /* Error reading region */
        return rc;
    }

    if(old_config.L == METAL_PMP_LOCKED) {
        /* Cannot modify locked region */
        return 4;
    }

    /* Update the address first, because if the region is being locked we won't
     * be able to modify it after we set the config */
    if(old_address != address) {
        switch(region) {
        case 0:
            __asm__("csrw pmpaddr0, %[addr]"
                    :: [addr] "r" (address) :);
            break;
        case 1:
            __asm__("csrw pmpaddr1, %[addr]"
                    :: [addr] "r" (address) :);
            break;
        case 2:
            __asm__("csrw pmpaddr2, %[addr]"
                    :: [addr] "r" (address) :);
            break;
        case 3:
            __asm__("csrw pmpaddr3, %[addr]"
                    :: [addr] "r" (address) :);
            break;
        case 4:
            __asm__("csrw pmpaddr4, %[addr]"
                    :: [addr] "r" (address) :);
            break;
        case 5:
            __asm__("csrw pmpaddr5, %[addr]"
                    :: [addr] "r" (address) :);
            break;
        case 6:
            __asm__("csrw pmpaddr6, %[addr]"
                    :: [addr] "r" (address) :);
            break;
        case 7:
            __asm__("csrw pmpaddr7, %[addr]"
                    :: [addr] "r" (address) :);
            break;
        case 8:
            __asm__("csrw pmpaddr8, %[addr]"
                    :: [addr] "r" (address) :);
            break;
        case 9:
            __asm__("csrw pmpaddr9, %[addr]"
                    :: [addr] "r" (address) :);
            break;
        case 10:
            __asm__("csrw pmpaddr10, %[addr]"
                    :: [addr] "r" (address) :);
            break;
        case 11:
            __asm__("csrw pmpaddr11, %[addr]"
                    :: [addr] "r" (address) :);
            break;
        case 12:
            __asm__("csrw pmpaddr12, %[addr]"
                    :: [addr] "r" (address) :);
            break;
        case 13:
            __asm__("csrw pmpaddr13, %[addr]"
                    :: [addr] "r" (address) :);
            break;
        case 14:
            __asm__("csrw pmpaddr14, %[addr]"
                    :: [addr] "r" (address) :);
            break;
        case 15:
            __asm__("csrw pmpaddr15, %[addr]"
                    :: [addr] "r" (address) :);
            break;
        }
    }

#if __riscv_xlen==32
    if(CONFIG_TO_INT(old_config) != CONFIG_TO_INT(config)) {
        /* Mask to clear old pmpcfg */
        cfgmask = (0xFF << (8 * (region % 4)) );
        pmpcfg = (CONFIG_TO_INT(config) << (8 * (region % 4)) );
        
        switch(region / 4) {
        case 0:
            __asm__("csrc pmpcfg0, %[mask]"
                    :: [mask] "r" (cfgmask) :);

            __asm__("csrs pmpcfg0, %[cfg]"
                    :: [cfg] "r" (pmpcfg) :);
            break;
        case 1:
            __asm__("csrc pmpcfg1, %[mask]"
                    :: [mask] "r" (cfgmask) :);

            __asm__("csrs pmpcfg1, %[cfg]"
                    :: [cfg] "r" (pmpcfg) :);
            break;
        case 2:
            __asm__("csrc pmpcfg2, %[mask]"
                    :: [mask] "r" (cfgmask) :);

            __asm__("csrs pmpcfg2, %[cfg]"
                    :: [cfg] "r" (pmpcfg) :);
            break;
        case 3:
            __asm__("csrc pmpcfg3, %[mask]"
                    :: [mask] "r" (cfgmask) :);

            __asm__("csrs pmpcfg3, %[cfg]"
                    :: [cfg] "r" (pmpcfg) :);
            break;
        }
    }
#elif __riscv_xlen==64
    if(CONFIG_TO_INT(old_config) != CONFIG_TO_INT(config)) {
        /* Mask to clear old pmpcfg */
        cfgmask = (0xFF << (8 * (region % 8)) );
        pmpcfg = (CONFIG_TO_INT(config) << (8 * (region % 8)) );
        
        switch(region / 8) {
        case 0:
            __asm__("csrc pmpcfg0, %[mask]"
                    :: [mask] "r" (cfgmask) :);

            __asm__("csrs pmpcfg0, %[cfg]"
                    :: [cfg] "r" (pmpcfg) :);
            break;
        case 1:
            __asm__("csrc pmpcfg2, %[mask]"
                    :: [mask] "r" (cfgmask) :);

            __asm__("csrs pmpcfg2, %[cfg]"
                    :: [cfg] "r" (pmpcfg) :);
            break;
        }
    }
#else
#error XLEN is not set to supported value for PMP driver
#endif

    return 0;
}

int metal_pmp_get_region(struct metal_pmp *pmp,
                       unsigned int region,
                       struct metal_pmp_config *config,
                       size_t *address)
{
    size_t pmpcfg = 0;
    char *pmpcfg_convert = (char *)&pmpcfg;

    if(!pmp || !config || !address) {
        /* NULL pointers are invalid arguments */
        return 1;
    }

    if(region > _pmp_regions()) {
        /* Region outside of supported range */
        return 2;
    }

#if __riscv_xlen==32
    switch(region / 4) {
    case 0:
        __asm__("csrr %[cfg], pmpcfg0"
                : [cfg] "=r" (pmpcfg) ::);
        break;
    case 1:
        __asm__("csrr %[cfg], pmpcfg1"
                : [cfg] "=r" (pmpcfg) ::);
        break;
    case 2:
        __asm__("csrr %[cfg], pmpcfg2"
                : [cfg] "=r" (pmpcfg) ::);
        break;
    case 3:
        __asm__("csrr %[cfg], pmpcfg3"
                : [cfg] "=r" (pmpcfg) ::);
        break;
    }

    pmpcfg = (0xFF & (pmpcfg >> (8 * (region % 4)) ) );

#elif __riscv_xlen==64
    switch(region / 8) {
    case 0:
        __asm__("csrr %[cfg], pmpcfg0"
                : [cfg] "=r" (pmpcfg) ::);
        break;
    case 1:
        __asm__("csrr %[cfg], pmpcfg2"
                : [cfg] "=r" (pmpcfg) ::);
        break;
    }

    pmpcfg = (0xFF & (pmpcfg >> (8 * (region % 8)) ) );

#else
#error XLEN is not set to supported value for PMP driver
#endif

    *config = INT_TO_CONFIG(*pmpcfg_convert);

    switch(region) {
    case 0:
        __asm__("csrr %[addr], pmpaddr0"
                : [addr] "=r" (*address) ::);
        break;
    case 1:
        __asm__("csrr %[addr], pmpaddr1"
                : [addr] "=r" (*address) ::);
        break;
    case 2:
        __asm__("csrr %[addr], pmpaddr2"
                : [addr] "=r" (*address) ::);
        break;
    case 3:
        __asm__("csrr %[addr], pmpaddr3"
                : [addr] "=r" (*address) ::);
        break;
    case 4:
        __asm__("csrr %[addr], pmpaddr4"
                : [addr] "=r" (*address) ::);
        break;
    case 5:
        __asm__("csrr %[addr], pmpaddr5"
                : [addr] "=r" (*address) ::);
        break;
    case 6:
        __asm__("csrr %[addr], pmpaddr6"
                : [addr] "=r" (*address) ::);
        break;
    case 7:
        __asm__("csrr %[addr], pmpaddr7"
                : [addr] "=r" (*address) ::);
        break;
    case 8:
        __asm__("csrr %[addr], pmpaddr8"
                : [addr] "=r" (*address) ::);
        break;
    case 9:
        __asm__("csrr %[addr], pmpaddr9"
                : [addr] "=r" (*address) ::);
        break;
    case 10:
        __asm__("csrr %[addr], pmpaddr10"
                : [addr] "=r" (*address) ::);
        break;
    case 11:
        __asm__("csrr %[addr], pmpaddr11"
                : [addr] "=r" (*address) ::);
        break;
    case 12:
        __asm__("csrr %[addr], pmpaddr12"
                : [addr] "=r" (*address) ::);
        break;
    case 13:
        __asm__("csrr %[addr], pmpaddr13"
                : [addr] "=r" (*address) ::);
        break;
    case 14:
        __asm__("csrr %[addr], pmpaddr14"
                : [addr] "=r" (*address) ::);
        break;
    case 15:
        __asm__("csrr %[addr], pmpaddr15"
                : [addr] "=r" (*address) ::);
        break;
    }

    return 0;
}

int metal_pmp_lock(struct metal_pmp *pmp, unsigned int region)
{
    struct metal_pmp_config config;
    size_t address;
    int rc = 0;

    rc = metal_pmp_get_region(pmp, region, &config, &address);
    if(rc) {
        return rc;
    }

    if(config.L == METAL_PMP_LOCKED) {
        return 0;
    }

    config.L = METAL_PMP_LOCKED;

    rc = metal_pmp_set_region(pmp, region, config, address);

    return rc;
}


int metal_pmp_set_address(struct metal_pmp *pmp, unsigned int region, size_t address)
{
    struct metal_pmp_config config;
    size_t old_address;
    int rc = 0;

    rc = metal_pmp_get_region(pmp, region, &config, &old_address);
    if(rc) {
        return rc;
    }

    rc = metal_pmp_set_region(pmp, region, config, address);

    return rc;
}

size_t metal_pmp_get_address(struct metal_pmp *pmp, unsigned int region)
{
    struct metal_pmp_config config;
    size_t address = 0;

    metal_pmp_get_region(pmp, region, &config, &address);

    return address;
}


int metal_pmp_set_address_mode(struct metal_pmp *pmp, unsigned int region, enum metal_pmp_address_mode mode)
{
    struct metal_pmp_config config;
    size_t address;
    int rc = 0;

    rc = metal_pmp_get_region(pmp, region, &config, &address);
    if(rc) {
        return rc;
    }

    config.A = mode;

    rc = metal_pmp_set_region(pmp, region, config, address);

    return rc;
}

enum metal_pmp_address_mode metal_pmp_get_address_mode(struct metal_pmp *pmp, unsigned int region)
{
    struct metal_pmp_config config;
    size_t address = 0;

    metal_pmp_get_region(pmp, region, &config, &address);

    return config.A;
}


int metal_pmp_set_executable(struct metal_pmp *pmp, unsigned int region, int X)
{
    struct metal_pmp_config config;
    size_t address;
    int rc = 0;

    rc = metal_pmp_get_region(pmp, region, &config, &address);
    if(rc) {
        return rc;
    }

    config.X = X;

    rc = metal_pmp_set_region(pmp, region, config, address);

    return rc;
}

int metal_pmp_get_executable(struct metal_pmp *pmp, unsigned int region)
{
    struct metal_pmp_config config;
    size_t address = 0;

    metal_pmp_get_region(pmp, region, &config, &address);

    return config.X;
}


int metal_pmp_set_writeable(struct metal_pmp *pmp, unsigned int region, int W)
{
    struct metal_pmp_config config;
    size_t address;
    int rc = 0;

    rc = metal_pmp_get_region(pmp, region, &config, &address);
    if(rc) {
        return rc;
    }

    config.W = W;

    rc = metal_pmp_set_region(pmp, region, config, address);

    return rc;
}

int metal_pmp_get_writeable(struct metal_pmp *pmp, unsigned int region)
{
    struct metal_pmp_config config;
    size_t address = 0;

    metal_pmp_get_region(pmp, region, &config, &address);

    return config.W;
}


int metal_pmp_set_readable(struct metal_pmp *pmp, unsigned int region, int R)
{
    struct metal_pmp_config config;
    size_t address;
    int rc = 0;

    rc = metal_pmp_get_region(pmp, region, &config, &address);
    if(rc) {
        return rc;
    }

    config.R = R;

    rc = metal_pmp_set_region(pmp, region, config, address);

    return rc;
}

int metal_pmp_get_readable(struct metal_pmp *pmp, unsigned int region)
{
    struct metal_pmp_config config;
    size_t address = 0;

    metal_pmp_get_region(pmp, region, &config, &address);

    return config.R;
}

