/*
    Copyright (c) 2023 Raspberry Pi (Trading) Ltd.

    SPDX-License-Identifier: BSD-3-Clause
*/

#if defined(ARDUINO_RASPBERRY_PI_PICO_W)
#include <btstack.h>
#include <pico/btstack_flash_bank.h>
#include <hardware/flash.h>
#include <hardware/sync.h>
#include <string.h>
#include <Arduino.h>

const uint8_t __bluetooth_tlv[8192] __attribute__((aligned(4096))) = { 0 };
extern const uint8_t __flash_binary_start;

#undef PICO_FLASH_BANK_TOTAL_SIZE
#undef PICO_FLASH_BANK_STORAGE_OFFSET
#define PICO_FLASH_BANK_TOTAL_SIZE sizeof(__bluetooth_tlv)
#define PICO_FLASH_BANK_STORAGE_OFFSET ((unsigned int)(__bluetooth_tlv - &__flash_binary_start))

#if 0
// Check sizes
static_assert(PICO_FLASH_BANK_TOTAL_SIZE % (FLASH_SECTOR_SIZE * 2) == 0, "PICO_FLASH_BANK_TOTAL_SIZE invalid");
static_assert(PICO_FLASH_BANK_TOTAL_SIZE <= PICO_FLASH_SIZE_BYTES, "PICO_FLASH_BANK_TOTAL_SIZE too big");
static_assert(PICO_FLASH_BANK_STORAGE_OFFSET + PICO_FLASH_BANK_TOTAL_SIZE <= PICO_FLASH_SIZE_BYTES, "PICO_FLASH_BANK_TOTAL_SIZE too big");
#endif

// Size of one bank
#define PICO_FLASH_BANK_SIZE (PICO_FLASH_BANK_TOTAL_SIZE / 2)

#if 0
#define DEBUG_PRINT(format,args...) printf(format, ## args)
#else
#define DEBUG_PRINT(...)
#endif

static uint32_t pico_flash_bank_get_size(void * context) {
    (void)(context);
    return PICO_FLASH_BANK_SIZE;
}

static uint32_t pico_flash_bank_get_alignment(void * context) {
    (void)(context);
    return 1;
}

static void pico_flash_bank_erase(void * context, int bank) {
    (void)(context);
    DEBUG_PRINT("erase: bank %d\n", bank);
    noInterrupts();
    rp2040.idleOtherCore();
    flash_range_erase(PICO_FLASH_BANK_STORAGE_OFFSET + (PICO_FLASH_BANK_SIZE * bank), PICO_FLASH_BANK_SIZE);
    rp2040.resumeOtherCore();
    interrupts();
}

static void pico_flash_bank_read(void *context, int bank, uint32_t offset, uint8_t *buffer, uint32_t size) {
    (void)(context);
    DEBUG_PRINT("read: bank %d offset %u size %u\n", bank, offset, size);

    assert(bank <= 1);
    if (bank > 1) {
        return;
    }

    assert(offset < PICO_FLASH_BANK_SIZE);
    if (offset >= PICO_FLASH_BANK_SIZE) {
        return;
    }

    assert((offset + size) <= PICO_FLASH_BANK_SIZE);
    if ((offset + size) > PICO_FLASH_BANK_SIZE) {
        return;
    }

    // Flash is xip
    memcpy(buffer, (void *)(XIP_BASE + PICO_FLASH_BANK_STORAGE_OFFSET + (PICO_FLASH_BANK_SIZE * bank) + offset), size);
}

static void pico_flash_bank_write(void * context, int bank, uint32_t offset, const uint8_t *data, uint32_t size) {
    (void)(context);
    DEBUG_PRINT("write: bank %d offset %u size %u\n", bank, offset, size);

    assert(bank <= 1);
    if (bank > 1) {
        return;
    }

    assert(offset < PICO_FLASH_BANK_SIZE);
    if (offset >= PICO_FLASH_BANK_SIZE) {
        return;
    }

    assert((offset + size) <= PICO_FLASH_BANK_SIZE);
    if ((offset + size) > PICO_FLASH_BANK_SIZE) {
        return;
    }

    if (size == 0) {
        return;
    }

    // calc bank start position
    const uint32_t bank_start_pos = PICO_FLASH_BANK_STORAGE_OFFSET + (PICO_FLASH_BANK_SIZE * bank);

    // Calculate first and last page in the bank
    const uint32_t first_page = offset / FLASH_PAGE_SIZE;
    const uint32_t last_page = (offset + size + FLASH_PAGE_SIZE - 1) / FLASH_PAGE_SIZE;

    // Now we only care about the offset in the first page
    offset %= FLASH_PAGE_SIZE;

    // Amount of data we've copied
    uint32_t data_pos = 0;
    uint32_t size_left = size;

    // Write all the pages required
    for (uint32_t page = first_page; page < last_page; page++) {
        uint8_t page_data[FLASH_PAGE_SIZE];

        assert(data_pos < size && size_left <= size);

        // Copy data we're not going to overwrite in the first page
        if (page == first_page && offset > 0) {
            memcpy(page_data,
                   (void *)(XIP_BASE + bank_start_pos + (page * FLASH_PAGE_SIZE)),
                   offset);
        }

        // Copy the data we're not going to overwrite in the last page
        if (page == last_page - 1 && (offset + size_left) < FLASH_PAGE_SIZE) {
            memcpy(page_data + offset + size_left,
                   (void *)(XIP_BASE + bank_start_pos + (page * FLASH_PAGE_SIZE) + offset + size_left),
                   FLASH_PAGE_SIZE - offset - size_left);
        }

        // Now copy the new data into the page
        const uint32_t size_to_copy = MIN(size_left, FLASH_PAGE_SIZE - offset);
        memcpy(page_data + offset, data + data_pos, size_to_copy);

        data_pos += size_to_copy;
        size_left -= size_to_copy;

        // zero offset for the following pages
        offset = 0;

        // Now program the entire page
        noInterrupts();
        rp2040.idleOtherCore();
        flash_range_program(bank_start_pos + (page * FLASH_PAGE_SIZE), page_data, FLASH_PAGE_SIZE);
        rp2040.resumeOtherCore();
        interrupts();
    }
}

static const hal_flash_bank_t pico_flash_bank_instance_obj = {
    /* uint32_t (*get_size)(..) */ 		 &pico_flash_bank_get_size,
    /* uint32_t (*get_alignment)(..); */ &pico_flash_bank_get_alignment,
    /* void (*erase)(..);    */ 		 &pico_flash_bank_erase,
    /* void (*read)(..);      */ 		 &pico_flash_bank_read,
    /* void (*write)(..);     */ 		 &pico_flash_bank_write,
};

extern "C" {
    const hal_flash_bank_t *pico_flash_bank_instance(void) {

#ifndef NDEBUG
        // Check we're not overlapping the binary in flash
        //extern char __flash_binary_end;
        //    assert((uintptr_t)&__flash_binary_end - XIP_BASE <= PICO_FLASH_BANK_STORAGE_OFFSET);
#endif

        return &pico_flash_bank_instance_obj;
    }
}
#endif
