/*
 * This file is part of the MicroPython project, http://micropython.org/
 *
 * The MIT License (MIT)
 *
 * SPDX-FileCopyrightText: Copyright (c) 2013, 2014 Damien P. George
 * Copyright (c) 2019 Lucian Copeland for Adafruit Industries
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */
#include "supervisor/internal_flash.h"

#include <stdint.h>
#include <string.h>
#include <stdbool.h>

#include "extmod/vfs.h"
#include "extmod/vfs_fat.h"
#include "py/mphal.h"
#include "py/obj.h"
#include "py/runtime.h"
#include "lib/oofatfs/ff.h"

#include "supervisor/flash.h"
#include "supervisor/usb.h"

#include "csr.h"
#include "irq.h"

enum pin {
    PIN_MOSI = 0,
    PIN_CLK = 1,
    PIN_CS = 2,
    PIN_MISO_EN = 3,
    PIN_MISO = 4, // Value is ignored
};

#define NO_CACHE        0xffffffff

static uint8_t _flash_cache[FLASH_PAGE_SIZE] __attribute__((aligned(4)));
static uint32_t _flash_page_addr = NO_CACHE;
static bool _flash_cache_dirty;
// -------------------------------------------------------------------------
// When performing SPI operations, the flash cannot be accessed.  Since we
// normally execute directly from SPI, this can cause problems.
// To work around this, we execute from RAM.  This is accomplished by marking
// functions as being in the section ".ramtext".
// When building under GCC with -O0 or -Od, the `inline` attribute is ignored.
// Therefore, we must re-implement these functions here and explicitly mark
// them as being in `.ramtext`, even though they really ought to be inlined.
__attribute__((section(".ramtext")))
static inline void spi_writel(uint32_t value, uint32_t addr) {
    *((volatile uint32_t *)addr) = value;
}

__attribute__((section(".ramtext")))
static inline uint32_t spi_readl(uint32_t addr) {
    return *(volatile uint32_t *)addr;
}

__attribute__((section(".ramtext")))
static inline void bb_spi_write(unsigned char value) {
    spi_writel(value, CSR_LXSPI_BITBANG_ADDR);
}

__attribute__((section(".ramtext")))
static inline uint32_t bb_read(void) {
    return spi_readl(CSR_LXSPI_MISO_ADDR);
}

__attribute__((section(".ramtext")))
static inline void bb_spi_en(unsigned int en) {
    spi_writel(en, CSR_LXSPI_BITBANG_EN_ADDR);
}

__attribute__((section(".ramtext")))
static inline void bb_spi_irq_setie(unsigned int ie) {
    if (ie) {
        csrs(mstatus, CSR_MSTATUS_MIE);
    } else {
        csrc(mstatus, CSR_MSTATUS_MIE);
    }
}

__attribute__((section(".ramtext")))
static inline void bb_spi_begin(void) {
    bb_spi_write((0 << PIN_CLK) | (0 << PIN_CS));
}

__attribute__((section(".ramtext")))
static inline void bb_spi_end(void) {
    bb_spi_write((0 << PIN_CLK) | (1 << PIN_CS));
}

__attribute__((section(".ramtext")))
static void spi_single_tx(uint8_t out) {
    int bit;

    for (bit = 7; bit >= 0; bit--) {
        if (out & (1 << bit)) {
            bb_spi_write((0 << PIN_CLK) | (1 << PIN_MOSI));
            bb_spi_write((1 << PIN_CLK) | (1 << PIN_MOSI));
            bb_spi_write((0 << PIN_CLK) | (1 << PIN_MOSI));
        } else {
            bb_spi_write((0 << PIN_CLK) | (0 << PIN_MOSI));
            bb_spi_write((1 << PIN_CLK) | (0 << PIN_MOSI));
            bb_spi_write((0 << PIN_CLK) | (0 << PIN_MOSI));
        }
    }
}

__attribute__((section(".ramtext")))
static uint8_t spi_single_rx(void) {
    int bit = 0;
    uint8_t in = 0;

    bb_spi_write((1 << PIN_MISO_EN) | (0 << PIN_CLK));

    while (bit++ < 8) {
        bb_spi_write((1 << PIN_MISO_EN) | (1 << PIN_CLK));
        in = (in << 1) | bb_read();
        bb_spi_write((1 << PIN_MISO_EN) | (0 << PIN_CLK));
    }

    return in;
}

__attribute__((section(".ramtext")))
static int bb_spi_beginErase4(uint32_t erase_addr) {
    // Enable Write-Enable Latch (WEL)
    bb_spi_begin();
    spi_single_tx(0x06);
    bb_spi_end();

    bb_spi_begin();
    spi_single_tx(0x20);
    spi_single_tx(erase_addr >> 16);
    spi_single_tx(erase_addr >> 8);
    spi_single_tx(erase_addr >> 0);
    bb_spi_end();
    return 0;
}

__attribute__((section(".ramtext")))
static int bb_spi_beginWrite(uint32_t addr, const void *v_data, unsigned int count) {
    const uint8_t write_cmd = 0x02;
    const uint8_t *data = v_data;
    unsigned int i;

    #ifdef NDEBUG
    if (v_data < (const void *)_flash_cache) {
        asm ("ebreak");
    }
    if ((v_data + count) > (const void *)&_flash_cache[4096]) {
        asm ("ebreak");
    }
    #endif

    // Enable Write-Enable Latch (WEL)
    bb_spi_begin();
    spi_single_tx(0x06);
    bb_spi_end();

    bb_spi_begin();
    spi_single_tx(write_cmd);
    spi_single_tx(addr >> 16);
    spi_single_tx(addr >> 8);
    spi_single_tx(addr >> 0);
    for (i = 0; (i < count) && (i < 256); i++) {
        spi_single_tx(*data++);
    }
    bb_spi_end();

    return 0;
}

__attribute__((section(".ramtext")))
static uint8_t spi_read_status(void) {
    uint8_t val;

    bb_spi_begin();
    spi_single_tx(0x05);
    val = spi_single_rx();
    bb_spi_end();
    return val;
}

__attribute__((section(".ramtext")))
static int bb_spi_is_busy(void) {
    return spi_read_status() & (1 << 0);
}

__attribute__((used))
uint32_t page_write_log[128];
__attribute__((used))
uint32_t page_write_log_offset;

__attribute__((section(".ramtext")))
static void bb_spi_write_page(uint32_t flash_address, const uint8_t *data) {
    const uint32_t flash_address_end = flash_address + FLASH_PAGE_SIZE;

    // Ensure we're within the target flash address range.
    if ((flash_address - FLASH_PARTITION_OFFSET_BYTES) > FLASH_SIZE) {
        asm ("ebreak");
        return;
    }
    if (flash_address < FLASH_PARTITION_OFFSET_BYTES) {
        asm ("ebreak");
        return;
    }

    if ((flash_address_end - FLASH_PARTITION_OFFSET_BYTES) > FLASH_SIZE) {
        asm ("ebreak");
        return;
    }
    if (flash_address_end < FLASH_PARTITION_OFFSET_BYTES) {
        asm ("ebreak");
        return;
    }

    // Ensure we're not erasing the middle of a flash bank
    if ((flash_address & 0xfff) != 0) {
        asm ("ebreak");
        return;
    }

    page_write_log[page_write_log_offset++] = flash_address;
    if (page_write_log_offset > sizeof(page_write_log) / sizeof(*page_write_log)) {
        page_write_log_offset = 0;
    }

    while (bb_spi_is_busy()) {
        ; // relax
    }
    bb_spi_beginErase4(flash_address);
    while (bb_spi_is_busy()) {
        ; // relax
    }
    while (flash_address < flash_address_end) {
        bb_spi_beginWrite(flash_address, data, 256);
        while (bb_spi_is_busy()) {
            ; // relax
        }
        flash_address += 256;
        data += 256;
    }
}

static inline uint32_t lba2addr(uint32_t block) {
    return (0x20000000 + FLASH_PARTITION_OFFSET_BYTES) + (block * FILESYSTEM_BLOCK_SIZE);
}

void supervisor_flash_init(void) {
}

uint32_t supervisor_flash_get_block_size(void) {
    return FILESYSTEM_BLOCK_SIZE;
}

uint32_t supervisor_flash_get_block_count(void) {
    return FLASH_SIZE / FILESYSTEM_BLOCK_SIZE;
}

__attribute__((section(".ramtext")))
void port_internal_flash_flush(void) {
    // Skip if data is the same, or if there is no data in the cache
    if (_flash_page_addr == NO_CACHE) {
        return;
    }
    if (!_flash_cache_dirty) {
        return;
    }

    // Disable interrupts and enable bit-bang mode on the SPI flash.
    // This function is running from RAM -- otherwise enabling bitbang mode
    // would crash the CPU as the program suddenly became an endless stream
    // of `0xffffffff`.
    bb_spi_irq_setie(0);
    bb_spi_write((0 << PIN_CLK) | (1 << PIN_CS));
    bb_spi_en(1);

    bb_spi_write_page(_flash_page_addr & 0x00ffffff, (const uint8_t *)_flash_cache);

    bb_spi_en(0);
    bb_spi_irq_setie(1);

    _flash_cache_dirty = false;
}

mp_uint_t supervisor_flash_read_blocks(uint8_t *dest, uint32_t block, uint32_t num_blocks) {
    // Must write out anything in cache before trying to read.
    supervisor_flash_flush();

    uint32_t src = lba2addr(block);
    memcpy(dest, (uint8_t *)src, FILESYSTEM_BLOCK_SIZE * num_blocks);

    #if CIRCUITPY_USB
    usb_background();
    #endif

    return 0;
}

mp_uint_t supervisor_flash_write_blocks(const uint8_t *src, uint32_t lba, uint32_t num_blocks) {
    while (num_blocks) {
        uint32_t const addr = lba2addr(lba);
        uint32_t const page_addr = addr & ~(FLASH_PAGE_SIZE - 1);

        uint32_t count = 8 - (lba % 8); // up to page boundary
        count = MIN(num_blocks, count);

        if (page_addr != _flash_page_addr) {
            // Write out anything in cache before overwriting it.
            supervisor_flash_flush();

            _flash_page_addr = page_addr;
            _flash_cache_dirty = false;

            // Copy the current contents of the entire page into the cache.
            memcpy(_flash_cache, (void *)page_addr, FLASH_PAGE_SIZE);
        }

        // Overwrite part or all of the page cache with the src data, but only if it's changed.
        if (_flash_cache_dirty || memcmp(_flash_cache + (addr & (FLASH_PAGE_SIZE - 1)), src, count * FILESYSTEM_BLOCK_SIZE)) {
            memcpy(_flash_cache + (addr & (FLASH_PAGE_SIZE - 1)), src, count * FILESYSTEM_BLOCK_SIZE);
            _flash_cache_dirty = true;
        }

        // adjust for next run
        lba += count;
        src += count * FILESYSTEM_BLOCK_SIZE;
        num_blocks -= count;

        #if CIRCUITPY_USB
        usb_background();
        #endif
    }

    return 0; // success
}

void supervisor_flash_release_cache(void) {
}
