/*
 * This file is part of the MicroPython project, http://micropython.org/
 *
 * The MIT License (MIT)
 *
 * Copyright (c) 2019 Nicko van Someren
 *
 * 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 <string.h>

#include "py/runtime.h"
#include "py/mphal.h"
#include "py/mperrno.h"
#include "extmod/modmachine.h"
#include "extmod/vfs_fat.h"

#if MICROPY_PY_MACHINE_SDCARD

#if SOC_SDMMC_HOST_SUPPORTED
#include "driver/sdmmc_host.h"
#if SOC_SDMMC_IO_POWER_EXTERNAL
#include "sd_pwr_ctrl_by_on_chip_ldo.h"
#endif
#endif
#include "driver/sdspi_host.h"
#include "sdmmc_cmd.h"
#include "esp_log.h"

#include "machine_sdcard.h"

#define DEBUG 0
#if DEBUG
#define DEBUG_printf(...) ESP_LOGI("modsdcard", __VA_ARGS__)
#else
#define DEBUG_printf(...) (void)0
#endif

//
// There are three layers of abstraction: host, slot and card.
// Creating an SD Card object will initialise the host and slot.
// Cards gets initialised by ioctl op==1 and de-inited by ioctl 2
// Hosts are de-inited in __del__. Slots do not need de-initing.
//

typedef struct _sdcard_obj_t {
    mp_obj_base_t base;
    mp_int_t flags;
    sdmmc_host_t host;
    // The card structure duplicates the host. It's not clear if we
    // can avoid this given the way that it is copied.
    sdmmc_card_t card;
} sdcard_card_obj_t;

#define SDCARD_CARD_FLAGS_HOST_INIT_DONE 0x01
#define SDCARD_CARD_FLAGS_CARD_INIT_DONE 0x02

#define _SECTOR_SIZE(self) (self->card.csd.sector_size)

// Number SPI buses available for firmware app (including for SD)
#define NUM_SD_SPI_BUS (SOC_SPI_PERIPH_NUM - 1)

#if CONFIG_IDF_TARGET_ESP32
#define SD_SLOT_MIN 1
#elif SOC_SDMMC_HOST_SUPPORTED
#define SD_SLOT_MIN 0
#else
#define SD_SLOT_MIN 2
#endif
#define SD_SLOT_MAX (NUM_SD_SPI_BUS + 1) // Inclusive

// SPI bus default bus and device configuration.

static const spi_bus_config_t spi_bus_defaults[NUM_SD_SPI_BUS] = {
    {
        // Primary SPI SD bus (slot 2): board-configurable via machine_sdcard.h.
        .miso_io_num = MICROPY_HW_SDCARD_SPI_MISO,
        .mosi_io_num = MICROPY_HW_SDCARD_SPI_MOSI,
        .sclk_io_num = MICROPY_HW_SDCARD_SPI_SCK,
        .data2_io_num = GPIO_NUM_NC,
        .data3_io_num = GPIO_NUM_NC,
        .data4_io_num = GPIO_NUM_NC,
        .data5_io_num = GPIO_NUM_NC,
        .data6_io_num = GPIO_NUM_NC,
        .data7_io_num = GPIO_NUM_NC,
        .max_transfer_sz = 4000,
        .flags = SPICOMMON_BUSFLAG_MASTER | SPICOMMON_BUSFLAG_SCLK | SPICOMMON_BUSFLAG_MISO | SPICOMMON_BUSFLAG_MOSI,
        .intr_flags = 0,
    },
    #if NUM_SD_SPI_BUS > 1
    {
        .miso_io_num = GPIO_NUM_2,
        .mosi_io_num = GPIO_NUM_15,
        .sclk_io_num = GPIO_NUM_14,
        .data2_io_num = GPIO_NUM_NC,
        .data3_io_num = GPIO_NUM_NC,
        .data4_io_num = GPIO_NUM_NC,
        .data5_io_num = GPIO_NUM_NC,
        .data6_io_num = GPIO_NUM_NC,
        .data7_io_num = GPIO_NUM_NC,
        .max_transfer_sz = 4000,
        .flags = SPICOMMON_BUSFLAG_MASTER | SPICOMMON_BUSFLAG_SCLK | SPICOMMON_BUSFLAG_MISO | SPICOMMON_BUSFLAG_MOSI,
        .intr_flags = 0,
    },
    #endif
};

#if CONFIG_IDF_TARGET_ESP32
static const uint8_t spi_dma_channel_defaults[NUM_SD_SPI_BUS] = {
    2,
    1,
};
#endif

static const sdspi_device_config_t spi_dev_defaults[NUM_SD_SPI_BUS] = {
    #if NUM_SD_SPI_BUS > 1
    {
        // Primary SPI SD bus (slot 2): CS is board-configurable via
        // machine_sdcard.h; host_id stays chip-specific.
        #if CONFIG_IDF_TARGET_ESP32
        .host_id = VSPI_HOST,
        #else
        .host_id = SPI3_HOST,
        #endif
        .gpio_cs = MICROPY_HW_SDCARD_SPI_CS,
        .gpio_cd = SDSPI_SLOT_NO_CD,
        .gpio_wp = SDSPI_SLOT_NO_WP,
        .gpio_int = SDSPI_SLOT_NO_INT,
    },
    #endif
    SDSPI_DEVICE_CONFIG_DEFAULT(), // HSPI (ESP32) / SPI2 (ESP32S3)
};

#define SET_CONFIG_PIN(config, pin_var, arg_id) \
    if (arg_vals[arg_id].u_obj != mp_const_none) \
    config.pin_var = machine_pin_get_id(arg_vals[arg_id].u_obj)

static esp_err_t sdcard_ensure_card_init(sdcard_card_obj_t *self, bool force) {
    if (force || !(self->flags & SDCARD_CARD_FLAGS_CARD_INIT_DONE)) {
        DEBUG_printf("  Calling card init");

        esp_err_t err = sdmmc_card_init(&(self->host), &(self->card));
        if (err == ESP_OK) {
            // Ensure card's host structure has the correct slot after init
            self->card.host.slot = self->host.slot;
            self->flags |= SDCARD_CARD_FLAGS_CARD_INIT_DONE;
        } else {
            self->flags &= ~SDCARD_CARD_FLAGS_CARD_INIT_DONE;
        }
        DEBUG_printf("  Card init returned: %i", err);

        return err;
    } else {
        return ESP_OK;
    }
}

/******************************************************************************/
// MicroPython bindings
//
// Expose the SD card or MMC as an object with the block protocol.

// Create a new SDCard object
//
// SD/MMC or SPI mode is determined by the slot argument
// 0,1 is SD/MMC mode where supported.
// 2,3 is SPI mode where supported (1-bit only)
//
// Original ESP32 can't use 0
// ESP32-C3/C6/etc can only use 2 (only one SPI bus, no SD/MMC controller)
//
// Consult machine.SDCard docs for more details.

static mp_obj_t machine_sdcard_make_new(const mp_obj_type_t *type, size_t n_args, size_t n_kw, const mp_obj_t *args) {
    // check arguments
    enum {
        ARG_slot,
        ARG_width,
        ARG_cd,
        ARG_wp,
        ARG_miso,
        ARG_mosi,
        ARG_sck,
        ARG_cs,
        #if SOC_SDMMC_USE_GPIO_MATRIX
        ARG_cmd,
        ARG_data,
        #endif
        #if SOC_SDMMC_IO_POWER_EXTERNAL
        ARG_ldo,
        #endif
        ARG_freq,
    };
    static const mp_arg_t allowed_args[] = {
        #if SOC_SDMMC_HOST_SUPPORTED
        { MP_QSTR_slot,     MP_ARG_KW_ONLY | MP_ARG_INT, {.u_int = MICROPY_HW_SDMMC_DEFAULT_SLOT} },
        { MP_QSTR_width,    MP_ARG_KW_ONLY | MP_ARG_INT, {.u_int = MICROPY_HW_SDMMC_DEFAULT_WIDTH} },
        #else
        { MP_QSTR_slot,     MP_ARG_KW_ONLY | MP_ARG_INT, {.u_int = SD_SLOT_MAX} },
        { MP_QSTR_width,    MP_ARG_KW_ONLY | MP_ARG_INT, {.u_int = 1} },
        #endif
        { MP_QSTR_cd,       MP_ARG_KW_ONLY | MP_ARG_OBJ, {.u_obj = mp_const_none} },
        { MP_QSTR_wp,       MP_ARG_KW_ONLY | MP_ARG_OBJ, {.u_obj = mp_const_none} },
        // These are only needed if using SPI mode
        { MP_QSTR_miso,     MP_ARG_KW_ONLY | MP_ARG_OBJ, {.u_obj = mp_const_none} },
        { MP_QSTR_mosi,     MP_ARG_KW_ONLY | MP_ARG_OBJ, {.u_obj = mp_const_none} },
        { MP_QSTR_sck,      MP_ARG_KW_ONLY | MP_ARG_OBJ, {.u_obj = mp_const_none} },
        { MP_QSTR_cs,       MP_ARG_KW_ONLY | MP_ARG_OBJ, {.u_obj = mp_const_none} },
        // Optional assignment of SDMMC interface pins, if host supports this
        #if SOC_SDMMC_USE_GPIO_MATRIX
        { MP_QSTR_cmd,      MP_ARG_KW_ONLY | MP_ARG_OBJ, {.u_obj = mp_const_none} },
        { MP_QSTR_data,     MP_ARG_KW_ONLY | MP_ARG_OBJ, {.u_obj = mp_const_none} },
        #endif
        #if SOC_SDMMC_IO_POWER_EXTERNAL
        #ifdef MICROPY_HW_SDMMC_LDO_CHAN_ID
        { MP_QSTR_ldo,      MP_ARG_KW_ONLY | MP_ARG_OBJ, {.u_obj = MP_OBJ_NEW_SMALL_INT(MICROPY_HW_SDMMC_LDO_CHAN_ID)} },
        #else
        { MP_QSTR_ldo,      MP_ARG_KW_ONLY | MP_ARG_OBJ, {.u_obj = mp_const_none} },
        #endif
        #endif
        // freq is valid for both SPI and SDMMC interfaces
        { MP_QSTR_freq,     MP_ARG_KW_ONLY | MP_ARG_INT, {.u_int = 20000000} },
    };
    mp_arg_val_t arg_vals[MP_ARRAY_SIZE(allowed_args)];
    mp_map_t kw_args;

    DEBUG_printf("Making new SDCard:n");
    DEBUG_printf("  Unpacking arguments");

    mp_map_init_fixed_table(&kw_args, n_kw, args + n_args);

    mp_arg_parse_all(n_args, args, &kw_args,
        MP_ARRAY_SIZE(allowed_args), allowed_args, arg_vals);

    DEBUG_printf("  slot=%d, width=%d, cd=%p, wp=%p",
        arg_vals[ARG_slot].u_int, arg_vals[ARG_width].u_int,
        arg_vals[ARG_cd].u_obj, arg_vals[ARG_wp].u_obj);

    DEBUG_printf("  miso=%p, mosi=%p, sck=%p, cs=%p",
        arg_vals[ARG_miso].u_obj, arg_vals[ARG_mosi].u_obj,
        arg_vals[ARG_sck].u_obj, arg_vals[ARG_cs].u_obj);

    #if SOC_SDMMC_USE_GPIO_MATRIX
    DEBUG_printf("  cmd=%p, data=%p",
        arg_vals[ARG_cmd].u_obj, arg_vals[ARG_data].u_obj);
    #endif

    int slot_num = arg_vals[ARG_slot].u_int;
    if (slot_num < SD_SLOT_MIN || slot_num > SD_SLOT_MAX) {
        mp_raise_ValueError(MP_ERROR_TEXT("invalid slot number"));
    }

    #if SOC_SDMMC_HOST_SUPPORTED
    // Slots 0 and 1 are native SD/MMC, slots 2 and 3 are SPI
    bool is_spi = (slot_num >= 2);
    #else
    bool is_spi = true;
    #endif
    if (is_spi) {
        slot_num -= 2;
        assert(slot_num < NUM_SD_SPI_BUS);
    }

    // Verify valid argument combinations
    #if SOC_SDMMC_USE_GPIO_MATRIX
    if (is_spi && (arg_vals[ARG_cmd].u_obj != mp_const_none
                   || arg_vals[ARG_data].u_obj != mp_const_none)) {
        mp_raise_ValueError(MP_ERROR_TEXT("invalid config: SPI slot with SDMMC pin arguments"));
    }
    #endif
    #if SOC_SDMMC_HOST_SUPPORTED
    if (!is_spi && (arg_vals[ARG_miso].u_obj != mp_const_none
                    || arg_vals[ARG_mosi].u_obj != mp_const_none
                    || arg_vals[ARG_cs].u_obj != mp_const_none)) {
        mp_raise_ValueError(MP_ERROR_TEXT("invalid config: SDMMC slot with SPI pin arguments"));
    }
    #endif

    DEBUG_printf("  Setting up host configuration");

    sdcard_card_obj_t *self = mp_obj_malloc_with_finaliser(sdcard_card_obj_t, &machine_sdcard_type);
    self->flags = 0;
    // Note that these defaults are macros that expand to structure
    // constants so we can't directly assign them to fields.
    int freq = arg_vals[ARG_freq].u_int;
    if (is_spi) {
        sdmmc_host_t _temp_host = SDSPI_HOST_DEFAULT();
        _temp_host.max_freq_khz = freq / 1000;
        // SPI SDMMC sets the slot to the SPI host ID
        _temp_host.slot = spi_dev_defaults[slot_num].host_id;
        self->host = _temp_host;
    }
    #if SOC_SDMMC_HOST_SUPPORTED
    else {
        sdmmc_host_t _temp_host = SDMMC_HOST_DEFAULT();
        _temp_host.max_freq_khz = freq / 1000;
        _temp_host.slot = slot_num;
        self->host = _temp_host;
    }
    #endif
    #if SOC_SDMMC_IO_POWER_EXTERNAL
    if (arg_vals[ARG_ldo].u_obj != mp_const_none) {
        sd_pwr_ctrl_ldo_config_t ldo_config = {
            .ldo_chan_id = mp_obj_get_int(arg_vals[ARG_ldo].u_obj),
        };
        sd_pwr_ctrl_handle_t pwr_ctrl_handle = NULL;
        check_esp_err(sd_pwr_ctrl_new_on_chip_ldo(&ldo_config, &pwr_ctrl_handle));
        self->host.pwr_ctrl_handle = pwr_ctrl_handle;
    }
    #endif

    DEBUG_printf("  Calling host.init()");

    check_esp_err(self->host.init());
    self->flags |= SDCARD_CARD_FLAGS_HOST_INIT_DONE;

    if (is_spi) {
        // SPI interface
        DEBUG_printf("  Setting up SPI slot configuration");
        spi_host_device_t spi_host_id = self->host.slot;
        spi_bus_config_t bus_config = spi_bus_defaults[slot_num];
        #if CONFIG_IDF_TARGET_ESP32
        spi_dma_chan_t dma_channel = spi_dma_channel_defaults[slot_num];
        #else
        spi_dma_chan_t dma_channel = SPI_DMA_CH_AUTO;
        #endif
        sdspi_device_config_t dev_config = spi_dev_defaults[slot_num];

        #if NUM_SD_SPI_BUS == 1
        // Single-bus chips use SDSPI_DEVICE_CONFIG_DEFAULT() for the only SPI SD
        // bus (slot 2), which hard-codes its CS pin. If a board provides a
        // default CS, apply it instead (keeping the IDF default otherwise). An
        // explicit cs= argument still takes precedence below.
        if (MICROPY_HW_SDCARD_SPI_CS != GPIO_NUM_NC) {
            dev_config.gpio_cs = MICROPY_HW_SDCARD_SPI_CS;
        }
        #endif

        SET_CONFIG_PIN(bus_config, miso_io_num, ARG_miso);
        SET_CONFIG_PIN(bus_config, mosi_io_num, ARG_mosi);
        SET_CONFIG_PIN(bus_config, sclk_io_num, ARG_sck);

        SET_CONFIG_PIN(dev_config, gpio_cs, ARG_cs);
        SET_CONFIG_PIN(dev_config, gpio_cd, ARG_cd);
        SET_CONFIG_PIN(dev_config, gpio_wp, ARG_wp);

        // On chips other than original ESP32 and S3, there are not
        // always default SPI pins assigned
        if (dev_config.gpio_cs == GPIO_NUM_NC
            || bus_config.miso_io_num == GPIO_NUM_NC
            || bus_config.mosi_io_num == GPIO_NUM_NC
            || bus_config.sclk_io_num == GPIO_NUM_NC) {
            mp_raise_ValueError(MP_ERROR_TEXT("SPI pin values required"));
        }

        DEBUG_printf("  Calling spi_bus_initialize()");
        check_esp_err(spi_bus_initialize(spi_host_id, &bus_config, dma_channel));

        DEBUG_printf("  Calling sdspi_host_init_device()");
        sdspi_dev_handle_t sdspi_handle;
        esp_err_t ret = sdspi_host_init_device(&dev_config, &sdspi_handle);
        if (ret != ESP_OK) {
            spi_bus_free(spi_host_id);
            check_esp_err(ret);
        }
        if (self->host.slot != sdspi_handle) {
            // MicroPython restriction: the SPI bus must be exclusively for the SD card.
            spi_bus_free(spi_host_id);
            mp_raise_ValueError(MP_ERROR_TEXT("SPI bus already in use"));
        }
    }
    #if SOC_SDMMC_HOST_SUPPORTED
    else {
        // SD/MMC interface
        DEBUG_printf("  Setting up SDMMC slot configuration");
        sdmmc_slot_config_t slot_config = SDMMC_SLOT_CONFIG_DEFAULT();

        // Stronger external pull-ups are still needed but apparently
        // it is a good idea to set the internal pull-ups anyway.
        // slot_config.flags = SDMMC_SLOT_FLAG_INTERNAL_PULLUP;

        SET_CONFIG_PIN(slot_config, gpio_cd, ARG_cd);
        SET_CONFIG_PIN(slot_config, gpio_wp, ARG_wp);

        int width = arg_vals[ARG_width].u_int;
        if (width == 1 || width == 4 || (width == 8 && slot_num == 0)) {
            slot_config.width = width;
        } else {
            mp_raise_ValueError(MP_ERROR_TEXT("width must be 1 or 4 (or 8 on slot 0)"));
        }

        #if SOC_SDMMC_USE_GPIO_MATRIX
        // Apply board-level pin defaults (mpconfigboard.h), then let an explicit
        // clk/cmd/data argument override below. Grouped by bus width and nested
        // so each wider mode builds on the narrower one: CLK/CMD/D0 (1-bit), then
        // D1-D3 (4-bit), then D4-D7 (8-bit eMMC). An unset group stays at the
        // driver default (GPIO_NUM_NC).
        if (MICROPY_HW_SDMMC_CLK != GPIO_NUM_NC) {
            slot_config.clk = MICROPY_HW_SDMMC_CLK;
            slot_config.cmd = MICROPY_HW_SDMMC_CMD;
            slot_config.d0 = MICROPY_HW_SDMMC_D0;
            if (MICROPY_HW_SDMMC_D1 != GPIO_NUM_NC) {
                slot_config.d1 = MICROPY_HW_SDMMC_D1;
                slot_config.d2 = MICROPY_HW_SDMMC_D2;
                slot_config.d3 = MICROPY_HW_SDMMC_D3;
                if (MICROPY_HW_SDMMC_D4 != GPIO_NUM_NC) {
                    slot_config.d4 = MICROPY_HW_SDMMC_D4;
                    slot_config.d5 = MICROPY_HW_SDMMC_D5;
                    slot_config.d6 = MICROPY_HW_SDMMC_D6;
                    slot_config.d7 = MICROPY_HW_SDMMC_D7;
                }
            }
        }
        SET_CONFIG_PIN(slot_config, clk, ARG_sck); // reuse SPI SCK for CLK
        SET_CONFIG_PIN(slot_config, cmd, ARG_cmd);
        if (arg_vals[ARG_data].u_obj != mp_const_none) {
            mp_obj_t *data_vals;
            size_t data_len;
            mp_obj_get_array(arg_vals[ARG_data].u_obj, &data_len, &data_vals);
            if (data_len != width) {
                mp_raise_msg_varg(&mp_type_ValueError, MP_ERROR_TEXT("data argument length must match width %d"), width);
            }
            slot_config.d0 = machine_pin_get_id(data_vals[0]);
            if (width > 1) {
                slot_config.d1 = machine_pin_get_id(data_vals[1]);
                slot_config.d2 = machine_pin_get_id(data_vals[2]);
                slot_config.d3 = machine_pin_get_id(data_vals[3]);
            }
            if (width == 8) {
                slot_config.d4 = machine_pin_get_id(data_vals[4]);
                slot_config.d5 = machine_pin_get_id(data_vals[5]);
                slot_config.d6 = machine_pin_get_id(data_vals[6]);
                slot_config.d7 = machine_pin_get_id(data_vals[7]);
            }
        }
        #endif

        DEBUG_printf("  Calling init_slot()");
        check_esp_err(sdmmc_host_init_slot(self->host.slot, &slot_config));
    }
    #endif // SOC_SDMMC_HOST_SUPPORTED

    DEBUG_printf("  Returning new card object: %p", self);
    return MP_OBJ_FROM_PTR(self);
}

static mp_obj_t sd_deinit(mp_obj_t self_in) {
    sdcard_card_obj_t *self = self_in;

    DEBUG_printf("De-init host\n");

    if (self->flags & SDCARD_CARD_FLAGS_HOST_INIT_DONE) {
        if (self->host.flags & SDMMC_HOST_FLAG_DEINIT_ARG) {
            self->host.deinit_p(self->host.slot);
        } else {
            self->host.deinit();
        }
        if (self->host.flags & SDMMC_HOST_FLAG_SPI) {
            // SD card used a (dedicated) SPI bus, so free that SPI bus.
            spi_bus_free(self->host.slot);
        }
        #if SOC_SDMMC_IO_POWER_EXTERNAL
        if (self->host.pwr_ctrl_handle) {
            check_esp_err(sd_pwr_ctrl_del_on_chip_ldo(self->host.pwr_ctrl_handle));
        }
        #endif
        self->flags &= ~SDCARD_CARD_FLAGS_HOST_INIT_DONE;
    }

    return mp_const_none;
}
static MP_DEFINE_CONST_FUN_OBJ_1(sd_deinit_obj, sd_deinit);

static mp_obj_t sd_info(mp_obj_t self_in) {
    sdcard_card_obj_t *self = self_in;
    // We could potential return a great deal more SD card data but it
    // is not clear that it is worth the extra code space to do
    // so. For the most part people only care about the card size and
    // block size.

    check_esp_err(sdcard_ensure_card_init((sdcard_card_obj_t *)self, false));

    uint32_t log_block_nbr = self->card.csd.capacity;
    uint32_t log_block_size = _SECTOR_SIZE(self);

    mp_obj_t tuple[2] = {
        mp_obj_new_int_from_ull((uint64_t)log_block_nbr * (uint64_t)log_block_size),
        mp_obj_new_int_from_uint(log_block_size),
    };
    return mp_obj_new_tuple(2, tuple);
}
static MP_DEFINE_CONST_FUN_OBJ_1(sd_info_obj, sd_info);

static mp_obj_t machine_sdcard_readblocks(mp_obj_t self_in, mp_obj_t block_num, mp_obj_t buf) {
    sdcard_card_obj_t *self = self_in;
    mp_buffer_info_t bufinfo;
    esp_err_t err;

    err = sdcard_ensure_card_init((sdcard_card_obj_t *)self, false);
    if (err != ESP_OK) {
        return mp_const_false;
    }

    mp_get_buffer_raise(buf, &bufinfo, MP_BUFFER_WRITE);
    err = sdmmc_read_sectors(&(self->card), bufinfo.buf, mp_obj_get_int(block_num), bufinfo.len / _SECTOR_SIZE(self));

    int ret = err == ESP_OK ? 0 : -MP_EIO;
    return MP_OBJ_NEW_SMALL_INT(ret);
}
static MP_DEFINE_CONST_FUN_OBJ_3(machine_sdcard_readblocks_obj, machine_sdcard_readblocks);

static mp_obj_t machine_sdcard_writeblocks(mp_obj_t self_in, mp_obj_t block_num, mp_obj_t buf) {
    sdcard_card_obj_t *self = self_in;
    mp_buffer_info_t bufinfo;
    esp_err_t err;

    err = sdcard_ensure_card_init((sdcard_card_obj_t *)self, false);
    if (err != ESP_OK) {
        return mp_const_false;
    }

    mp_get_buffer_raise(buf, &bufinfo, MP_BUFFER_READ);
    err = sdmmc_write_sectors(&(self->card), bufinfo.buf, mp_obj_get_int(block_num), bufinfo.len / _SECTOR_SIZE(self));

    int ret = err == ESP_OK ? 0 : -MP_EIO;
    return MP_OBJ_NEW_SMALL_INT(ret);
}
static MP_DEFINE_CONST_FUN_OBJ_3(machine_sdcard_writeblocks_obj, machine_sdcard_writeblocks);

static mp_obj_t machine_sdcard_ioctl(mp_obj_t self_in, mp_obj_t cmd_in, mp_obj_t arg_in) {
    sdcard_card_obj_t *self = self_in;
    esp_err_t err = ESP_OK;
    mp_int_t cmd = mp_obj_get_int(cmd_in);

    switch (cmd) {
        case MP_BLOCKDEV_IOCTL_INIT:
            err = sdcard_ensure_card_init(self, false);
            return MP_OBJ_NEW_SMALL_INT((err == ESP_OK) ? 0 : -1);

        case MP_BLOCKDEV_IOCTL_DEINIT:
            // Ensure that future attempts to look at info re-read the card
            self->flags &= ~SDCARD_CARD_FLAGS_CARD_INIT_DONE;
            return MP_OBJ_NEW_SMALL_INT(0); // success

        case MP_BLOCKDEV_IOCTL_SYNC:
            // nothing to do
            return MP_OBJ_NEW_SMALL_INT(0); // success

        case MP_BLOCKDEV_IOCTL_BLOCK_COUNT:
            err = sdcard_ensure_card_init(self, false);
            if (err != ESP_OK) {
                return MP_OBJ_NEW_SMALL_INT(-1);
            }
            return MP_OBJ_NEW_SMALL_INT(self->card.csd.capacity);

        case MP_BLOCKDEV_IOCTL_BLOCK_SIZE:
            err = sdcard_ensure_card_init(self, false);
            if (err != ESP_OK) {
                return MP_OBJ_NEW_SMALL_INT(-1);
            }
            return MP_OBJ_NEW_SMALL_INT(_SECTOR_SIZE(self));

        default: // unknown command
            return MP_OBJ_NEW_SMALL_INT(-1); // error
    }
}
static MP_DEFINE_CONST_FUN_OBJ_3(machine_sdcard_ioctl_obj, machine_sdcard_ioctl);

static const mp_rom_map_elem_t machine_sdcard_locals_dict_table[] = {
    { MP_ROM_QSTR(MP_QSTR_info), MP_ROM_PTR(&sd_info_obj) },
    { MP_ROM_QSTR(MP_QSTR___del__), MP_ROM_PTR(&sd_deinit_obj) },
    { MP_ROM_QSTR(MP_QSTR_deinit), MP_ROM_PTR(&sd_deinit_obj) },
    // block device protocol
    { MP_ROM_QSTR(MP_QSTR_readblocks), MP_ROM_PTR(&machine_sdcard_readblocks_obj) },
    { MP_ROM_QSTR(MP_QSTR_writeblocks), MP_ROM_PTR(&machine_sdcard_writeblocks_obj) },
    { MP_ROM_QSTR(MP_QSTR_ioctl), MP_ROM_PTR(&machine_sdcard_ioctl_obj) },
};

static MP_DEFINE_CONST_DICT(machine_sdcard_locals_dict, machine_sdcard_locals_dict_table);

MP_DEFINE_CONST_OBJ_TYPE(
    machine_sdcard_type,
    MP_QSTR_SDCard,
    MP_TYPE_FLAG_NONE,
    make_new, machine_sdcard_make_new,
    locals_dict, &machine_sdcard_locals_dict
    );

#endif // MICROPY_PY_MACHINE_SDCARD
