#include <stdio.h>

#include <esp_log.h>

#include <driver/gpio.h>
#include <driver/spi_master.h>

#include <freertos/FreeRTOS.h>

#include <array>
#include <bitset>

DRAM_ATTR static const char* tag = "PINPOKER";

static const uint64_t kMISOPin = GPIO_NUM_35;
static const uint64_t kMOSIPin = GPIO_NUM_37;
static const uint64_t kClkPin = GPIO_NUM_36;
static const uint64_t kDirPin = GPIO_NUM_44;
static const uint64_t kStepPin = GPIO_NUM_43;
static const uint64_t kDiagPin = GPIO_NUM_18;
static const uint64_t kCSPin = GPIO_NUM_17;
static const uint64_t kEnPin = GPIO_NUM_2;

static constexpr uint8_t kQueueSize = 16;
static constexpr uint8_t kReceiveBufferSize = 128;

void spiPostTransactionCallback(spi_transaction_t*);

class App {
public:
    App();

    void run(void);

private:
    struct TransactionContext {
    public:
        alignas(32) uint8_t receiveBuffer[kReceiveBufferSize];
        App* app;
        uint16_t bufferIndex;
    };

    uint16_t _transactionContextIndex();
    void _returnTransactionContextAtIndex(uint16_t idx);

    spi_device_handle_t _ezdc;

    std::array<TransactionContext, kQueueSize> _transactionContexts;
    std::bitset<kQueueSize> _usedBuffers{false};

    friend void spiPostTransactionCallback(spi_transaction_t*);
};

void IRAM_ATTR spiPreTransactionCallback(spi_transaction_t* transaction) {
    ESP_DRAM_LOGI(tag, "Pre Transaction");
}

void IRAM_ATTR spiPostTransactionCallback(spi_transaction_t* transaction) {
    if (transaction->rxlength != 0) {
        unsigned char buf[kReceiveBufferSize + 1];
        memcpy(buf, transaction->rx_buffer, transaction->rxlength);
        buf[transaction->rxlength / 8] = '\0';
        ESP_DRAM_LOGI(tag, "Rx length: %u", transaction->rxlength / 8);
        ESP_DRAM_LOGI(tag, "%s", buf);
    }

    App::TransactionContext* transactionContext = reinterpret_cast<App::TransactionContext*>(transaction->user);
    transactionContext->app->_returnTransactionContextAtIndex(transactionContext->bufferIndex);

    ESP_DRAM_LOGI(tag, "Post Transaction");
}

App::App() {
    for (size_t i = 0; i < kQueueSize; ++i) {
        _transactionContexts[i].app = this;
        _transactionContexts[i].bufferIndex = i;
    }

    esp_err_t err = ESP_OK;
    spi_bus_config_t busConfig = {.mosi_io_num = kMOSIPin,
                                  .miso_io_num = kMISOPin,
                                  .sclk_io_num = kClkPin,
                                  .quadwp_io_num = -1,
                                  .quadhd_io_num = -1,
                                  .data4_io_num = -1,
                                  .data5_io_num = -1,
                                  .data6_io_num = -1,
                                  .data7_io_num = -1,
                                  .data_io_default_level = 0,
                                  .max_transfer_sz = 0,
                                  .flags = SPICOMMON_BUSFLAG_MASTER | SPICOMMON_BUSFLAG_MISO | SPICOMMON_BUSFLAG_MOSI | SPICOMMON_BUSFLAG_SCLK,
                                  .isr_cpu_id = ESP_INTR_CPU_AFFINITY_AUTO,
                                  .intr_flags = 0};
    err = spi_bus_initialize(SPI2_HOST, &busConfig, SPI_DMA_CH_AUTO);
    ESP_ERROR_CHECK(err);
    spi_device_interface_config_t ezdcConfig = {.command_bits = 8,
                                                .address_bits = 16,
                                                .dummy_bits = 0,
                                                .mode = 0,
                                                .clock_source = SPI_CLK_SRC_DEFAULT,
                                                .duty_cycle_pos = 0,
                                                .cs_ena_pretrans = 2,
                                                .cs_ena_posttrans = 2,
                                                .clock_speed_hz = 10'000'000,
                                                .input_delay_ns = 0,
                                                .sample_point = SPI_SAMPLING_POINT_PHASE_0,
                                                .spics_io_num = kCSPin,
                                                .flags = SPI_DEVICE_HALFDUPLEX,
                                                .queue_size = kQueueSize,
                                                .pre_cb = spiPreTransactionCallback,
                                                .post_cb = spiPostTransactionCallback};
    err = spi_bus_add_device(SPI2_HOST, &ezdcConfig, &_ezdc);
    ESP_ERROR_CHECK(err);
}

void App::run(void) {
    while (true) {
        esp_err_t err = ESP_OK;
        uint16_t bufferIndex = _transactionContextIndex();
        TransactionContext& transactionContext = _transactionContexts[bufferIndex];
        spi_transaction_t transaction = {
            .flags = 0,
            .cmd = 0x0,
            .addr = 0x0,
            .length = 0,
            .rxlength = 8 * kReceiveBufferSize,
            .override_freq_hz = 0,
            .user = &transactionContext,
            .tx_buffer = nullptr,
            .rx_buffer = transactionContext.receiveBuffer,
        };

        err = spi_device_transmit(_ezdc, &transaction);
        ESP_ERROR_CHECK(err);

        vTaskDelay(100 / portTICK_PERIOD_MS);
    }
}

uint16_t App::_transactionContextIndex() {
    for (uint8_t i = 0; i < kQueueSize; ++i) {
        if (!_usedBuffers[i]) {
            _usedBuffers[i] = true;
            return i;
        }
    }

    assert(false);
    return 0;
}

void App::_returnTransactionContextAtIndex(uint16_t i) {
    _usedBuffers[i] = false;
}

extern "C" {
void app_main(void) {
    App app;
    app.run();
}
}
