/*
 * ESP32-C3 ICache emulation
 *
 * Copyright (c) 2023 Espressif Systems (Shanghai) Co. Ltd.
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License version 2 or
 * (at your option) any later version.
 */


#include "qemu/osdep.h"
#include "qemu/log.h"
#include "qemu/module.h"
#include "qemu/timer.h"
#include "qapi/error.h"
#include "qemu/error-report.h"
#include "hw/hw.h"
#include "hw/sysbus.h"
#include "hw/irq.h"
#include "hw/qdev-properties.h"
#include "hw/misc/esp32c3_cache.h"
#include "hw/misc/esp32c3_xts_aes.h"
#include "sysemu/block-backend-io.h"
#include "hw/misc/esp32c3_reg.h"


#define CACHE_DEBUG      0
#define CACHE_WARNING    0


/**
 * @brief Checks that the enable flag is enabled in the I/O register. If that's the case,
 *        `done` flag is returned and the `enable` flag is cleared from register.
 *        Else, 0 is returned.
*/
static inline uint32_t check_and_reset_ena(uint32_t* hwreg, uint32_t ena_mask, uint32_t done_mask)
{
    uint32_t regval = *hwreg;

    if (regval & ena_mask) {
        regval &= ~ena_mask;
        regval |= done_mask;
        *hwreg = regval;
    }

    return regval;
}


static inline uint32_t esp32c3_read_mmu_value(ESP32C3CacheState *s, hwaddr reg_addr)
{
    /* Make the assumption that the address is aligned on sizeof(uint32_t) */
    const uint32_t index = reg_addr / sizeof(uint32_t);
    return (uint32_t) s->mmu[index].val;
}


static inline void esp32c3_write_mmu_value(ESP32C3CacheState *s, hwaddr reg_addr, uint32_t value)
{
    /* Make the assumption that the address is aligned on sizeof(uint32_t) */
    const uint32_t index = reg_addr / sizeof(uint32_t);
    /* Reserved bits shall always be 0 */
    ESP32C3MMUEntry e = { .val = value };
    /* Always keep reserved as 0 */
    e.reserved = 0;
    if (s->mmu[index].val != e.val) {
        /* Update the cache (MemoryRegion) */
        const uint32_t virtual_address = index * ESP32C3_PAGE_SIZE;
        /* The entry contains the index of the 64KB block from the flash memory */
        const uint32_t physical_address = e.page_number * ESP32C3_PAGE_SIZE;
        uint8_t* cache_data = ((uint8_t*) memory_region_get_ram_ptr(&s->dcache)) + virtual_address;

        if (e.invalid) {
            const uint32_t invalid_value = 0xdeadbeef;
            uint32_t* cache_word_data = (uint32_t*) cache_data;
            for (int i = 0; i < ESP32C3_PAGE_SIZE / sizeof(invalid_value); i++) {
                cache_word_data[i] = invalid_value;
            }
        } else {
            if (s->flash_blk != NULL) {
                blk_pread(s->flash_blk, physical_address, ESP32C3_PAGE_SIZE, cache_data, 0);
            }
            // XTS-AES Flash Decryption - Only wired when built with CONFIG_GCRYPT; absent here means flash is treated as unencrypted, matching hardware with flash encryption disabled. Encrypted-flash firmware needs the gcrypt build.
            if (s->xts_aes != NULL) {
                ESP32C3XtsAesClass *xts_aes_class = ESP32C3_XTS_AES_GET_CLASS(s->xts_aes);
                if (xts_aes_class->is_flash_enc_enabled(s->xts_aes)) {
                    xts_aes_class->decrypt(s->xts_aes, physical_address, cache_data, ESP32C3_PAGE_SIZE);
                }
            }
        }
        s->mmu[index].val = e.val;
    }
}


static uint64_t esp32c3_cache_read(void *opaque, hwaddr addr, unsigned int size)
{
    ESP32C3CacheState *s = ESP32C3_CACHE(opaque);
    const hwaddr index = ESP32C3_CACHE_REG_IDX(addr);
    uint64_t r = 0;

    if (addr & 0x3) {
        /* Unaligned access, should we fail? */
        error_report("[QEMU] unaligned access to the cache registers");
    }

    switch(addr) {
        case A_EXTMEM_ICACHE_CTRL:
            r = s->icache_enable;
            break;
        /* For the following registers, mark the bit as done only if the feature was enabled */
        case A_EXTMEM_ICACHE_SYNC_CTRL:
            r = check_and_reset_ena(&s->regs[index],
                                    R_EXTMEM_ICACHE_SYNC_CTRL_INVALIDATE_ENA_MASK,
                                    R_EXTMEM_ICACHE_SYNC_CTRL_SYNC_DONE_MASK);
            break;
        case A_EXTMEM_ICACHE_AUTOLOAD_CTRL:
            r = check_and_reset_ena(&s->regs[index],
                                    R_EXTMEM_ICACHE_AUTOLOAD_CTRL_AUTOLOAD_ENA_MASK,
                                    R_EXTMEM_ICACHE_AUTOLOAD_CTRL_AUTOLOAD_DONE_MASK);
            break;
        case A_EXTMEM_ICACHE_PRELOAD_CTRL:
            r = check_and_reset_ena(&s->regs[index],
                                    R_EXTMEM_ICACHE_PRELOAD_CTRL_PRELOAD_ENA_MASK,
                                    R_EXTMEM_ICACHE_PRELOAD_CTRL_PRELOAD_DONE_MASK);
            break;
        case A_EXTMEM_ICACHE_FREEZE:
            r = s->regs[index];
            break;
        case A_EXTMEM_CACHE_STATE:
            /* Return the state of ICache as idle:
             * 1: Idle
             * 0: Busy/Not idle */
            r = 1 << R_EXTMEM_CACHE_STATE_ICACHE_STATE_SHIFT;
            break;
        case ESP32C3_MMU_TABLE_OFFSET ... (ESP32C3_MMU_TABLE_OFFSET + ESP32C3_MMU_SIZE):
            r = esp32c3_read_mmu_value(s, addr - ESP32C3_MMU_TABLE_OFFSET);
            break;
        default:
#if CACHE_WARNING
            warn_report("[CACHE] Unsupported read to 0x%lx", addr);
#endif
            break;
    }

#if CACHE_DEBUG
    info_report("[CACHE] Reading 0x%lx (0x%lx)", addr, r);
#endif

    return r;
}

static void esp32c3_cache_write(void *opaque, hwaddr addr, uint64_t value,
                                unsigned int size)
{
    ESP32C3CacheState *s = ESP32C3_CACHE(opaque);

    const hwaddr index = ESP32C3_CACHE_REG_IDX(addr);

    if (index < ESP32C3_CACHE_REG_COUNT) {
        switch (addr) {
            case A_EXTMEM_ICACHE_CTRL:
                s->icache_enable = value & 1;
                break;
            case A_EXTMEM_ICACHE_FREEZE:
                if (value & R_EXTMEM_ICACHE_FREEZE_ICACHE_FREEZE_ENA_MASK) {
                    /* Enable freeze, set DONE bit */
                    s->regs[index] |= R_EXTMEM_ICACHE_FREEZE_ICACHE_FREEZE_DONE_MASK;
                } else {
                    /* Disable freeze, clear DONE bit */
                    s->regs[index] &= ~R_EXTMEM_ICACHE_FREEZE_ICACHE_FREEZE_DONE_MASK;
                }
                break;
            default:
                s->regs[index] = value;
                break;
        }
    } else if (addr >= ESP32C3_MMU_TABLE_OFFSET) {
        esp32c3_write_mmu_value(s, addr - ESP32C3_MMU_TABLE_OFFSET, value);
    }

#if CACHE_DEBUG
    info_report("[CACHE] Writing 0x%lx = %08lx", addr, value);
#endif

}

static const MemoryRegionOps esp32c3_cache_ops = {
    .read =  esp32c3_cache_read,
    .write = esp32c3_cache_write,
    .endianness = DEVICE_LITTLE_ENDIAN,
};

static bool esp32c3_cache_mem_accepts(void *opaque, hwaddr addr,
                                      unsigned size, bool is_write,
                                      MemTxAttrs attrs)
{
    /* Only accept operation in the cache if there are in READ access.
     * TODO: Refuse any access to the cache if disable and trigger an exception. */
    return !is_write;
}

static const MemoryRegionOps esp32c3_cache_mem_ops = {
    .write = NULL,
    .endianness = DEVICE_LITTLE_ENDIAN,
    .valid.accepts = esp32c3_cache_mem_accepts,
};

static void esp32c3_cache_reset_hold(Object *obj, ResetType type)
{
    ESP32C3CacheState *s = ESP32C3_CACHE(obj);
    memset(s->regs, 0, ESP32C3_CACHE_REG_COUNT * sizeof(*s->regs));

    /* Initialize the MMU with invalid entries */
    for (int i = 0; i < ESP32C3_MMU_TABLE_ENTRY_COUNT; i++) {
        s->mmu[i].invalid = 1;
    }

    /* On reset, autoload must be set to done (ready) */
    s->regs[ESP32C3_CACHE_REG_IDX(A_EXTMEM_ICACHE_AUTOLOAD_CTRL)] = R_EXTMEM_ICACHE_AUTOLOAD_CTRL_AUTOLOAD_DONE_MASK;
    /* Same goes for the manual preload */
    s->regs[ESP32C3_CACHE_REG_IDX(A_EXTMEM_ICACHE_PRELOAD_CTRL)] = R_EXTMEM_ICACHE_PRELOAD_CTRL_PRELOAD_DONE_MASK;
}

static void esp32c3_cache_realize(DeviceState *dev, Error **errp)
{
    /* Initialize the registers */
    esp32c3_cache_reset_hold(OBJECT(dev), RESET_TYPE_COLD);

}

static void esp32c3_cache_init(Object *obj)
{
    ESP32C3CacheState *s = ESP32C3_CACHE(obj);
    SysBusDevice *sbd = SYS_BUS_DEVICE(obj);

    /* Since the cache I/O region and the MMU I/O region are adjacent, let's use the same MemoryRegion object
     * for both, this will simplify the machine architecture. */
    memory_region_init_io(&s->iomem, obj, &esp32c3_cache_ops, s,
                          TYPE_ESP32C3_CACHE, TYPE_ESP32C3_CACHE_IO_SIZE + ESP32C3_MMU_SIZE);

    /* Initialize the data cache area first */
    s->dcache_base = ESP32C3_DCACHE_BASE;
    memory_region_init_rom_device(&s->dcache, OBJECT(s),
                                  &esp32c3_cache_mem_ops, s,
                                  "cpu0-dcache", ESP32C3_EXTMEM_REGION_SIZE, &error_abort);

    /* Same goes for the instruction cache */
    s->icache_base = ESP32C3_ICACHE_BASE;
    memory_region_init_alias(&s->icache, OBJECT(s), "cpu0-icache", &s->dcache, 0, ESP32C3_EXTMEM_REGION_SIZE);

    sysbus_init_mmio(sbd, &s->iomem);
}

static Property esp32c3_cache_properties[] = {
    DEFINE_PROP_END_OF_LIST(),
};

static void esp32c3_cache_class_init(ObjectClass *klass, void *data)
{
    DeviceClass *dc = DEVICE_CLASS(klass);
    ResettableClass *rc = RESETTABLE_CLASS(klass);

    rc->phases.hold = esp32c3_cache_reset_hold;
    dc->realize = esp32c3_cache_realize;
    device_class_set_props(dc, esp32c3_cache_properties);
}

static const TypeInfo esp32c3_cache_info = {
    .name = TYPE_ESP32C3_CACHE,
    .parent = TYPE_SYS_BUS_DEVICE,
    .instance_size = sizeof(ESP32C3CacheState),
    .instance_init = esp32c3_cache_init,
    .class_init = esp32c3_cache_class_init
};

static void esp32c3_cache_register_types(void)
{
    type_register_static(&esp32c3_cache_info);
}

type_init(esp32c3_cache_register_types)
