/*
 * soft_i2c_slave.c
 *
 * hastily written. likely has bugs.
 *
 * known quirks:
 * - a fast repeated start may not work as this code
 *   does not check for START after the beginning
 */

#include <CH59x_common.h>

#include "soft_i2c_slave.h"

#include <stdint.h>



#define I2CSS_ERR_TIMEOUT   6      // timeout in bit-times before i2c slave releases control
#define I2CSS_IDLE_TIMEOUT  3      // idle timeout required in bit-times before i2c slave can be addressed again


// todo: use with struct instead of hard-coding to allow for multiple slaves, but make it fast too
// note: will have issues with non-responsive slaves with more than one, so is it necessary?
#define SDA_PIN         13
#define SDA_BIT         (1 << (SDA_PIN % 8))
#define SDA_DIR_REG     R8_PA_DIR_0
#define SDA_DRV_REG     R8_PA_PD_DRV_0
#define SDA_CLR_REG     R8_PA_CLR_0
#define SDA_GET_REG     R8_PA_PIN_0

#define SCL_PIN         12
#define SCL_BIT         (1 << (SCL_PIN % 8))
#define SCL_DRV_REG     R8_PA_PD_DRV_1
#define SCL_DIR_REG     R8_PA_DIR_1
#define SCL_CLR_REG     R8_PA_CLR_1
#define SCL_GET_REG     R8_PA_PIN_1


#define SDA_IN_HI()     { SDA_DRV_REG &= ~SDA_BIT; SDA_DIR_REG &= ~SDA_BIT; }
#define SDA_OUTLO()     { SDA_DIR_REG |=  SDA_BIT; SDA_DRV_REG |=  SDA_BIT; }
#define SDA_SET_LO()    SDA_CLR_REG = SDA_BIT
#define SDA_GET()       ( SDA_GET_REG & SDA_BIT )

#define SCL_IN_HI()     { SCL_DRV_REG &= ~SCL_BIT; SCL_DIR_REG &= ~SCL_BIT; }
#define SCL_OUTLO()     { SCL_DIR_REG |=  SCL_BIT; SCL_DRV_REG |=  SCL_BIT; }
#define SCL_SET_LO()    SCL_CLR_REG = SCL_BIT
#define SCL_GET()       ( SCL_GET_REG & SCL_BIT )



static uint16_t bit_time;
static uint16_t idle_timeout = 0;



static int8_t i2css_wait_for_scl_hi()
{
    int16_t timeout = I2CSS_ERR_TIMEOUT * bit_time;

    while (!SCL_GET()) { if (!timeout--) return I2CSS_SCL_TIMEOUT; }
    return I2CSS_OK;
}

static int8_t i2css_wait_for_scl_lo()
{
    uint8_t old_data = SDA_GET();
    int16_t timeout = I2CSS_ERR_TIMEOUT * bit_time;

    timeout--;  // compensation

    while (SCL_GET()) {
        if (timeout <= 1) return I2CSS_SCL_TIMEOUT;
        if (SDA_GET() != old_data) return I2CSS_START_STOP;
        timeout -= 2;
    }

    return I2CSS_OK;
}

static inline int8_t i2css_ack(uint16_t bit_time)
{
    uint8_t ret;

    SDA_OUTLO();

    if (i2css_wait_for_scl_hi()) return I2CSS_ACK_ERROR;
    ret = i2css_wait_for_scl_lo();

    SDA_IN_HI();

    if (ret == I2CSS_SCL_TIMEOUT) return I2CSS_ACK_TIMEOUT;
    return ret;
}


int8_t i2css_idle_init(struct I2CSoftSlave *slave)
{
    // set up a hardware timer to tick our idle timer
    // todo
    // idle_timeout = slave->bit_time * I2CSS_IDLE_TIMEOUT;
}

// returns 0 if idle, or -1 if not idle.
// if not idle, restarts the idle timer.
int8_t i2css_idle_check(struct I2CSoftSlave *slave)
{
    // requires a hardware timer or something to manage bus idle time

    // if we're idle, then we're done
    if (!idle_timeout) {
        return 0;
    }

    // not idle. restart the idle timer.
    i2css_idle_init(slave);
    return -1;
}

int8_t i2css_read(struct I2CSoftSlave *slave)
{
    int8_t i;
    int8_t ret;

    uint8_t ack = 0;
    uint8_t dat = 0;


    // with no read function configured, we can't do anything
    if (!slave->rd_cb) {
        SCL_IN_HI();
        return I2CSS_NO_READ_FUNCTION;
    }

    // clock low for first bit / stretch, get data to send to master
    SCL_OUTLO();
    dat = slave->rd_cb();

    // release clock, wait for clock to be low by the master
    SCL_IN_HI();
    ret = i2css_wait_for_scl_lo();
    if (ret == I2CSS_SCL_TIMEOUT) return I2CSS_READ_TIMEOUT;
    if (ret) return ret;

    // send data to master
    for (i = 8; i; i--) {
        if (dat &= 0x80) { SDA_IN_HI(); }
        else             { SDA_OUTLO(); }

        dat <<= 1;

        if (i2css_wait_for_scl_hi()) return I2CSS_READ_ERROR;

        ret = i2css_wait_for_scl_lo();
        if (ret == I2CSS_SCL_TIMEOUT) return I2CSS_READ_TIMEOUT;
        if (ret) return ret;
    }

    // get (n)ack from master
    if (i2css_wait_for_scl_hi()) return I2CSS_READ_ERROR;

    ack = SDA_GET();

    return ack ? I2CSS_NACK : I2CSS_OK;
}

int8_t i2css_write(struct I2CSoftSlave *slave)
{
    int8_t i;
    int8_t ret;

    uint8_t dat = 0;
    uint8_t ack;


    // release clock for master
    SCL_IN_HI();

    // with no read function configured, we can't do anything
    if (!slave->wr_cb) {
        return I2CSS_NO_WRITE_FUNCTION;
    }

    // get data from master
    for (i = 8; i; i--) {
        if (i2css_wait_for_scl_hi()) return I2CSS_WRITE_ERROR;

        dat |= SDA_GET() ? 1 : 0;
        dat <<=1;

        ret = i2css_wait_for_scl_lo();
        if (ret == I2CSS_SCL_TIMEOUT) return I2CSS_WRITE_TIMEOUT;
        if (ret) return ret;
    }

    // stretch clock, write our data with callback
    SCL_OUTLO();
    ack = slave->wr_cb(dat);

    // send (n)ack to master
    if (ack) { SDA_IN_HI(); }       // nack
    else     { SDA_OUTLO(); }       // ack

    // release clock and wait
    SCL_IN_HI();
    if (i2css_wait_for_scl_hi()) return I2CSS_WRITE_ERROR;

    // wait for clock to go low
    ret = i2css_wait_for_scl_lo();
    if (ret == I2CSS_SCL_TIMEOUT) return I2CSS_WRITE_TIMEOUT;
    if (ret) return ret;

    // then stretch
    SCL_OUTLO();

    return 0;
}

// blocking implementation of i2c slave, with 8-bit address register.
// basically, simulates a 24C02 EEPROM.
// externally kicked off by a low transition on SCL.
static inline int8_t i2css_process(struct I2CSoftSlave *slave)
{
    int32_t i;
    int8_t ret;
    uint8_t rx_addr = 0;


    // wait for SCL low to indicate data has begun
    ret = i2css_wait_for_scl_lo();
    if (ret == I2CSS_SCL_TIMEOUT) return I2CSS_START_SCL_NOT_LOW;
    if (ret) return ret;


    // read address from bus
    for (i = 7; i; i--) {
        if (i2css_wait_for_scl_hi()) return I2CSS_ADDR_READ_ERROR;

        rx_addr |= SDA_GET() ? 1 : 0;
        rx_addr <<=1;

        ret = i2css_wait_for_scl_lo();
        if (ret == I2CSS_SCL_TIMEOUT) return I2CSS_ADDR_READ_TIMEOUT;
        if (ret) return ret;
    }

    // is this us?
    slave->addr_last_seen = rx_addr;
    if (slave->addr != rx_addr) return I2CSS_ADDR_NO_MATCH;

    // seems so. get the read/write bit
    if (i2css_wait_for_scl_hi()) return I2CSS_ADDR_READ_LASTBIT_TIMEOUT;

    rx_addr |= SDA_GET() ? 1 : 0;

    // let the host know we're ready to serve
    ret = i2css_ack(slave->bit_time);
    if (ret) return ret;

    // stretch clock while preparing slave to receive
    SCL_OUTLO();
    if (slave->start_cb) slave->start_cb();

    switch (rx_addr & 1) {
        case 0: {   // master is writing to us
            do {
                ret = i2css_write(slave);
            } while (!ret);
            break;
        }
        case 1: {   // master is reading from us
            do {
                // stretch clock
                SCL_OUTLO();
                ret = i2css_read(slave);
            } while (!ret);
            break;
        }
    }

    return ret;
}

int8_t i2css_blocking(struct I2CSoftSlave *slave)
{
    int8_t ret;
    int8_t start;

    do {
        ret = i2css_process(slave);
        start = 0;
        if (ret == I2CSS_START_STOP) {
            // determine which
            // high = stop, low = start
            start = SDA_GET() ? 0 : 1;
        }
    } while (start);

    return ret;
}

// call this function with SDA falling low interrupt.
int8_t i2css_start(struct I2CSoftSlave *slave)
{
    uint8_t scl;

    // idle our clock and check for start/stop
    SCL_IN_HI();
    scl = SCL_GET();

    // module will use this bit time for this communication
    bit_time = slave->bit_time;

    // ensure SDA pin will go low when set as an output
    SDA_SET_LO();

    // if SCL is high, this should be a start condition.
    if (scl) {
        return i2css_blocking(slave);
    }

    // if it isn't, there must be some other communication going on, and it doesn't involve us
    return I2CSS_BUS_BUSY;
}
