// I2C communication with GreenPAK.
// This uses bitbanging for I2C so that internal GPIO pull-up can be used
// and to avoid the bugs that are present in STM32F2 I2C peripheral,
// it is uncertain if the same bugs apply to GD32F2.

#include "ZuluSCSI_platform.h"
#include "ZuluSCSI_log.h"
#include "greenpak.h"
#include "greenpak_fw.h"

#ifndef GREENPAK_I2C_PORT

bool greenpak_write(uint16_t regaddr, const uint8_t *data, int length) { return false; }
bool greenpak_read(uint16_t regaddr, uint8_t *data, int length) { return false; }
bool greenpak_load_firmware() { return false; }
bool greenpak_is_ready() { return false; }

#else

bool g_greenpak_is_ready;

// SCL is driven as push-pull, SDA is driven as IPU / OUT_OD
#define I2C_SCL_HI() GPIO_BOP(GREENPAK_I2C_PORT) = GREENPAK_I2C_SCL
#define I2C_SCL_LO() GPIO_BC(GREENPAK_I2C_PORT) = GREENPAK_I2C_SCL
#define I2C_SDA_HI() gpio_init(GREENPAK_I2C_PORT, GPIO_MODE_IPU, 0, GREENPAK_I2C_SDA)
#define I2C_SDA_LO() gpio_init(GREENPAK_I2C_PORT, GPIO_MODE_OUT_OD, GPIO_OSPEED_2MHZ, GREENPAK_I2C_SDA), GPIO_BC(GREENPAK_I2C_PORT) = GREENPAK_I2C_SDA
#define I2C_SDA_READ() (GPIO_ISTAT(GREENPAK_I2C_PORT) & GREENPAK_I2C_SDA)
#define I2C_DELAY() delay_ns(10000);

static void greenpak_gpio_init()
{
    gpio_bit_set(GREENPAK_I2C_PORT, GREENPAK_I2C_SCL | GREENPAK_I2C_SDA);
    gpio_init(GREENPAK_I2C_PORT, GPIO_MODE_IPU, 0, GREENPAK_I2C_SDA);
    gpio_init(GREENPAK_I2C_PORT, GPIO_MODE_OUT_PP, GPIO_OSPEED_2MHZ, GREENPAK_I2C_SCL);

    // Data bits used for communication
    uint32_t greenpak_io = GREENPAK_PLD_IO1 | GREENPAK_PLD_IO2 | GREENPAK_PLD_IO3;
    gpio_bit_reset(SCSI_OUT_PORT, greenpak_io);
    gpio_init(SCSI_OUT_PORT, GPIO_MODE_OUT_PP, GPIO_OSPEED_50MHZ, greenpak_io);
}

static void i2c_writebit(bool bit)
{
    if (bit)
        I2C_SDA_HI();
    else
        I2C_SDA_LO();

    I2C_DELAY();
    I2C_SCL_HI();
    I2C_DELAY();
    I2C_SCL_LO();
}

static bool i2c_readbit()
{
    I2C_SDA_HI(); // Pull-up
    I2C_DELAY();
    I2C_SCL_HI();
    I2C_DELAY();
    bool result = I2C_SDA_READ();
    I2C_SCL_LO();
    return result;
}

// Write byte to I2C bus, return ACK bit status
static bool i2c_writebyte(uint8_t byte)
{
    for (int i = 0; i < 8; i++)
    {
        i2c_writebit(byte & (0x80 >> i));
    }
    return !i2c_readbit();
}

// Read byte from I2C bus
static uint8_t i2c_readbyte(bool ack)
{
    uint8_t result = 0;
    for (int i = 0; i < 8; i++)
    {
        result |= i2c_readbit() << (7 - i);
    }

    i2c_writebyte(!ack);
    return result;
}

static bool i2c_start(uint8_t device_addr)
{
    // Initial signal state
    I2C_SCL_HI();
    I2C_SDA_HI();
    I2C_DELAY();

    // Start condition
    I2C_SDA_LO();
    I2C_DELAY();
    
    I2C_SCL_LO();
    I2C_DELAY();

    // Device address
    return i2c_writebyte(device_addr);
}

static void i2c_stop()
{
    I2C_SDA_LO();
    I2C_DELAY();
    I2C_SCL_HI();
    I2C_DELAY();
    I2C_SDA_HI();
    I2C_DELAY();
}

bool greenpak_write(uint16_t regaddr, const uint8_t *data, int length)
{
    bool status = true;
    uint16_t blockaddr = regaddr >> 8;
    status &= i2c_start(GREENPAK_I2C_ADDR | (blockaddr << 1));
    status &= i2c_writebyte(regaddr & 0xFF);
    
    for (int i = 0; i < length; i++)
    {
        status &= i2c_writebyte(data[i]);
    }

    i2c_stop();
    return status;
}

bool greenpak_read(uint16_t regaddr, uint8_t *data, int length)
{
    bool status = true;
    uint16_t blockaddr = (regaddr >> 8) & 7;
    status &= i2c_start(GREENPAK_I2C_ADDR | (blockaddr << 1));
    status &= i2c_writebyte(regaddr & 0xFF);

    status &= i2c_start(GREENPAK_I2C_ADDR | (blockaddr << 1) | 1);
    
    for (int i = 0; i < length; i++)
    {
        data[i] = i2c_readbyte(i < length - 1);
    }

    i2c_stop();
    return status;
}

bool greenpak_load_firmware()
{
    uint8_t dummy;
    greenpak_gpio_init();

    if (!greenpak_read(0, &dummy, 1))
    {
        azlog("Optional GreenPAK not detected");
        return false;
    }
    else
    {
        azlog("Optional GreenPAK detected, loading firmware");
    }

    if (!greenpak_write(0, g_greenpak_fw, sizeof(g_greenpak_fw)))
    {
        azlog("GreenPAK firmware loading failed");
        return false;
    }
    else
    {
        azlog("GreenPAK firmware successfully loaded");
        LED_ON();
        delay(10);
        LED_OFF();
        delay(100);
        g_greenpak_is_ready = true;
        return true;
    }
}

bool greenpak_is_ready()
{
    return g_greenpak_is_ready;
}

#endif