#include <Arduino.h>
#include <SPI.h>
#include "AD9102.h"

const std::vector<std::vector<const char *>> status_strings = {
    {
        // 0
        "MSB",
        "LSB",
    },
    {
        // 1
        "4-Wire SPI",
        "3-Wire SPI",
    },
    {
        // 2
        "1 data line",
        "2 data lines",
    },
    {
        // 3
        "Single drive",
        "Double drive",
    },
    {
        // 4
        "SDO/SDI2",
        "DOUT",
    },
    {
        // 5
        "0°",
        "180°",
    },
};

uint16_t AD9102::spi_write(uint16_t address, uint16_t value)
{
    digitalWrite(CS_, LOW);
    SPI.beginTransaction(spi_settings);
    SPI.transfer16(address & DDS_WRITE);
    uint16_t output = SPI.transfer16(value);
    SPI.endTransaction();
    digitalWrite(CS_, HIGH);
    if (spi_debug_enabled)
        Serial.printf("[%d] DDS SPI Write: [0x%04x] 0x%04x -> 0x%04x\r\n", millis(), address, value, output);
    return output;
}

uint32_t AD9102::spi_write_32(uint16_t address, uint32_t value)
{
    uint32_t output = 0;

    digitalWrite(CS_, LOW);
    SPI.beginTransaction(spi_settings);
    SPI.transfer16(address & DDS_WRITE);
    output = SPI.transfer16((value & 0xFFFF0000) >> 16) << 16;
    output |= SPI.transfer16(value & 0xFFFF);
    SPI.endTransaction();
    digitalWrite(CS_, HIGH);
    if (spi_debug_enabled)
        Serial.printf("[%d] DDS SPI Write: [0x%04x] 0x%08x -> 0x%08x\r\n", millis(), address, value, output);
    return output;
}

uint16_t AD9102::spi_read(uint16_t address)
{
    digitalWrite(CS_, LOW);
    SPI.beginTransaction(spi_settings);
    SPI.transfer16(address | DDS_READ);
    uint16_t output = SPI.transfer16(0x0000);
    SPI.endTransaction();
    digitalWrite(CS_, HIGH);
    if (spi_debug_enabled)
        Serial.printf("[%d] DDS SPI Read: [0x%04x] 0x%04x\r\n", millis(), address, output);
    return output;
}

// Constructor
AD9102::AD9102(pin_size_t CS, uint32_t clock_speed)
{
    spi_settings = SPISettings(clock_speed, MSBFIRST, SPI_MODE0);
    CS_ = CS;
    status = AD9102_status();
}

void AD9102::spi_debug(bool enable)
{
    spi_debug_enabled = enable;
}

void AD9102::reg_spicfg(bool lsb, bool mode_3_wire, bool reset, bool double_spi, bool drive, bool dout)
{
    uint16_t val = lsb << 15 | mode_3_wire << 14 | reset << 13 | double_spi << 12 | drive << 11 | dout << 10;
    val |= lsb | mode_3_wire << 1 | reset << 2 | double_spi << 3 | drive << 4 | dout << 5;
    spi_write(REG_SPICONFIG, val);
}

void AD9102::reg_pat_status(bool buff_read, bool mem_access, bool run)
{
    uint16_t val = run | mem_access << 2 | buff_read << 3;
    spi_write(REG_PAT_STATUS, val);
}

void AD9102::reg_calconfig(bool cal_reset, bool cal_mode_enable, uint8_t comp_cal_range, bool cal_clk_enable, uint8_t cal_clk_div)
{
    uint16_t val = cal_reset << 8 | cal_mode_enable << 6 | (comp_cal_range & 0b00000011) << 4 | cal_clk_enable << 3 | (cal_clk_div & 0b00000111);
    spi_write(REG_CALCONFIG, val);
}

void AD9102::reg_compoffset(bool start)
{
    spi_write(REG_COMPOFFSET, start);
}

void AD9102::reg_wav_config(byte wav_type, byte prestored_type)
{
    spi_write(REG_WAV_CONFIG, ((prestored_type & 3) << 4) | (wav_type & 3));
}

void AD9102::reg_dds_config(bool tw_ram, bool pw_ram, bool dds_clock, bool dds_cos)
{
    spi_write(REG_DDS_CONFIG, dds_cos << 3 | dds_clock << 2 | pw_ram << 1 | tw_ram);
}

void AD9102::set_tw(uint32_t tw)
{
    spi_write_32(REG_DDS_TW32, tw << 8);
    update();
}

void AD9102::set_dac_dgain(double gain)
{
    spi_write(REG_DAC_DGAIN, (uint16_t)((2 - gain) / 0.000976562) << 4);
}

void AD9102::update()
{
    spi_write(REG_RAMUPDATE, 1);
}

void AD9102::reset()
{
    reg_spicfg(0, 0, 1, 0, 0, 0);
}

void AD9102::run()
{
    update();                // update all SPI settings
    reg_pat_status(0, 0, 1); // run
}

void AD9102::print_status()
{
    const char *bools[2] = {"false", "TRUE"};
    const char *bools_alt[2] = {"off", "ON"};
    uint16_t data = 0;

    Serial.println("\r\nDDS status report:");

    // SPICONFIG
    data = spi_read(REG_SPICONFIG);
    status.SPICONFIG_LSBFIRST = !!(data & 0x8000);
    status.SPICONFIG_SPI3WIRE = !!(data & 0x4000);
    status.SPICONFIG_RESET = !!(data & 0x2000);
    status.SPICONFIG_DOUBLESPI = !!(data & 0x1000);
    status.SPICONFIG_SPI_DRV = !!(data & 0x800);
    status.SPICONFIG_DOUT_EN = !!(data & 0x400);

    Serial.printf("=== SPICONFIG: %s | %s | %s | %s | %s | %s\r\n",
                  status_strings[0][status.SPICONFIG_LSBFIRST],
                  status_strings[1][status.SPICONFIG_SPI3WIRE],
                  bools[status.SPICONFIG_RESET],
                  status_strings[2][status.SPICONFIG_DOUBLESPI],
                  status_strings[3][status.SPICONFIG_SPI_DRV],
                  status_strings[4][status.SPICONFIG_DOUT_EN]);

    // POWERCONFIG
    data = spi_read(REG_POWERCONFIG);
    status.POWERCONFIG_CLK_LDO_STAT = !!(data & 0x800);
    status.POWERCONFIG_DIG1_LDO_STAT = !!(data & 0x400);
    status.POWERCONFIG_DIG2_LDO_STAT = !!(data & 0x200);
    status.POWERCONFIG_PDN_LDO_CLK = !!(data & 0x100);
    status.POWERCONFIG_PDN_LDO_DIG1 = !!(data & 0x80);
    status.POWERCONFIG_PDN_LDO_DIG2 = !!(data & 0x40);
    status.POWERCONFIG_REF_PDN = !!(data & 0x20);
    status.POWERCONFIG_REF_EXT = !!(data & 0x10);
    status.POWERCONFIG_DAC_SLEEP = !!(data & 0x8);

    Serial.printf("=== POWERCONFIG: "
                  "CLKVDD LDO: %s | "
                  "DVDD1 LDO: %s | "
                  "DVDD2 LDO: %s | "
                  "CLKVDD LDO: %s | "
                  "DVDD1 LDO: %s | "
                  "DVDD2 LDO: %s | "
                  "REFIO: %s | "
                  "REF_EXT: %s | "
                  "DAC: %s\r\n",
                  bools_alt[status.POWERCONFIG_CLK_LDO_STAT],
                  bools_alt[status.POWERCONFIG_DIG1_LDO_STAT],
                  bools_alt[status.POWERCONFIG_DIG2_LDO_STAT],
                  bools_alt[!status.POWERCONFIG_PDN_LDO_CLK],
                  bools_alt[!status.POWERCONFIG_PDN_LDO_DIG1],
                  bools_alt[!status.POWERCONFIG_PDN_LDO_DIG2],
                  bools_alt[!status.POWERCONFIG_REF_PDN],
                  bools[status.POWERCONFIG_REF_EXT],
                  bools_alt[!status.POWERCONFIG_DAC_SLEEP]);

    // CLOCKCONFIG
    data = spi_read(REG_CLOCKCONFIG);
    status.CLOCKCONFIG_CLK = !(data & 0x800),
    status.CLOCKCONFIG_DCLK = !(data & 0x80);
    status.CLOCKCONFIG_CLK_SLEEP = !!(data & 0x40);
    status.CLOCKCONFIG_CLK_PDN = !!(data & 0x20);
    status.CLOCKCONFIG_EPS = !!(data & 0x10);
    status.CLOCKCONFIG_DAC_CLK = !!(data & 0x8);

    Serial.printf("=== CLOCKCONFIG: "
                  "CLK: %s | "
                  "DCLK: %s | "
                  "CLK_SLEEP: %s | "
                  "CLK_PDN: %s | "
                  "EPS: %s | "
                  "DAC_CLK: %s\r\n",
                  bools_alt[status.CLOCKCONFIG_CLK],
                  bools_alt[status.CLOCKCONFIG_DCLK],
                  bools_alt[status.CLOCKCONFIG_CLK_SLEEP],
                  bools_alt[status.CLOCKCONFIG_CLK_PDN],
                  bools_alt[status.CLOCKCONFIG_EPS],
                  status_strings[5][status.CLOCKCONFIG_DAC_CLK]);

    // REFADJ
    data = spi_read(REG_REFADJ);

    status.BGDR = 1.04 + ((data & 0x3F) * 0.0625);
    if (data & 0x3F >= 32)
        status.BGDR = 0.84 + (((data & 0x3F) - 32) * 0.0625);

    Serial.printf("=== REFADJ:\r\n"
                  "\tBGDR: 0x%02x (%.3f V)\r\n",
                  data & 0x3F, status.BGDR);

    // DACAGAIN
    data = spi_read(REG_DACAGAIN);
    Serial.printf("=== DACAGAIN:\r\n"
                  "\tDAC_GAIN_CAL: 0x%02x"
                  "\tDAC_GAIN: 0x%02x\r\n",
                  data & 0x7F00,
                  data & 0x7F);

    // DACRANGE
    data = spi_read(REG_DACRANGE);
    Serial.printf("=== DACRANGE:\r\n"
                  "\tDAC_GAIN_RNG: 0x%01x\r\n",
                  data & 0x3);

    // DACRSET
    data = spi_read(REG_DACRSET);
    Serial.printf("=== DACRSET:\r\n"
                  "\tDAC_RSET_EN: %s"
                  "\tDAC_RSET_CAL: 0x%02x"
                  "\tDAC_RSET: 0x%02x\r\n",
                  bools[!!(data & 0x8000)],
                  data & 0x1F00,
                  data & 0x1F);

    // CALCONFIG
    data = spi_read(REG_CALCONFIG);
    Serial.printf("=== CALCONFIG:\r\n"
                  "\tCOMP_OFFSET_OF: %s"
                  "\tCOMP_OFFSET_UF: %s"
                  "\tRSET_CAL_OF: %s"
                  "\tRSET_CAL_UF: %s"
                  "\tGAIN_CAL_OF: %s"
                  "\tGAIN_CAL_UF: %s"
                  "\tCAL_RESET: %s"
                  "\tCAL_MODE: %s"
                  "\tCAL_MODE_EN: %s"
                  "\tCOMP_CAL_RNG: 0x%01x"
                  "\tCAL_CLK_EN: %s"
                  "\tCAL_CLK_DIV: 0x%01x\r\n",
                  bools[!!(data & 0x4000)],
                  bools[!!(data & 0x2000)],
                  bools[!!(data & 0x1000)],
                  bools[!!(data & 0x800)],
                  bools[!!(data & 0x400)],
                  bools[!!(data & 0x200)],
                  bools[!!(data & 0x100)],
                  bools[!!(data & 0x80)],
                  bools[!!(data & 0x40)],
                  data & 0x30,
                  bools[!!(data & 0x10)],
                  data & 0x7);

    // COMPOFFSET
    data = spi_read(REG_COMPOFFSET);
    Serial.printf("=== COMPOFFSET:\r\n"
                  "\tCOMP_OFFSET_CAL: 0x%02x"
                  "\tCAL_FIN: %s"
                  "\tSTART_CAL: %s\r\n",
                  data & 0x7F00,
                  bools[!!(data & 0x2)],
                  bools[!!(data & 0x1)]);

    // RAMUPDATE
    data = spi_read(REG_RAMUPDATE);
    Serial.printf("=== RAMUPDATE:\r\n"
                  "\tUPDATE: %s\r\n",
                  bools[!!(data & 0x1)]);

    // PAT_STATUS
    data = spi_read(REG_PAT_STATUS);
    Serial.printf("=== PAT_STATUS:\r\n"
                  "\tBUF_READ: %s"
                  "\tMEM_ACCESS: %s"
                  "\tPATTERN: %s"
                  "\tRUN: %s\r\n",
                  bools[!!(data & 0x8)],
                  bools[!!(data & 0x4)],
                  bools[!!(data & 0x2)],
                  bools[!!(data & 0x1)]);

    // PAT_TYPE
    data = spi_read(REG_PAT_TYPE);
    Serial.printf("=== PAT_TYPE:\r\n"
                  "\tPATTERN_RPT: %s\r\n",
                  bools[!!(data & 0x1)]);

    // PATTERN_DLY
    data = spi_read(REG_PATTERN_DLY);
    Serial.printf("=== PATTERN_DLY:\r\n"
                  "\tPATTERN_DELAY: %d clock cycles\r\n",
                  data);

    // DACDOF
    data = spi_read(REG_DACDOF);
    Serial.printf("=== DACDOF:\r\n"
                  "\tDAC_DIG_OFFSET: 0x%04x\r\n",
                  data & 0xFFF0);

    // WAV_CONFIG
    const char *prestore_sel[4] = {"Constant value", "Sawtooth", "Pseudorandom sequence", "DDS output"};
    const char *wave_sel[4] = {"Waveform from RAM", "Prestored waveform", "Prestored waveform using START_DELAY and PATTERN_PERIOD", "Prestored waveform modulated by waveform from RAM"};
    data = spi_read(REG_WAV_CONFIG);
    Serial.printf("=== WAV_CONFIG:\r\n"
                  "\tPRESTORE_SEL: %s"
                  "\tCH_ADD: %s"
                  "\tWAVE_SEL: %s\r\n",
                  prestore_sel[(data & 0x30) >> 4],
                  bools[!!(data & 0x4)],
                  wave_sel[data & 0x3]);

    // PAT_TIMEBASE
    data = spi_read(REG_PAT_TIMEBASE);
    Serial.printf("=== PAT_TIMEBASE:\r\n"
                  "\tHOLD: 0x%01x"
                  "\tPAT_PERIOD_BASE: 0x%01x"
                  "\tSTART_DELAY_BASE: 0x%01x\r\n",
                  (data & 0xF00) >> 8,
                  (data & 0xF0) >> 4,
                  data & 0xF);

    // PAT_PERIOD
    data = spi_read(REG_PAT_PERIOD);
    Serial.printf("=== PAT_PERIOD:\r\n"
                  "\tPATTERN_PERIOD: %d\r\n",
                  data);

    // DAC_PAT
    data = spi_read(REG_DAC_PAT);
    Serial.printf("=== DAC_PAT:\r\n"
                  "\tDAC_REPEAT_CYCLE: %d repeat cycles\r\n",
                  data & 0xFF);

    // DOUT_START
    data = spi_read(REG_DOUT_START);
    Serial.printf("=== DOUT_START:\r\n"
                  "\tDOUT_START: %d clock cycles\r\n",
                  data);

    // DOUT_CONFIG
    data = spi_read(REG_DOUT_CONFIG);
    const char *dout_config[2] = {"Manual DOUT", "Controlled DOUT"};
    Serial.printf("=== DOUT_CONFIG:\r\n"
                  "\tDOUT_VAL: %s"
                  "\tDOUT_MODE: %s"
                  "\tDOUT_STOP: %d clock cycles\r\n",
                  bools[!!(data & 0x20)],
                  dout_config[!!(data & 0x10)],
                  data & 0xF);

    // DAC_CST
    data = spi_read(REG_DAC_CST);
    Serial.printf("=== DAC_CST:\r\n"
                  "\tDAC_CONST: 0x%04x\r\n",
                  data & 0xFFF0);

    // DAC_DGAIN
    data = spi_read(REG_DAC_DGAIN);
    Serial.printf("=== DAC_DGAIN:\r\n"
                  "\tDAC_DIG_GAIN: %f \r\n",
                  2 - (((data & 0xFFF0) >> 4) * 0.00006105));

    // SAW_CONFIG
    data = spi_read(REG_SAW_CONFIG);
    const char *saw_type[4] = {"Ramp-up sawtooth", "Ramp-down sawtooth", "Triangle sawtooth", "No wave"};
    Serial.printf("=== SAW_CONFIG:\r\n"
                  "\tSAW_STEP: %d samples per step\r\n"
                  "\tSAW_TYPE: %s\r\n",
                  data & 0xFC >> 2,
                  saw_type[data & 0x3]);

    // DDS_TW32
    uint32_t tuning_word = (spi_read(REG_DDS_TW32) << 8) | (spi_read(REG_DDS_TW1) & 0xFF00) >> 8;
    Serial.printf("=== DDS_TW32:\r\n"
                  "\tDDSTW: 0x%06x\r\n",
                  tuning_word);

    // DDS_PW
    data = spi_read(REG_DDS_PW);
    Serial.printf("=== DDS_PW:\r\n"
                  "\tDDS_PHASE: 0x%04x\r\n",
                  data);

    // TRIG_TW_SEL
    data = spi_read(REG_TRIG_TW_SEL);
    const char *trig_tw_sel[2] = {"Delay repeats for all patterns", "Delay only at start of first pattern"};
    Serial.printf("=== TRIG_TW_SEL:\r\n"
                  "\tTRIG_DELAY_EN: %s\r\n",
                  trig_tw_sel[!!(data & 0x2)]);

    // DDS_CONFIG
    data = spi_read(REG_DDS_CONFIG);
    const char *dds_cos_en[2] = {"Sine wave", "Cosine wave"};
    const char *dds_msb_en[2] = {"CLKP/CLKN", "MSB"};
    const char *phase_mem_en[2] = {"DDS_PW", "SRAM"};
    const char *tw_mem_en[2] = {"SRAM and DDS_TW", "DDS_TW"};
    Serial.printf("=== DDS_CONFIG:\r\n"
                  "    DDS_COS_EN: %s\r\n"
                  "    DDS_MSB_EN: %s\r\n"
                  "    PHASE_MEM_EN: %s\r\n"
                  "    TW_MEM_EN: %s\r\n",
                  dds_cos_en[!!(data & 0x8)],
                  dds_msb_en[!!(data & 0x4)],
                  phase_mem_en[!!(data & 0x2)],
                  tw_mem_en[!!(data & 0x1)]);

    // TW_RAM_CONFIG
    data = spi_read(REG_TW_RAM_CONFIG);
    Serial.printf("=== TW_RAM_CONFIG:\r\n"
                  "\tTW_MEM_SHIFT: 0x%02x\r\n",
                  data & 0x1F);

    // START_DLY
    data = spi_read(REG_START_DELAY);
    Serial.printf("=== START_DLY:\r\n"
                  "\tSTART_DELAY: 0x%04x\r\n",
                  data);

    // START_ADDR
    data = spi_read(REG_START_ADDR);
    Serial.printf("=== START_ADDR:\r\n"
                  "\tSTART_ADDR: 0x%04x\r\n",
                  data & 0xFFF0 >> 4);

    // STOP_ADDR
    data = spi_read(REG_STOP_ADDR);
    Serial.printf("=== STOP_ADDR:\r\n"
                  "\tSTOP_ADDR: 0x%04x\r\n",
                  data & 0xFFF0 >> 4);

    // DDS_CYC
    data = spi_read(REG_DDS_CYC);
    Serial.printf("=== DDS_CYC:\r\n"
                  "\tDDS_CYC: %d cycles\r\n",
                  data);

    // CFG_ERROR
    data = spi_read(REG_CFG_ERROR);
    Serial.printf("=== CFG_ERROR: "
                  "ERROR_CLEAR: %s | "
                  "DOUT_START_LG_ERR: %s | "
                  "PAT_DLY_SHORT_ERR: %s | "
                  "DOUT_START_SHORT_ERR: %s | "
                  "PERIOD_SHORT_ERR: %s | "
                  "ODD_ADDR_ERR: %s | "
                  "MEM_READ_ERR: %s\r\n",
                  bools[!!(data & 0x8000)],
                  bools[!!(data & 0x20)],
                  bools[!!(data & 0x10)],
                  bools[!!(data & 0x8)],
                  bools[!!(data & 0x4)],
                  bools[!!(data & 0x2)],
                  bools[!!(data & 0x1)]);
}

void AD9102::print_ram()
{
    Serial.print("=== RAM CONTENTS:\r\n        ");
    for (uint8_t i = 0; i < 64; i++)
    {
        Serial.printf("%04x ", i);
    }
    for (uint16_t i = 0; i < 4096; i++)
    {
        if (!(i % 64))
        {
            Serial.print("\r\n");
            Serial.printf("0x%04x:", 0x6000 + i);
        }
        uint16_t data = spi_read(0x6000 | i);
        Serial.printf(" %04x", data);
    }
}

void AD9102::calibrate()
{
    reg_calconfig(0, 1, 0, 1, 0);
    reg_compoffset(1);
    // wait for calibration
    Serial.println("Waiting for calibration...");
    do
    {
        calibration_running = spi_read(REG_CALCONFIG) & 0x80;
        calibration_running |= !(spi_read(REG_COMPOFFSET) & 0x2);
    } while (calibration_running);

    Serial.println("DDS calibration done.");
}

void AD9102::initialize()
{
    Serial.println("Initializing ..");
    // reset DDS registers to default values
    reset();
    delay(10);
    // calibrate DDS
    calibrate();
    Serial.println("Done.");
}

void AD9102::mode_sine()
{
    reg_wav_config(WAVE_SEL_PRES, PRESTORE_SEL_DDS);
    reg_dds_config(0, 0, 1); // disable TW + PW from RAM + set DDS MSB as clock

    set_tw(1);
}

void AD9102::mode_sine_ram()
{
    reg_pat_status(0, 1, 0); // enable memory access via SPI

    int16_t data_shifted = 0;
    for (int i = 0; i < 4096; i++)
    {
        int16_t a = (int16_t)(sinf(2.0f * PI * ((float)i / 4096.0f)) * 8000.0f);
        data_shifted = a << 2;
        spi_write(0x6000 + i, (uint16_t)data_shifted);
    }

    spi_write(REG_WAV_CONFIG, 0x30);      // DDS output
    spi_write(REG_START_ADDR, 0x2 << 4);  // start from 0x6002
    spi_write(REG_STOP_ADDR, 0xFFF << 4); // stop at 0x6FFF
    spi_write(REG_TW_RAM_CONFIG, 0xA);
    spi_write(REG_DDS_TW32, 0x0FF9);
    spi_write(REG_DDS_TW1, 0x99 << 8);
    spi_write(REG_TRIG_TW_SEL, 0x2); // delay only on pattern start
    spi_write(REG_RAMUPDATE, 0x1);   // update all SPI settings
}

void AD9102::mode_sqaure(uint16_t freq)
{
    reg_pat_status(0, 1, 0); // enable memory access via SPI

    bool dir = 0;
    for (uint16_t i = 0; i < 4096; i++)
    {
        if (!(i % freq))
            dir = !dir;

        spi_write(0x6000 + i, dir * 0xFFFC);
    }

    reg_wav_config(WAVE_SEL_RAM, PRESTORE_SEL_DDS);
    reg_dds_config(0, 0);               // disable TW + PW from RAM
    spi_write(REG_PAT_TYPE, 0);         // repeat continuously
    spi_write(REG_TRIG_TW_SEL, 1 << 1); // delay only on pattern start
    spi_write(REG_START_ADDR, 0x0);
    spi_write(REG_STOP_ADDR, 0xFFF << 4);
    spi_write(REG_RAMUPDATE, 0x1); // update all SPI settings
}
