/*
 * This file is part of the MicroPython project, http://micropython.org/
 *
 * The MIT License (MIT)
 *
 * Copyright (c) 2019 Adafruit Industries
 * Copyright (c) 2023 Robert Hammelrath
 *
 * 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.
 *
 * Port of the Adafruit QSPIflash driver for SAMD devices
 *
 */

#include <stdint.h>
#include <string.h>
#include "py/obj.h"
#include "py/runtime.h"
#include "py/mphal.h"
#include "py/mperrno.h"
#include "modmachine.h"
#include "extmod/modmachine.h"
#include "extmod/vfs.h"
#include "pin_af.h"
#include "clock_config.h"
#include "sam.h"

#ifdef MICROPY_HW_QSPIFLASH

#include "drivers/memory/external_flash_device.h"

// QSPI command codes
enum
{
    QSPI_CMD_READ              = 0x03,
    QSPI_CMD_READ_4B           = 0x13,
    QSPI_CMD_QUAD_READ         = 0x6B,// 1 line address, 4 line data

    QSPI_CMD_READ_JEDEC_ID     = 0x9f,

    QSPI_CMD_PAGE_PROGRAM      = 0x02,
    QSPI_CMD_PAGE_PROGRAM_4B   = 0x12,
    QSPI_CMD_QUAD_PAGE_PROGRAM = 0x32, // 1 line address, 4 line data

    QSPI_CMD_READ_STATUS       = 0x05,
    QSPI_CMD_READ_STATUS2      = 0x35,

    QSPI_CMD_WRITE_STATUS      = 0x01,
    QSPI_CMD_WRITE_STATUS2     = 0x31,

    QSPI_CMD_ENABLE_RESET      = 0x66,
    QSPI_CMD_RESET             = 0x99,

    QSPI_CMD_WRITE_ENABLE      = 0x06,
    QSPI_CMD_WRITE_DISABLE     = 0x04,

    QSPI_CMD_ERASE_SECTOR      = 0x20,
    QSPI_CMD_ERASE_SECTOR_4B   = 0x21,
    QSPI_CMD_ERASE_BLOCK       = 0xD8,
    QSPI_CMD_ERASE_CHIP        = 0xC7,

    QSPI_CMD_READ_SFDP_PARAMETER = 0x5A,
};

// QSPI flash pins are: CS=PB11, SCK=PB10, IO0-IO3=PA08, PA09, PA10 and PA11.
#define PIN_CS  (43)
#define PIN_SCK (42)
#define PIN_IO0 (8)
#define PIN_IO1 (9)
#define PIN_IO2 (10)
#define PIN_IO3 (11)

#define PAGE_SIZE (256)
#define SECTOR_SIZE (4096)

typedef struct _samd_qspiflash_obj_t {
    mp_obj_base_t base;
    uint16_t pagesize;
    uint16_t sectorsize;
    uint32_t size;
    uint8_t phase;
    uint8_t polarity;
} samd_qspiflash_obj_t;

/// List of all possible flash devices used by Adafruit boards
static const external_flash_device possible_devices[] = {
    MICROPY_HW_QSPIFLASH
};

#define EXTERNAL_FLASH_DEVICE_COUNT MP_ARRAY_SIZE(possible_devices)
static external_flash_device const *flash_device;
static external_flash_device generic_config = GENERIC;
extern const mp_obj_type_t samd_qspiflash_type;

// The QSPIflash object is a singleton
static samd_qspiflash_obj_t qspiflash_obj = { { &samd_qspiflash_type } };

// Turn off cache and invalidate all data in it.
static void samd_peripherals_disable_and_clear_cache(void) {
    CMCC->CTRL.bit.CEN = 0;
    while (CMCC->SR.bit.CSTS) {
    }
    CMCC->MAINT0.bit.INVALL = 1;
}

// Enable cache
static void samd_peripherals_enable_cache(void) {
    CMCC->CTRL.bit.CEN = 1;
}

// Run a single QSPI instruction.
// Parameters are:
// - command instruction code
// - iframe iframe register value (configured by caller according to command code)
// - addr the address to read or write from. If the instruction doesn't require an address, this parameter is meaningless.
// - buffer pointer to the data to be written or stored depending on the type is Read or Write
// - size the number of bytes to read or write.
bool run_instruction(uint8_t command, uint32_t iframe, uint32_t addr, uint8_t *buffer, uint32_t size) {

    samd_peripherals_disable_and_clear_cache();

    uint8_t *qspi_mem = (uint8_t *)QSPI_AHB;
    if (addr) {
        qspi_mem += addr;
    }

    QSPI->INSTRCTRL.bit.INSTR = command;
    QSPI->INSTRADDR.reg = addr;
    QSPI->INSTRFRAME.reg = iframe;

    // Dummy read of INSTRFRAME needed to synchronize.
    // See Instruction Transmission Flow Diagram, figure 37.9, page 995
    // and Example 4, page 998, section 37.6.8.5.
    (volatile uint32_t)QSPI->INSTRFRAME.reg;

    if (buffer && size) {
        uint32_t const tfr_type = iframe & QSPI_INSTRFRAME_TFRTYPE_Msk;
        if ((tfr_type == QSPI_INSTRFRAME_TFRTYPE_READ) || (tfr_type == QSPI_INSTRFRAME_TFRTYPE_READMEMORY)) {
            memcpy(buffer, qspi_mem, size);
        } else {
            memcpy(qspi_mem, buffer, size);
        }
    }

    __asm volatile ("dsb");
    __asm volatile ("isb");

    QSPI->CTRLA.reg = QSPI_CTRLA_ENABLE | QSPI_CTRLA_LASTXFER;
    while (!QSPI->INTFLAG.bit.INSTREND) {
    }
    QSPI->INTFLAG.reg = QSPI_INTFLAG_INSTREND;

    samd_peripherals_enable_cache();
    return true;
}

bool run_command(uint8_t command) {
    uint32_t iframe = QSPI_INSTRFRAME_WIDTH_SINGLE_BIT_SPI | QSPI_INSTRFRAME_ADDRLEN_24BITS |
        QSPI_INSTRFRAME_TFRTYPE_READ | QSPI_INSTRFRAME_INSTREN;
    return run_instruction(command, iframe, 0, NULL, 0);
}

bool read_command(uint8_t command, uint8_t *response, uint32_t len) {
    uint32_t iframe = QSPI_INSTRFRAME_WIDTH_SINGLE_BIT_SPI | QSPI_INSTRFRAME_ADDRLEN_24BITS |
        QSPI_INSTRFRAME_TFRTYPE_READ | QSPI_INSTRFRAME_INSTREN | QSPI_INSTRFRAME_DATAEN;
    return run_instruction(command, iframe, 0, response, len);
}

bool read_memory_single(uint8_t command, uint32_t addr, uint8_t *response, uint32_t len) {
    uint32_t iframe = QSPI_INSTRFRAME_WIDTH_SINGLE_BIT_SPI | QSPI_INSTRFRAME_ADDRLEN_24BITS |
        QSPI_INSTRFRAME_TFRTYPE_READ | QSPI_INSTRFRAME_INSTREN | QSPI_INSTRFRAME_ADDREN |
        QSPI_INSTRFRAME_DATAEN | QSPI_INSTRFRAME_DUMMYLEN(8);
    return run_instruction(command, iframe, addr, response, len);
}

bool write_command(uint8_t command, uint8_t const *data, uint32_t len) {
    uint32_t iframe = QSPI_INSTRFRAME_WIDTH_SINGLE_BIT_SPI | QSPI_INSTRFRAME_ADDRLEN_24BITS |
        QSPI_INSTRFRAME_TFRTYPE_WRITE | QSPI_INSTRFRAME_INSTREN | (data != NULL ? QSPI_INSTRFRAME_DATAEN : 0);
    return run_instruction(command, iframe, 0, (uint8_t *)data, len);
}

bool erase_command(uint8_t command, uint32_t address) {
    // Sector Erase
    uint32_t iframe = QSPI_INSTRFRAME_WIDTH_SINGLE_BIT_SPI | QSPI_INSTRFRAME_ADDRLEN_24BITS |
        QSPI_INSTRFRAME_TFRTYPE_WRITE | QSPI_INSTRFRAME_INSTREN | QSPI_INSTRFRAME_ADDREN;
    return run_instruction(command, iframe, address, NULL, 0);
}

bool read_memory_quad(uint8_t command, uint32_t addr, uint8_t *data, uint32_t len) {
    uint32_t iframe = QSPI_INSTRFRAME_WIDTH_QUAD_OUTPUT | QSPI_INSTRFRAME_ADDRLEN_24BITS |
        QSPI_INSTRFRAME_TFRTYPE_READMEMORY | QSPI_INSTRFRAME_INSTREN | QSPI_INSTRFRAME_ADDREN | QSPI_INSTRFRAME_DATAEN |
        /*QSPI_INSTRFRAME_CRMODE |*/ QSPI_INSTRFRAME_DUMMYLEN(8);
    return run_instruction(command, iframe, addr, data, len);
}

bool write_memory_quad(uint8_t command, uint32_t addr, uint8_t *data, uint32_t len) {
    uint32_t iframe = QSPI_INSTRFRAME_WIDTH_QUAD_OUTPUT | QSPI_INSTRFRAME_ADDRLEN_24BITS |
        QSPI_INSTRFRAME_TFRTYPE_WRITEMEMORY | QSPI_INSTRFRAME_INSTREN | QSPI_INSTRFRAME_ADDREN | QSPI_INSTRFRAME_DATAEN;
    return run_instruction(command, iframe, addr, data, len);
}

static uint8_t read_status(void) {
    uint8_t r;
    read_command(QSPI_CMD_READ_STATUS, &r, 1);
    return r;
}

static uint8_t read_status2(void) {
    uint8_t r;
    read_command(QSPI_CMD_READ_STATUS2, &r, 1);
    return r;
}

static bool write_enable(void) {
    return run_command(QSPI_CMD_WRITE_ENABLE);
}

static void wait_for_flash_ready(void) {
    // both WIP and WREN bit should be clear
    while (read_status() & 0x03) {
    }
}

static uint8_t get_baud(int32_t freq_mhz) {
    int baud = get_peripheral_freq() / (freq_mhz * 1000000) - 1;
    if (baud < 1) {
        baud = 1;
    }
    if (baud > 255) {
        baud = 255;
    }
    return baud;
}

int get_sfdp_table(uint8_t *table, int maxlen) {
    uint8_t header[16];
    read_memory_single(QSPI_CMD_READ_SFDP_PARAMETER, 0, header, sizeof(header));
    int len = MIN(header[11] * 4, maxlen);
    int addr = header[12] + (header[13] << 8) + (header[14] << 16);
    read_memory_single(QSPI_CMD_READ_SFDP_PARAMETER, addr, table, len);
    return len;
}

STATIC mp_obj_t samd_qspiflash_make_new(const mp_obj_type_t *type, size_t n_args, size_t n_kw, const mp_obj_t *all_args) {
    mp_arg_check_num(n_args, n_kw, 0, 0, false);

    // The QSPI is a singleton
    samd_qspiflash_obj_t *self = &qspiflash_obj;
    self->phase = 0;
    self->polarity = 0;
    self->pagesize = PAGE_SIZE;
    self->sectorsize = SECTOR_SIZE;

    // Enable the device clock
    MCLK->AHBMASK.reg |= MCLK_AHBMASK_QSPI;
    MCLK->AHBMASK.reg |= MCLK_AHBMASK_QSPI_2X;
    MCLK->APBCMASK.reg |= MCLK_APBCMASK_QSPI;

    // Configure the pins.
    mp_hal_set_pin_mux(PIN_CS, ALT_FCT_QSPI);
    mp_hal_set_pin_mux(PIN_SCK, ALT_FCT_QSPI);
    mp_hal_set_pin_mux(PIN_IO0, ALT_FCT_QSPI);
    mp_hal_set_pin_mux(PIN_IO1, ALT_FCT_QSPI);
    mp_hal_set_pin_mux(PIN_IO2, ALT_FCT_QSPI);
    mp_hal_set_pin_mux(PIN_IO3, ALT_FCT_QSPI);

    // Configure the QSPI interface
    QSPI->CTRLA.bit.SWRST = 1;
    mp_hal_delay_us(1000);  // Maybe not required.

    QSPI->CTRLB.reg = QSPI_CTRLB_MODE_MEMORY |
        QSPI_CTRLB_CSMODE_NORELOAD |
        QSPI_CTRLB_DATALEN_8BITS |
        QSPI_CTRLB_CSMODE_LASTXFER;
    // start with low 4Mhz, Mode 0
    QSPI->BAUD.reg = QSPI_BAUD_BAUD(get_baud(4)) |
        (self->phase << QSPI_BAUD_CPHA_Pos) |
        (self->polarity << QSPI_BAUD_CPOL_Pos);
    QSPI->CTRLA.bit.ENABLE = 1;

    uint8_t jedec_ids[3];
    read_command(QSPI_CMD_READ_JEDEC_ID, jedec_ids, sizeof(jedec_ids));

    // Read the common sfdp table
    // Check the device addr length, support of 1-1-4 mode and get the sector size
    uint8_t sfdp_table[128];
    int len = get_sfdp_table(sfdp_table, sizeof(sfdp_table));
    if (len >= 29) {
        self->sectorsize = 1 << sfdp_table[28];
        bool addr4b = ((sfdp_table[2] >> 1) & 0x03) == 0x02;
        bool supports_qspi_114 = (sfdp_table[2] & 0x40) != 0;
        if (addr4b || !supports_qspi_114) {
            mp_raise_ValueError(MP_ERROR_TEXT("QSPI mode not supported"));
        }
    }

    // Check, if the flash device is known and get it's properties.
    flash_device = NULL;
    for (uint8_t i = 0; i < EXTERNAL_FLASH_DEVICE_COUNT; i++) {
        const external_flash_device *possible_device = &possible_devices[i];
        if (jedec_ids[0] == possible_device->manufacturer_id &&
            jedec_ids[1] == possible_device->memory_type &&
            jedec_ids[2] == possible_device->capacity) {
            flash_device = possible_device;
            break;
        }
    }

    // If the flash device is not known, try generic config options
    if (flash_device == NULL) {
        if (jedec_ids[0] == 0xc2) { // Macronix devices
            generic_config.quad_enable_bit_mask = 0x04;
            generic_config.single_status_byte = true;
        }
        generic_config.total_size = 1 << jedec_ids[2];
        flash_device = &generic_config;
    }

    self->size = flash_device->total_size;

    // The write in progress bit should be low.
    while (read_status() & 0x01) {
    }
    // The suspended write/erase bit should be low.
    while (read_status2() & 0x80) {
    }
    run_command(QSPI_CMD_ENABLE_RESET);
    run_command(QSPI_CMD_RESET);
    // Wait 30us for the reset
    mp_hal_delay_us(30);
    // Speed up the frequency
    QSPI->BAUD.bit.BAUD = get_baud(flash_device->max_clock_speed_mhz);

    // Enable Quad Mode if available
    uint8_t status = 0;
    if (flash_device->quad_enable_bit_mask) {
        // Verify that QSPI mode is enabled.
        status = flash_device->single_status_byte ? read_status() : read_status2();
    }

    // Check the quad enable bit.
    if ((status & flash_device->quad_enable_bit_mask) == 0) {
        write_enable();
        uint8_t full_status[2] = {0x00, flash_device->quad_enable_bit_mask};

        if (flash_device->write_status_register_split) {
            write_command(QSPI_CMD_WRITE_STATUS2, full_status + 1, 1);
        } else if (flash_device->single_status_byte) {
            write_command(QSPI_CMD_WRITE_STATUS, full_status + 1, 1);
        } else {
            write_command(QSPI_CMD_WRITE_STATUS, full_status, 2);
        }
    }
    // Turn off writes in case this is a microcontroller only reset.
    run_command(QSPI_CMD_WRITE_DISABLE);
    wait_for_flash_ready();

    return self;
}

STATIC mp_obj_t samd_qspiflash_read(samd_qspiflash_obj_t *self, uint32_t addr, uint8_t *dest, uint32_t len) {
    if (len > 0) {
        wait_for_flash_ready();
        // Command 0x6B 1 line address, 4 line Data
        // with Continuous Read Mode and Quad output mode, read memory type
        read_memory_quad(QSPI_CMD_QUAD_READ, addr, dest, len);
    }

    return mp_const_none;
}

STATIC mp_obj_t samd_qspiflash_write(samd_qspiflash_obj_t *self, uint32_t addr, uint8_t *src, uint32_t len) {
    uint32_t length = len;
    uint32_t pos = 0;
    uint8_t *buf = src;

    while (pos < length) {
        uint16_t maxsize = self->pagesize - pos % self->pagesize;
        uint16_t size = (length - pos) > maxsize ? maxsize : length - pos;

        wait_for_flash_ready();
        write_enable();
        write_memory_quad(QSPI_CMD_QUAD_PAGE_PROGRAM, addr, buf + pos, size);

        addr += size;
        pos += size;
    }

    return mp_const_none;
}

STATIC mp_obj_t samd_qspiflash_erase(uint32_t addr) {
    wait_for_flash_ready();
    write_enable();
    erase_command(QSPI_CMD_ERASE_SECTOR, addr);

    return mp_const_none;
}

STATIC mp_obj_t samd_qspiflash_readblocks(size_t n_args, const mp_obj_t *args) {
    samd_qspiflash_obj_t *self = MP_OBJ_TO_PTR(args[0]);
    uint32_t offset = (mp_obj_get_int(args[1]) * self->sectorsize);
    mp_buffer_info_t bufinfo;
    mp_get_buffer_raise(args[2], &bufinfo, MP_BUFFER_WRITE);
    if (n_args == 4) {
        offset += mp_obj_get_int(args[3]);
    }

    // Read data to flash (adf4 API)
    samd_qspiflash_read(self, offset, bufinfo.buf, bufinfo.len);

    return mp_const_none;
}
STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(samd_qspiflash_readblocks_obj, 3, 4, samd_qspiflash_readblocks);

STATIC mp_obj_t samd_qspiflash_writeblocks(size_t n_args, const mp_obj_t *args) {
    samd_qspiflash_obj_t *self = MP_OBJ_TO_PTR(args[0]);
    uint32_t offset = (mp_obj_get_int(args[1]) * self->sectorsize);
    mp_buffer_info_t bufinfo;
    mp_get_buffer_raise(args[2], &bufinfo, MP_BUFFER_READ);
    if (n_args == 3) {
        samd_qspiflash_erase(offset);
        // TODO check return value
    } else {
        offset += mp_obj_get_int(args[3]);
    }
    // Write data to flash (adf4 API)
    samd_qspiflash_write(self,  offset, bufinfo.buf, bufinfo.len);
    // TODO check return value
    return mp_const_none;
}
STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(samd_qspiflash_writeblocks_obj, 3, 4, samd_qspiflash_writeblocks);

STATIC mp_obj_t samd_qspiflash_ioctl(mp_obj_t self_in, mp_obj_t cmd_in, mp_obj_t arg_in) {
    samd_qspiflash_obj_t *self = MP_OBJ_TO_PTR(self_in);
    mp_int_t cmd = mp_obj_get_int(cmd_in);

    switch (cmd) {
        case MP_BLOCKDEV_IOCTL_INIT:
            return MP_OBJ_NEW_SMALL_INT(0);
        case MP_BLOCKDEV_IOCTL_DEINIT:
            return MP_OBJ_NEW_SMALL_INT(0);
        case MP_BLOCKDEV_IOCTL_SYNC:
            return MP_OBJ_NEW_SMALL_INT(0);
        case MP_BLOCKDEV_IOCTL_BLOCK_COUNT:
            return MP_OBJ_NEW_SMALL_INT(self->size / self->sectorsize);
        case MP_BLOCKDEV_IOCTL_BLOCK_SIZE:
            return MP_OBJ_NEW_SMALL_INT(self->sectorsize);
        case MP_BLOCKDEV_IOCTL_BLOCK_ERASE: {
            samd_qspiflash_erase(mp_obj_get_int(arg_in) * self->sectorsize);
            // TODO check return value
            return MP_OBJ_NEW_SMALL_INT(0);
        }
        default:
            return mp_const_none;
    }
}
STATIC MP_DEFINE_CONST_FUN_OBJ_3(samd_qspiflash_ioctl_obj, samd_qspiflash_ioctl);

STATIC const mp_rom_map_elem_t samd_qspiflash_locals_dict_table[] = {
    { MP_ROM_QSTR(MP_QSTR_readblocks), MP_ROM_PTR(&samd_qspiflash_readblocks_obj) },
    { MP_ROM_QSTR(MP_QSTR_writeblocks), MP_ROM_PTR(&samd_qspiflash_writeblocks_obj) },
    { MP_ROM_QSTR(MP_QSTR_ioctl), MP_ROM_PTR(&samd_qspiflash_ioctl_obj) },
};
STATIC MP_DEFINE_CONST_DICT(samd_qspiflash_locals_dict, samd_qspiflash_locals_dict_table);

MP_DEFINE_CONST_OBJ_TYPE(
    samd_qspiflash_type,
    MP_QSTR_Flash,
    MP_TYPE_FLAG_NONE,
    make_new, samd_qspiflash_make_new,
    locals_dict, &samd_qspiflash_locals_dict
    );

#endif // MICROPY_HW_QSPI_FLASH
